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() } diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index ad0004af..e2863bff 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -44,6 +44,7 @@ import { getEntrySource, hashString, normalizeRelativePath, + getFetchHandlerExport, 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 = 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 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 = getFetchHandlerExport(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..42dfe42d 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 getFetchHandlerExport(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') +}