diff --git a/examples/08_cookies/package.json b/examples/08_cookies/package.json index afb057d49..c68ceeda9 100644 --- a/examples/08_cookies/package.json +++ b/examples/08_cookies/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "cookie": "0.6.0", + "hono": "4.6.1", "react": "19.0.0-rc-d6cb4e77-20240911", "react-dom": "19.0.0-rc-d6cb4e77-20240911", "react-server-dom-webpack": "19.0.0-rc-d6cb4e77-20240911", diff --git a/examples/08_cookies/src/components/App.tsx b/examples/08_cookies/src/components/App.tsx index 1a2240e78..d75ad1597 100644 --- a/examples/08_cookies/src/components/App.tsx +++ b/examples/08_cookies/src/components/App.tsx @@ -1,5 +1,6 @@ import { Suspense, cache } from 'react'; import { unstable_getCustomContext as getCustomContext } from 'waku/server'; +import { getContext } from 'hono/context-storage'; import { Counter } from './Counter'; @@ -13,7 +14,8 @@ const InternalAsyncComponent = async () => { throw new Error('Cache not working'); } // console.log(getCustomContext()); // fails when it's sent to the browser - console.log(Object.keys(getCustomContext())); + console.log('waku context', Object.keys(getCustomContext())); + console.log('hono context', Object.keys(getContext())); return null; }; diff --git a/packages/waku/src/cli.ts b/packages/waku/src/cli.ts index 0af3d3d38..052b7d959 100644 --- a/packages/waku/src/cli.ts +++ b/packages/waku/src/cli.ts @@ -95,6 +95,7 @@ if (values.version) { async function runDev() { const config = await loadConfig(); + const honoEnhancer = config.unstable_honoEnhancer || ((app) => app); const app = new Hono(); app.use(contextStorage()); app.use('*', runner({ cmd: 'dev', config, env: process.env as any })); @@ -107,7 +108,7 @@ async function runDev() { return c.text('404 Not Found', 404); }); const port = parseInt(values.port || '3000', 10); - await startServer(app, port); + await startServer(honoEnhancer(app), port); } async function runBuild() { @@ -136,7 +137,9 @@ async function runBuild() { } async function runStart() { - const { distDir = 'dist' } = await loadConfig(); + const config = await loadConfig(); + const { distDir = 'dist' } = config; + const honoEnhancer = config.unstable_honoEnhancer || ((app) => app); const loadEntries = () => import(pathToFileURL(path.resolve(distDir, DIST_ENTRIES_JS)).toString()); const app = new Hono(); @@ -152,7 +155,7 @@ async function runStart() { return c.text('404 Not Found', 404); }); const port = parseInt(values.port || '8080', 10); - await startServer(app, port); + await startServer(honoEnhancer(app), port); } function startServer(app: Hono, port: number) { diff --git a/packages/waku/src/config.ts b/packages/waku/src/config.ts index 54965c1f0..6bbad3c52 100644 --- a/packages/waku/src/config.ts +++ b/packages/waku/src/config.ts @@ -48,6 +48,11 @@ export interface Config { * ] */ middleware?: () => Promise<{ default: Middleware }>[]; + /** + * Enhander for Hono + * Defaults to `undefined` + */ + unstable_honoEnhancer?: ((app: Hono) => Hono) | undefined; } export function defineConfig(config: Config) { diff --git a/packages/waku/src/lib/builder/build.ts b/packages/waku/src/lib/builder/build.ts index 780acd34f..8c6ee8ac0 100644 --- a/packages/waku/src/lib/builder/build.ts +++ b/packages/waku/src/lib/builder/build.ts @@ -246,6 +246,7 @@ const buildServerBundle = async ( conditions: ['react-server'], externalConditions: ['react-server'], }, + external: ['hono/context-storage'], noExternal: /^(?!node:)/, }, esbuild: { @@ -776,8 +777,10 @@ export async function build(options: { await buildDeploy(rootDir, config); delete platformObject.buildOptions.unstable_phase; - await appendFile( - distEntriesFile, - `export const buildData = ${JSON.stringify(platformObject.buildData)};`, - ); + if (existsSync(distEntriesFile)) { + await appendFile( + distEntriesFile, + `export const buildData = ${JSON.stringify(platformObject.buildData)};`, + ); + } } diff --git a/packages/waku/src/lib/builder/serve-cloudflare.ts b/packages/waku/src/lib/builder/serve-cloudflare.ts deleted file mode 100644 index 4555b626a..000000000 --- a/packages/waku/src/lib/builder/serve-cloudflare.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Hono } from 'hono'; -import { runner } from '../hono/runner.js'; - -const loadEntries = () => import(import.meta.env.WAKU_ENTRIES_FILE!); -let serveWaku: ReturnType | undefined; - -export interface CloudflareEnv { - ASSETS: { - fetch: (input: RequestInit | URL, init?: RequestInit) => Promise; - }; -} - -export const app = new Hono<{ - Bindings: CloudflareEnv & { [k: string]: unknown }; -}>(); -app.use('*', (c, next) => serveWaku!(c, next)); -app.notFound(async (c) => { - const assetsFetcher = c.env.ASSETS; - const url = new URL(c.req.raw.url); - const errorHtmlUrl = `${url.origin}/404.html`; - const notFoundStaticAssetResponse = await assetsFetcher.fetch( - new URL(errorHtmlUrl), - ); - if (notFoundStaticAssetResponse && notFoundStaticAssetResponse.status < 400) { - return c.body(notFoundStaticAssetResponse.body, 404); - } - return c.text('404 Not Found', 404); -}); - -export default { - async fetch( - request: Request, - env: Record, - ctx: Parameters[2], - ) { - if (!serveWaku) { - serveWaku = runner({ cmd: 'start', loadEntries, env }); - } - return app.fetch(request, env, ctx); - }, -}; diff --git a/packages/waku/src/lib/config.ts b/packages/waku/src/lib/config.ts index aca16c503..2eecc6c6c 100644 --- a/packages/waku/src/lib/config.ts +++ b/packages/waku/src/lib/config.ts @@ -25,6 +25,7 @@ export async function resolveConfig(config: Config) { privateDir: 'private', rscPath: 'RSC', middleware: DEFAULT_MIDDLEWARE, + unstable_honoEnhancer: undefined, ...config, }; return resolvedConfig; diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index 3020e0648..f0003abc5 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -244,6 +244,7 @@ const createRscViteServer = ( conditions: ['react-server'], externalConditions: ['react-server'], }, + external: ['hono/context-storage'], noExternal: /^(?!node:)/, optimizeDeps: { include: [ diff --git a/packages/waku/src/lib/plugins/vite-plugin-deploy-cloudflare.ts b/packages/waku/src/lib/plugins/vite-plugin-deploy-cloudflare.ts index d8133bb14..64ecb137d 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-deploy-cloudflare.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-deploy-cloudflare.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import { + appendFileSync, existsSync, mkdirSync, readdirSync, @@ -7,35 +8,48 @@ import { rmSync, writeFileSync, } from 'node:fs'; -import { normalizePath } from 'vite'; import type { Plugin } from 'vite'; import { unstable_getPlatformObject } from '../../server.js'; -import { EXTENSIONS, SRC_ENTRIES } from '../constants.js'; -import { - decodeFilePathFromAbsolute, - extname, - fileURLToFilePath, - joinPath, -} from '../utils/path.js'; -import { DIST_SERVE_JS, DIST_PUBLIC } from '../builder/constants.js'; - -const resolveFileName = (fname: string) => { - for (const ext of EXTENSIONS) { - const resolvedName = fname.slice(0, -extname(fname).length) + ext; - if (existsSync(resolvedName)) { - return resolvedName; - } +import { SRC_ENTRIES } from '../constants.js'; +import { DIST_ENTRIES_JS, DIST_PUBLIC } from '../builder/constants.js'; + +const SERVE_JS = 'serve-cloudflare.js'; + +const getServeJsContent = (srcEntriesFile: string) => ` +import { runner, importHono, importHonoContextStorage } from 'waku/unstable_hono'; + +const { Hono } = await importHono(); +const { contextStorage } = await importHonoContextStorage(); + +const loadEntries = () => import('${srcEntriesFile}'); +let serveWaku; + +const app = new Hono(); +app.use(contextStorage()); +app.use('*', (c, next) => serveWaku(c, next)); +app.notFound(async (c) => { + const assetsFetcher = c.env.ASSETS; + const url = new URL(c.req.raw.url); + const errorHtmlUrl = url.origin + '/404.html'; + const notFoundStaticAssetResponse = await assetsFetcher.fetch( + new URL(errorHtmlUrl), + ); + if (notFoundStaticAssetResponse && notFoundStaticAssetResponse.status < 400) { + return c.body(notFoundStaticAssetResponse.body, 404); } - return fname; // returning the default one -}; + return c.text('404 Not Found', 404); +}); -const srcServeFile = decodeFilePathFromAbsolute( - joinPath( - fileURLToFilePath(import.meta.url), - '../../builder/serve-cloudflare.js', - ), -); +export default { + async fetch(request, env, ctx) { + if (!serveWaku) { + serveWaku = runner({ cmd: 'start', loadEntries, env }); + } + return app.fetch(request, env, ctx); + }, +}; +`; const getFiles = (dir: string, files: string[] = []): string[] => { const entries = readdirSync(dir, { withFileTypes: true }); @@ -64,6 +78,7 @@ export function deployCloudflarePlugin(opts: { }): Plugin { const platformObject = unstable_getPlatformObject(); let rootDir: string; + let entriesFile: string; return { name: 'deploy-cloudflare-plugin', config(viteConfig) { @@ -71,28 +86,14 @@ export function deployCloudflarePlugin(opts: { if (unstable_phase !== 'buildServerBundle' || deploy !== 'cloudflare') { return; } - - // FIXME This seems too hacky (The use of viteConfig.root, '.', path.resolve and resolveFileName) - const entriesFile = normalizePath( - resolveFileName( - path.resolve( - viteConfig.root || '.', - opts.srcDir, - SRC_ENTRIES + '.jsx', - ), - ), - ); const { input } = viteConfig.build?.rollupOptions ?? {}; if (input && !(typeof input === 'string') && !(input instanceof Array)) { - input[DIST_SERVE_JS.replace(/\.js$/, '')] = srcServeFile; + input[SERVE_JS.replace(/\.js$/, '')] = `${opts.srcDir}/${SERVE_JS}`; } - viteConfig.define = { - ...viteConfig.define, - 'import.meta.env.WAKU_ENTRIES_FILE': JSON.stringify(entriesFile), - }; }, configResolved(config) { rootDir = config.root; + entriesFile = `${rootDir}/${opts.srcDir}/${SRC_ENTRIES}`; const { deploy, unstable_phase } = platformObject.buildOptions || {}; if ( (unstable_phase !== 'buildServerBundle' && @@ -108,6 +109,16 @@ export function deployCloudflarePlugin(opts: { config.ssr.resolve.externalConditions ||= []; config.ssr.resolve.externalConditions.push('worker'); }, + resolveId(source) { + if (source === `${opts.srcDir}/${SERVE_JS}`) { + return source; + } + }, + load(id) { + if (id === `${opts.srcDir}/${SERVE_JS}`) { + return getServeJsContent(entriesFile); + } + }, closeBundle() { const { deploy, unstable_phase } = platformObject.buildOptions || {}; if (unstable_phase !== 'buildDeploy' || deploy !== 'cloudflare') { @@ -132,7 +143,7 @@ export function deployCloudflarePlugin(opts: { writeFileSync( workerEntrypoint, ` -import server from './${DIST_SERVE_JS}' +import server from './${SERVE_JS}' export default { ...server @@ -200,6 +211,11 @@ export default { force: true, }); + appendFileSync( + path.join(outDir, WORKER_JS_NAME, DIST_ENTRIES_JS), + `export const buildData = ${JSON.stringify(platformObject.buildData)};`, + ); + const wranglerTomlFile = path.join(rootDir, 'wrangler.toml'); if (!existsSync(wranglerTomlFile)) { writeFileSync( @@ -207,7 +223,7 @@ export default { ` # See https://developers.cloudflare.com/pages/functions/wrangler-configuration/ name = "waku-project" -compatibility_date = "2024-04-03" +compatibility_date = "2024-09-02" compatibility_flags = [ "nodejs_als" ] pages_build_output_dir = "./dist" `, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 029a5d9cc..f24e571dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -594,6 +594,9 @@ importers: cookie: specifier: 0.6.0 version: 0.6.0 + hono: + specifier: 4.6.1 + version: 4.6.1 react: specifier: 19.0.0-rc-d6cb4e77-20240911 version: 19.0.0-rc-d6cb4e77-20240911