@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/dist/vite.mjs ADDED
@@ -0,0 +1,181 @@
1
+ import { t as importSsrModule } from "./vite-runtime-Cuj_Fjkd.mjs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import fs from "node:fs";
5
+ //#region src/vite.ts
6
+ const adapterPackageNames = ["@mcansh/react-router-fastify", "@mcansh/remix-fastify"];
7
+ /**
8
+ * Vite plugin that lets `react-router dev` serve through a Fastify app.
9
+ *
10
+ * Vite continues to handle its internal client/HMR/module middleware first.
11
+ * Fastify receives the remaining requests, including the React Router catch-all
12
+ * installed by `fastifyReactRouter`.
13
+ *
14
+ * @param options Development server entry options.
15
+ * @returns Vite plugin.
16
+ */
17
+ function fastifyReactRouterDev(options = {}) {
18
+ let { entry = "./server.ts", exportName = "createServer", externalizeServerEntryImports = true } = options;
19
+ let command = "serve";
20
+ let root = process.cwd();
21
+ let serverEntryImportSpecifiers = /* @__PURE__ */ new Set();
22
+ let serverEntryImportFiles = /* @__PURE__ */ new Set();
23
+ return {
24
+ name: "fastify-react-router-dev",
25
+ enforce: "pre",
26
+ config(_config, environment) {
27
+ if (environment.command !== "serve") return;
28
+ return { ssr: { noExternal: adapterPackageNames } };
29
+ },
30
+ configResolved(config) {
31
+ command = config.command;
32
+ root = config.root;
33
+ let externals = collectServerEntryExternals(path.resolve(root, entry), externalizeServerEntryImports);
34
+ serverEntryImportSpecifiers = externals.specifiers;
35
+ serverEntryImportFiles = externals.files;
36
+ },
37
+ async resolveId(source, importer, resolveOptions) {
38
+ if (command !== "build" || !resolveOptions.ssr) return null;
39
+ if (shouldExternalizeSpecifier(source) === false) return null;
40
+ if (serverEntryImportSpecifiers.has(source)) return {
41
+ id: source,
42
+ external: true
43
+ };
44
+ if (importer == null) return null;
45
+ let resolved = await this.resolve(source, importer, {
46
+ ...resolveOptions,
47
+ skipSelf: true
48
+ });
49
+ if (resolved == null) return null;
50
+ let filePath = toFilePath(resolved.id);
51
+ if (filePath == null || serverEntryImportFiles.has(filePath) === false) return null;
52
+ return {
53
+ id: source.startsWith("#") ? source : filePath,
54
+ external: true
55
+ };
56
+ },
57
+ configureServer(vite) {
58
+ let resolvedEntry = path.resolve(vite.config.root, entry);
59
+ let appPromise;
60
+ async function closeApp() {
61
+ let current = appPromise;
62
+ appPromise = void 0;
63
+ if (current == null) return;
64
+ try {
65
+ await (await current).close();
66
+ } catch {}
67
+ }
68
+ async function loadApp() {
69
+ if (appPromise == null) appPromise = (async () => {
70
+ let module = await importSsrModule(vite, resolvedEntry);
71
+ let factory = module[exportName] ?? module.default;
72
+ if (typeof factory !== "function") throw new Error(`[fastify-react-router-dev] Expected ${entry} to export "${exportName}" or a default Fastify app factory.`);
73
+ let app = await factory(vite);
74
+ await app.ready();
75
+ return app;
76
+ })();
77
+ return appPromise;
78
+ }
79
+ vite.watcher.on("change", () => {
80
+ closeApp();
81
+ });
82
+ vite.watcher.on("unlink", () => {
83
+ closeApp();
84
+ });
85
+ vite.httpServer?.once("close", () => {
86
+ closeApp();
87
+ });
88
+ return () => {
89
+ vite.middlewares.use(async (request, response, next) => {
90
+ try {
91
+ (await loadApp()).routing(request, response);
92
+ } catch (error) {
93
+ if (error instanceof Error) vite.ssrFixStacktrace(error);
94
+ next(error);
95
+ }
96
+ });
97
+ };
98
+ }
99
+ };
100
+ }
101
+ function collectServerEntryExternals(entryPath, externalizeServerEntryImports) {
102
+ let specifiers = /* @__PURE__ */ new Set();
103
+ let files = /* @__PURE__ */ new Set();
104
+ if (externalizeServerEntryImports === false) return {
105
+ specifiers,
106
+ files
107
+ };
108
+ let imports = Array.isArray(externalizeServerEntryImports) ? externalizeServerEntryImports : readImportSpecifiers(entryPath);
109
+ for (let specifier of imports) {
110
+ if (shouldExternalizeSpecifier(specifier) === false) continue;
111
+ specifiers.add(specifier);
112
+ if (isLocalSpecifier(specifier)) {
113
+ let file = resolveLocalImport(entryPath, specifier);
114
+ if (file) files.add(file);
115
+ }
116
+ }
117
+ return {
118
+ specifiers,
119
+ files
120
+ };
121
+ }
122
+ function readImportSpecifiers(filePath) {
123
+ if (fs.existsSync(filePath) === false) return [];
124
+ let source = fs.readFileSync(filePath, "utf8");
125
+ let specifiers = /* @__PURE__ */ new Set();
126
+ for (let match of source.matchAll(/\b(?:import|export)\s+(?:[^'"]*?\s+from\s*)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g)) specifiers.add(match[1] ?? match[2]);
127
+ return [...specifiers];
128
+ }
129
+ function shouldExternalizeSpecifier(specifier) {
130
+ return specifier.startsWith("#") || isLocalSpecifier(specifier);
131
+ }
132
+ function isLocalSpecifier(specifier) {
133
+ return specifier.startsWith(".") || specifier.startsWith("/");
134
+ }
135
+ function resolveLocalImport(importer, specifier) {
136
+ return resolveExistingFile(path.resolve(path.dirname(importer), specifier));
137
+ }
138
+ function resolveExistingFile(filePath) {
139
+ if (isFile(filePath)) return normalizePath(filePath);
140
+ for (let extension of [
141
+ ".ts",
142
+ ".tsx",
143
+ ".mts",
144
+ ".mjs",
145
+ ".js",
146
+ ".jsx"
147
+ ]) {
148
+ let candidate = `${filePath}${extension}`;
149
+ if (isFile(candidate)) return normalizePath(candidate);
150
+ }
151
+ for (let extension of [
152
+ ".ts",
153
+ ".tsx",
154
+ ".mts",
155
+ ".mjs",
156
+ ".js",
157
+ ".jsx"
158
+ ]) {
159
+ let candidate = path.join(filePath, `index${extension}`);
160
+ if (isFile(candidate)) return normalizePath(candidate);
161
+ }
162
+ }
163
+ function isFile(filePath) {
164
+ try {
165
+ return fs.statSync(filePath).isFile();
166
+ } catch {
167
+ return false;
168
+ }
169
+ }
170
+ function toFilePath(id) {
171
+ let withoutQuery = id.replace(/[?#].*$/, "");
172
+ if (withoutQuery.startsWith("file://")) return normalizePath(fileURLToPath(withoutQuery));
173
+ if (path.isAbsolute(withoutQuery)) return normalizePath(withoutQuery);
174
+ }
175
+ function normalizePath(filePath) {
176
+ return filePath.split(path.sep).join("/");
177
+ }
178
+ //#endregion
179
+ export { fastifyReactRouterDev };
180
+
181
+ //# sourceMappingURL=vite.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.mjs","names":[],"sources":["../src/vite.ts"],"sourcesContent":["import fs from \"node:fs\"\nimport path from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\n\nimport type { FastifyInstance } from \"fastify\"\nimport type { Plugin, ViteDevServer } from \"vite\"\n\nimport { importSsrModule } from \"./vite-runtime.ts\"\n\nconst adapterPackageNames = [\n \"@mcansh/react-router-fastify\",\n \"@mcansh/remix-fastify\",\n]\n\nexport type FastifyAppFactory = (\n vite: ViteDevServer,\n) => FastifyInstance | Promise<FastifyInstance>\n\nexport interface FastifyReactRouterDevOptions {\n /** Server module loaded by Vite during `react-router dev`. */\n entry?: string\n /** Named export that creates the Fastify app. Falls back to `default`. */\n exportName?: string\n /**\n * Keeps local modules imported by the Fastify server entry external in the\n * React Router SSR build. This preserves singleton module identity for shared\n * values such as React Router context tokens.\n *\n * Pass an array to externalize explicit import specifiers instead.\n */\n externalizeServerEntryImports?: boolean | string[]\n}\n\n/**\n * Vite plugin that lets `react-router dev` serve through a Fastify app.\n *\n * Vite continues to handle its internal client/HMR/module middleware first.\n * Fastify receives the remaining requests, including the React Router catch-all\n * installed by `fastifyReactRouter`.\n *\n * @param options Development server entry options.\n * @returns Vite plugin.\n */\nexport function fastifyReactRouterDev(\n options: FastifyReactRouterDevOptions = {},\n): Plugin {\n let {\n entry = \"./server.ts\",\n exportName = \"createServer\",\n externalizeServerEntryImports = true,\n } = options\n let command: \"build\" | \"serve\" = \"serve\"\n let root = process.cwd()\n let serverEntryImportSpecifiers = new Set<string>()\n let serverEntryImportFiles = new Set<string>()\n\n return {\n name: \"fastify-react-router-dev\",\n enforce: \"pre\",\n config(_config, environment) {\n if (environment.command !== \"serve\") return\n\n return {\n ssr: {\n noExternal: adapterPackageNames,\n },\n }\n },\n configResolved(config) {\n command = config.command\n root = config.root\n\n let resolvedEntry = path.resolve(root, entry)\n let externals = collectServerEntryExternals(\n resolvedEntry,\n externalizeServerEntryImports,\n )\n serverEntryImportSpecifiers = externals.specifiers\n serverEntryImportFiles = externals.files\n },\n async resolveId(source, importer, resolveOptions) {\n if (command !== \"build\" || !resolveOptions.ssr) return null\n if (shouldExternalizeSpecifier(source) === false) return null\n\n if (serverEntryImportSpecifiers.has(source)) {\n return { id: source, external: true }\n }\n\n if (importer == null) return null\n\n let resolved = await this.resolve(source, importer, {\n ...resolveOptions,\n skipSelf: true,\n })\n if (resolved == null) return null\n\n let filePath = toFilePath(resolved.id)\n if (filePath == null || serverEntryImportFiles.has(filePath) === false)\n return null\n\n return {\n id: source.startsWith(\"#\") ? source : filePath,\n external: true,\n }\n },\n configureServer(vite) {\n let resolvedEntry = path.resolve(vite.config.root, entry)\n let appPromise: Promise<FastifyInstance> | undefined\n\n async function closeApp(): Promise<void> {\n let current = appPromise\n appPromise = undefined\n if (current == null) return\n\n try {\n let app = await current\n await app.close()\n } catch {\n // The app may have failed while loading; there is nothing to close.\n }\n }\n\n async function loadApp(): Promise<FastifyInstance> {\n if (appPromise == null) {\n appPromise = (async () => {\n let module = await importSsrModule(vite, resolvedEntry)\n let factory = module[exportName] ?? module.default\n\n if (typeof factory !== \"function\") {\n throw new Error(\n `[fastify-react-router-dev] Expected ${entry} to export \"${exportName}\" or a default Fastify app factory.`,\n )\n }\n\n let app = await (factory as FastifyAppFactory)(vite)\n await app.ready()\n return app\n })()\n }\n\n return appPromise\n }\n\n vite.watcher.on(\"change\", () => {\n void closeApp()\n })\n vite.watcher.on(\"unlink\", () => {\n void closeApp()\n })\n vite.httpServer?.once(\"close\", () => {\n void closeApp()\n })\n\n return () => {\n vite.middlewares.use(async (request, response, next) => {\n try {\n let app = await loadApp()\n app.routing(request, response)\n } catch (error) {\n if (error instanceof Error) vite.ssrFixStacktrace(error)\n next(error)\n }\n })\n }\n },\n }\n}\n\nfunction collectServerEntryExternals(\n entryPath: string,\n externalizeServerEntryImports: boolean | string[],\n): {\n specifiers: Set<string>\n files: Set<string>\n} {\n let specifiers = new Set<string>()\n let files = new Set<string>()\n\n if (externalizeServerEntryImports === false) {\n return { specifiers, files }\n }\n\n let imports = Array.isArray(externalizeServerEntryImports)\n ? externalizeServerEntryImports\n : readImportSpecifiers(entryPath)\n\n for (let specifier of imports) {\n if (shouldExternalizeSpecifier(specifier) === false) continue\n\n specifiers.add(specifier)\n\n if (isLocalSpecifier(specifier)) {\n let file = resolveLocalImport(entryPath, specifier)\n if (file) files.add(file)\n }\n }\n\n return { specifiers, files }\n}\n\nfunction readImportSpecifiers(filePath: string): string[] {\n if (fs.existsSync(filePath) === false) return []\n\n let source = fs.readFileSync(filePath, \"utf8\")\n let specifiers = new Set<string>()\n let importPattern =\n /\\b(?:import|export)\\s+(?:[^'\"]*?\\s+from\\s*)?[\"']([^\"']+)[\"']|import\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)/g\n\n for (let match of source.matchAll(importPattern)) {\n specifiers.add(match[1] ?? match[2])\n }\n\n return [...specifiers]\n}\n\nfunction shouldExternalizeSpecifier(specifier: string): boolean {\n return specifier.startsWith(\"#\") || isLocalSpecifier(specifier)\n}\n\nfunction isLocalSpecifier(specifier: string): boolean {\n return specifier.startsWith(\".\") || specifier.startsWith(\"/\")\n}\n\nfunction resolveLocalImport(\n importer: string,\n specifier: string,\n): string | undefined {\n let resolved = path.resolve(path.dirname(importer), specifier)\n return resolveExistingFile(resolved)\n}\n\nfunction resolveExistingFile(filePath: string): string | undefined {\n if (isFile(filePath)) return normalizePath(filePath)\n\n for (let extension of [\".ts\", \".tsx\", \".mts\", \".mjs\", \".js\", \".jsx\"]) {\n let candidate = `${filePath}${extension}`\n if (isFile(candidate)) return normalizePath(candidate)\n }\n\n for (let extension of [\".ts\", \".tsx\", \".mts\", \".mjs\", \".js\", \".jsx\"]) {\n let candidate = path.join(filePath, `index${extension}`)\n if (isFile(candidate)) return normalizePath(candidate)\n }\n\n return undefined\n}\n\nfunction isFile(filePath: string): boolean {\n try {\n return fs.statSync(filePath).isFile()\n } catch {\n return false\n }\n}\n\nfunction toFilePath(id: string): string | undefined {\n let withoutQuery = id.replace(/[?#].*$/, \"\")\n\n if (withoutQuery.startsWith(\"file://\")) {\n return normalizePath(fileURLToPath(withoutQuery))\n }\n\n if (path.isAbsolute(withoutQuery)) {\n return normalizePath(withoutQuery)\n }\n\n return undefined\n}\n\nfunction normalizePath(filePath: string): string {\n return filePath.split(path.sep).join(\"/\")\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@mcansh/react-router-fastify",
3
+ "version": "5.0.0",
4
+ "description": "Fastify adapter and Vite development plugin for React Router v8 framework mode",
5
+ "keywords": [
6
+ "adapter",
7
+ "fastify",
8
+ "react-router",
9
+ "vite"
10
+ ],
11
+ "bugs": {
12
+ "url": "https://github.com/mcansh/remix-fastify/issues"
13
+ },
14
+ "license": "MIT",
15
+ "author": "Logan McAnsh <logan@mcan.sh> (https://mcan.sh)",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/mcansh/remix-fastify.git",
19
+ "directory": "packages/react-router-fastify"
20
+ },
21
+ "funding": [
22
+ {
23
+ "type": "github",
24
+ "url": "https://github.com/sponsors/mcansh"
25
+ }
26
+ ],
27
+ "files": [
28
+ "CHANGELOG.md",
29
+ "dist",
30
+ "src",
31
+ "!src/**/*.test.ts",
32
+ "package.json",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "type": "module",
37
+ "sideEffects": false,
38
+ "exports": {
39
+ ".": "./dist/index.mjs",
40
+ "./vite": "./dist/vite.mjs",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "provenance": true
46
+ },
47
+ "dependencies": {
48
+ "@fastify/static": "^9.1.3"
49
+ },
50
+ "devDependencies": {
51
+ "@arethetypeswrong/core": "^0.18.3",
52
+ "@react-router/dev": "8.0.1",
53
+ "@react-router/node": "8.0.1",
54
+ "@types/node": "^26.0.0",
55
+ "@vitest/coverage-v8": "4.1.9",
56
+ "fastify": "^5.8.5",
57
+ "publint": "^0.3.21",
58
+ "react": "^19.2.7",
59
+ "react-dom": "^19.2.7",
60
+ "react-router": "8.0.1",
61
+ "tsdown": "^0.22.3",
62
+ "typescript": "^6.0.3",
63
+ "vite": "^8.0.16",
64
+ "vitest": "^4.0.15"
65
+ },
66
+ "peerDependencies": {
67
+ "@react-router/node": "^8.0.0",
68
+ "fastify": "^5.0.0",
69
+ "react-router": "^8.0.0",
70
+ "vite": "^7.0.0 || ^8.0.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "vite": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "engines": {
78
+ "node": ">=22.22.0"
79
+ },
80
+ "scripts": {
81
+ "build": "tsdown",
82
+ "clean": "git clean -fdX",
83
+ "test": "vitest",
84
+ "typecheck": "tsc --noEmit",
85
+ "validate": "pnpm run typecheck && pnpm run test && pnpm run build"
86
+ }
87
+ }
package/src/fastify.ts ADDED
@@ -0,0 +1,131 @@
1
+ import path from "node:path"
2
+ import { pathToFileURL } from "node:url"
3
+
4
+ import fastifyStatic from "@fastify/static"
5
+ import type { FastifyStaticOptions } from "@fastify/static"
6
+ import type { FastifyInstance, RouteShorthandOptions } from "fastify"
7
+ import type { ServerBuild } from "react-router"
8
+ import type { ViteDevServer } from "vite"
9
+
10
+ import { createRequestHandler, type GetLoadContextFunction } from "./handler.ts"
11
+ import { importSsrModule } from "./vite-runtime.ts"
12
+
13
+ export interface FastifyReactRouterOptions {
14
+ devServer?: ViteDevServer
15
+ basePath?: string
16
+ serverBuildPath?: string
17
+ clientBuildDirectory?: string
18
+ mode?: string
19
+ getLoadContext?: GetLoadContextFunction
20
+ build?: ServerBuild | (() => ServerBuild | Promise<ServerBuild>)
21
+ staticOptions?: FastifyStaticOptions
22
+ assetCacheControl?: string
23
+ fileCacheControl?: string
24
+ routeOptions?: RouteShorthandOptions
25
+ }
26
+
27
+ /**
28
+ * Fastify plugin that serves React Router framework builds.
29
+ *
30
+ * @param fastify Fastify instance.
31
+ * @param options Adapter options.
32
+ */
33
+ export async function fastifyReactRouter(
34
+ fastify: FastifyInstance,
35
+ options: FastifyReactRouterOptions,
36
+ ): Promise<void> {
37
+ let {
38
+ devServer,
39
+ basePath = "/",
40
+ serverBuildPath = "build/server/index.js",
41
+ clientBuildDirectory = "build/client",
42
+ mode = process.env.NODE_ENV,
43
+ getLoadContext,
44
+ build,
45
+ staticOptions,
46
+ assetCacheControl = "public, max-age=31536000, immutable",
47
+ fileCacheControl = "public, max-age=3600",
48
+ routeOptions,
49
+ } = options
50
+
51
+ let serverBuild =
52
+ devServer == null
53
+ ? build ?? createBuildLoader(devServer, path.resolve(serverBuildPath))
54
+ : createBuildLoader(devServer, path.resolve(serverBuildPath))
55
+
56
+ if (devServer == null) {
57
+ await registerStaticFiles(fastify, {
58
+ basePath,
59
+ clientBuildDirectory: path.resolve(clientBuildDirectory),
60
+ assetCacheControl,
61
+ fileCacheControl,
62
+ staticOptions,
63
+ })
64
+ }
65
+
66
+ let handler = createRequestHandler({
67
+ build: serverBuild,
68
+ getLoadContext,
69
+ mode,
70
+ })
71
+
72
+ fastify.removeAllContentTypeParsers()
73
+ fastify.addContentTypeParser("*", (_request, payload, done) => {
74
+ done(null, payload)
75
+ })
76
+
77
+ if (routeOptions) {
78
+ fastify.all("*", routeOptions, handler)
79
+ } else {
80
+ fastify.all("*", handler)
81
+ }
82
+ }
83
+
84
+ function createBuildLoader(
85
+ devServer: ViteDevServer | undefined,
86
+ serverBuildPath: string,
87
+ ): ServerBuild | (() => ServerBuild | Promise<ServerBuild>) {
88
+ if (devServer != null) {
89
+ return () =>
90
+ importSsrModule<ServerBuild>(
91
+ devServer,
92
+ "virtual:react-router/server-build",
93
+ )
94
+ }
95
+
96
+ return async () =>
97
+ import(
98
+ /* @vite-ignore */ pathToFileURL(serverBuildPath).href
99
+ ) as Promise<ServerBuild>
100
+ }
101
+
102
+ async function registerStaticFiles(
103
+ fastify: FastifyInstance,
104
+ options: {
105
+ basePath: string
106
+ clientBuildDirectory: string
107
+ assetCacheControl: string
108
+ fileCacheControl: string
109
+ staticOptions?: FastifyStaticOptions
110
+ },
111
+ ): Promise<void> {
112
+ let assetsDirectory = path.join(options.clientBuildDirectory, "assets")
113
+
114
+ await fastify.register(fastifyStatic, {
115
+ root: options.clientBuildDirectory,
116
+ prefix: options.basePath,
117
+ wildcard: false,
118
+ cacheControl: false,
119
+ dotfiles: "ignore",
120
+ etag: true,
121
+ lastModified: true,
122
+ setHeaders(res, filePath) {
123
+ let isAsset = filePath.startsWith(assetsDirectory)
124
+ res.setHeader(
125
+ "cache-control",
126
+ isAsset ? options.assetCacheControl : options.fileCacheControl,
127
+ )
128
+ },
129
+ ...options.staticOptions,
130
+ })
131
+ }
package/src/handler.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { FastifyReply, FastifyRequest } from "fastify"
2
+ import type { RouterContextProvider, ServerBuild } from "react-router"
3
+ import { createRequestHandler as createReactRouterHandler } from "react-router"
4
+
5
+ import { createRequest } from "./request.ts"
6
+ import { sendResponse } from "./response.ts"
7
+
8
+ export type HttpRequest = FastifyRequest["raw"]
9
+ export type HttpResponse = FastifyReply["raw"]
10
+
11
+ export type ReactRouterLoadContext = RouterContextProvider
12
+
13
+ export type GetLoadContextFunction = (
14
+ request: FastifyRequest,
15
+ reply: FastifyReply,
16
+ ) =>
17
+ | ReactRouterLoadContext
18
+ | undefined
19
+ | Promise<ReactRouterLoadContext | undefined>
20
+
21
+ export type RequestHandler = (
22
+ request: FastifyRequest,
23
+ reply: FastifyReply,
24
+ ) => Promise<void>
25
+
26
+ export interface CreateRequestHandlerOptions {
27
+ build: ServerBuild | (() => ServerBuild | Promise<ServerBuild>)
28
+ getLoadContext?: GetLoadContextFunction
29
+ mode?: string
30
+ }
31
+
32
+ /**
33
+ * Creates a Fastify route handler backed by React Router's server runtime.
34
+ *
35
+ * @param options React Router build, mode, and optional load context hook.
36
+ * @returns Fastify route handler.
37
+ */
38
+ export function createRequestHandler(
39
+ options: CreateRequestHandlerOptions,
40
+ ): RequestHandler {
41
+ let reactRouterHandler = createReactRouterHandler(
42
+ options.build,
43
+ options.mode ?? process.env.NODE_ENV,
44
+ )
45
+
46
+ return async (request, reply) => {
47
+ let webRequest = createRequest(request, reply)
48
+ let context = await options.getLoadContext?.(request, reply)
49
+ let webResponse = await reactRouterHandler(webRequest, context)
50
+ await sendResponse(reply, webResponse)
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { fastifyReactRouter } from "./fastify.ts"
2
+ export type { FastifyReactRouterOptions } from "./fastify.ts"
3
+ export { createRequestHandler } from "./handler.ts"
4
+ export { createHeaders, createRequest, createUrl } from "./request.ts"
5
+ export { sendResponse } from "./response.ts"
6
+ export type {
7
+ CreateRequestHandlerOptions,
8
+ GetLoadContextFunction,
9
+ HttpRequest,
10
+ HttpResponse,
11
+ ReactRouterLoadContext,
12
+ RequestHandler,
13
+ } from "./handler.ts"
package/src/request.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { Readable } from "node:stream"
2
+
3
+ import { createReadableStreamFromReadable } from "@react-router/node"
4
+ import type { FastifyReply, FastifyRequest } from "fastify"
5
+
6
+ /**
7
+ * Copies Fastify's normalized request headers into Web Fetch `Headers`.
8
+ *
9
+ * @param source Fastify request headers.
10
+ * @returns Fetch-compatible headers.
11
+ */
12
+ export function createHeaders(source: FastifyRequest["headers"]): Headers {
13
+ let headers = new Headers()
14
+
15
+ for (let [name, value] of Object.entries(source)) {
16
+ if (value == null) continue
17
+
18
+ if (Array.isArray(value)) {
19
+ for (let item of value) {
20
+ headers.append(name, item)
21
+ }
22
+ } else {
23
+ headers.set(name, value)
24
+ }
25
+ }
26
+
27
+ return headers
28
+ }
29
+
30
+ /**
31
+ * Builds the absolute request URL that React Router expects.
32
+ *
33
+ * @param request Fastify request.
34
+ * @returns Absolute URL for the original incoming request.
35
+ */
36
+ export function createUrl(request: FastifyRequest): string {
37
+ return `${request.protocol}://${request.host}${request.originalUrl}`
38
+ }
39
+
40
+ /**
41
+ * Adapts a Fastify request to a Web Fetch `Request`.
42
+ *
43
+ * @param request Fastify request.
44
+ * @param reply Fastify reply, used to abort work when the connection closes.
45
+ * @returns Fetch-compatible request for React Router.
46
+ */
47
+ export function createRequest(
48
+ request: FastifyRequest,
49
+ reply: FastifyReply,
50
+ ): Request {
51
+ let controller: AbortController | null = new AbortController()
52
+
53
+ let init: RequestInit & { duplex?: "half" } = {
54
+ method: request.method,
55
+ headers: createHeaders(request.headers),
56
+ signal: controller.signal,
57
+ }
58
+
59
+ reply.raw.once("finish", () => {
60
+ controller = null
61
+ })
62
+ reply.raw.once("close", () => {
63
+ controller?.abort()
64
+ })
65
+
66
+ if (["GET", "HEAD"].includes(request.method) === false) {
67
+ init.body = getBody(request)
68
+ init.duplex = "half"
69
+ }
70
+
71
+ return new Request(createUrl(request), init)
72
+ }
73
+
74
+ function getBody(request: FastifyRequest): BodyInit | null {
75
+ let body = request.body
76
+
77
+ if (body == null) return createReadableStreamFromReadable(request.raw)
78
+ if (body instanceof Readable) return createReadableStreamFromReadable(body)
79
+ if (body instanceof ReadableStream) return body
80
+ if (body instanceof URLSearchParams) return body
81
+ if (body instanceof ArrayBuffer) return body
82
+ if (body instanceof Blob) return body
83
+ if (body instanceof FormData) return body
84
+ if (ArrayBuffer.isView(body)) {
85
+ return new Uint8Array(
86
+ body.buffer as ArrayBuffer,
87
+ body.byteOffset,
88
+ body.byteLength,
89
+ )
90
+ }
91
+ if (typeof body === "string") return body
92
+
93
+ return JSON.stringify(body)
94
+ }
@@ -0,0 +1,65 @@
1
+ import { Readable } from "node:stream"
2
+
3
+ import type { FastifyReply } from "fastify"
4
+
5
+ /**
6
+ * Writes a Web Fetch `Response` through a Fastify reply.
7
+ *
8
+ * @param reply Fastify reply.
9
+ * @param response React Router response.
10
+ * @returns A promise that settles after the response is sent.
11
+ */
12
+ export async function sendResponse(
13
+ reply: FastifyReply,
14
+ response: Response,
15
+ ): Promise<void> {
16
+ reply.status(response.status)
17
+ writeHeaders(reply, response.headers)
18
+
19
+ if (response.body == null) {
20
+ return reply.send()
21
+ }
22
+
23
+ return reply.send(readableFromWeb(response.body))
24
+ }
25
+
26
+ function writeHeaders(reply: FastifyReply, headers: Headers): void {
27
+ let cookies = readSetCookies(headers)
28
+
29
+ for (let [name, value] of headers) {
30
+ if (name.toLowerCase() === "set-cookie" && cookies.length > 0) continue
31
+ reply.header(name, value)
32
+ }
33
+
34
+ if (cookies.length > 0) {
35
+ reply.header("set-cookie", cookies)
36
+ }
37
+ }
38
+
39
+ function readSetCookies(headers: Headers): string[] {
40
+ let withCookies = headers as Headers & { getSetCookie?: () => string[] }
41
+ let cookies = withCookies.getSetCookie?.()
42
+ if (cookies && cookies.length > 0) return cookies
43
+
44
+ let cookie = headers.get("Set-Cookie")
45
+ return cookie ? [cookie] : []
46
+ }
47
+
48
+ function readableFromWeb(body: ReadableStream<Uint8Array>): Readable {
49
+ let reader = body.getReader()
50
+
51
+ return new Readable({
52
+ read() {
53
+ reader.read().then(
54
+ ({ done, value }) => {
55
+ this.push(done ? null : Buffer.from(value))
56
+ },
57
+ (error: unknown) => {
58
+ this.destroy(
59
+ error instanceof Error ? error : new Error(String(error)),
60
+ )
61
+ },
62
+ )
63
+ },
64
+ })
65
+ }
@@ -0,0 +1,27 @@
1
+ import type { DevEnvironment, ViteDevServer } from "vite"
2
+
3
+ interface RunnableEnvironment extends DevEnvironment {
4
+ runner: {
5
+ import(id: string): Promise<Record<string, unknown>>
6
+ }
7
+ }
8
+
9
+ /**
10
+ * Imports an SSR module using Vite's current Environment runner, with
11
+ * `ssrLoadModule` as a compatibility fallback.
12
+ *
13
+ * @param vite Vite dev server.
14
+ * @param id Module ID to import.
15
+ * @returns Imported module namespace.
16
+ */
17
+ export async function importSsrModule<T = Record<string, unknown>>(
18
+ vite: ViteDevServer,
19
+ id: string,
20
+ ): Promise<T> {
21
+ let ssr = vite.environments?.ssr as Partial<RunnableEnvironment> | undefined
22
+ if (typeof ssr?.runner?.import === "function") {
23
+ return ssr.runner.import(id) as Promise<T>
24
+ }
25
+
26
+ return vite.ssrLoadModule(id) as unknown as Promise<T>
27
+ }