@reckona/mreact-router 0.0.175 → 0.0.176
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/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +5 -49
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/build.d.ts +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -0
- package/dist/build.js.map +1 -1
- package/dist/built-assets.d.ts +11 -0
- package/dist/built-assets.d.ts.map +1 -0
- package/dist/built-assets.js +130 -0
- package/dist/built-assets.js.map +1 -0
- package/dist/built-runtime.d.ts +53 -0
- package/dist/built-runtime.d.ts.map +1 -0
- package/dist/built-runtime.js +178 -0
- package/dist/built-runtime.js.map +1 -0
- package/dist/built-server-module-artifacts.d.ts +19 -0
- package/dist/built-server-module-artifacts.d.ts.map +1 -0
- package/dist/built-server-module-artifacts.js +217 -0
- package/dist/built-server-module-artifacts.js.map +1 -0
- package/dist/client-manifest-assets.d.ts +16 -0
- package/dist/client-manifest-assets.d.ts.map +1 -0
- package/dist/client-manifest-assets.js +60 -0
- package/dist/client-manifest-assets.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -3
- package/dist/client.js.map +1 -1
- package/dist/node-server.d.ts +29 -0
- package/dist/node-server.d.ts.map +1 -0
- package/dist/node-server.js +81 -0
- package/dist/node-server.js.map +1 -0
- package/dist/render.d.ts +9 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +20 -41
- package/dist/render.js.map +1 -1
- package/dist/route-dispatch.d.ts +2 -0
- package/dist/route-dispatch.d.ts.map +1 -0
- package/dist/route-dispatch.js +16 -0
- package/dist/route-dispatch.js.map +1 -0
- package/dist/route-loader-runtime.d.ts +22 -0
- package/dist/route-loader-runtime.d.ts.map +1 -0
- package/dist/route-loader-runtime.js +30 -0
- package/dist/route-loader-runtime.js.map +1 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +48 -1
- package/dist/routes.js.map +1 -1
- package/dist/serve.d.ts +4 -3
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +84 -582
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/adapters/cloudflare.ts +5 -60
- package/src/build.ts +143 -0
- package/src/built-assets.ts +164 -0
- package/src/built-runtime.ts +312 -0
- package/src/built-server-module-artifacts.ts +340 -0
- package/src/client-manifest-assets.ts +92 -0
- package/src/client.ts +31 -3
- package/src/node-server.ts +129 -0
- package/src/render.ts +38 -60
- package/src/route-dispatch.ts +17 -0
- package/src/route-loader-runtime.ts +54 -0
- package/src/routes.ts +70 -1
- package/src/serve.ts +105 -863
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, sep } from "node:path";
|
|
3
|
+
import type { BuiltServerModuleArtifact } from "./build.js";
|
|
4
|
+
import { routeShellCandidates } from "./route-shells.js";
|
|
5
|
+
|
|
6
|
+
export interface BuiltServerModuleArtifactRuntime {
|
|
7
|
+
appDir: string;
|
|
8
|
+
serverModuleArtifactLoads: Map<string, Promise<void>>;
|
|
9
|
+
serverModuleClosureFiles: Map<string, readonly string[]>;
|
|
10
|
+
serverModuleFiles: ReadonlyMap<string, string>;
|
|
11
|
+
serverModuleRenderFiles: ReadonlyMap<string, string>;
|
|
12
|
+
serverModuleRequestFiles: ReadonlyMap<string, string>;
|
|
13
|
+
serverModules: Map<string, BuiltServerModuleArtifact>;
|
|
14
|
+
serverSourceFiles: ReadonlyMap<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type BuiltServerModuleArtifactKind = "all" | "render" | "request";
|
|
18
|
+
|
|
19
|
+
export async function loadBuiltServerModuleArtifacts(
|
|
20
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
21
|
+
files: Iterable<string>,
|
|
22
|
+
kind: BuiltServerModuleArtifactKind = "all",
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
await loadBuiltServerModuleArtifact(runtime, file, kind);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function loadBuiltServerModuleArtifact(
|
|
30
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
31
|
+
file: string,
|
|
32
|
+
kind: BuiltServerModuleArtifactKind = "all",
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
if (kind === "all") {
|
|
35
|
+
await loadBuiltServerModuleArtifact(runtime, file, "request");
|
|
36
|
+
await loadBuiltServerModuleArtifact(runtime, file, "render");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const artifactPath =
|
|
41
|
+
kind === "request"
|
|
42
|
+
? runtime.serverModuleRequestFiles.get(file) ?? runtime.serverModuleFiles.get(file)
|
|
43
|
+
: runtime.serverModuleRenderFiles.get(file) ?? runtime.serverModuleFiles.get(file);
|
|
44
|
+
|
|
45
|
+
if (artifactPath === undefined) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cached = runtime.serverModuleArtifactLoads.get(`${kind}\0${file}`);
|
|
50
|
+
|
|
51
|
+
if (cached !== undefined) {
|
|
52
|
+
await cached;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const loaded = readFile(artifactPath, "utf8")
|
|
57
|
+
.then((text) => {
|
|
58
|
+
const existing = runtime.serverModules.get(file) ?? {};
|
|
59
|
+
runtime.serverModules.set(
|
|
60
|
+
file,
|
|
61
|
+
mergeBuiltServerModuleArtifacts(
|
|
62
|
+
existing,
|
|
63
|
+
hydrateBuiltServerModuleArtifact(
|
|
64
|
+
parseBuiltJsonArtifact<BuiltServerModuleArtifact>(
|
|
65
|
+
text,
|
|
66
|
+
artifactPath,
|
|
67
|
+
`built server module artifact for ${file}`,
|
|
68
|
+
),
|
|
69
|
+
builtServerDirForArtifactPath(artifactPath),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
})
|
|
74
|
+
.catch((error) => {
|
|
75
|
+
runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
|
|
76
|
+
if (isMissingFileError(error)) {
|
|
77
|
+
throw builtArtifactReadError(`built server module artifact for ${file}`, artifactPath, error);
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
});
|
|
81
|
+
runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
|
|
82
|
+
|
|
83
|
+
await loaded;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function allBuiltServerModuleFiles(
|
|
87
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
88
|
+
): Iterable<string> {
|
|
89
|
+
return new Set([
|
|
90
|
+
...runtime.serverModuleFiles.keys(),
|
|
91
|
+
...runtime.serverModuleRequestFiles.keys(),
|
|
92
|
+
...runtime.serverModuleRenderFiles.keys(),
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function builtServerDirForArtifactPath(artifactPath: string): string {
|
|
97
|
+
const marker = `${sep}server-modules${sep}`;
|
|
98
|
+
const index = artifactPath.lastIndexOf(marker);
|
|
99
|
+
|
|
100
|
+
return index === -1 ? dirname(dirname(artifactPath)) : artifactPath.slice(0, index);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function hydrateBuiltServerModuleArtifact(
|
|
104
|
+
artifact: BuiltServerModuleArtifact,
|
|
105
|
+
serverDir: string,
|
|
106
|
+
): BuiltServerModuleArtifact {
|
|
107
|
+
return {
|
|
108
|
+
...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
|
|
109
|
+
...(artifact.loader === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { loader: hydrateBuiltServerModuleOutput(artifact.loader, serverDir) }),
|
|
112
|
+
...(artifact.routeMetadata === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { routeMetadata: hydrateBuiltServerModuleOutput(artifact.routeMetadata, serverDir) }),
|
|
115
|
+
...(artifact.request === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { request: hydrateBuiltServerModuleOutput(artifact.request, serverDir) }),
|
|
118
|
+
...(artifact.stream === undefined
|
|
119
|
+
? {}
|
|
120
|
+
: { stream: hydrateBuiltServerModuleOutput(artifact.stream, serverDir) }),
|
|
121
|
+
...(artifact.string === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { string: hydrateBuiltServerModuleOutput(artifact.string, serverDir) }),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hydrateBuiltServerModuleOutput<T extends { moduleFile?: string | undefined }>(
|
|
128
|
+
output: T,
|
|
129
|
+
serverDir: string,
|
|
130
|
+
): T {
|
|
131
|
+
if (output.moduleFile === undefined || isAbsolute(output.moduleFile)) {
|
|
132
|
+
return output;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...output,
|
|
137
|
+
moduleFile: join(serverDir, safeBuiltServerManifestFilePath(output.moduleFile)),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mergeBuiltServerModuleArtifacts(
|
|
142
|
+
existing: BuiltServerModuleArtifact,
|
|
143
|
+
loaded: BuiltServerModuleArtifact,
|
|
144
|
+
): BuiltServerModuleArtifact {
|
|
145
|
+
return {
|
|
146
|
+
...existing,
|
|
147
|
+
...loaded,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function loadBuiltServerModuleArtifactsForRequest(
|
|
152
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
153
|
+
routeFile: string | undefined,
|
|
154
|
+
options: {
|
|
155
|
+
includeRender?: boolean | undefined;
|
|
156
|
+
includeShells?: boolean | undefined;
|
|
157
|
+
} = {},
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
const roots = [
|
|
160
|
+
join(runtime.appDir, "middleware.ts"),
|
|
161
|
+
join(runtime.appDir, "middleware.mreact.ts"),
|
|
162
|
+
...(routeFile === undefined
|
|
163
|
+
? []
|
|
164
|
+
: [
|
|
165
|
+
routeFile,
|
|
166
|
+
...(options.includeShells === false ? [] : shellFilesForRoute(runtime, routeFile)),
|
|
167
|
+
]),
|
|
168
|
+
];
|
|
169
|
+
const seen = new Set<string>();
|
|
170
|
+
|
|
171
|
+
for (const file of roots) {
|
|
172
|
+
await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "request");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (options.includeRender === true) {
|
|
176
|
+
seen.clear();
|
|
177
|
+
for (const file of roots) {
|
|
178
|
+
await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "render");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function loadBuiltServerModuleArtifactClosure(
|
|
184
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
185
|
+
file: string,
|
|
186
|
+
seen: Set<string>,
|
|
187
|
+
kind: BuiltServerModuleArtifactKind,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
for (const closureFile of builtServerModuleClosureFiles(runtime, file)) {
|
|
190
|
+
if (seen.has(closureFile)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
seen.add(closureFile);
|
|
194
|
+
await loadBuiltServerModuleArtifact(runtime, closureFile, kind);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function builtServerModuleClosureFiles(
|
|
199
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
200
|
+
file: string,
|
|
201
|
+
): readonly string[] {
|
|
202
|
+
const cached = runtime.serverModuleClosureFiles.get(file);
|
|
203
|
+
if (cached !== undefined) {
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const closure: string[] = [];
|
|
208
|
+
collectBuiltServerModuleClosureFiles(runtime, file, new Set(), closure);
|
|
209
|
+
runtime.serverModuleClosureFiles.set(file, closure);
|
|
210
|
+
return closure;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function collectBuiltServerModuleClosureFiles(
|
|
214
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
215
|
+
file: string,
|
|
216
|
+
seen: Set<string>,
|
|
217
|
+
closure: string[],
|
|
218
|
+
): void {
|
|
219
|
+
if (seen.has(file)) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
seen.add(file);
|
|
223
|
+
closure.push(file);
|
|
224
|
+
|
|
225
|
+
const source = runtime.serverSourceFiles.get(file);
|
|
226
|
+
if (source === undefined) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const specifier of localServerModuleSpecifiers(source)) {
|
|
231
|
+
const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
|
|
232
|
+
if (resolved !== undefined) {
|
|
233
|
+
collectBuiltServerModuleClosureFiles(runtime, resolved, seen, closure);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const localServerModuleImportPattern =
|
|
239
|
+
/\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
|
|
240
|
+
|
|
241
|
+
function localServerModuleSpecifiers(code: string): string[] {
|
|
242
|
+
const specifiers = new Set<string>();
|
|
243
|
+
localServerModuleImportPattern.lastIndex = 0;
|
|
244
|
+
|
|
245
|
+
for (const match of code.matchAll(localServerModuleImportPattern)) {
|
|
246
|
+
const source = match.groups?.source;
|
|
247
|
+
|
|
248
|
+
if (source !== undefined) {
|
|
249
|
+
specifiers.add(source);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return Array.from(specifiers);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function resolveBuiltLocalServerSourceImport(
|
|
257
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
258
|
+
fromFile: string,
|
|
259
|
+
specifier: string,
|
|
260
|
+
): string | undefined {
|
|
261
|
+
const base = join(dirname(fromFile), specifier);
|
|
262
|
+
|
|
263
|
+
for (const candidate of localServerSourceImportCandidates(base)) {
|
|
264
|
+
if (runtime.serverSourceFiles.has(candidate)) {
|
|
265
|
+
return candidate;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function localServerSourceImportCandidates(base: string): string[] {
|
|
273
|
+
const candidates = [base];
|
|
274
|
+
|
|
275
|
+
if (base.endsWith(".js")) {
|
|
276
|
+
const withoutJs = base.slice(0, -".js".length);
|
|
277
|
+
candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
|
|
278
|
+
} else if (base.endsWith(".jsx")) {
|
|
279
|
+
const withoutJsx = base.slice(0, -".jsx".length);
|
|
280
|
+
candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
|
|
281
|
+
} else if (base.endsWith(".mreact")) {
|
|
282
|
+
candidates.push(`${base}.tsx`);
|
|
283
|
+
} else {
|
|
284
|
+
candidates.push(
|
|
285
|
+
`${base}.ts`,
|
|
286
|
+
`${base}.tsx`,
|
|
287
|
+
`${base}.mreact.tsx`,
|
|
288
|
+
join(base, "index.ts"),
|
|
289
|
+
join(base, "index.tsx"),
|
|
290
|
+
join(base, "index.mreact.tsx"),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return candidates;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function shellFilesForRoute(
|
|
298
|
+
runtime: BuiltServerModuleArtifactRuntime,
|
|
299
|
+
routeFile: string,
|
|
300
|
+
): string[] {
|
|
301
|
+
return routeShellCandidates(runtime.appDir, routeFile)
|
|
302
|
+
.map((candidate) => candidate.file)
|
|
303
|
+
.filter((file) => runtime.serverSourceFiles.has(file));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isMissingFileError(error: unknown): boolean {
|
|
307
|
+
return (
|
|
308
|
+
typeof error === "object" &&
|
|
309
|
+
error !== null &&
|
|
310
|
+
"code" in error &&
|
|
311
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function builtArtifactReadError(label: string, artifactPath: string, error: unknown): Error {
|
|
316
|
+
const prefix = isMissingFileError(error) ? "Missing" : "Unable to read";
|
|
317
|
+
const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
|
|
318
|
+
|
|
319
|
+
return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
|
|
323
|
+
try {
|
|
324
|
+
return JSON.parse(text) as T;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
|
|
327
|
+
|
|
328
|
+
throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function safeBuiltServerManifestFilePath(pathname: string): string {
|
|
333
|
+
const normalized = normalize(pathname);
|
|
334
|
+
|
|
335
|
+
if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
|
|
336
|
+
throw new Error(`Invalid built app manifest file path: ${pathname}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return normalized;
|
|
340
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface ClientManifestAssetRoute {
|
|
2
|
+
css?: readonly string[] | undefined;
|
|
3
|
+
imports?: readonly string[] | undefined;
|
|
4
|
+
navigationScript?: string | undefined;
|
|
5
|
+
script?: string | undefined;
|
|
6
|
+
sourceMap?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ClientManifestAssetSource {
|
|
10
|
+
assets?: readonly string[] | undefined;
|
|
11
|
+
routes: readonly ClientManifestAssetRoute[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clientManifestAssetPaths(
|
|
15
|
+
manifest: ClientManifestAssetSource,
|
|
16
|
+
options: {
|
|
17
|
+
extraPaths?: readonly string[] | undefined;
|
|
18
|
+
prefix?: string | undefined;
|
|
19
|
+
} = {},
|
|
20
|
+
): Set<string> {
|
|
21
|
+
const prefix = normalizeClientManifestAssetPrefix(options.prefix ?? "");
|
|
22
|
+
const paths = new Set<string>([`${prefix}manifest.json`]);
|
|
23
|
+
|
|
24
|
+
for (const route of manifest.routes) {
|
|
25
|
+
for (const asset of [
|
|
26
|
+
route.script,
|
|
27
|
+
route.sourceMap,
|
|
28
|
+
route.navigationScript,
|
|
29
|
+
...(route.css ?? []),
|
|
30
|
+
...(route.imports ?? []),
|
|
31
|
+
]) {
|
|
32
|
+
const path = safeClientManifestAssetPath(asset);
|
|
33
|
+
|
|
34
|
+
if (path !== undefined) {
|
|
35
|
+
paths.add(`${prefix}${path}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const asset of manifest.assets ?? []) {
|
|
41
|
+
const path = safeClientManifestAssetPath(asset);
|
|
42
|
+
|
|
43
|
+
if (path !== undefined) {
|
|
44
|
+
paths.add(`${prefix}${path}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const asset of options.extraPaths ?? []) {
|
|
49
|
+
const path = safeClientManifestAssetPath(asset);
|
|
50
|
+
|
|
51
|
+
if (path !== undefined) {
|
|
52
|
+
paths.add(`${prefix}${path}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return paths;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeClientManifestAssetPrefix(prefix: string): string {
|
|
60
|
+
if (prefix === "") {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
65
|
+
|
|
66
|
+
return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeClientManifestAssetPath(asset: string | undefined): string | undefined {
|
|
70
|
+
if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const segments = asset.split("/");
|
|
75
|
+
|
|
76
|
+
return segments.some((segment) => unsafeClientManifestAssetSegment(segment))
|
|
77
|
+
? undefined
|
|
78
|
+
: segments.join("/");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function unsafeClientManifestAssetSegment(segment: string): boolean {
|
|
82
|
+
if (segment === "" || segment === "." || segment === "..") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const decoded = decodeURIComponent(segment);
|
|
88
|
+
return decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\");
|
|
89
|
+
} catch {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -2657,7 +2657,7 @@ export async function __mreactPrefetch(url) {
|
|
|
2657
2657
|
return true;
|
|
2658
2658
|
}
|
|
2659
2659
|
|
|
2660
|
-
|
|
2660
|
+
__mreactRememberPrefetchedUrl(href);
|
|
2661
2661
|
|
|
2662
2662
|
const script = __mreactRouteScriptForNavigationUrl(href);
|
|
2663
2663
|
|
|
@@ -2684,6 +2684,33 @@ function __mreactPrefetchNavigationHtml(href) {
|
|
|
2684
2684
|
}
|
|
2685
2685
|
|
|
2686
2686
|
const __mreactNavigationHtmlCacheMaxEntries = 64;
|
|
2687
|
+
const __mreactNavigationPrefetchHistoryMaxEntries = 64;
|
|
2688
|
+
|
|
2689
|
+
function __mreactRememberPrefetchedUrl(href) {
|
|
2690
|
+
__mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedUrls, href);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function __mreactRememberPrefetchedScript(href) {
|
|
2694
|
+
__mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedScripts, href);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
function __mreactRememberPrefetchHistory(history, href) {
|
|
2698
|
+
if (history.has(href)) {
|
|
2699
|
+
history.delete(href);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
history.add(href);
|
|
2703
|
+
|
|
2704
|
+
while (history.size > __mreactNavigationPrefetchHistoryMaxEntries) {
|
|
2705
|
+
const oldestHref = history.keys().next().value;
|
|
2706
|
+
|
|
2707
|
+
if (oldestHref === undefined) {
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
history.delete(oldestHref);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2687
2714
|
|
|
2688
2715
|
function __mreactCachedNavigationHtml(href) {
|
|
2689
2716
|
const html = __mreactNavigationState.cache.get(href);
|
|
@@ -2712,6 +2739,7 @@ function __mreactRememberNavigationHtml(href, html) {
|
|
|
2712
2739
|
}
|
|
2713
2740
|
|
|
2714
2741
|
__mreactNavigationState.cache.delete(oldestHref);
|
|
2742
|
+
__mreactNavigationState.prefetchedUrls.delete(oldestHref);
|
|
2715
2743
|
}
|
|
2716
2744
|
}
|
|
2717
2745
|
|
|
@@ -2732,7 +2760,7 @@ function __mreactPrefetchRouteScript(script) {
|
|
|
2732
2760
|
|
|
2733
2761
|
for (const link of Array.from(document.querySelectorAll('link[rel="modulepreload"][href]'))) {
|
|
2734
2762
|
if (link.href === href) {
|
|
2735
|
-
|
|
2763
|
+
__mreactRememberPrefetchedScript(href);
|
|
2736
2764
|
return true;
|
|
2737
2765
|
}
|
|
2738
2766
|
}
|
|
@@ -2741,7 +2769,7 @@ function __mreactPrefetchRouteScript(script) {
|
|
|
2741
2769
|
link.rel = "modulepreload";
|
|
2742
2770
|
link.href = href;
|
|
2743
2771
|
document.head.appendChild(link);
|
|
2744
|
-
|
|
2772
|
+
__mreactRememberPrefetchedScript(href);
|
|
2745
2773
|
return true;
|
|
2746
2774
|
}
|
|
2747
2775
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createServer, type Server } from "node:http";
|
|
2
|
+
import { nodeRequestToWebRequest, sendResponse } from "./http.js";
|
|
3
|
+
import {
|
|
4
|
+
emitRouterLog,
|
|
5
|
+
logDurationMs,
|
|
6
|
+
logError,
|
|
7
|
+
logNow,
|
|
8
|
+
nodeRequestPath,
|
|
9
|
+
requestLogFields,
|
|
10
|
+
type AppRouterLogger,
|
|
11
|
+
} from "./logger.js";
|
|
12
|
+
import type { HttpUpgradeHandler } from "./upgrade.js";
|
|
13
|
+
|
|
14
|
+
export interface StartNodeRequestServerOptions {
|
|
15
|
+
allowedHosts?: readonly string[] | undefined;
|
|
16
|
+
errorHandler?: ((error: unknown) => {
|
|
17
|
+
body: string;
|
|
18
|
+
status: number;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}) | undefined;
|
|
21
|
+
hostname?: string | undefined;
|
|
22
|
+
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
23
|
+
logger?: AppRouterLogger | undefined;
|
|
24
|
+
onUpgrade?: HttpUpgradeHandler | undefined;
|
|
25
|
+
port: number;
|
|
26
|
+
render(request: Request): Promise<Response>;
|
|
27
|
+
resolveHost?: ((options: {
|
|
28
|
+
allowedHosts?: readonly string[] | undefined;
|
|
29
|
+
fallbackHost: string;
|
|
30
|
+
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
31
|
+
rawHost: string | undefined;
|
|
32
|
+
}) => string) | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function startNodeRequestServer(
|
|
36
|
+
options: StartNodeRequestServerOptions,
|
|
37
|
+
): Promise<{ close(): Promise<void>; server: Server; url: string }> {
|
|
38
|
+
const server = createServer(async (incoming, outgoing) => {
|
|
39
|
+
const startedAt = logNow();
|
|
40
|
+
const fallbackRequestFields = {
|
|
41
|
+
method: incoming.method ?? "GET",
|
|
42
|
+
path: nodeRequestPath(incoming.url),
|
|
43
|
+
runtime: "node" as const,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const fallbackHost = `${options.hostname ?? "127.0.0.1"}:${options.port}`;
|
|
48
|
+
const host = (options.resolveHost ?? defaultResolveRequestHost)({
|
|
49
|
+
allowedHosts: options.allowedHosts,
|
|
50
|
+
fallbackHost,
|
|
51
|
+
hostPolicy: options.hostPolicy,
|
|
52
|
+
rawHost: incoming.headers.host,
|
|
53
|
+
});
|
|
54
|
+
const request = nodeRequestToWebRequest(incoming, `http://${host}`);
|
|
55
|
+
const logFields = requestLogFields(request, "node");
|
|
56
|
+
emitRouterLog(options.logger, "info", {
|
|
57
|
+
...logFields,
|
|
58
|
+
type: "router:request:start",
|
|
59
|
+
});
|
|
60
|
+
const response = await options.render(request);
|
|
61
|
+
emitRouterLog(options.logger, "info", {
|
|
62
|
+
...logFields,
|
|
63
|
+
durationMs: logDurationMs(startedAt),
|
|
64
|
+
status: response.status,
|
|
65
|
+
type: "router:request:end",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await sendResponse(outgoing, response);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Log the full stack to stderr for operator visibility; never put
|
|
71
|
+
// it in the response body where attackers can scrape it.
|
|
72
|
+
emitRouterLog(options.logger, "error", {
|
|
73
|
+
...fallbackRequestFields,
|
|
74
|
+
durationMs: logDurationMs(startedAt),
|
|
75
|
+
error: logError(error),
|
|
76
|
+
type: "router:request:error",
|
|
77
|
+
});
|
|
78
|
+
if (options.logger === undefined) {
|
|
79
|
+
console.error("[mreact] startServer request failed:", error);
|
|
80
|
+
}
|
|
81
|
+
const payload = options.errorHandler
|
|
82
|
+
? options.errorHandler(error)
|
|
83
|
+
: { body: "Internal Server Error", status: 500 };
|
|
84
|
+
outgoing.statusCode = payload.status;
|
|
85
|
+
outgoing.setHeader(
|
|
86
|
+
"content-type",
|
|
87
|
+
payload.headers?.["content-type"] ?? "text/plain; charset=utf-8",
|
|
88
|
+
);
|
|
89
|
+
for (const [name, value] of Object.entries(payload.headers ?? {})) {
|
|
90
|
+
if (name.toLowerCase() === "content-type") continue;
|
|
91
|
+
outgoing.setHeader(name, value);
|
|
92
|
+
}
|
|
93
|
+
outgoing.end(payload.body);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (options.onUpgrade !== undefined) {
|
|
98
|
+
server.on("upgrade", options.onUpgrade);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await new Promise<void>((resolve) =>
|
|
102
|
+
server.listen(options.port, options.hostname ?? "127.0.0.1", resolve),
|
|
103
|
+
);
|
|
104
|
+
const address = server.address();
|
|
105
|
+
const port = typeof address === "object" && address !== null ? address.port : options.port;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
server,
|
|
109
|
+
url: `http://${options.hostname ?? "127.0.0.1"}:${port}`,
|
|
110
|
+
close: () =>
|
|
111
|
+
new Promise<void>((resolve, reject) =>
|
|
112
|
+
server.close((error) => (error ? reject(error) : resolve())),
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function defaultResolveRequestHost(options: {
|
|
118
|
+
allowedHosts?: readonly string[] | undefined;
|
|
119
|
+
fallbackHost: string;
|
|
120
|
+
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
121
|
+
rawHost: string | undefined;
|
|
122
|
+
}): string {
|
|
123
|
+
const raw = options.rawHost;
|
|
124
|
+
if (raw === undefined || raw === "") return options.fallbackHost;
|
|
125
|
+
if (options.allowedHosts === undefined) {
|
|
126
|
+
return options.hostPolicy === "strict" ? options.fallbackHost : raw;
|
|
127
|
+
}
|
|
128
|
+
return options.allowedHosts.includes(raw) ? raw : options.fallbackHost;
|
|
129
|
+
}
|