From 3241ae1226d31ce50363e7663eec82ea2231b63d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 12 Sep 2025 14:09:29 +0900 Subject: [PATCH 1/3] feat(rsc): support `export default { fetch }` as server handler entry --- packages/plugin-rsc/src/plugin.ts | 7 +++++-- packages/plugin-rsc/src/plugins/utils.ts | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index ad0004af..2048cf33 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -44,6 +44,7 @@ import { getEntrySource, hashString, normalizeRelativePath, + normalizeServerHandler, sortObject, withRollupError, } from './plugins/utils' @@ -513,13 +514,14 @@ export default function vitePluginRsc( `[vite-rsc] failed to resolve server handler '${source}'`, ) const mod = await environment.runner.import(resolved.id) + const fetchHandler = normalizeServerHandler(mod) // expose original request url to server handler. // for example, this restores `base` which is automatically stripped by Vite. // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20 req.url = req.originalUrl ?? req.url // ensure catching rejected promise // https://github.com/mjackson/remix-the-web/blob/b5aa2ae24558f5d926af576482caf6e9b35461dc/packages/node-fetch-server/src/lib/request-listener.ts#L87 - await createRequestListener(mod.default)(req, res) + await createRequestListener(fetchHandler)(req, res) } catch (e) { next(e) } @@ -540,7 +542,8 @@ export default function vitePluginRsc( ) const entry = pathToFileURL(entryFile).href const mod = await import(/* @vite-ignore */ entry) - const handler = createRequestListener(mod.default) + const fetchHandler = normalizeServerHandler(mod) + const handler = createRequestListener(fetchHandler) // disable compressions since it breaks html streaming // https://github.com/vitejs/vite/blob/9f5c59f07aefb1756a37bcb1c0aff24d54288950/packages/vite/src/node/preview.ts#L178 diff --git a/packages/plugin-rsc/src/plugins/utils.ts b/packages/plugin-rsc/src/plugins/utils.ts index c4cee3af..b9bd704b 100644 --- a/packages/plugin-rsc/src/plugins/utils.ts +++ b/packages/plugin-rsc/src/plugins/utils.ts @@ -78,3 +78,26 @@ export function getEntrySource( export function hashString(v: string): string { return createHash('sha256').update(v).digest().toString('hex').slice(0, 12) } + +// normalize server entry exports to align with server runtimes +// https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/ +// https://srvx.h3.dev/guide +// https://vercel.com/docs/functions/functions-api-reference?framework=other#fetch-web-standard +// https://github.com/jacob-ebey/rsbuild-rsc-playground/blob/eb1a54afa49cbc5ff93c315744d7754d5ed63498/plugin/fetch-server.ts#L59-L79 +export function normalizeServerHandler(exports: object): any { + if ('default' in exports) { + const default_ = exports.default + if ( + default_ && + typeof default_ === 'object' && + 'fetch' in default_ && + typeof default_.fetch === 'function' + ) { + return default_.fetch + } + if (typeof default_ === 'function') { + return default_ + } + } + throw new Error('Invalid server handler entry') +} From 40526a93f377ac6e6c591af4aa5d684fa24f3f2a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 12 Sep 2025 14:11:52 +0900 Subject: [PATCH 2/3] test: update example --- packages/plugin-rsc/examples/basic/src/server.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/basic/src/server.tsx b/packages/plugin-rsc/examples/basic/src/server.tsx index b845bfba..e69b144a 100644 --- a/packages/plugin-rsc/examples/basic/src/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/server.tsx @@ -1,7 +1,7 @@ import { handleRequest } from './framework/entry.rsc.tsx' import './styles.css' -export default async function handler(request: Request): Promise { +async function handler(request: Request): Promise { const url = new URL(request.url) const { Root } = await import('./routes/root.tsx') const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined @@ -38,6 +38,10 @@ export default async function handler(request: Request): Promise { return response } +export default { + fetch: handler, +} + if (import.meta.hot) { import.meta.hot.accept() } From b15c17f1d42c2e534c3739ca9c9574fe6b34ce53 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 12 Sep 2025 14:16:46 +0900 Subject: [PATCH 3/3] chore: rename --- packages/plugin-rsc/src/plugin.ts | 6 +++--- packages/plugin-rsc/src/plugins/utils.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 2048cf33..e2863bff 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -44,7 +44,7 @@ import { getEntrySource, hashString, normalizeRelativePath, - normalizeServerHandler, + getFetchHandlerExport, sortObject, withRollupError, } from './plugins/utils' @@ -514,7 +514,7 @@ export default function vitePluginRsc( `[vite-rsc] failed to resolve server handler '${source}'`, ) const mod = await environment.runner.import(resolved.id) - const fetchHandler = normalizeServerHandler(mod) + const fetchHandler = getFetchHandlerExport(mod) // expose original request url to server handler. // for example, this restores `base` which is automatically stripped by Vite. // https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20 @@ -542,7 +542,7 @@ export default function vitePluginRsc( ) const entry = pathToFileURL(entryFile).href const mod = await import(/* @vite-ignore */ entry) - const fetchHandler = normalizeServerHandler(mod) + const fetchHandler = getFetchHandlerExport(mod) const handler = createRequestListener(fetchHandler) // disable compressions since it breaks html streaming diff --git a/packages/plugin-rsc/src/plugins/utils.ts b/packages/plugin-rsc/src/plugins/utils.ts index b9bd704b..42dfe42d 100644 --- a/packages/plugin-rsc/src/plugins/utils.ts +++ b/packages/plugin-rsc/src/plugins/utils.ts @@ -84,7 +84,7 @@ export function hashString(v: string): string { // https://srvx.h3.dev/guide // https://vercel.com/docs/functions/functions-api-reference?framework=other#fetch-web-standard // https://github.com/jacob-ebey/rsbuild-rsc-playground/blob/eb1a54afa49cbc5ff93c315744d7754d5ed63498/plugin/fetch-server.ts#L59-L79 -export function normalizeServerHandler(exports: object): any { +export function getFetchHandlerExport(exports: object): any { if ('default' in exports) { const default_ = exports.default if (