@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/CHANGELOG.md +435 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.mjs +182 -0
- package/dist/index.mjs.map +1 -0
- package/dist/vite-runtime-Cuj_Fjkd.mjs +18 -0
- package/dist/vite-runtime-Cuj_Fjkd.mjs.map +1 -0
- package/dist/vite.d.mts +33 -0
- package/dist/vite.mjs +181 -0
- package/dist/vite.mjs.map +1 -0
- package/package.json +87 -0
- package/src/fastify.ts +131 -0
- package/src/handler.ts +52 -0
- package/src/index.ts +13 -0
- package/src/request.ts +94 -0
- package/src/response.ts +65 -0
- package/src/vite-runtime.ts +27 -0
- package/src/vite.ts +272 -0
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
|
+
}
|
package/src/response.ts
ADDED
|
@@ -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
|
+
}
|