diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2cb02e..12cc5fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,5 +14,6 @@ jobs: with: node-version: 20.x - run: yarn install --frozen-lockfile + - run: npx playwright install --with-deps - run: yarn build - run: yarn test diff --git a/examples/basic/.gitignore b/examples/basic/.gitignore new file mode 100644 index 0000000..01f1f28 --- /dev/null +++ b/examples/basic/.gitignore @@ -0,0 +1,2 @@ +yarn.lock +.yarn/* \ No newline at end of file diff --git a/examples/basic/app/client.ts b/examples/basic/app/client.ts new file mode 100644 index 0000000..16ecf96 --- /dev/null +++ b/examples/basic/app/client.ts @@ -0,0 +1,3 @@ +import { createClient } from 'honox/client' + +createClient() diff --git a/examples/basic/app/global.d.ts b/examples/basic/app/global.d.ts new file mode 100644 index 0000000..e9536ef --- /dev/null +++ b/examples/basic/app/global.d.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line node/no-extraneous-import +import 'hono' + +type Head = { + title?: string +} + +declare module 'hono' { + interface Env { + Variables: {} + Bindings: {} + } + interface ContextRenderer { + (content: string | Promise, head?: Head): Response | Promise + } +} diff --git a/examples/basic/app/islands/counter.tsx b/examples/basic/app/islands/counter.tsx new file mode 100644 index 0000000..9d50cf4 --- /dev/null +++ b/examples/basic/app/islands/counter.tsx @@ -0,0 +1,11 @@ +import { useState } from 'hono/jsx' + +export default function Counter() { + const [count, setCount] = useState(0) + return ( +
+

Count: {count}

+ +
+ ) +} diff --git a/examples/basic/app/routes/_renderer.tsx b/examples/basic/app/routes/_renderer.tsx new file mode 100644 index 0000000..df3dff7 --- /dev/null +++ b/examples/basic/app/routes/_renderer.tsx @@ -0,0 +1,24 @@ +import { jsxRenderer } from 'hono/jsx-renderer' + +export default jsxRenderer( + ({ children, title }) => { + return ( + + + + + {title ? {title} : <>} + {import.meta.env.PROD ? ( + + ) : ( + + )} + + {children} + + ) + }, + { + docType: true, + } +) diff --git a/examples/basic/app/routes/index.tsx b/examples/basic/app/routes/index.tsx new file mode 100644 index 0000000..9947d02 --- /dev/null +++ b/examples/basic/app/routes/index.tsx @@ -0,0 +1,15 @@ +import { createRoute } from 'honox/factory' +import Counter from '../islands/counter' + +export default createRoute((c) => { + const name = c.req.query('name') ?? 'Hono' + return c.render( +
+

Hello, {name}!

+ +
, + { + title: name, + } + ) +}) diff --git a/examples/basic/app/server.ts b/examples/basic/app/server.ts new file mode 100644 index 0000000..e3d696e --- /dev/null +++ b/examples/basic/app/server.ts @@ -0,0 +1,11 @@ +import { serveStatic } from 'hono/cloudflare-pages' +import { showRoutes } from 'hono/dev' +import { createApp } from 'honox/server' + +const app = createApp({ + init: (app) => app.get('/static/*', serveStatic()), +}) + +showRoutes(app) + +export default app diff --git a/examples/basic/package.json b/examples/basic/package.json new file mode 100644 index 0000000..6349527 --- /dev/null +++ b/examples/basic/package.json @@ -0,0 +1,21 @@ +{ + "name": "basic", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build && vite build --mode client", + "preview": "wrangler pages dev ./dist", + "deploy": "$npm_execpath build && wrangler pages deploy ./dist" + }, + "private": true, + "dependencies": { + "honox": "^0.0.0" + }, + "devDependencies": { + "vite": "^5.0.10", + "wrangler": "^3.22.1" + }, + "resolutions": { + "honox": "portal:../.." + } +} diff --git a/examples/basic/tsconfig.json b/examples/basic/tsconfig.json new file mode 100644 index 0000000..d52023d --- /dev/null +++ b/examples/basic/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "esModuleInterop": true, + "strict": true, + "lib": [ + "esnext" + ], + "types": [ + "vite/client" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, +} \ No newline at end of file diff --git a/examples/basic/vite.config.ts b/examples/basic/vite.config.ts new file mode 100644 index 0000000..c11482c --- /dev/null +++ b/examples/basic/vite.config.ts @@ -0,0 +1,33 @@ +import honox from 'honox/vite' +import { defineConfig } from '../../node_modules/vite' + +export default defineConfig(({ mode }) => { + if (mode === 'client') { + return { + build: { + rollupOptions: { + input: ['./app/client.ts'], + output: { + entryFileNames: 'static/client.js', + chunkFileNames: 'static/assets/[name]-[hash].js', + assetFileNames: 'static/assets/[name].[ext]', + }, + }, + emptyOutDir: false, + copyPublicDir: false, + }, + } + } else { + return { + build: { + rollupOptions: { + output: { + entryFileNames: '_worker.js', + }, + }, + minify: true, + }, + plugins: [honox()], + } + } +}) diff --git a/package.json b/package.json index a8d95eb..87ef126 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,12 @@ "main": "dist/index.js", "type": "module", "scripts": { - "test": "yarn typecheck && yarn test:unit && yarn test:integration", + "test": "yarn typecheck && yarn test:unit && yarn test:integration && yarn test:e2e", "test:unit": "vitest --run test/unit", "test:integration": "yarn test:integration:api && yarn test:integration:hono-jsx", "test:integration:hono-jsx": "vitest run -c ./test/hono-jsx/vitest.config.ts ./test/hono-jsx/integration.test.ts", "test:integration:api": "vitest run -c ./test/api/vitest.config.ts ./test/api/integration.test.ts", + "test:e2e": "playwright test -c ./test/hono-jsx/playwright.config.ts ./test/hono-jsx/e2e.test.ts", "typecheck": "tsc --noEmit", "build": "tsup && publint", "watch": "tsup --watch", @@ -91,6 +92,7 @@ }, "devDependencies": { "@hono/eslint-config": "^0.0.3", + "@playwright/test": "^1.41.0", "@types/babel__generator": "^7", "@types/babel__traverse": "^7", "@types/node": "^20.10.5", @@ -108,4 +110,4 @@ "engines": { "node": ">=18.14.1" } -} +} \ No newline at end of file diff --git a/src/client/client.ts b/src/client/client.ts index a183eb0..3d11cda 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,3 +1,5 @@ +import { jsx as jsxFn } from 'hono/jsx' +import { render } from 'hono/jsx/dom' import { COMPONENT_NAME, DATA_SERIALIZED_PROPS } from '../constants.js' import type { CreateElement, Hydrate } from '../types.js' @@ -5,15 +7,15 @@ import type { CreateElement, Hydrate } from '../types.js' type FileCallback = () => Promise<{ default: Promise }> export type ClientOptions = { - hydrate: Hydrate - createElement: CreateElement + hydrate?: Hydrate + createElement?: CreateElement ISLAND_FILES?: Record Promise> island_root?: string } -export const createClient = async (options: ClientOptions) => { - const FILES = options.ISLAND_FILES ?? import.meta.glob('/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)') - const root = options.island_root ?? '/app/islands/' +export const createClient = async (options?: ClientOptions) => { + const FILES = options?.ISLAND_FILES ?? import.meta.glob('/app/islands/**/[a-zA-Z0-9[-]+.(tsx|ts)') + const root = options?.island_root ?? '/app/islands/' const hydrateComponent = async () => { const filePromises = Object.keys(FILES).map(async (filePath) => { @@ -28,10 +30,11 @@ export const createClient = async (options: ClientOptions) => { const serializedProps = element.attributes.getNamedItem(DATA_SERIALIZED_PROPS)?.value const props = JSON.parse(serializedProps ?? '{}') as Record - const hydrate = options.hydrate - const createElement = options.createElement + const hydrate = options?.hydrate ?? render + const createElement = options?.createElement ?? jsxFn const newElem = await createElement(Component, props) + // @ts-expect-error default `render` cause a type error await hydrate(newElem, element) }) await Promise.all(elementPromises) diff --git a/src/server/server.ts b/src/server/server.ts index d3cdd81..aa4a300 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -7,6 +7,7 @@ import { groupByDirectory, listByDirectory, pathToDirectoryPath, + sortDirectoriesByDepth, } from '../utils/file.js' const NOTFOUND_FILENAME = '_404.tsx' @@ -76,67 +77,76 @@ export const createApp = (options?: ServerOptions): Hono => import.meta.glob('/app/routes/**/[a-z0-9[-][a-z0-9[_-]*.(ts|tsx|mdx)', { eager: true, }) - const routesMap = groupByDirectory(ROUTES_FILE) + const routesMap = sortDirectoriesByDepth(groupByDirectory(ROUTES_FILE)) - for (const [dir, content] of Object.entries(routesMap)) { - const subApp = new Hono() + for (const map of routesMap) { + for (const [dir, content] of Object.entries(map)) { + const subApp = new Hono() - // Renderer - let rendererFiles = rendererList[dir] + // Renderer + let rendererFiles = rendererList[dir] - if (rendererFiles) { - applyRenderer(rendererFiles[0]) - } + if (rendererFiles) { + applyRenderer(rendererFiles[0]) + } - if (!rendererFiles) { - const dirPaths = dir.split('/') - const getRendererPaths = (paths: string[]) => { - rendererFiles = rendererList[paths.join('/')] - if (!rendererFiles) { - paths.pop() - if (paths.length) { - getRendererPaths(paths) + if (!rendererFiles) { + const dirPaths = dir.split('/') + const getRendererPaths = (paths: string[]) => { + rendererFiles = rendererList[paths.join('/')] + if (!rendererFiles) { + paths.pop() + if (paths.length) { + getRendererPaths(paths) + } } + return rendererFiles + } + rendererFiles = getRendererPaths(dirPaths) + if (rendererFiles) { + applyRenderer(rendererFiles[0]) } - return rendererFiles - } - rendererFiles = getRendererPaths(dirPaths) - if (rendererFiles) { - applyRenderer(rendererFiles[0]) } - } - // Root path - let rootPath = dir.replace(rootRegExp, '') - rootPath = filePathToPath(rootPath) + // Root path + let rootPath = dir.replace(rootRegExp, '') + rootPath = filePathToPath(rootPath) - for (const [filename, route] of Object.entries(content)) { - const routeDefault = route.default - const path = filePathToPath(filename) + for (const [filename, route] of Object.entries(content)) { + const routeDefault = route.default + const path = filePathToPath(filename) - // Instance of Hono - if (routeDefault && 'fetch' in routeDefault) { - subApp.route(path, routeDefault) - } + // Instance of Hono + if (routeDefault && 'fetch' in routeDefault) { + subApp.route(path, routeDefault) + } + + // export const POST = factory.createHandlers(...) + for (const m of METHODS) { + const handlers = (route as Record)[m] + if (handlers) { + subApp.on(m, path, ...handlers) + } + } - // export const POST = factory.createHandlers(...) - for (const m of METHODS) { - const handlers = (route as Record)[m] - if (handlers) { - subApp.on(m, path, ...handlers) + // export default factory.createHandlers(...) + if (routeDefault && Array.isArray(routeDefault)) { + subApp.get(path, ...(routeDefault as H[])) } - } - // export default factory.createHandlers(...) - if (routeDefault && Array.isArray(routeDefault)) { - subApp.get(path, ...(routeDefault as H[])) + // export default function Helle() {} + if (typeof routeDefault === 'function') { + subApp.get(path, (c) => { + return c.render(routeDefault()) + }) + } } + // Not Found + applyNotFound(subApp, dir, notFoundMap) + // Error + applyError(subApp, dir, errorMap) + app.route(rootPath, subApp) } - // Not Found - applyNotFound(subApp, dir, notFoundMap) - // Error - applyError(subApp, dir, errorMap) - app.route(rootPath, subApp) } return app diff --git a/src/utils/file.ts b/src/utils/file.ts index cf33467..b1ef7ee 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -55,6 +55,20 @@ export const groupByDirectory = (files: Record) => { return organizedFiles } +export const sortDirectoriesByDepth = (directories: Record) => { + const sortedKeys = Object.keys(directories).sort((a, b) => { + const depthA = a.split('/').length + const depthB = b.split('/').length + return depthB - depthA + }) + + const sortedDirectories: Record[] = sortedKeys.map((key) => { + return { [key]: directories[key] } + }) + + return sortedDirectories +} + /* /app/routes/_renderer.tsx /app/routes/blog/_renderer.tsx diff --git a/test/api/integration.test.ts b/test/api/integration.test.ts index 384d198..acfc3c9 100644 --- a/test/api/integration.test.ts +++ b/test/api/integration.test.ts @@ -18,6 +18,7 @@ describe('Basic', () => { it('Should have correct routes', () => { const routes = [ { path: '/*', method: 'ALL', handler: expect.anything() }, + { path: '/about/:name', method: 'GET', @@ -28,8 +29,6 @@ describe('Basic', () => { method: 'GET', handler: expect.anything(), }, - { path: '/', method: 'GET', handler: expect.anything() }, - { path: '/foo', method: 'GET', handler: expect.anything() }, { path: '/middleware/*', method: 'ALL', @@ -40,12 +39,16 @@ describe('Basic', () => { method: 'GET', handler: expect.anything(), }, + { path: '/middleware/foo', method: 'GET', handler: expect.anything(), }, + { path: '/', method: 'GET', handler: expect.anything() }, + { path: '/foo', method: 'GET', handler: expect.anything() }, ] + expect(app.routes).toEqual(routes) }) diff --git a/test/hono-jsx/app/client.ts b/test/hono-jsx/app/client.ts new file mode 100644 index 0000000..42f4444 --- /dev/null +++ b/test/hono-jsx/app/client.ts @@ -0,0 +1,3 @@ +import { createClient } from '../../../src/client' + +createClient() diff --git a/test/hono-jsx/global.d.ts b/test/hono-jsx/app/global.d.ts similarity index 100% rename from test/hono-jsx/global.d.ts rename to test/hono-jsx/app/global.d.ts diff --git a/test/hono-jsx/app/islands/Counter.tsx b/test/hono-jsx/app/islands/Counter.tsx new file mode 100644 index 0000000..8ee0332 --- /dev/null +++ b/test/hono-jsx/app/islands/Counter.tsx @@ -0,0 +1,12 @@ +import { useState } from 'hono/jsx' + +export default function Counter({ initial }: { initial: number }) { + const [count, setCount] = useState(initial) + const increment = () => setCount(count + 1) + return ( +
+

Count: {count}

+ +
+ ) +} diff --git a/test/hono-jsx/app/routes/_renderer.tsx b/test/hono-jsx/app/routes/_renderer.tsx index 32351cf..ba8020b 100644 --- a/test/hono-jsx/app/routes/_renderer.tsx +++ b/test/hono-jsx/app/routes/_renderer.tsx @@ -5,6 +5,7 @@ export default jsxRenderer(({ children, title }) => { {title} + {children} diff --git a/test/hono-jsx/app/routes/interaction/index.tsx b/test/hono-jsx/app/routes/interaction/index.tsx new file mode 100644 index 0000000..7a81e45 --- /dev/null +++ b/test/hono-jsx/app/routes/interaction/index.tsx @@ -0,0 +1,5 @@ +import Counter from '../../islands/Counter' + +export default function Interaction() { + return +} diff --git a/test/hono-jsx/app/routes/post.mdx b/test/hono-jsx/app/routes/post.mdx deleted file mode 100644 index fe98d56..0000000 --- a/test/hono-jsx/app/routes/post.mdx +++ /dev/null @@ -1 +0,0 @@ -## Hello MDX! \ No newline at end of file diff --git a/test/hono-jsx/app/server.ts b/test/hono-jsx/app/server.ts new file mode 100644 index 0000000..855487a --- /dev/null +++ b/test/hono-jsx/app/server.ts @@ -0,0 +1,39 @@ +import { showRoutes } from 'hono/dev' +import { logger } from 'hono/logger' +import { createApp } from '../../../src/server' + +const ROUTES = import.meta.glob('./routes/**/[a-z[-][a-z[_-]*.(tsx|ts)', { + eager: true, +}) + +const RENDERER = import.meta.glob('./routes/**/_renderer.tsx', { + eager: true, +}) + +const NOT_FOUND = import.meta.glob('./routes/**/_404.(ts|tsx', { + eager: true, +}) + +const ERROR = import.meta.glob('./routes/**/_error.(ts|tsx)', { + eager: true, +}) + +const app = createApp({ + // @ts-expect-error type is not specified + ROUTES, + // @ts-expect-error type is not specified + RENDERER, + // @ts-expect-error type is not specified + NOT_FOUND, + // @ts-expect-error type is not specified + ERROR, + root: './routes', + init: (app) => { + app.use(logger()) + }, +}) +showRoutes(app, { + verbose: true, +}) + +export default app diff --git a/test/hono-jsx/e2e.test.ts b/test/hono-jsx/e2e.test.ts new file mode 100644 index 0000000..039fd28 --- /dev/null +++ b/test/hono-jsx/e2e.test.ts @@ -0,0 +1,10 @@ +import { test } from '@playwright/test' + +test('test counter', async ({ page }) => { + await page.goto('/interaction', { waitUntil: 'domcontentloaded' }) + await page.getByText('Count: 5').click() + await page.getByRole('button', { name: 'Increment' }).click({ + clickCount: 1, + }) + await page.getByText('Count: 6').click() +}) diff --git a/test/hono-jsx/integration.test.ts b/test/hono-jsx/integration.test.ts index 0ca71d5..b76708e 100644 --- a/test/hono-jsx/integration.test.ts +++ b/test/hono-jsx/integration.test.ts @@ -22,6 +22,11 @@ describe('Basic', () => { method: 'ALL', handler: expect.any(Function), }, + { + path: '/about/:name/address', + method: 'GET', + handler: expect.any(Function), + }, { path: '/about/:name', method: 'POST', @@ -33,7 +38,7 @@ describe('Basic', () => { handler: expect.any(Function), }, { - path: '/about/:name/address', + path: '/interaction', method: 'GET', handler: expect.any(Function), }, @@ -111,7 +116,7 @@ describe('Basic', () => { const res = await app.request('/') expect(res.status).toBe(200) expect(await res.text()).toBe( - 'This is a title

Hello

' + 'This is a title

Hello

' ) }) @@ -119,7 +124,7 @@ describe('Basic', () => { const res = await app.request('/foo') expect(res.status).toBe(404) expect(await res.text()).toBe( - 'Not Found

Not Found

' + 'Not Found

Not Found

' ) }) @@ -128,7 +133,7 @@ describe('Basic', () => { expect(res.status).toBe(200) // hono/jsx escape a single quote to ' expect(await res.text()).toBe( - 'me

It's me

My name is me' + 'me

It's me

My name is me' ) }) @@ -145,7 +150,7 @@ describe('Basic', () => { const res = await app.request('/throw_error') expect(res.status).toBe(500) expect(await res.text()).toBe( - 'Internal Server Error

Custom Error Message: Foo

' + 'Internal Server Error

Custom Error Message: Foo

' ) }) }) diff --git a/test/hono-jsx/playwright.config.ts b/test/hono-jsx/playwright.config.ts new file mode 100644 index 0000000..0084dad --- /dev/null +++ b/test/hono-jsx/playwright.config.ts @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + use: { + baseURL: 'http://localhost:6173', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + timeout: 5000, + retries: 2, + }, + ], + webServer: { + command: 'yarn vite --port 6173 -c ./vite.config.ts', + port: 6173, + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/test/hono-jsx/vite.config.ts b/test/hono-jsx/vite.config.ts new file mode 100644 index 0000000..0aeb329 --- /dev/null +++ b/test/hono-jsx/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import honox from '../../src/vite' + +export default defineConfig({ + plugins: [ + honox({ + entry: './app/server.ts', + }), + ], +}) diff --git a/test/unit/utils/file.test.ts b/test/unit/utils/file.test.ts index ef688f4..b5ac8e6 100644 --- a/test/unit/utils/file.test.ts +++ b/test/unit/utils/file.test.ts @@ -3,6 +3,7 @@ import { groupByDirectory, listByDirectory, pathToDirectoryPath, + sortDirectoriesByDepth, } from '../../../src/utils/file.js' describe('filePathToPath', () => { @@ -55,6 +56,40 @@ describe('groupByDirectory', () => { }) }) +describe('sortDirectoriesByDepth', () => { + it('Should sort directories by the depth', () => { + expect( + sortDirectoriesByDepth({ + '/app/routes': { + 'index.tsx': 'file1', + }, + '/app/routes/blog/posts': { + 'index.tsx': 'file2', + }, + '/app/routes/blog': { + 'index.tsx': 'file3', + }, + }) + ).toEqual([ + { + '/app/routes/blog/posts': { + 'index.tsx': 'file2', + }, + }, + { + '/app/routes/blog': { + 'index.tsx': 'file3', + }, + }, + { + '/app/routes': { + 'index.tsx': 'file1', + }, + }, + ]) + }) +}) + describe('listByDirectory', () => { it('Should list files by their directory', () => { const files = { diff --git a/vitest.config.ts b/vitest.config.ts index 0070765..b09c4cf 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,6 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, - exclude: ['node_modules', 'dist', '.git', '.cache', 'test-presets', 'sandbox'], + exclude: ['node_modules', 'dist', '.git', '.cache', 'test-presets', 'sandbox', 'examples'], }, }) diff --git a/yarn.lock b/yarn.lock index 2b1c438..54a1f84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -572,6 +572,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.41.0": + version: 1.41.0 + resolution: "@playwright/test@npm:1.41.0" + dependencies: + playwright: "npm:1.41.0" + bin: + playwright: cli.js + checksum: f6b1934b84cf10a356b356b8b9d43271d479c7292deaf5c6dc1742d8d5f28e86f6bef83a79a8eb0d557dc30377b1c33d3613dd6ddbbf31f21d5ebe92c65b1608 + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.9.1": version: 4.9.1 resolution: "@rollup/rollup-android-arm-eabi@npm:4.9.1" @@ -2140,6 +2151,16 @@ __metadata: languageName: node linkType: hard +"fsevents@npm:2.3.2": + version: 2.3.2 + resolution: "fsevents@npm:2.3.2" + dependencies: + node-gyp: "npm:latest" + checksum: be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -2150,6 +2171,15 @@ __metadata: languageName: node linkType: hard +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": + version: 2.3.2 + resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" + dependencies: + node-gyp: "npm:latest" + conditions: os=darwin + languageName: node + linkType: hard + "fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -2455,6 +2485,7 @@ __metadata: "@babel/types": "npm:^7.23.6" "@hono/eslint-config": "npm:^0.0.3" "@hono/vite-dev-server": "npm:^0.3.4" + "@playwright/test": "npm:^1.41.0" "@types/babel__generator": "npm:^7" "@types/babel__traverse": "npm:^7" "@types/node": "npm:^20.10.5" @@ -3621,6 +3652,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.41.0": + version: 1.41.0 + resolution: "playwright-core@npm:1.41.0" + bin: + playwright-core: cli.js + checksum: ea74d755c9c8cfec53566fb8ca689b55b493bf67d49ad631c39669006d018e1127688d31a689ef05672d7ae7f9ebb8c3aef92dbe31636c3c3826d4cb90c14cf2 + languageName: node + linkType: hard + +"playwright@npm:1.41.0": + version: 1.41.0 + resolution: "playwright@npm:1.41.0" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.41.0" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: db12f408c02326ba9efdd7d6c10b9d7509e85cddf99d8043a2fe0d24322a4129ed6c958c301cc99b39c28b335fe659e9dc30b31f2af7fc1428207aa0ba1c9180 + languageName: node + linkType: hard + "postcss-load-config@npm:^4.0.1": version: 4.0.2 resolution: "postcss-load-config@npm:4.0.2"