Skip to content

Commit cb5ce55

Browse files
authored
feat(rsc): support export default { fetch } as server handler entry (#839)
1 parent e37788b commit cb5ce55

File tree

3 files changed

+33
-3
lines changed

3 files changed

+33
-3
lines changed

packages/plugin-rsc/examples/basic/src/server.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { handleRequest } from './framework/entry.rsc.tsx'
22
import './styles.css'
33

4-
export default async function handler(request: Request): Promise<Response> {
4+
async function handler(request: Request): Promise<Response> {
55
const url = new URL(request.url)
66
const { Root } = await import('./routes/root.tsx')
77
const nonce = !process.env.NO_CSP ? crypto.randomUUID() : undefined
@@ -38,6 +38,10 @@ export default async function handler(request: Request): Promise<Response> {
3838
return response
3939
}
4040

41+
export default {
42+
fetch: handler,
43+
}
44+
4145
if (import.meta.hot) {
4246
import.meta.hot.accept()
4347
}

packages/plugin-rsc/src/plugin.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
getEntrySource,
4646
hashString,
4747
normalizeRelativePath,
48+
getFetchHandlerExport,
4849
sortObject,
4950
withRollupError,
5051
} from './plugins/utils'
@@ -514,13 +515,14 @@ export default function vitePluginRsc(
514515
`[vite-rsc] failed to resolve server handler '${source}'`,
515516
)
516517
const mod = await environment.runner.import(resolved.id)
518+
const fetchHandler = getFetchHandlerExport(mod)
517519
// expose original request url to server handler.
518520
// for example, this restores `base` which is automatically stripped by Vite.
519521
// https://github.com/vitejs/vite/blob/84079a84ad94de4c1ef4f1bdb2ab448ff2c01196/packages/vite/src/node/server/middlewares/base.ts#L18-L20
520522
req.url = req.originalUrl ?? req.url
521523
// ensure catching rejected promise
522524
// https://github.com/mjackson/remix-the-web/blob/b5aa2ae24558f5d926af576482caf6e9b35461dc/packages/node-fetch-server/src/lib/request-listener.ts#L87
523-
await createRequestListener(mod.default)(req, res)
525+
await createRequestListener(fetchHandler)(req, res)
524526
} catch (e) {
525527
next(e)
526528
}
@@ -541,7 +543,8 @@ export default function vitePluginRsc(
541543
)
542544
const entry = pathToFileURL(entryFile).href
543545
const mod = await import(/* @vite-ignore */ entry)
544-
const handler = createRequestListener(mod.default)
546+
const fetchHandler = getFetchHandlerExport(mod)
547+
const handler = createRequestListener(fetchHandler)
545548

546549
// disable compressions since it breaks html streaming
547550
// https://github.com/vitejs/vite/blob/9f5c59f07aefb1756a37bcb1c0aff24d54288950/packages/vite/src/node/preview.ts#L178

packages/plugin-rsc/src/plugins/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,26 @@ export function getEntrySource(
7878
export function hashString(v: string): string {
7979
return createHash('sha256').update(v).digest().toString('hex').slice(0, 12)
8080
}
81+
82+
// normalize server entry exports to align with server runtimes
83+
// https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/
84+
// https://srvx.h3.dev/guide
85+
// https://vercel.com/docs/functions/functions-api-reference?framework=other#fetch-web-standard
86+
// https://github.com/jacob-ebey/rsbuild-rsc-playground/blob/eb1a54afa49cbc5ff93c315744d7754d5ed63498/plugin/fetch-server.ts#L59-L79
87+
export function getFetchHandlerExport(exports: object): any {
88+
if ('default' in exports) {
89+
const default_ = exports.default
90+
if (
91+
default_ &&
92+
typeof default_ === 'object' &&
93+
'fetch' in default_ &&
94+
typeof default_.fetch === 'function'
95+
) {
96+
return default_.fetch
97+
}
98+
if (typeof default_ === 'function') {
99+
return default_
100+
}
101+
}
102+
throw new Error('Invalid server handler entry')
103+
}

0 commit comments

Comments
 (0)