@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/src/vite.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
import type { FastifyInstance } from "fastify"
|
|
6
|
+
import type { Plugin, ViteDevServer } from "vite"
|
|
7
|
+
|
|
8
|
+
import { importSsrModule } from "./vite-runtime.ts"
|
|
9
|
+
|
|
10
|
+
const adapterPackageNames = [
|
|
11
|
+
"@mcansh/react-router-fastify",
|
|
12
|
+
"@mcansh/remix-fastify",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export type FastifyAppFactory = (
|
|
16
|
+
vite: ViteDevServer,
|
|
17
|
+
) => FastifyInstance | Promise<FastifyInstance>
|
|
18
|
+
|
|
19
|
+
export interface FastifyReactRouterDevOptions {
|
|
20
|
+
/** Server module loaded by Vite during `react-router dev`. */
|
|
21
|
+
entry?: string
|
|
22
|
+
/** Named export that creates the Fastify app. Falls back to `default`. */
|
|
23
|
+
exportName?: string
|
|
24
|
+
/**
|
|
25
|
+
* Keeps local modules imported by the Fastify server entry external in the
|
|
26
|
+
* React Router SSR build. This preserves singleton module identity for shared
|
|
27
|
+
* values such as React Router context tokens.
|
|
28
|
+
*
|
|
29
|
+
* Pass an array to externalize explicit import specifiers instead.
|
|
30
|
+
*/
|
|
31
|
+
externalizeServerEntryImports?: boolean | string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Vite plugin that lets `react-router dev` serve through a Fastify app.
|
|
36
|
+
*
|
|
37
|
+
* Vite continues to handle its internal client/HMR/module middleware first.
|
|
38
|
+
* Fastify receives the remaining requests, including the React Router catch-all
|
|
39
|
+
* installed by `fastifyReactRouter`.
|
|
40
|
+
*
|
|
41
|
+
* @param options Development server entry options.
|
|
42
|
+
* @returns Vite plugin.
|
|
43
|
+
*/
|
|
44
|
+
export function fastifyReactRouterDev(
|
|
45
|
+
options: FastifyReactRouterDevOptions = {},
|
|
46
|
+
): Plugin {
|
|
47
|
+
let {
|
|
48
|
+
entry = "./server.ts",
|
|
49
|
+
exportName = "createServer",
|
|
50
|
+
externalizeServerEntryImports = true,
|
|
51
|
+
} = options
|
|
52
|
+
let command: "build" | "serve" = "serve"
|
|
53
|
+
let root = process.cwd()
|
|
54
|
+
let serverEntryImportSpecifiers = new Set<string>()
|
|
55
|
+
let serverEntryImportFiles = new Set<string>()
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name: "fastify-react-router-dev",
|
|
59
|
+
enforce: "pre",
|
|
60
|
+
config(_config, environment) {
|
|
61
|
+
if (environment.command !== "serve") return
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ssr: {
|
|
65
|
+
noExternal: adapterPackageNames,
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
configResolved(config) {
|
|
70
|
+
command = config.command
|
|
71
|
+
root = config.root
|
|
72
|
+
|
|
73
|
+
let resolvedEntry = path.resolve(root, entry)
|
|
74
|
+
let externals = collectServerEntryExternals(
|
|
75
|
+
resolvedEntry,
|
|
76
|
+
externalizeServerEntryImports,
|
|
77
|
+
)
|
|
78
|
+
serverEntryImportSpecifiers = externals.specifiers
|
|
79
|
+
serverEntryImportFiles = externals.files
|
|
80
|
+
},
|
|
81
|
+
async resolveId(source, importer, resolveOptions) {
|
|
82
|
+
if (command !== "build" || !resolveOptions.ssr) return null
|
|
83
|
+
if (shouldExternalizeSpecifier(source) === false) return null
|
|
84
|
+
|
|
85
|
+
if (serverEntryImportSpecifiers.has(source)) {
|
|
86
|
+
return { id: source, external: true }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (importer == null) return null
|
|
90
|
+
|
|
91
|
+
let resolved = await this.resolve(source, importer, {
|
|
92
|
+
...resolveOptions,
|
|
93
|
+
skipSelf: true,
|
|
94
|
+
})
|
|
95
|
+
if (resolved == null) return null
|
|
96
|
+
|
|
97
|
+
let filePath = toFilePath(resolved.id)
|
|
98
|
+
if (filePath == null || serverEntryImportFiles.has(filePath) === false)
|
|
99
|
+
return null
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id: source.startsWith("#") ? source : filePath,
|
|
103
|
+
external: true,
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
configureServer(vite) {
|
|
107
|
+
let resolvedEntry = path.resolve(vite.config.root, entry)
|
|
108
|
+
let appPromise: Promise<FastifyInstance> | undefined
|
|
109
|
+
|
|
110
|
+
async function closeApp(): Promise<void> {
|
|
111
|
+
let current = appPromise
|
|
112
|
+
appPromise = undefined
|
|
113
|
+
if (current == null) return
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
let app = await current
|
|
117
|
+
await app.close()
|
|
118
|
+
} catch {
|
|
119
|
+
// The app may have failed while loading; there is nothing to close.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function loadApp(): Promise<FastifyInstance> {
|
|
124
|
+
if (appPromise == null) {
|
|
125
|
+
appPromise = (async () => {
|
|
126
|
+
let module = await importSsrModule(vite, resolvedEntry)
|
|
127
|
+
let factory = module[exportName] ?? module.default
|
|
128
|
+
|
|
129
|
+
if (typeof factory !== "function") {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`[fastify-react-router-dev] Expected ${entry} to export "${exportName}" or a default Fastify app factory.`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let app = await (factory as FastifyAppFactory)(vite)
|
|
136
|
+
await app.ready()
|
|
137
|
+
return app
|
|
138
|
+
})()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return appPromise
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
vite.watcher.on("change", () => {
|
|
145
|
+
void closeApp()
|
|
146
|
+
})
|
|
147
|
+
vite.watcher.on("unlink", () => {
|
|
148
|
+
void closeApp()
|
|
149
|
+
})
|
|
150
|
+
vite.httpServer?.once("close", () => {
|
|
151
|
+
void closeApp()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
vite.middlewares.use(async (request, response, next) => {
|
|
156
|
+
try {
|
|
157
|
+
let app = await loadApp()
|
|
158
|
+
app.routing(request, response)
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof Error) vite.ssrFixStacktrace(error)
|
|
161
|
+
next(error)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function collectServerEntryExternals(
|
|
170
|
+
entryPath: string,
|
|
171
|
+
externalizeServerEntryImports: boolean | string[],
|
|
172
|
+
): {
|
|
173
|
+
specifiers: Set<string>
|
|
174
|
+
files: Set<string>
|
|
175
|
+
} {
|
|
176
|
+
let specifiers = new Set<string>()
|
|
177
|
+
let files = new Set<string>()
|
|
178
|
+
|
|
179
|
+
if (externalizeServerEntryImports === false) {
|
|
180
|
+
return { specifiers, files }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let imports = Array.isArray(externalizeServerEntryImports)
|
|
184
|
+
? externalizeServerEntryImports
|
|
185
|
+
: readImportSpecifiers(entryPath)
|
|
186
|
+
|
|
187
|
+
for (let specifier of imports) {
|
|
188
|
+
if (shouldExternalizeSpecifier(specifier) === false) continue
|
|
189
|
+
|
|
190
|
+
specifiers.add(specifier)
|
|
191
|
+
|
|
192
|
+
if (isLocalSpecifier(specifier)) {
|
|
193
|
+
let file = resolveLocalImport(entryPath, specifier)
|
|
194
|
+
if (file) files.add(file)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { specifiers, files }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readImportSpecifiers(filePath: string): string[] {
|
|
202
|
+
if (fs.existsSync(filePath) === false) return []
|
|
203
|
+
|
|
204
|
+
let source = fs.readFileSync(filePath, "utf8")
|
|
205
|
+
let specifiers = new Set<string>()
|
|
206
|
+
let importPattern =
|
|
207
|
+
/\b(?:import|export)\s+(?:[^'"]*?\s+from\s*)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g
|
|
208
|
+
|
|
209
|
+
for (let match of source.matchAll(importPattern)) {
|
|
210
|
+
specifiers.add(match[1] ?? match[2])
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return [...specifiers]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function shouldExternalizeSpecifier(specifier: string): boolean {
|
|
217
|
+
return specifier.startsWith("#") || isLocalSpecifier(specifier)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isLocalSpecifier(specifier: string): boolean {
|
|
221
|
+
return specifier.startsWith(".") || specifier.startsWith("/")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveLocalImport(
|
|
225
|
+
importer: string,
|
|
226
|
+
specifier: string,
|
|
227
|
+
): string | undefined {
|
|
228
|
+
let resolved = path.resolve(path.dirname(importer), specifier)
|
|
229
|
+
return resolveExistingFile(resolved)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveExistingFile(filePath: string): string | undefined {
|
|
233
|
+
if (isFile(filePath)) return normalizePath(filePath)
|
|
234
|
+
|
|
235
|
+
for (let extension of [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"]) {
|
|
236
|
+
let candidate = `${filePath}${extension}`
|
|
237
|
+
if (isFile(candidate)) return normalizePath(candidate)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (let extension of [".ts", ".tsx", ".mts", ".mjs", ".js", ".jsx"]) {
|
|
241
|
+
let candidate = path.join(filePath, `index${extension}`)
|
|
242
|
+
if (isFile(candidate)) return normalizePath(candidate)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return undefined
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isFile(filePath: string): boolean {
|
|
249
|
+
try {
|
|
250
|
+
return fs.statSync(filePath).isFile()
|
|
251
|
+
} catch {
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function toFilePath(id: string): string | undefined {
|
|
257
|
+
let withoutQuery = id.replace(/[?#].*$/, "")
|
|
258
|
+
|
|
259
|
+
if (withoutQuery.startsWith("file://")) {
|
|
260
|
+
return normalizePath(fileURLToPath(withoutQuery))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (path.isAbsolute(withoutQuery)) {
|
|
264
|
+
return normalizePath(withoutQuery)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function normalizePath(filePath: string): string {
|
|
271
|
+
return filePath.split(path.sep).join("/")
|
|
272
|
+
}
|