@mcansh/react-router-fastify 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # react-router-fastify
2
+
3
+ A Fastify adapter for React Router v8 framework mode, plus a Vite plugin so `react-router dev` runs through your Fastify server.
4
+
5
+ ## Features
6
+
7
+ - Adapts Fastify requests and replies to React Router's Web Fetch request/response runtime
8
+ - Registers React Router as a Fastify catch-all route while preserving your own Fastify routes
9
+ - Loads `virtual:react-router/server-build` during development
10
+ - Serves `build/client` with `@fastify/static` in production
11
+ - Provides a Vite plugin that mounts your Fastify app during `react-router dev`
12
+ - Preserves shared server/app module identity for React Router context tokens
13
+
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm i @mcansh/react-router-fastify fastify react-router @react-router/node
18
+ npm i -D @react-router/dev vite
19
+ ```
20
+
21
+ React Router v8 currently requires Node `>=22.22.0`.
22
+
23
+ ## Usage
24
+
25
+ Create a server module that exports a factory. The factory receives the Vite dev server in development and `undefined` in production.
26
+
27
+ ```ts
28
+ // server.ts
29
+ import { pathToFileURL } from "node:url"
30
+
31
+ import { fastifyReactRouter } from "@mcansh/react-router-fastify"
32
+ import { fastify } from "fastify"
33
+ import type { ViteDevServer } from "vite"
34
+
35
+ export async function createServer(vite?: ViteDevServer) {
36
+ let app = fastify()
37
+
38
+ app.get("/api/health", async () => ({ ok: true }))
39
+
40
+ await app.register(fastifyReactRouter, { devServer: vite })
41
+
42
+ return app
43
+ }
44
+
45
+ let isMain = import.meta.url === pathToFileURL(process.argv[1]).href
46
+ if (isMain) {
47
+ let app = await createServer()
48
+ await app.listen({ port: 3000, host: "0.0.0.0" })
49
+ }
50
+ ```
51
+
52
+ Add the Vite plugin so `react-router dev` uses that Fastify server:
53
+
54
+ ```ts
55
+ // vite.config.ts
56
+ import { reactRouter } from "@react-router/dev/vite"
57
+ import { fastifyReactRouterDev } from "@mcansh/react-router-fastify/vite"
58
+ import { defineConfig } from "vite"
59
+
60
+ export default defineConfig({
61
+ plugins: [reactRouter(), fastifyReactRouterDev({ entry: "./server.ts" })],
62
+ })
63
+ ```
64
+
65
+ Use the normal React Router commands:
66
+
67
+ ```json
68
+ {
69
+ "scripts": {
70
+ "dev": "react-router dev",
71
+ "build": "react-router build",
72
+ "start": "NODE_ENV=production node ./server.js"
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Example
78
+
79
+ A runnable example lives in `examples/basic`:
80
+
81
+ ```sh
82
+ pnpm install
83
+ pnpm run example:dev
84
+ ```
85
+
86
+ The example includes a Fastify API route at `/api/health`, `getLoadContext`
87
+ that seeds React Router context, root route middleware that updates that same
88
+ context, and a `vite.config.ts` that uses `fastifyReactRouterDev` with the
89
+ standard `react-router dev` command.
90
+
91
+ ## Shared Context
92
+
93
+ React Router v8 uses `RouterContextProvider` for loaders, actions, and
94
+ middleware. When your Fastify server and React Router app both need the same
95
+ context token, put that token in a module that both sides import through the
96
+ same package import specifier.
97
+
98
+ Add a package import for the shared module:
99
+
100
+ ```json
101
+ {
102
+ "imports": {
103
+ "#request-info": "./app/request-info.ts"
104
+ }
105
+ }
106
+ ```
107
+
108
+ Create the context token in your app:
109
+
110
+ ```ts
111
+ // app/request-info.ts
112
+ import { createContext } from "react-router"
113
+
114
+ export interface RequestInfo {
115
+ requestId: string
116
+ userAgent: string
117
+ }
118
+
119
+ export let requestInfoContext = createContext<RequestInfo>()
120
+ ```
121
+
122
+ Seed it from `getLoadContext`:
123
+
124
+ ```ts
125
+ // server.ts
126
+ import { requestInfoContext } from "#request-info"
127
+ import { fastifyReactRouter } from "@mcansh/react-router-fastify"
128
+ import { RouterContextProvider } from "react-router"
129
+
130
+ app.register(fastifyReactRouter, {
131
+ devServer: vite,
132
+ getLoadContext(request) {
133
+ let context = new RouterContextProvider()
134
+ context.set(requestInfoContext, {
135
+ requestId: request.id,
136
+ userAgent: request.headers["user-agent"] ?? "unknown",
137
+ })
138
+ return context
139
+ },
140
+ })
141
+ ```
142
+
143
+ Read or update the same context from React Router middleware and route modules:
144
+
145
+ ```ts
146
+ // app/root.tsx
147
+ import { requestInfoContext } from "#request-info"
148
+ import type { MiddlewareFunction } from "react-router"
149
+
150
+ export const middleware: MiddlewareFunction[] = [
151
+ async ({ context }, next) => {
152
+ let requestInfo = context.get(requestInfoContext)
153
+ context.set(requestInfoContext, requestInfo)
154
+ return next()
155
+ },
156
+ ]
157
+ ```
158
+
159
+ ```ts
160
+ // app/routes/home.tsx
161
+ import { requestInfoContext } from "#request-info"
162
+ import type { LoaderFunctionArgs } from "react-router"
163
+
164
+ export async function loader({ context }: LoaderFunctionArgs) {
165
+ return {
166
+ requestInfo: context.get(requestInfoContext),
167
+ }
168
+ }
169
+ ```
170
+
171
+ Tell the Vite plugin to keep that package import external in React Router's SSR
172
+ build:
173
+
174
+ ```ts
175
+ // vite.config.ts
176
+ import { reactRouter } from "@react-router/dev/vite"
177
+ import { fastifyReactRouterDev } from "@mcansh/react-router-fastify/vite"
178
+ import { defineConfig } from "vite"
179
+
180
+ export default defineConfig({
181
+ plugins: [
182
+ reactRouter(),
183
+ fastifyReactRouterDev({
184
+ entry: "./server.ts",
185
+ externalizeServerEntryImports: ["#request-info"],
186
+ }),
187
+ ],
188
+ })
189
+ ```
190
+
191
+ This prevents the production server bundle from inlining a second
192
+ `createContext()` instance. The built React Router server keeps
193
+ `import { requestInfoContext } from "#request-info"`, so `server.ts`,
194
+ middleware, and loaders all use the same token.
195
+
196
+ ## Production
197
+
198
+ Without a Vite dev server, `fastifyReactRouter` imports `build/server/index.js` and serves static files from `build/client`:
199
+
200
+ ```sh
201
+ react-router build
202
+ NODE_ENV=production node ./server.js
203
+ ```
204
+
205
+ If your build output uses different paths, pass them directly:
206
+
207
+ ```ts
208
+ await app.register(fastifyReactRouter, {
209
+ serverBuildPath: "dist/server.js",
210
+ clientBuildDirectory: "dist/client",
211
+ })
212
+ ```
213
+
214
+ ## API
215
+
216
+ `fastifyReactRouter` options:
217
+
218
+ - `devServer` - Vite dev server, provided by `fastifyReactRouterDev` during development
219
+ - `basePath` - URL base path for static files and the catch-all route, default `/`
220
+ - `serverBuildPath` - production server build module, default `build/server/index.js`
221
+ - `clientBuildDirectory` - production client asset directory, default `build/client`
222
+ - `mode` - value passed to React Router's request handler, default `process.env.NODE_ENV`
223
+ - `getLoadContext` - returns a `RouterContextProvider` for each request
224
+ - `build` - production-only React Router server build or build loader override
225
+ - `staticOptions` - options forwarded to `@fastify/static`
226
+ - `assetCacheControl` - cache-control string for files under `<clientBuildDirectory>/assets`
227
+ - `fileCacheControl` - cache-control string for other files in `clientBuildDirectory`
228
+ - `routeOptions` - Fastify route options for the catch-all route
229
+
230
+ When `devServer` is provided, `fastifyReactRouter` always uses Vite's
231
+ `virtual:react-router/server-build` module. This keeps development requests
232
+ hot-reloaded even if a production `build` override is configured.
233
+
234
+ `fastifyReactRouterDev` options:
235
+
236
+ - `entry` - server module loaded by Vite, default `./server.ts`
237
+ - `exportName` - named server factory export, default `createServer`
238
+ - `externalizeServerEntryImports` - keeps local `#` and relative imports from
239
+ the server entry external in the React Router SSR build; pass an explicit
240
+ list such as `["#request-info"]` for tighter production packaging control.
241
+ Externalized modules must be available to Node in production.
242
+
243
+ ## Lower-level Handler
244
+
245
+ You can wire the route yourself with `createRequestHandler`:
246
+
247
+ ```ts
248
+ import { createRequestHandler } from "@mcansh/react-router-fastify"
249
+
250
+ app.all(
251
+ "*",
252
+ createRequestHandler({
253
+ build: () => import("./build/server/index.js"),
254
+ }),
255
+ )
256
+ ```
257
+
258
+ ## License
259
+
260
+ See [LICENSE](https://github.com/mcansh/remix-fastify/blob/main/LICENSE)
@@ -0,0 +1,82 @@
1
+ import { FastifyStaticOptions } from "@fastify/static";
2
+ import { RouterContextProvider, ServerBuild } from "react-router";
3
+ import { FastifyInstance, FastifyReply, FastifyRequest, RouteShorthandOptions } from "fastify";
4
+ import { ViteDevServer } from "vite";
5
+
6
+ //#region src/handler.d.ts
7
+ type HttpRequest = FastifyRequest["raw"];
8
+ type HttpResponse = FastifyReply["raw"];
9
+ type ReactRouterLoadContext = RouterContextProvider;
10
+ type GetLoadContextFunction = (request: FastifyRequest, reply: FastifyReply) => ReactRouterLoadContext | undefined | Promise<ReactRouterLoadContext | undefined>;
11
+ type RequestHandler = (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
12
+ interface CreateRequestHandlerOptions {
13
+ build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>);
14
+ getLoadContext?: GetLoadContextFunction;
15
+ mode?: string;
16
+ }
17
+ /**
18
+ * Creates a Fastify route handler backed by React Router's server runtime.
19
+ *
20
+ * @param options React Router build, mode, and optional load context hook.
21
+ * @returns Fastify route handler.
22
+ */
23
+ declare function createRequestHandler(options: CreateRequestHandlerOptions): RequestHandler;
24
+ //#endregion
25
+ //#region src/fastify.d.ts
26
+ interface FastifyReactRouterOptions {
27
+ devServer?: ViteDevServer;
28
+ basePath?: string;
29
+ serverBuildPath?: string;
30
+ clientBuildDirectory?: string;
31
+ mode?: string;
32
+ getLoadContext?: GetLoadContextFunction;
33
+ build?: ServerBuild | (() => ServerBuild | Promise<ServerBuild>);
34
+ staticOptions?: FastifyStaticOptions;
35
+ assetCacheControl?: string;
36
+ fileCacheControl?: string;
37
+ routeOptions?: RouteShorthandOptions;
38
+ }
39
+ /**
40
+ * Fastify plugin that serves React Router framework builds.
41
+ *
42
+ * @param fastify Fastify instance.
43
+ * @param options Adapter options.
44
+ */
45
+ declare function fastifyReactRouter(fastify: FastifyInstance, options: FastifyReactRouterOptions): Promise<void>;
46
+ //#endregion
47
+ //#region src/request.d.ts
48
+ /**
49
+ * Copies Fastify's normalized request headers into Web Fetch `Headers`.
50
+ *
51
+ * @param source Fastify request headers.
52
+ * @returns Fetch-compatible headers.
53
+ */
54
+ declare function createHeaders(source: FastifyRequest["headers"]): Headers;
55
+ /**
56
+ * Builds the absolute request URL that React Router expects.
57
+ *
58
+ * @param request Fastify request.
59
+ * @returns Absolute URL for the original incoming request.
60
+ */
61
+ declare function createUrl(request: FastifyRequest): string;
62
+ /**
63
+ * Adapts a Fastify request to a Web Fetch `Request`.
64
+ *
65
+ * @param request Fastify request.
66
+ * @param reply Fastify reply, used to abort work when the connection closes.
67
+ * @returns Fetch-compatible request for React Router.
68
+ */
69
+ declare function createRequest(request: FastifyRequest, reply: FastifyReply): Request;
70
+ //#endregion
71
+ //#region src/response.d.ts
72
+ /**
73
+ * Writes a Web Fetch `Response` through a Fastify reply.
74
+ *
75
+ * @param reply Fastify reply.
76
+ * @param response React Router response.
77
+ * @returns A promise that settles after the response is sent.
78
+ */
79
+ declare function sendResponse(reply: FastifyReply, response: Response): Promise<void>;
80
+ //#endregion
81
+ export { type CreateRequestHandlerOptions, type FastifyReactRouterOptions, type GetLoadContextFunction, type HttpRequest, type HttpResponse, type ReactRouterLoadContext, type RequestHandler, createHeaders, createRequest, createRequestHandler, createUrl, fastifyReactRouter, sendResponse };
82
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,182 @@
1
+ import { t as importSsrModule } from "./vite-runtime-Cuj_Fjkd.mjs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import fastifyStatic from "@fastify/static";
5
+ import { createRequestHandler as createRequestHandler$1 } from "react-router";
6
+ import { Readable } from "node:stream";
7
+ import { createReadableStreamFromReadable } from "@react-router/node";
8
+ //#region src/request.ts
9
+ /**
10
+ * Copies Fastify's normalized request headers into Web Fetch `Headers`.
11
+ *
12
+ * @param source Fastify request headers.
13
+ * @returns Fetch-compatible headers.
14
+ */
15
+ function createHeaders(source) {
16
+ let headers = new Headers();
17
+ for (let [name, value] of Object.entries(source)) {
18
+ if (value == null) continue;
19
+ if (Array.isArray(value)) for (let item of value) headers.append(name, item);
20
+ else headers.set(name, value);
21
+ }
22
+ return headers;
23
+ }
24
+ /**
25
+ * Builds the absolute request URL that React Router expects.
26
+ *
27
+ * @param request Fastify request.
28
+ * @returns Absolute URL for the original incoming request.
29
+ */
30
+ function createUrl(request) {
31
+ return `${request.protocol}://${request.host}${request.originalUrl}`;
32
+ }
33
+ /**
34
+ * Adapts a Fastify request to a Web Fetch `Request`.
35
+ *
36
+ * @param request Fastify request.
37
+ * @param reply Fastify reply, used to abort work when the connection closes.
38
+ * @returns Fetch-compatible request for React Router.
39
+ */
40
+ function createRequest(request, reply) {
41
+ let controller = new AbortController();
42
+ let init = {
43
+ method: request.method,
44
+ headers: createHeaders(request.headers),
45
+ signal: controller.signal
46
+ };
47
+ reply.raw.once("finish", () => {
48
+ controller = null;
49
+ });
50
+ reply.raw.once("close", () => {
51
+ controller?.abort();
52
+ });
53
+ if (["GET", "HEAD"].includes(request.method) === false) {
54
+ init.body = getBody(request);
55
+ init.duplex = "half";
56
+ }
57
+ return new Request(createUrl(request), init);
58
+ }
59
+ function getBody(request) {
60
+ let body = request.body;
61
+ if (body == null) return createReadableStreamFromReadable(request.raw);
62
+ if (body instanceof Readable) return createReadableStreamFromReadable(body);
63
+ if (body instanceof ReadableStream) return body;
64
+ if (body instanceof URLSearchParams) return body;
65
+ if (body instanceof ArrayBuffer) return body;
66
+ if (body instanceof Blob) return body;
67
+ if (body instanceof FormData) return body;
68
+ if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
69
+ if (typeof body === "string") return body;
70
+ return JSON.stringify(body);
71
+ }
72
+ //#endregion
73
+ //#region src/response.ts
74
+ /**
75
+ * Writes a Web Fetch `Response` through a Fastify reply.
76
+ *
77
+ * @param reply Fastify reply.
78
+ * @param response React Router response.
79
+ * @returns A promise that settles after the response is sent.
80
+ */
81
+ async function sendResponse(reply, response) {
82
+ reply.status(response.status);
83
+ writeHeaders(reply, response.headers);
84
+ if (response.body == null) return reply.send();
85
+ return reply.send(readableFromWeb(response.body));
86
+ }
87
+ function writeHeaders(reply, headers) {
88
+ let cookies = readSetCookies(headers);
89
+ for (let [name, value] of headers) {
90
+ if (name.toLowerCase() === "set-cookie" && cookies.length > 0) continue;
91
+ reply.header(name, value);
92
+ }
93
+ if (cookies.length > 0) reply.header("set-cookie", cookies);
94
+ }
95
+ function readSetCookies(headers) {
96
+ let cookies = headers.getSetCookie?.();
97
+ if (cookies && cookies.length > 0) return cookies;
98
+ let cookie = headers.get("Set-Cookie");
99
+ return cookie ? [cookie] : [];
100
+ }
101
+ function readableFromWeb(body) {
102
+ let reader = body.getReader();
103
+ return new Readable({ read() {
104
+ reader.read().then(({ done, value }) => {
105
+ this.push(done ? null : Buffer.from(value));
106
+ }, (error) => {
107
+ this.destroy(error instanceof Error ? error : new Error(String(error)));
108
+ });
109
+ } });
110
+ }
111
+ //#endregion
112
+ //#region src/handler.ts
113
+ /**
114
+ * Creates a Fastify route handler backed by React Router's server runtime.
115
+ *
116
+ * @param options React Router build, mode, and optional load context hook.
117
+ * @returns Fastify route handler.
118
+ */
119
+ function createRequestHandler(options) {
120
+ let reactRouterHandler = createRequestHandler$1(options.build, options.mode ?? process.env.NODE_ENV);
121
+ return async (request, reply) => {
122
+ await sendResponse(reply, await reactRouterHandler(createRequest(request, reply), await options.getLoadContext?.(request, reply)));
123
+ };
124
+ }
125
+ //#endregion
126
+ //#region src/fastify.ts
127
+ /**
128
+ * Fastify plugin that serves React Router framework builds.
129
+ *
130
+ * @param fastify Fastify instance.
131
+ * @param options Adapter options.
132
+ */
133
+ async function fastifyReactRouter(fastify, options) {
134
+ let { devServer, basePath = "/", serverBuildPath = "build/server/index.js", clientBuildDirectory = "build/client", mode = process.env.NODE_ENV, getLoadContext, build, staticOptions, assetCacheControl = "public, max-age=31536000, immutable", fileCacheControl = "public, max-age=3600", routeOptions } = options;
135
+ let serverBuild = devServer == null ? build ?? createBuildLoader(devServer, path.resolve(serverBuildPath)) : createBuildLoader(devServer, path.resolve(serverBuildPath));
136
+ if (devServer == null) await registerStaticFiles(fastify, {
137
+ basePath,
138
+ clientBuildDirectory: path.resolve(clientBuildDirectory),
139
+ assetCacheControl,
140
+ fileCacheControl,
141
+ staticOptions
142
+ });
143
+ let handler = createRequestHandler({
144
+ build: serverBuild,
145
+ getLoadContext,
146
+ mode
147
+ });
148
+ fastify.removeAllContentTypeParsers();
149
+ fastify.addContentTypeParser("*", (_request, payload, done) => {
150
+ done(null, payload);
151
+ });
152
+ if (routeOptions) fastify.all("*", routeOptions, handler);
153
+ else fastify.all("*", handler);
154
+ }
155
+ function createBuildLoader(devServer, serverBuildPath) {
156
+ if (devServer != null) return () => importSsrModule(devServer, "virtual:react-router/server-build");
157
+ return async () => import(
158
+ /* @vite-ignore */
159
+ pathToFileURL(serverBuildPath).href
160
+ );
161
+ }
162
+ async function registerStaticFiles(fastify, options) {
163
+ let assetsDirectory = path.join(options.clientBuildDirectory, "assets");
164
+ await fastify.register(fastifyStatic, {
165
+ root: options.clientBuildDirectory,
166
+ prefix: options.basePath,
167
+ wildcard: false,
168
+ cacheControl: false,
169
+ dotfiles: "ignore",
170
+ etag: true,
171
+ lastModified: true,
172
+ setHeaders(res, filePath) {
173
+ let isAsset = filePath.startsWith(assetsDirectory);
174
+ res.setHeader("cache-control", isAsset ? options.assetCacheControl : options.fileCacheControl);
175
+ },
176
+ ...options.staticOptions
177
+ });
178
+ }
179
+ //#endregion
180
+ export { createHeaders, createRequest, createRequestHandler, createUrl, fastifyReactRouter, sendResponse };
181
+
182
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["withCookies","createReactRouterHandler"],"sources":["../src/request.ts","../src/response.ts","../src/handler.ts","../src/fastify.ts"],"sourcesContent":["import { Readable } from \"node:stream\"\n\nimport { createReadableStreamFromReadable } from \"@react-router/node\"\nimport type { FastifyReply, FastifyRequest } from \"fastify\"\n\n/**\n * Copies Fastify's normalized request headers into Web Fetch `Headers`.\n *\n * @param source Fastify request headers.\n * @returns Fetch-compatible headers.\n */\nexport function createHeaders(source: FastifyRequest[\"headers\"]): Headers {\n let headers = new Headers()\n\n for (let [name, value] of Object.entries(source)) {\n if (value == null) continue\n\n if (Array.isArray(value)) {\n for (let item of value) {\n headers.append(name, item)\n }\n } else {\n headers.set(name, value)\n }\n }\n\n return headers\n}\n\n/**\n * Builds the absolute request URL that React Router expects.\n *\n * @param request Fastify request.\n * @returns Absolute URL for the original incoming request.\n */\nexport function createUrl(request: FastifyRequest): string {\n return `${request.protocol}://${request.host}${request.originalUrl}`\n}\n\n/**\n * Adapts a Fastify request to a Web Fetch `Request`.\n *\n * @param request Fastify request.\n * @param reply Fastify reply, used to abort work when the connection closes.\n * @returns Fetch-compatible request for React Router.\n */\nexport function createRequest(\n request: FastifyRequest,\n reply: FastifyReply,\n): Request {\n let controller: AbortController | null = new AbortController()\n\n let init: RequestInit & { duplex?: \"half\" } = {\n method: request.method,\n headers: createHeaders(request.headers),\n signal: controller.signal,\n }\n\n reply.raw.once(\"finish\", () => {\n controller = null\n })\n reply.raw.once(\"close\", () => {\n controller?.abort()\n })\n\n if ([\"GET\", \"HEAD\"].includes(request.method) === false) {\n init.body = getBody(request)\n init.duplex = \"half\"\n }\n\n return new Request(createUrl(request), init)\n}\n\nfunction getBody(request: FastifyRequest): BodyInit | null {\n let body = request.body\n\n if (body == null) return createReadableStreamFromReadable(request.raw)\n if (body instanceof Readable) return createReadableStreamFromReadable(body)\n if (body instanceof ReadableStream) return body\n if (body instanceof URLSearchParams) return body\n if (body instanceof ArrayBuffer) return body\n if (body instanceof Blob) return body\n if (body instanceof FormData) return body\n if (ArrayBuffer.isView(body)) {\n return new Uint8Array(\n body.buffer as ArrayBuffer,\n body.byteOffset,\n body.byteLength,\n )\n }\n if (typeof body === \"string\") return body\n\n return JSON.stringify(body)\n}\n","import { Readable } from \"node:stream\"\n\nimport type { FastifyReply } from \"fastify\"\n\n/**\n * Writes a Web Fetch `Response` through a Fastify reply.\n *\n * @param reply Fastify reply.\n * @param response React Router response.\n * @returns A promise that settles after the response is sent.\n */\nexport async function sendResponse(\n reply: FastifyReply,\n response: Response,\n): Promise<void> {\n reply.status(response.status)\n writeHeaders(reply, response.headers)\n\n if (response.body == null) {\n return reply.send()\n }\n\n return reply.send(readableFromWeb(response.body))\n}\n\nfunction writeHeaders(reply: FastifyReply, headers: Headers): void {\n let cookies = readSetCookies(headers)\n\n for (let [name, value] of headers) {\n if (name.toLowerCase() === \"set-cookie\" && cookies.length > 0) continue\n reply.header(name, value)\n }\n\n if (cookies.length > 0) {\n reply.header(\"set-cookie\", cookies)\n }\n}\n\nfunction readSetCookies(headers: Headers): string[] {\n let withCookies = headers as Headers & { getSetCookie?: () => string[] }\n let cookies = withCookies.getSetCookie?.()\n if (cookies && cookies.length > 0) return cookies\n\n let cookie = headers.get(\"Set-Cookie\")\n return cookie ? [cookie] : []\n}\n\nfunction readableFromWeb(body: ReadableStream<Uint8Array>): Readable {\n let reader = body.getReader()\n\n return new Readable({\n read() {\n reader.read().then(\n ({ done, value }) => {\n this.push(done ? null : Buffer.from(value))\n },\n (error: unknown) => {\n this.destroy(\n error instanceof Error ? error : new Error(String(error)),\n )\n },\n )\n },\n })\n}\n","import type { FastifyReply, FastifyRequest } from \"fastify\"\nimport type { RouterContextProvider, ServerBuild } from \"react-router\"\nimport { createRequestHandler as createReactRouterHandler } from \"react-router\"\n\nimport { createRequest } from \"./request.ts\"\nimport { sendResponse } from \"./response.ts\"\n\nexport type HttpRequest = FastifyRequest[\"raw\"]\nexport type HttpResponse = FastifyReply[\"raw\"]\n\nexport type ReactRouterLoadContext = RouterContextProvider\n\nexport type GetLoadContextFunction = (\n request: FastifyRequest,\n reply: FastifyReply,\n) =>\n | ReactRouterLoadContext\n | undefined\n | Promise<ReactRouterLoadContext | undefined>\n\nexport type RequestHandler = (\n request: FastifyRequest,\n reply: FastifyReply,\n) => Promise<void>\n\nexport interface CreateRequestHandlerOptions {\n build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>)\n getLoadContext?: GetLoadContextFunction\n mode?: string\n}\n\n/**\n * Creates a Fastify route handler backed by React Router's server runtime.\n *\n * @param options React Router build, mode, and optional load context hook.\n * @returns Fastify route handler.\n */\nexport function createRequestHandler(\n options: CreateRequestHandlerOptions,\n): RequestHandler {\n let reactRouterHandler = createReactRouterHandler(\n options.build,\n options.mode ?? process.env.NODE_ENV,\n )\n\n return async (request, reply) => {\n let webRequest = createRequest(request, reply)\n let context = await options.getLoadContext?.(request, reply)\n let webResponse = await reactRouterHandler(webRequest, context)\n await sendResponse(reply, webResponse)\n }\n}\n","import path from \"node:path\"\nimport { pathToFileURL } from \"node:url\"\n\nimport fastifyStatic from \"@fastify/static\"\nimport type { FastifyStaticOptions } from \"@fastify/static\"\nimport type { FastifyInstance, RouteShorthandOptions } from \"fastify\"\nimport type { ServerBuild } from \"react-router\"\nimport type { ViteDevServer } from \"vite\"\n\nimport { createRequestHandler, type GetLoadContextFunction } from \"./handler.ts\"\nimport { importSsrModule } from \"./vite-runtime.ts\"\n\nexport interface FastifyReactRouterOptions {\n devServer?: ViteDevServer\n basePath?: string\n serverBuildPath?: string\n clientBuildDirectory?: string\n mode?: string\n getLoadContext?: GetLoadContextFunction\n build?: ServerBuild | (() => ServerBuild | Promise<ServerBuild>)\n staticOptions?: FastifyStaticOptions\n assetCacheControl?: string\n fileCacheControl?: string\n routeOptions?: RouteShorthandOptions\n}\n\n/**\n * Fastify plugin that serves React Router framework builds.\n *\n * @param fastify Fastify instance.\n * @param options Adapter options.\n */\nexport async function fastifyReactRouter(\n fastify: FastifyInstance,\n options: FastifyReactRouterOptions,\n): Promise<void> {\n let {\n devServer,\n basePath = \"/\",\n serverBuildPath = \"build/server/index.js\",\n clientBuildDirectory = \"build/client\",\n mode = process.env.NODE_ENV,\n getLoadContext,\n build,\n staticOptions,\n assetCacheControl = \"public, max-age=31536000, immutable\",\n fileCacheControl = \"public, max-age=3600\",\n routeOptions,\n } = options\n\n let serverBuild =\n devServer == null\n ? build ?? createBuildLoader(devServer, path.resolve(serverBuildPath))\n : createBuildLoader(devServer, path.resolve(serverBuildPath))\n\n if (devServer == null) {\n await registerStaticFiles(fastify, {\n basePath,\n clientBuildDirectory: path.resolve(clientBuildDirectory),\n assetCacheControl,\n fileCacheControl,\n staticOptions,\n })\n }\n\n let handler = createRequestHandler({\n build: serverBuild,\n getLoadContext,\n mode,\n })\n\n fastify.removeAllContentTypeParsers()\n fastify.addContentTypeParser(\"*\", (_request, payload, done) => {\n done(null, payload)\n })\n\n if (routeOptions) {\n fastify.all(\"*\", routeOptions, handler)\n } else {\n fastify.all(\"*\", handler)\n }\n}\n\nfunction createBuildLoader(\n devServer: ViteDevServer | undefined,\n serverBuildPath: string,\n): ServerBuild | (() => ServerBuild | Promise<ServerBuild>) {\n if (devServer != null) {\n return () =>\n importSsrModule<ServerBuild>(\n devServer,\n \"virtual:react-router/server-build\",\n )\n }\n\n return async () =>\n import(\n /* @vite-ignore */ pathToFileURL(serverBuildPath).href\n ) as Promise<ServerBuild>\n}\n\nasync function registerStaticFiles(\n fastify: FastifyInstance,\n options: {\n basePath: string\n clientBuildDirectory: string\n assetCacheControl: string\n fileCacheControl: string\n staticOptions?: FastifyStaticOptions\n },\n): Promise<void> {\n let assetsDirectory = path.join(options.clientBuildDirectory, \"assets\")\n\n await fastify.register(fastifyStatic, {\n root: options.clientBuildDirectory,\n prefix: options.basePath,\n wildcard: false,\n cacheControl: false,\n dotfiles: \"ignore\",\n etag: true,\n lastModified: true,\n setHeaders(res, filePath) {\n let isAsset = filePath.startsWith(assetsDirectory)\n res.setHeader(\n \"cache-control\",\n isAsset ? options.assetCacheControl : options.fileCacheControl,\n )\n },\n ...options.staticOptions,\n })\n}\n"],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ //#region src/vite-runtime.ts
2
+ /**
3
+ * Imports an SSR module using Vite's current Environment runner, with
4
+ * `ssrLoadModule` as a compatibility fallback.
5
+ *
6
+ * @param vite Vite dev server.
7
+ * @param id Module ID to import.
8
+ * @returns Imported module namespace.
9
+ */
10
+ async function importSsrModule(vite, id) {
11
+ let ssr = vite.environments?.ssr;
12
+ if (typeof ssr?.runner?.import === "function") return ssr.runner.import(id);
13
+ return vite.ssrLoadModule(id);
14
+ }
15
+ //#endregion
16
+ export { importSsrModule as t };
17
+
18
+ //# sourceMappingURL=vite-runtime-Cuj_Fjkd.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-runtime-Cuj_Fjkd.mjs","names":[],"sources":["../src/vite-runtime.ts"],"sourcesContent":["import type { DevEnvironment, ViteDevServer } from \"vite\"\n\ninterface RunnableEnvironment extends DevEnvironment {\n runner: {\n import(id: string): Promise<Record<string, unknown>>\n }\n}\n\n/**\n * Imports an SSR module using Vite's current Environment runner, with\n * `ssrLoadModule` as a compatibility fallback.\n *\n * @param vite Vite dev server.\n * @param id Module ID to import.\n * @returns Imported module namespace.\n */\nexport async function importSsrModule<T = Record<string, unknown>>(\n vite: ViteDevServer,\n id: string,\n): Promise<T> {\n let ssr = vite.environments?.ssr as Partial<RunnableEnvironment> | undefined\n if (typeof ssr?.runner?.import === \"function\") {\n return ssr.runner.import(id) as Promise<T>\n }\n\n return vite.ssrLoadModule(id) as unknown as Promise<T>\n}\n"],"mappings":";;;;;;;;;AAgBA,eAAsB,gBACpB,MACA,IACY;CACZ,IAAI,MAAM,KAAK,cAAc;CAC7B,IAAI,OAAO,KAAK,QAAQ,WAAW,YACjC,OAAO,IAAI,OAAO,OAAO,EAAE;CAG7B,OAAO,KAAK,cAAc,EAAE;AAC9B"}
@@ -0,0 +1,33 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { Plugin, ViteDevServer } from "vite";
3
+
4
+ //#region src/vite.d.ts
5
+ type FastifyAppFactory = (vite: ViteDevServer) => FastifyInstance | Promise<FastifyInstance>;
6
+ interface FastifyReactRouterDevOptions {
7
+ /** Server module loaded by Vite during `react-router dev`. */
8
+ entry?: string;
9
+ /** Named export that creates the Fastify app. Falls back to `default`. */
10
+ exportName?: string;
11
+ /**
12
+ * Keeps local modules imported by the Fastify server entry external in the
13
+ * React Router SSR build. This preserves singleton module identity for shared
14
+ * values such as React Router context tokens.
15
+ *
16
+ * Pass an array to externalize explicit import specifiers instead.
17
+ */
18
+ externalizeServerEntryImports?: boolean | string[];
19
+ }
20
+ /**
21
+ * Vite plugin that lets `react-router dev` serve through a Fastify app.
22
+ *
23
+ * Vite continues to handle its internal client/HMR/module middleware first.
24
+ * Fastify receives the remaining requests, including the React Router catch-all
25
+ * installed by `fastifyReactRouter`.
26
+ *
27
+ * @param options Development server entry options.
28
+ * @returns Vite plugin.
29
+ */
30
+ declare function fastifyReactRouterDev(options?: FastifyReactRouterDevOptions): Plugin;
31
+ //#endregion
32
+ export { FastifyAppFactory, FastifyReactRouterDevOptions, fastifyReactRouterDev };
33
+ //# sourceMappingURL=vite.d.mts.map