From 66f3462740f731072fdd3303130030d4f77f3382 Mon Sep 17 00:00:00 2001 From: Tom Meagher Date: Wed, 13 Mar 2024 17:09:46 -0400 Subject: [PATCH] chore: updates --- pnpm-lock.yaml | 123 +++------------ src/cli/commands/dev.ts | 20 ++- src/cli/vite/dev.ts | 258 ++++++++++++++++++++++++++++++++ src/dev/devtools.tsx | 15 +- src/frog-base.tsx | 4 + src/package.json | 1 - ui/src/App.tsx | 42 +++--- ui/src/assets/icon.png | Bin 0 -> 12910 bytes ui/src/assets/react.svg | 1 - ui/src/assets/vite.svg | 1 - ui/src/{lib => }/frog-client.ts | 11 +- ui/src/lib/store.ts | 4 +- ui/src/main.tsx | 3 +- ui/vite.config.ts | 2 +- 14 files changed, 336 insertions(+), 149 deletions(-) create mode 100644 src/cli/vite/dev.ts create mode 100644 ui/src/assets/icon.png delete mode 100644 ui/src/assets/react.svg delete mode 100644 ui/src/assets/vite.svg rename ui/src/{lib => }/frog-client.ts (93%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d22a0d7..4fdbcef8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,9 +176,6 @@ importers: '@hono/node-server': specifier: ^1.8.2 version: 1.8.2 - '@hono/vite-dev-server': - specifier: 0.7.0 - version: 0.7.0(hono@4.1.0) '@noble/curves': specifier: ^1.3.0 version: 1.3.0 @@ -212,6 +209,9 @@ importers: lz-string: specifier: ^1.5.0 version: 1.5.0 + minimatch: + specifier: ^9.0.3 + version: 9.0.3 path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -1028,15 +1028,6 @@ packages: mime: 3.0.0 dev: true - /@cloudflare/workerd-darwin-64@1.20240129.0: - resolution: {integrity: sha512-DfVVB5IsQLVcWPJwV019vY3nEtU88c2Qu2ST5SQxqcGivZ52imagLRK0RHCIP8PK4piSiq90qUC6ybppUsw8eg==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@cloudflare/workerd-darwin-64@1.20240208.0: resolution: {integrity: sha512-64qjsCUz6VtjXnUex5D6dWoJDuUBRw1ps2TEVH9wGJ4ubiLVUxKhj3bzkVy0RoJ8FhaCKzJWWRyTo4yc192UTA==} engines: {node: '>=16'} @@ -1046,15 +1037,6 @@ packages: dev: true optional: true - /@cloudflare/workerd-darwin-arm64@1.20240129.0: - resolution: {integrity: sha512-t0q8ABkmumG1zRM/MZ/vIv/Ysx0vTAXnQAPy/JW5aeQi/tqrypXkO9/NhPc0jbF/g/hIPrWEqpDgEp3CB7Da7Q==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - /@cloudflare/workerd-darwin-arm64@1.20240208.0: resolution: {integrity: sha512-eVQrAV200LhwLY6JZLx3l2lDrjsTC86lqnvH+RSeM43bAcdneC6lVfykHnTaOTgYFvYQbqRkn9ICWxXj1V9L5g==} engines: {node: '>=16'} @@ -1064,15 +1046,6 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-64@1.20240129.0: - resolution: {integrity: sha512-sFV1uobHgDI+6CKBS/ZshQvOvajgwl6BtiYaH4PSFSpvXTmRx+A9bcug+6BnD+V4WgwxTiEO2iR97E1XuwDAVw==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@cloudflare/workerd-linux-64@1.20240208.0: resolution: {integrity: sha512-ivZ2UuCvi44j8JZ++XlQzSYajt5ptvAdwlh3WPpCcygtHXEh6SVo8QXEUOXhPbv861C0HZMYxLCaLqlpQDWB8g==} engines: {node: '>=16'} @@ -1082,15 +1055,6 @@ packages: dev: true optional: true - /@cloudflare/workerd-linux-arm64@1.20240129.0: - resolution: {integrity: sha512-O7q7htHaFRp8PgTqNJx1/fYc3+LnvAo6kWWB9a14C5OWak6AAZk42PNpKPx+DXTmGvI+8S1+futBGUeJ8NPDXg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - /@cloudflare/workerd-linux-arm64@1.20240208.0: resolution: {integrity: sha512-aLfvl9kXQKbM7aLvfL0HbOt5VEgv15mEZGyFKyDldJ8+nOXH6nYPma1ccwF8BHmu8otHc420eyPr2xPKhLSJnw==} engines: {node: '>=16'} @@ -1100,15 +1064,6 @@ packages: dev: true optional: true - /@cloudflare/workerd-windows-64@1.20240129.0: - resolution: {integrity: sha512-YqGno0XSqqqkDmNoGEX6M8kJlI2lEfWntbTPVtHaZlaXVR9sWfoD7TEno0NKC95cXFz+ioyFLbgbOdnfWwmVAA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - /@cloudflare/workerd-windows-64@1.20240208.0: resolution: {integrity: sha512-Y6KMukWnorsSmPx6d82IuJ4SU8sX1+2y+w1uFJ76sucSgXqUAN1fmjG+EyzRVbcbsxRGBCD9c1Pn8T1amMLEYA==} engines: {node: '>=16'} @@ -1123,6 +1078,7 @@ packages: engines: {node: '>=12'} dependencies: '@jridgewell/trace-mapping': 0.3.9 + dev: true /@edge-runtime/cookies@3.4.1: resolution: {integrity: sha512-z27BvgPxI73CgSlxU/NAUf1Q/shnqi6cobHEowf6VuLdSjGR3NjI2Y5dZUIBbK2zOJVZbXcHsVzJjz8LklteFQ==} @@ -1790,6 +1746,7 @@ packages: /@fastify/busboy@2.1.0: resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} engines: {node: '>=14'} + dev: true /@floating-ui/core@1.6.0: resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} @@ -1841,22 +1798,6 @@ packages: engines: {node: '>=18.14.1'} dev: false - /@hono/vite-dev-server@0.7.0(hono@4.1.0): - resolution: {integrity: sha512-NFcBuyklOammhiExJUYvTfzoU9Mzv4F7ddUCjTe9hy37/AHGZXoksZaBWFl2fRSPVZaPvmX9eiRLBMIrML2iYw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4.1.0 - dependencies: - '@hono/node-server': 1.8.2 - hono: 4.1.0 - miniflare: 3.20240129.3 - minimatch: 9.0.3 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1910,6 +1851,7 @@ packages: dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + dev: true /@manypkg/find-root@1.1.0: resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -3979,6 +3921,7 @@ packages: /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} + dev: true /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} @@ -4141,6 +4084,7 @@ packages: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} dependencies: printable-characters: 1.0.42 + dev: true /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -4409,6 +4353,7 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - supports-color + dev: true /ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4652,6 +4597,7 @@ packages: /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} + dev: true /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -4747,6 +4693,7 @@ packages: /data-uri-to-buffer@2.0.2: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} + dev: true /dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} @@ -5554,6 +5501,7 @@ packages: /exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} + dev: true /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -5812,6 +5760,7 @@ packages: dependencies: data-uri-to-buffer: 2.0.2 source-map: 0.6.1 + dev: true /get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} @@ -5864,6 +5813,7 @@ packages: /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true /glob@10.3.10: resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} @@ -7426,29 +7376,6 @@ packages: engines: {node: '>=4'} dev: true - /miniflare@3.20240129.3: - resolution: {integrity: sha512-PCmLJ+UvtbpPj/fgNzTGbd+U5QBkt3akRNcdks9RBJU2SH+gUCp7iahsaI4GA344NX5MIbC6ctw1A6TfcA+aFA==} - engines: {node: '>=16.13'} - hasBin: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.11.3 - acorn-walk: 8.3.2 - capnp-ts: 0.7.0 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - stoppable: 1.1.0 - undici: 5.28.3 - workerd: 1.20240129.0 - ws: 8.16.0 - youch: 3.3.3 - zod: 3.22.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: false - /miniflare@3.20240208.0: resolution: {integrity: sha512-NnP3MQFh2pV7iETNmJzSlMBF/KhRA+XT4A7JLCfxunadQSPbTMMgbsZo9EfLloMwHMUhZGNVot3Pvh+VnT2joQ==} engines: {node: '>=16.13'} @@ -7590,6 +7517,7 @@ packages: /mustache@4.2.0: resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} hasBin: true + dev: true /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -8181,6 +8109,7 @@ packages: /printable-characters@1.0.42: resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} + dev: true /promisepipe@3.0.0: resolution: {integrity: sha512-V6TbZDJ/ZswevgkDNpGt/YqNCiZP9ASfgU+p83uJE6NrGtvSGoOcHLiDCqkMs2+yg7F5qHdLV8d0aS8O26G/KA==} @@ -8944,6 +8873,7 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + dev: true /source-map@0.7.4: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} @@ -9001,6 +8931,7 @@ packages: dependencies: as-table: 1.0.55 get-source: 2.0.12 + dev: true /stat-mode@0.3.0: resolution: {integrity: sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng==} @@ -9030,6 +8961,7 @@ packages: /stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} + dev: true /stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} @@ -9602,6 +9534,7 @@ packages: engines: {node: '>=14.0'} dependencies: '@fastify/busboy': 2.1.0 + dev: true /unicode-trie@2.0.0: resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} @@ -10230,19 +10163,6 @@ packages: string-width: 4.2.3 dev: true - /workerd@1.20240129.0: - resolution: {integrity: sha512-t4pnsmjjk/u+GdVDgH2M1AFmJaBUABshYK/vT/HNrAXsHSwN6VR8Yqw0JQ845OokO34VLkuUtYQYyxHHKpdtsw==} - engines: {node: '>=16'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240129.0 - '@cloudflare/workerd-darwin-arm64': 1.20240129.0 - '@cloudflare/workerd-linux-64': 1.20240129.0 - '@cloudflare/workerd-linux-arm64': 1.20240129.0 - '@cloudflare/workerd-windows-64': 1.20240129.0 - dev: false - /workerd@1.20240208.0: resolution: {integrity: sha512-edFdwHU95Ww2SmjBvBJhbc7hhVXMEo6Y7qqSWCl6W9lGScTlCMCXd4AU3f/EGJ3P++FC+CWqu+XuAywebbKF2Q==} engines: {node: '>=16'} @@ -10350,6 +10270,7 @@ packages: optional: true utf-8-validate: optional: true + dev: true /xdg-app-paths@5.1.0: resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} @@ -10483,9 +10404,11 @@ packages: cookie: 0.5.0 mustache: 4.2.0 stacktracey: 2.1.8 + dev: true /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + dev: true /zustand@4.5.2(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 7c42e599..9261829f 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -1,11 +1,12 @@ import { existsSync } from 'node:fs' import { join, resolve } from 'node:path' -import devServer, { defaultOptions } from '@hono/vite-dev-server' import pc from 'picocolors' import { createLogger, createServer } from 'vite' import { version } from '../../version.js' import { findEntrypoint } from '../utils/findEntrypoint.js' +import { defaultOptions, devServer } from '../vite/dev.js' +import type { Frog } from '../../frog.js' type DevOptions = { host?: boolean @@ -35,7 +36,6 @@ export async function dev( // Note: we are not relying on the default export so we can be compatible with // runtimes that rely on it (e.g. Vercel Serverless Functions). export: 'app', - injectClientScript: false, }), ], publicDir: staticDir ?? 'public', @@ -47,10 +47,12 @@ export async function dev( }) const module = await server.ssrLoadModule(entry_resolved) - const basePath = module.app?.basePath || '/' + const app = module.app as Frog | undefined + const basePath = app?.basePath || '/' await server.listen() server.bindCLIShortcuts() + const url = `http://localhost:${server.config.server.port}` const logger = createLogger() logger.clearScreen('info') @@ -59,9 +61,11 @@ export async function dev( ` ${pc.green('[running]')} ${pc.bold('frog')}@${pc.dim(`v${version}`)}`, ) logger.info('') - logger.info( - ` ${pc.green('➜')} ${pc.bold('Local')}: ${pc.cyan( - `http://localhost:${server.config.server.port}${basePath}`, - )}`, - ) + const appUrl = `${url}${basePath}` + logger.info(` ${pc.green('➜')} ${pc.bold('Local')}: ${pc.cyan(appUrl)}`) + + if (app?.dev) { + const devUrl = `${url}${app.dev}` + logger.info(` ${pc.green('➜')} ${pc.bold('Inspect')}: ${pc.cyan(devUrl)}`) + } } diff --git a/src/cli/vite/dev.ts b/src/cli/vite/dev.ts new file mode 100644 index 00000000..fc4eef70 --- /dev/null +++ b/src/cli/vite/dev.ts @@ -0,0 +1,258 @@ +import { IncomingMessage, ServerResponse } from 'http' +import { getRequestListener } from '@hono/node-server' +import type { Plugin as VitePlugin, ViteDevServer, Connect } from 'vite' + +export type DevServerOptions = { + entry?: string + export?: string + injectClientScript?: boolean + exclude?: RegExp[] + ignoreWatching?: (string | RegExp)[] + env?: Env | EnvFunc + plugins?: Plugin[] + adapter?: Adapter | Promise | (() => Adapter | Promise) +} + +export const defaultOptions = { + entry: './src/index.ts', + export: 'default', + injectClientScript: true, + exclude: [ + /.*\.css$/, + /.*\.ts$/, + /.*\.tsx$/, + /^\/@.+$/, + /\?t\=\d+$/, + /^\/favicon\.ico$/, + /^\/static\/.+/, + /^\/node_modules\/.*/, + ], + ignoreWatching: [/\.wrangler/], + plugins: [], +} satisfies Required> + +export function devServer(options?: DevServerOptions): VitePlugin { + const entry = options?.entry ?? defaultOptions.entry + + async function createMiddleware( + server: ViteDevServer, + ): Promise { + return async ( + req: IncomingMessage, + res: ServerResponse, + next: Connect.NextFunction, + ) => { + const exclude = options?.exclude ?? defaultOptions.exclude + + const devtoolsAssetsRegex = /assets\/.*(css|js|png)/ + for (const pattern of exclude) { + if (!req.url) continue + if (devtoolsAssetsRegex.test(req.url)) continue + if (pattern.test(req.url)) return next() + } + + let appModule: Record + try { + appModule = await server.ssrLoadModule(entry) + } catch (e) { + return next(e) + } + + const exportName = options?.export ?? defaultOptions.export + const app = appModule[exportName] as { fetch: Fetch } + + if (!app) + return next( + new Error( + `Failed to find a named export "${exportName}" from ${entry}`, + ), + ) + + getRequestListener( + async (request) => { + let env: Env = {} + + if (options?.env) { + if (typeof options.env === 'function') + env = { ...env, ...(await options.env()) } + else env = { ...env, ...options.env } + } + if (options?.plugins) + for (const plugin of options.plugins) { + if (plugin.env) + env = { + ...env, + ...(typeof plugin.env === 'function' + ? await plugin.env() + : plugin.env), + } + } + + const adapter = await getAdapterFromOptions(options) + + if (adapter?.env) env = { ...env, ...adapter.env } + + const executionContext = adapter?.executionContext ?? { + waitUntil: async (fn) => fn, + passThroughOnException: () => { + throw new Error('`passThroughOnException` is not supported') + }, + } + + const response = await app.fetch(request, env, executionContext) + + /** + * If the response is not instance of `Response`, throw it so that it can be handled + * by our custom errorHandler and passed through to Vite + */ + if (!(response instanceof Response)) throw response + + if ( + options?.injectClientScript !== false && + response.headers.get('content-type')?.match(/^text\/html/) + ) { + const text = await response.clone().text() + const isDevtools = text.includes('id="root"') + const script = isDevtools + ? '' + : '' + const area = isDevtools ? 'head' : 'body' + return injectStringToResponse(response, script, area) + } + return response + }, + { + errorHandler: (e) => { + let err: Error + if (e instanceof Error) { + err = e + server.ssrFixStacktrace(err) + } else if (typeof e === 'string') + err = new Error( + `The response is not an instance of "Response", but: ${e}`, + ) + else err = new Error(`Unknown error: ${e}`) + + next(err) + }, + }, + )(req, res) + } + } + + return { + name: 'frog:dev', + configureServer: async (server) => { + server.middlewares.use(await createMiddleware(server)) + server.httpServer?.on('close', async () => { + if (options?.plugins) + for (const plugin of options.plugins) { + if (plugin.onServerClose) await plugin.onServerClose() + } + const adapter = await getAdapterFromOptions(options) + if (adapter?.onServerClose) await adapter.onServerClose() + }) + }, + config: () => { + return { + server: { + watch: { + ignored: options?.ignoreWatching ?? defaultOptions.ignoreWatching, + }, + }, + } + }, + } +} + +const getAdapterFromOptions = async ( + options: DevServerOptions | undefined, +): Promise => { + let adapter = options?.adapter + if (typeof adapter === 'function') adapter = adapter() + if (adapter instanceof Promise) adapter = await adapter + return adapter +} + +function injectStringToResponse( + response: Response, + content: string, + area: 'head' | 'body', +) { + const stream = response.body + const newContent = new TextEncoder().encode(content) + + if (!stream) return null + + const reader = stream.getReader() + const newContentReader = new ReadableStream({ + start(controller) { + controller.enqueue(newContent) + controller.close() + }, + }).getReader() + + const combinedStream = new ReadableStream({ + async start(controller) { + for (;;) { + const [existingResult, newContentResult] = await Promise.all( + area === 'head' + ? [newContentReader.read(), reader.read()] + : [reader.read(), newContentReader.read()], + ) + + if (existingResult.done && newContentResult.done) { + controller.close() + break + } + + if (!existingResult.done) controller.enqueue(existingResult.value) + if (!newContentResult.done) controller.enqueue(newContentResult.value) + } + }, + }) + + const headers = new Headers(response.headers) + headers.delete('content-length') + + return new Response(combinedStream, { + headers, + status: response.status, + }) +} + +type Env = Record | Promise> +type EnvFunc = () => Env | Promise +interface ExecutionContext { + waitUntil(promise: Promise): void + passThroughOnException(): void +} + +type Fetch = ( + request: Request, + env: {}, + executionContext: ExecutionContext, +) => Promise + +interface Plugin { + env?: Env | EnvFunc + onServerClose?: () => void | Promise +} + +interface Adapter { + /** + * Environment variables to be injected into the worker + */ + env?: Env + /** + * Function called when the vite dev server is closed + */ + onServerClose?: () => Promise + /** + * Implementation of waitUntil and passThroughOnException + */ + executionContext?: { + waitUntil(promise: Promise): void + passThroughOnException(): void + } +} diff --git a/src/dev/devtools.tsx b/src/dev/devtools.tsx index eab7bdf5..c0f72dff 100644 --- a/src/dev/devtools.tsx +++ b/src/dev/devtools.tsx @@ -60,6 +60,9 @@ export function devtools< else if (serveStatic) publicPath = `.${basePath}` else publicPath = frog.assetsPath === '/' ? '' : frog.assetsPath + const rootBasePath = frog.basePath === '/' ? '' : frog.basePath + const devBasePath = `${rootBasePath}${basePath}` + app .get('/', (c) => { return c.html( @@ -77,9 +80,6 @@ export function devtools< -