@reckona/mreact-router 0.0.201 → 0.0.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/actions.d.ts +2 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +61 -41
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +3 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +74 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/node.d.ts +7 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +7 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +14 -3
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +137 -14
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +4 -1
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +27 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +83 -16
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +7 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +27 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +294 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/node-server.d.ts +6 -0
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +19 -1
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +7 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +99 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +20 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +190 -110
- package/dist/render.js.map +1 -1
- package/dist/request-header-tracking.d.ts +44 -0
- package/dist/request-header-tracking.d.ts.map +1 -0
- package/dist/request-header-tracking.js +116 -0
- package/dist/request-header-tracking.js.map +1 -0
- package/dist/route-source.d.ts +16 -0
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +55 -1
- package/dist/route-source.js.map +1 -1
- package/dist/security-headers.d.ts +16 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +37 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/serve.d.ts +10 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +132 -42
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +72 -56
- package/src/adapters/cloudflare.ts +118 -16
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +176 -12
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +127 -15
- package/src/cli-options.ts +39 -2
- package/src/cli.ts +7 -1
- package/src/client.ts +15 -2
- package/src/navigation-marker.ts +353 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +129 -0
- package/src/render.ts +267 -121
- package/src/request-header-tracking.ts +161 -0
- package/src/route-source.ts +71 -0
- package/src/security-headers.ts +41 -0
- package/src/serve.ts +188 -48
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observes request inputs read by application code during a render.
|
|
3
|
+
*
|
|
4
|
+
* The shared route cache is keyed on the app directory, route path, pathname
|
|
5
|
+
* and query only. A render that reads a request header therefore produces HTML
|
|
6
|
+
* that is only correct for that header value, and storing it would serve one
|
|
7
|
+
* visitor's variant to the next: `accept-language` mixes locales, `user-agent`
|
|
8
|
+
* mixes mobile and desktop shells, and `cf-connecting-ip` or `referer` disclose
|
|
9
|
+
* one visitor's data to another. Reads are observed rather than inferred from
|
|
10
|
+
* the source so that headers read through helper modules are covered too.
|
|
11
|
+
*
|
|
12
|
+
* Only the request handed to application code is tracked. Router internals keep
|
|
13
|
+
* using the original request, so their own header reads never mark a route as
|
|
14
|
+
* header dependent.
|
|
15
|
+
*
|
|
16
|
+
* Reconstructing the request with `new Request(request)` copies headers from
|
|
17
|
+
* internal slots that no interception can reach. Application contexts therefore
|
|
18
|
+
* mark access when their lazy `request` property is retrieved, before opaque
|
|
19
|
+
* helpers can reconstruct it. `render.ts` also uses conservative source closure
|
|
20
|
+
* analysis so a previously stored entry cannot bypass the runtime observation.
|
|
21
|
+
*/
|
|
22
|
+
export interface TrackedHeaderRequest {
|
|
23
|
+
/** Marks application access to the Request before it can escape tracking. */
|
|
24
|
+
markRequestAccess(): void;
|
|
25
|
+
/** Returns true once application code has observed request-specific input. */
|
|
26
|
+
requestDependent(): boolean;
|
|
27
|
+
/** The request to hand to loaders, metadata functions and components. */
|
|
28
|
+
request: Request;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const HEADER_READ_METHODS = new Set<string | symbol>([
|
|
32
|
+
"entries",
|
|
33
|
+
"forEach",
|
|
34
|
+
"get",
|
|
35
|
+
"getSetCookie",
|
|
36
|
+
"has",
|
|
37
|
+
"keys",
|
|
38
|
+
"values",
|
|
39
|
+
Symbol.iterator,
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Wraps a request so that header reads performed through it are recorded.
|
|
44
|
+
*
|
|
45
|
+
* Falls back to reporting every request as header dependent when the request
|
|
46
|
+
* cannot be cloned, so an unexpected runtime never silently enables sharing.
|
|
47
|
+
*/
|
|
48
|
+
export function trackRequestHeaderReads(request: Request): TrackedHeaderRequest {
|
|
49
|
+
let readAny = false;
|
|
50
|
+
|
|
51
|
+
let tracked: Request;
|
|
52
|
+
try {
|
|
53
|
+
// Cloning a request that carries a body would tee its stream and buffer
|
|
54
|
+
// whichever half goes unread. Only bodyless requests are worth tracking:
|
|
55
|
+
// a request with a body is reported as header dependent so that its
|
|
56
|
+
// response is never shared.
|
|
57
|
+
if (request.body !== null) {
|
|
58
|
+
return {
|
|
59
|
+
markRequestAccess() {},
|
|
60
|
+
requestDependent: () => true,
|
|
61
|
+
request,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
tracked = request.clone();
|
|
66
|
+
} catch {
|
|
67
|
+
return {
|
|
68
|
+
markRequestAccess() {},
|
|
69
|
+
requestDependent: () => true,
|
|
70
|
+
request,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const originalHeaders = tracked.headers;
|
|
75
|
+
const originalUrl = tracked.url;
|
|
76
|
+
const headers = new Proxy(originalHeaders, {
|
|
77
|
+
get(target, property) {
|
|
78
|
+
const value = Reflect.get(target, property, target);
|
|
79
|
+
|
|
80
|
+
if (typeof value !== "function" || !HEADER_READ_METHODS.has(property)) {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (...args: unknown[]): unknown => {
|
|
85
|
+
readAny = true;
|
|
86
|
+
return Reflect.apply(value as (...callArgs: unknown[]) => unknown, target, args);
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Origin/host are not part of the route cache key. Treat any URL observation
|
|
92
|
+
// as request dependence so a multi-host process cannot replay one tenant's
|
|
93
|
+
// absolute URLs to another. Path and query reads are conservatively included.
|
|
94
|
+
Object.defineProperty(tracked, "url", {
|
|
95
|
+
configurable: true,
|
|
96
|
+
get: () => {
|
|
97
|
+
readAny = true;
|
|
98
|
+
return originalUrl;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Shadow the prototype getter on a real Request so that `fetch(request)`,
|
|
103
|
+
// `request.json()` and instanceof checks keep working in every runtime.
|
|
104
|
+
// Taking a reference counts as a read on its own: a caller that reached for
|
|
105
|
+
// the headers is assumed to depend on them, which keeps the decision closed
|
|
106
|
+
// when the reference escapes into code this module cannot observe.
|
|
107
|
+
Object.defineProperty(tracked, "headers", {
|
|
108
|
+
configurable: true,
|
|
109
|
+
get: () => {
|
|
110
|
+
readAny = true;
|
|
111
|
+
return headers;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// `clone()` copies headers from internal slots rather than through the
|
|
116
|
+
// property above, so an unshadowed clone would hand back untracked headers.
|
|
117
|
+
Object.defineProperty(tracked, "clone", {
|
|
118
|
+
configurable: true,
|
|
119
|
+
value: (): Request => {
|
|
120
|
+
readAny = true;
|
|
121
|
+
return Request.prototype.clone.call(tracked);
|
|
122
|
+
},
|
|
123
|
+
writable: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
markRequestAccess: () => {
|
|
128
|
+
readAny = true;
|
|
129
|
+
},
|
|
130
|
+
requestDependent: () => readAny,
|
|
131
|
+
request: tracked,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Adds an enumerable Request property that records application access before
|
|
137
|
+
* opaque code can copy native Request internal slots.
|
|
138
|
+
*/
|
|
139
|
+
export function withTrackedRequest<T extends object>(
|
|
140
|
+
values: T,
|
|
141
|
+
request: Request,
|
|
142
|
+
tracked: TrackedHeaderRequest | undefined,
|
|
143
|
+
): T & { request: Request } {
|
|
144
|
+
const context = values as T & { request: Request };
|
|
145
|
+
let currentRequest = request;
|
|
146
|
+
|
|
147
|
+
Object.defineProperty(context, "request", {
|
|
148
|
+
configurable: true,
|
|
149
|
+
enumerable: true,
|
|
150
|
+
get() {
|
|
151
|
+
tracked?.markRequestAccess();
|
|
152
|
+
return currentRequest;
|
|
153
|
+
},
|
|
154
|
+
set(value: Request) {
|
|
155
|
+
tracked?.markRequestAccess();
|
|
156
|
+
currentRequest = value;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return context;
|
|
161
|
+
}
|
package/src/route-source.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, isAbsolute, join } from "node:path";
|
|
2
2
|
import {
|
|
3
|
+
collectIdentifierReferenceNames,
|
|
3
4
|
collectFormActionExpressionReferences,
|
|
4
5
|
collectTopLevelValueExportNames,
|
|
5
6
|
collectJsxComponentRootNames,
|
|
@@ -203,6 +204,76 @@ export function routeClosureMayUseAwaitBoundary(options: {
|
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
const REQUEST_INPUT_IDENTIFIERS = new Set(["fetch", "request", "Request"]);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Conservatively detects route closures that may consume Request inputs which
|
|
211
|
+
* are not represented by the shared route cache key.
|
|
212
|
+
*
|
|
213
|
+
* This deliberately over-approximates: a false positive disables shared HTML
|
|
214
|
+
* reuse, while a false negative can disclose one request's rendered values to
|
|
215
|
+
* another. Local server imports are traversed so helpers that reconstruct a
|
|
216
|
+
* Request from its internal slots are covered before cache lookup.
|
|
217
|
+
*/
|
|
218
|
+
export function routeClosureMayUseRequestInput(options: {
|
|
219
|
+
filename: string;
|
|
220
|
+
files: RouteSourceLookup;
|
|
221
|
+
projectRoot: string;
|
|
222
|
+
seen?: Set<string> | undefined;
|
|
223
|
+
source: string;
|
|
224
|
+
}): boolean {
|
|
225
|
+
const seen = options.seen ?? new Set<string>();
|
|
226
|
+
if (seen.has(options.filename)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
seen.add(options.filename);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
if (hasModuleDirective({ code: options.source, directive: "use client" })) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sourceFilename = sourceFilenameForCompiler(options.projectRoot, options.filename);
|
|
238
|
+
if (
|
|
239
|
+
collectIdentifierReferenceNames({
|
|
240
|
+
code: options.source,
|
|
241
|
+
filename: sourceFilename,
|
|
242
|
+
}).some((name) => REQUEST_INPUT_IDENTIFIERS.has(name))
|
|
243
|
+
) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const reference of collectStaticImportReferences({
|
|
248
|
+
code: options.source,
|
|
249
|
+
filename: sourceFilename,
|
|
250
|
+
})) {
|
|
251
|
+
const resolved = resolveLocalSourceImport(options.files, options.filename, reference.source);
|
|
252
|
+
|
|
253
|
+
if (
|
|
254
|
+
resolved !== undefined &&
|
|
255
|
+
routeClosureMayUseRequestInput({
|
|
256
|
+
filename: resolved.filename,
|
|
257
|
+
files: options.files,
|
|
258
|
+
projectRoot: options.projectRoot,
|
|
259
|
+
seen,
|
|
260
|
+
source: resolved.source,
|
|
261
|
+
})
|
|
262
|
+
) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
} catch {
|
|
269
|
+
// An analysis parser mismatch must not turn an unknown route into a shared
|
|
270
|
+
// cache candidate. The normal compiler still reports actual source errors.
|
|
271
|
+
return true;
|
|
272
|
+
} finally {
|
|
273
|
+
seen.delete(options.filename);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
206
277
|
export function hasPrerenderExport(code: string, filename?: string | undefined): boolean {
|
|
207
278
|
return hasTopLevelExportDeclaration({ code, filename, names: ["prerender"] });
|
|
208
279
|
}
|
package/src/security-headers.ts
CHANGED
|
@@ -58,6 +58,47 @@ export function routeSecurityHeaders(options: {
|
|
|
58
58
|
return headers;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Returns the configured Strict-Transport-Security value regardless of scheme.
|
|
63
|
+
*
|
|
64
|
+
* `routeSecurityHeaders` only emits the header for secure requests, so callers
|
|
65
|
+
* that persist a response across requests need the configured value on its own:
|
|
66
|
+
* the scheme of the request that happened to produce the response must not
|
|
67
|
+
* decide what later requests receive.
|
|
68
|
+
*
|
|
69
|
+
* An invalid value yields `undefined` rather than throwing. This runs on every
|
|
70
|
+
* render that may be cached, including plain ones that would never emit the
|
|
71
|
+
* header, and reporting the configuration error is `routeSecurityHeaders`'s
|
|
72
|
+
* job on the request that actually emits it.
|
|
73
|
+
*/
|
|
74
|
+
export function configuredHstsHeader(
|
|
75
|
+
security: RouteSecurityHeaders | undefined,
|
|
76
|
+
): string | undefined {
|
|
77
|
+
if (security?.hsts === undefined || security.hsts === false || security.hsts === null) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
return serializeHsts(security.hsts);
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Returns whether HSTS is configured but cannot be serialized safely. */
|
|
89
|
+
export function hasInvalidConfiguredHsts(security: RouteSecurityHeaders | undefined): boolean {
|
|
90
|
+
if (security?.hsts === undefined || security.hsts === false || security.hsts === null) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
serializeHsts(security.hsts);
|
|
96
|
+
return false;
|
|
97
|
+
} catch {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
61
102
|
function serializeHsts(hsts: NonNullable<RouteSecurityHeaders["hsts"]>): string {
|
|
62
103
|
if (hsts === false) {
|
|
63
104
|
throw new TypeError("Invalid security header value for hsts.");
|
package/src/serve.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Server } from "node:http";
|
|
2
|
+
import type { DehydrateOptions } from "@reckona/mreact-query";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import type { BuiltPrerenderedRoute } from "./build.js";
|
|
@@ -57,6 +58,13 @@ import { startNodeRequestServer } from "./node-server.js";
|
|
|
57
58
|
import { builtAppRuntimePreloadPlan } from "./preload-policy.js";
|
|
58
59
|
import { normalizeRoutePath } from "./route-path.js";
|
|
59
60
|
import type { HttpUpgradeHandler } from "./upgrade.js";
|
|
61
|
+
import {
|
|
62
|
+
isCurrentPrerenderedRoute,
|
|
63
|
+
isVisitorDependentResponse,
|
|
64
|
+
PRERENDERED_ROUTE_SCHEMA_VERSION,
|
|
65
|
+
replayedPrerenderedRouteHeaders,
|
|
66
|
+
storedPrerenderedRouteHeaders,
|
|
67
|
+
} from "./prerender-entry.js";
|
|
60
68
|
|
|
61
69
|
interface BuiltRuntimeCacheEntry {
|
|
62
70
|
clientManifestText: string;
|
|
@@ -94,6 +102,7 @@ let warnedImplicitHostTrust = false;
|
|
|
94
102
|
* Configures rendering a request against a built app-router output directory.
|
|
95
103
|
*/
|
|
96
104
|
export interface RenderBuiltAppRequestOptions {
|
|
105
|
+
dehydrateOptions?: DehydrateOptions | undefined;
|
|
97
106
|
outDir: string;
|
|
98
107
|
importPolicy?: AppRouterImportPolicy | undefined;
|
|
99
108
|
instrumentation?: RouterInstrumentation | undefined;
|
|
@@ -151,6 +160,7 @@ export interface BuiltAppRuntimePreloadStrategy {
|
|
|
151
160
|
* Configures the Node HTTP server used to serve a built app-router output.
|
|
152
161
|
*/
|
|
153
162
|
export interface StartServerOptions {
|
|
163
|
+
dehydrateOptions?: DehydrateOptions | undefined;
|
|
154
164
|
outDir: string;
|
|
155
165
|
port: number;
|
|
156
166
|
hostname?: string;
|
|
@@ -176,6 +186,13 @@ export interface StartServerOptions {
|
|
|
176
186
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
177
187
|
sinkStrategy?: ResponseSinkStrategy;
|
|
178
188
|
onUpgrade?: HttpUpgradeHandler | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* Trusts the first `X-Forwarded-Proto` value when the socket is not TLS.
|
|
191
|
+
*
|
|
192
|
+
* Defaults to `false`. Enable only behind a proxy that overwrites the header
|
|
193
|
+
* and prevents untrusted clients from reaching the Node listener directly.
|
|
194
|
+
*/
|
|
195
|
+
trustForwardedProto?: boolean | undefined;
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
export function resolveRequestHost(options: {
|
|
@@ -449,7 +466,14 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
449
466
|
let normalizedPath = normalizeRoutePath(url.pathname);
|
|
450
467
|
let matched = options.runtime.routeMatcher.match(normalizedPath);
|
|
451
468
|
|
|
452
|
-
|
|
469
|
+
// Prerendered HTML may only be served ahead of middleware when the app has
|
|
470
|
+
// no middleware at all. Otherwise a middleware that gates access (auth,
|
|
471
|
+
// geo blocking, maintenance mode) would be bypassed for exactly the routes
|
|
472
|
+
// it is most often used to protect.
|
|
473
|
+
if (
|
|
474
|
+
!options.runtime.hasMiddleware &&
|
|
475
|
+
(request.method === "GET" || request.method === "HEAD")
|
|
476
|
+
) {
|
|
453
477
|
// Sync fast path when no external prerender store is configured (the
|
|
454
478
|
// common case): skip the Promise wrap that `readPrerenderedRoute`
|
|
455
479
|
// would otherwise introduce just to satisfy the async signature.
|
|
@@ -461,18 +485,10 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
461
485
|
normalizedPath,
|
|
462
486
|
options.prerenderStore,
|
|
463
487
|
);
|
|
488
|
+
const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
|
|
464
489
|
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
return new Response(null, {
|
|
468
|
-
headers: prerendered.headers,
|
|
469
|
-
status: prerendered.status,
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
return htmlResponse(prerendered.html, {
|
|
473
|
-
headers: prerendered.headers,
|
|
474
|
-
status: prerendered.status,
|
|
475
|
-
});
|
|
490
|
+
if (prerenderedResponse !== undefined) {
|
|
491
|
+
return prerenderedResponse;
|
|
476
492
|
}
|
|
477
493
|
}
|
|
478
494
|
|
|
@@ -489,6 +505,29 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
489
505
|
matched = options.runtime.routeMatcher.match(normalizedPath);
|
|
490
506
|
}
|
|
491
507
|
|
|
508
|
+
// Middleware apps resolve prerendered HTML here instead, so that a
|
|
509
|
+
// middleware response or rewrite is honoured before the stored HTML is
|
|
510
|
+
// served. The lookup uses the post-middleware path.
|
|
511
|
+
if (
|
|
512
|
+
options.runtime.hasMiddleware &&
|
|
513
|
+
(request.method === "GET" || request.method === "HEAD")
|
|
514
|
+
) {
|
|
515
|
+
const prerendered =
|
|
516
|
+
options.prerenderStore === undefined
|
|
517
|
+
? options.runtime.prerenderedRoutes.get(normalizedPath)
|
|
518
|
+
: await readPrerenderedRoute(
|
|
519
|
+
options.runtime,
|
|
520
|
+
normalizedPath,
|
|
521
|
+
options.prerenderStore,
|
|
522
|
+
);
|
|
523
|
+
const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
|
|
524
|
+
|
|
525
|
+
if (prerenderedResponse !== undefined) {
|
|
526
|
+
emitBuiltRenderTiming(options, request, timing, prerenderedResponse.status);
|
|
527
|
+
return prerenderedResponse;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
492
531
|
if (request.method === "GET" && options.runtime.prerenderableRoutes.has(normalizedPath)) {
|
|
493
532
|
await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
|
|
494
533
|
includeRender: false,
|
|
@@ -517,6 +556,33 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
517
556
|
: response;
|
|
518
557
|
}
|
|
519
558
|
|
|
559
|
+
function prerenderedRouteResponse(
|
|
560
|
+
prerendered: BuiltPrerenderedRoute | undefined,
|
|
561
|
+
request: Request,
|
|
562
|
+
): Response | undefined {
|
|
563
|
+
if (!isCurrentPrerenderedRoute(prerendered)) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const navigation = request.headers.get("x-mreact-navigation") === "1";
|
|
568
|
+
const html = navigation ? prerendered.navigationHtml : prerendered.html;
|
|
569
|
+
if (html === undefined) {
|
|
570
|
+
return undefined;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (request.method === "HEAD") {
|
|
574
|
+
return new Response(null, {
|
|
575
|
+
headers: replayedPrerenderedRouteHeaders(prerendered, request),
|
|
576
|
+
status: prerendered.status,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return htmlResponse(html, {
|
|
581
|
+
headers: replayedPrerenderedRouteHeaders(prerendered, request),
|
|
582
|
+
status: prerendered.status,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
520
586
|
async function resolveBuiltMiddleware(
|
|
521
587
|
options: RenderBuiltAppRequestOptions & {
|
|
522
588
|
runtime: BuiltRuntime;
|
|
@@ -651,8 +717,10 @@ export async function startServer(
|
|
|
651
717
|
onUpgrade: options.onUpgrade,
|
|
652
718
|
port: options.port,
|
|
653
719
|
resolveHost: resolveRequestHost,
|
|
720
|
+
trustForwardedProto: options.trustForwardedProto,
|
|
654
721
|
render: (request) =>
|
|
655
722
|
runtime.render(request, {
|
|
723
|
+
dehydrateOptions: options.dehydrateOptions,
|
|
656
724
|
instrumentation: options.instrumentation,
|
|
657
725
|
logger: options.logger,
|
|
658
726
|
onResponse: options.onResponse,
|
|
@@ -815,13 +883,22 @@ async function readPrerenderedRoute(
|
|
|
815
883
|
const stored = await store?.get(path);
|
|
816
884
|
|
|
817
885
|
if (stored !== undefined) {
|
|
818
|
-
|
|
819
|
-
|
|
886
|
+
if (isCurrentPrerenderedRoute(stored)) {
|
|
887
|
+
runtime.prerenderedRoutes.set(path, stored);
|
|
888
|
+
return stored;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
await store?.delete(path);
|
|
820
892
|
}
|
|
821
893
|
|
|
822
894
|
const manifestEntry = runtime.prerenderedRoutes.get(path);
|
|
823
895
|
|
|
824
|
-
if (manifestEntry
|
|
896
|
+
if (!isCurrentPrerenderedRoute(manifestEntry)) {
|
|
897
|
+
runtime.prerenderedRoutes.delete(path);
|
|
898
|
+
return undefined;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (store !== undefined) {
|
|
825
902
|
await store.set(path, manifestEntry);
|
|
826
903
|
}
|
|
827
904
|
|
|
@@ -831,11 +908,15 @@ async function readPrerenderedRoute(
|
|
|
831
908
|
function renderBuiltDynamicResponse(
|
|
832
909
|
options: RenderBuiltAppRequestOptions & {
|
|
833
910
|
matchedRoute?: MatchedRoute | undefined;
|
|
911
|
+
renderSignals?: { headerDependent: () => boolean } | undefined;
|
|
834
912
|
requestUrl?: URL | undefined;
|
|
835
913
|
runtime: BuiltRuntime;
|
|
836
914
|
},
|
|
837
915
|
): Promise<Response> {
|
|
838
|
-
return renderAppRequest(
|
|
916
|
+
return renderAppRequest({
|
|
917
|
+
...builtRenderAppRequestOptions(options),
|
|
918
|
+
...(options.renderSignals === undefined ? {} : { renderSignals: options.renderSignals }),
|
|
919
|
+
});
|
|
839
920
|
}
|
|
840
921
|
|
|
841
922
|
function builtRenderAppRequestOptions(
|
|
@@ -858,6 +939,7 @@ function builtRenderAppRequestOptions(
|
|
|
858
939
|
clientScripts: options.runtime.clientScripts,
|
|
859
940
|
clientStylesByFile: options.runtime.clientStylesByFile,
|
|
860
941
|
clientStyles: options.runtime.clientStyles,
|
|
942
|
+
dehydrateOptions: options.dehydrateOptions,
|
|
861
943
|
importPolicy: {
|
|
862
944
|
...options.importPolicy,
|
|
863
945
|
allowedSourceDirs: options.runtime.allowedSourceDirs,
|
|
@@ -907,51 +989,106 @@ async function renderAndCachePrerenderWithLock(
|
|
|
907
989
|
options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
|
|
908
990
|
path: string,
|
|
909
991
|
): Promise<Response> {
|
|
910
|
-
const
|
|
992
|
+
const lockKey =
|
|
993
|
+
options.request.headers.get("x-mreact-navigation") === "1"
|
|
994
|
+
? `${path}\0navigation`
|
|
995
|
+
: path;
|
|
996
|
+
const existing = options.runtime.prerenderLocks.get(lockKey);
|
|
911
997
|
|
|
912
998
|
if (existing !== undefined) {
|
|
913
|
-
|
|
999
|
+
const result = await existing;
|
|
1000
|
+
if (result.shareable) {
|
|
1001
|
+
return cloneResponse(result.response);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return (await runPrerenderRegeneration(options, path)).response;
|
|
914
1005
|
}
|
|
915
1006
|
|
|
916
1007
|
const task = runPrerenderRegeneration(options, path);
|
|
917
|
-
options.runtime.prerenderLocks.set(
|
|
1008
|
+
options.runtime.prerenderLocks.set(lockKey, task);
|
|
918
1009
|
|
|
919
1010
|
try {
|
|
920
|
-
return cloneResponse(await task);
|
|
1011
|
+
return cloneResponse((await task).response);
|
|
921
1012
|
} finally {
|
|
922
|
-
options.runtime.prerenderLocks.delete(
|
|
1013
|
+
options.runtime.prerenderLocks.delete(lockKey);
|
|
923
1014
|
}
|
|
924
1015
|
}
|
|
925
1016
|
|
|
926
1017
|
async function runPrerenderRegeneration(
|
|
927
1018
|
options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
|
|
928
1019
|
path: string,
|
|
929
|
-
): Promise<Response> {
|
|
1020
|
+
): Promise<{ response: Response; shareable: boolean }> {
|
|
1021
|
+
const lockKey =
|
|
1022
|
+
options.request.headers.get("x-mreact-navigation") === "1"
|
|
1023
|
+
? `${path}\0navigation`
|
|
1024
|
+
: path;
|
|
930
1025
|
const regenerate = async () => {
|
|
931
1026
|
const stored = await readPrerenderedRoute(options.runtime, path, options.prerenderStore);
|
|
1027
|
+
const storedResponse = prerenderedRouteResponse(stored, options.request);
|
|
932
1028
|
|
|
933
|
-
if (
|
|
934
|
-
return
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
1029
|
+
if (storedResponse !== undefined) {
|
|
1030
|
+
return {
|
|
1031
|
+
response: storedResponse,
|
|
1032
|
+
shareable: true,
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// A prerendered entry is replayed to every visitor by path alone, so a
|
|
1037
|
+
// render that depended on this visitor's request headers must not become
|
|
1038
|
+
// one; it is returned to the caller without being stored. The signal starts
|
|
1039
|
+
// closed so that a render which never reports back is not stored either.
|
|
1040
|
+
const renderSignals = {
|
|
1041
|
+
headerDependent: () => true,
|
|
1042
|
+
strictTransportSecurity: () => undefined as string | undefined,
|
|
1043
|
+
};
|
|
1044
|
+
const response = await renderBuiltDynamicResponse({ ...options, renderSignals });
|
|
1045
|
+
|
|
1046
|
+
if (!response.ok) {
|
|
1047
|
+
return { response, shareable: false };
|
|
938
1048
|
}
|
|
939
1049
|
|
|
940
|
-
|
|
1050
|
+
// Draining the body finishes a streamed render, so the signal is only
|
|
1051
|
+
// final once the whole response has been read.
|
|
1052
|
+
const body = await response.text();
|
|
1053
|
+
|
|
1054
|
+
if (renderSignals.headerDependent() || isVisitorDependentResponse(response)) {
|
|
1055
|
+
return {
|
|
1056
|
+
response: htmlResponse(body, {
|
|
1057
|
+
headers: response.headers,
|
|
1058
|
+
status: response.status,
|
|
1059
|
+
}),
|
|
1060
|
+
shareable: false,
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
941
1063
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
response,
|
|
947
|
-
|
|
948
|
-
)
|
|
949
|
-
|
|
1064
|
+
const navigation = options.request.headers.get("x-mreact-navigation") === "1";
|
|
1065
|
+
if (navigation && stored === undefined) {
|
|
1066
|
+
return {
|
|
1067
|
+
response: htmlResponse(body, {
|
|
1068
|
+
headers: response.headers,
|
|
1069
|
+
status: response.status,
|
|
1070
|
+
}),
|
|
1071
|
+
shareable: false,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
response: await cacheRegeneratedPrerenderedRoute(
|
|
1077
|
+
options.runtime,
|
|
1078
|
+
path,
|
|
1079
|
+
body,
|
|
1080
|
+
response,
|
|
1081
|
+
renderSignals.strictTransportSecurity(),
|
|
1082
|
+
options.prerenderStore,
|
|
1083
|
+
navigation ? stored : undefined,
|
|
1084
|
+
),
|
|
1085
|
+
shareable: true,
|
|
1086
|
+
};
|
|
950
1087
|
};
|
|
951
1088
|
|
|
952
1089
|
return options.prerenderStore?.withLock === undefined
|
|
953
1090
|
? await regenerate()
|
|
954
|
-
: await options.prerenderStore.withLock(
|
|
1091
|
+
: await options.prerenderStore.withLock(lockKey, regenerate);
|
|
955
1092
|
}
|
|
956
1093
|
|
|
957
1094
|
async function applyBuiltPrerenderInvalidations(
|
|
@@ -975,20 +1112,23 @@ async function applyBuiltPrerenderInvalidations(
|
|
|
975
1112
|
async function cacheRegeneratedPrerenderedRoute(
|
|
976
1113
|
runtime: BuiltRuntime,
|
|
977
1114
|
path: string,
|
|
1115
|
+
body: string,
|
|
978
1116
|
response: Response,
|
|
1117
|
+
strictTransportSecurity: string | undefined,
|
|
979
1118
|
store: AppRouterPrerenderStore | undefined,
|
|
1119
|
+
existing?: BuiltPrerenderedRoute | undefined,
|
|
980
1120
|
): Promise<Response> {
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1121
|
+
const headers = storedPrerenderedRouteHeaders(response.headers);
|
|
1122
|
+
const entry: BuiltPrerenderedRoute =
|
|
1123
|
+
existing === undefined
|
|
1124
|
+
? {
|
|
1125
|
+
headers,
|
|
1126
|
+
html: body,
|
|
1127
|
+
schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
|
|
1128
|
+
status: response.status,
|
|
1129
|
+
...(strictTransportSecurity === undefined ? {} : { strictTransportSecurity }),
|
|
1130
|
+
}
|
|
1131
|
+
: { ...existing, navigationHtml: body };
|
|
992
1132
|
runtime.prerenderedRoutes.set(path, entry);
|
|
993
1133
|
await store?.set(path, entry);
|
|
994
1134
|
|