@reckona/mreact-router 0.0.201 → 0.0.202
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 +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +51 -4
- 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 +3 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +42 -5
- 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 +26 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +78 -12
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +4 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +17 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.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 +5 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +41 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +139 -87
- 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 +7 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +94 -28
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/adapters/cloudflare.ts +81 -9
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +71 -4
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +117 -11
- package/src/cli-options.ts +24 -0
- package/src/cli.ts +5 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +56 -0
- package/src/render.ts +188 -98
- 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 +133 -30
|
@@ -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
|
@@ -57,6 +57,11 @@ import { startNodeRequestServer } from "./node-server.js";
|
|
|
57
57
|
import { builtAppRuntimePreloadPlan } from "./preload-policy.js";
|
|
58
58
|
import { normalizeRoutePath } from "./route-path.js";
|
|
59
59
|
import type { HttpUpgradeHandler } from "./upgrade.js";
|
|
60
|
+
import {
|
|
61
|
+
isCurrentPrerenderedRoute,
|
|
62
|
+
isVisitorDependentResponse,
|
|
63
|
+
PRERENDERED_ROUTE_SCHEMA_VERSION,
|
|
64
|
+
} from "./prerender-entry.js";
|
|
60
65
|
|
|
61
66
|
interface BuiltRuntimeCacheEntry {
|
|
62
67
|
clientManifestText: string;
|
|
@@ -176,6 +181,13 @@ export interface StartServerOptions {
|
|
|
176
181
|
serverActions?: AppRouterServerActionOptions | undefined;
|
|
177
182
|
sinkStrategy?: ResponseSinkStrategy;
|
|
178
183
|
onUpgrade?: HttpUpgradeHandler | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Trusts the first `X-Forwarded-Proto` value when the socket is not TLS.
|
|
186
|
+
*
|
|
187
|
+
* Defaults to `false`. Enable only behind a proxy that overwrites the header
|
|
188
|
+
* and prevents untrusted clients from reaching the Node listener directly.
|
|
189
|
+
*/
|
|
190
|
+
trustForwardedProto?: boolean | undefined;
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
export function resolveRequestHost(options: {
|
|
@@ -449,7 +461,14 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
449
461
|
let normalizedPath = normalizeRoutePath(url.pathname);
|
|
450
462
|
let matched = options.runtime.routeMatcher.match(normalizedPath);
|
|
451
463
|
|
|
452
|
-
|
|
464
|
+
// Prerendered HTML may only be served ahead of middleware when the app has
|
|
465
|
+
// no middleware at all. Otherwise a middleware that gates access (auth,
|
|
466
|
+
// geo blocking, maintenance mode) would be bypassed for exactly the routes
|
|
467
|
+
// it is most often used to protect.
|
|
468
|
+
if (
|
|
469
|
+
!options.runtime.hasMiddleware &&
|
|
470
|
+
(request.method === "GET" || request.method === "HEAD")
|
|
471
|
+
) {
|
|
453
472
|
// Sync fast path when no external prerender store is configured (the
|
|
454
473
|
// common case): skip the Promise wrap that `readPrerenderedRoute`
|
|
455
474
|
// would otherwise introduce just to satisfy the async signature.
|
|
@@ -461,18 +480,10 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
461
480
|
normalizedPath,
|
|
462
481
|
options.prerenderStore,
|
|
463
482
|
);
|
|
483
|
+
const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
|
|
464
484
|
|
|
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
|
-
});
|
|
485
|
+
if (prerenderedResponse !== undefined) {
|
|
486
|
+
return prerenderedResponse;
|
|
476
487
|
}
|
|
477
488
|
}
|
|
478
489
|
|
|
@@ -489,6 +500,29 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
489
500
|
matched = options.runtime.routeMatcher.match(normalizedPath);
|
|
490
501
|
}
|
|
491
502
|
|
|
503
|
+
// Middleware apps resolve prerendered HTML here instead, so that a
|
|
504
|
+
// middleware response or rewrite is honoured before the stored HTML is
|
|
505
|
+
// served. The lookup uses the post-middleware path.
|
|
506
|
+
if (
|
|
507
|
+
options.runtime.hasMiddleware &&
|
|
508
|
+
(request.method === "GET" || request.method === "HEAD")
|
|
509
|
+
) {
|
|
510
|
+
const prerendered =
|
|
511
|
+
options.prerenderStore === undefined
|
|
512
|
+
? options.runtime.prerenderedRoutes.get(normalizedPath)
|
|
513
|
+
: await readPrerenderedRoute(
|
|
514
|
+
options.runtime,
|
|
515
|
+
normalizedPath,
|
|
516
|
+
options.prerenderStore,
|
|
517
|
+
);
|
|
518
|
+
const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
|
|
519
|
+
|
|
520
|
+
if (prerenderedResponse !== undefined) {
|
|
521
|
+
emitBuiltRenderTiming(options, request, timing, prerenderedResponse.status);
|
|
522
|
+
return prerenderedResponse;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
492
526
|
if (request.method === "GET" && options.runtime.prerenderableRoutes.has(normalizedPath)) {
|
|
493
527
|
await loadBuiltServerModuleArtifactsForRequest(options.runtime, matched?.route.file, {
|
|
494
528
|
includeRender: false,
|
|
@@ -517,6 +551,27 @@ async function renderBuiltAppRequestWithRuntime(
|
|
|
517
551
|
: response;
|
|
518
552
|
}
|
|
519
553
|
|
|
554
|
+
function prerenderedRouteResponse(
|
|
555
|
+
prerendered: BuiltPrerenderedRoute | undefined,
|
|
556
|
+
method: string,
|
|
557
|
+
): Response | undefined {
|
|
558
|
+
if (!isCurrentPrerenderedRoute(prerendered)) {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (method === "HEAD") {
|
|
563
|
+
return new Response(null, {
|
|
564
|
+
headers: prerendered.headers,
|
|
565
|
+
status: prerendered.status,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return htmlResponse(prerendered.html, {
|
|
570
|
+
headers: prerendered.headers,
|
|
571
|
+
status: prerendered.status,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
520
575
|
async function resolveBuiltMiddleware(
|
|
521
576
|
options: RenderBuiltAppRequestOptions & {
|
|
522
577
|
runtime: BuiltRuntime;
|
|
@@ -651,6 +706,7 @@ export async function startServer(
|
|
|
651
706
|
onUpgrade: options.onUpgrade,
|
|
652
707
|
port: options.port,
|
|
653
708
|
resolveHost: resolveRequestHost,
|
|
709
|
+
trustForwardedProto: options.trustForwardedProto,
|
|
654
710
|
render: (request) =>
|
|
655
711
|
runtime.render(request, {
|
|
656
712
|
instrumentation: options.instrumentation,
|
|
@@ -815,13 +871,22 @@ async function readPrerenderedRoute(
|
|
|
815
871
|
const stored = await store?.get(path);
|
|
816
872
|
|
|
817
873
|
if (stored !== undefined) {
|
|
818
|
-
|
|
819
|
-
|
|
874
|
+
if (isCurrentPrerenderedRoute(stored)) {
|
|
875
|
+
runtime.prerenderedRoutes.set(path, stored);
|
|
876
|
+
return stored;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
await store?.delete(path);
|
|
820
880
|
}
|
|
821
881
|
|
|
822
882
|
const manifestEntry = runtime.prerenderedRoutes.get(path);
|
|
823
883
|
|
|
824
|
-
if (manifestEntry
|
|
884
|
+
if (!isCurrentPrerenderedRoute(manifestEntry)) {
|
|
885
|
+
runtime.prerenderedRoutes.delete(path);
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (store !== undefined) {
|
|
825
890
|
await store.set(path, manifestEntry);
|
|
826
891
|
}
|
|
827
892
|
|
|
@@ -831,11 +896,15 @@ async function readPrerenderedRoute(
|
|
|
831
896
|
function renderBuiltDynamicResponse(
|
|
832
897
|
options: RenderBuiltAppRequestOptions & {
|
|
833
898
|
matchedRoute?: MatchedRoute | undefined;
|
|
899
|
+
renderSignals?: { headerDependent: () => boolean } | undefined;
|
|
834
900
|
requestUrl?: URL | undefined;
|
|
835
901
|
runtime: BuiltRuntime;
|
|
836
902
|
},
|
|
837
903
|
): Promise<Response> {
|
|
838
|
-
return renderAppRequest(
|
|
904
|
+
return renderAppRequest({
|
|
905
|
+
...builtRenderAppRequestOptions(options),
|
|
906
|
+
...(options.renderSignals === undefined ? {} : { renderSignals: options.renderSignals }),
|
|
907
|
+
});
|
|
839
908
|
}
|
|
840
909
|
|
|
841
910
|
function builtRenderAppRequestOptions(
|
|
@@ -910,14 +979,19 @@ async function renderAndCachePrerenderWithLock(
|
|
|
910
979
|
const existing = options.runtime.prerenderLocks.get(path);
|
|
911
980
|
|
|
912
981
|
if (existing !== undefined) {
|
|
913
|
-
|
|
982
|
+
const result = await existing;
|
|
983
|
+
if (result.shareable) {
|
|
984
|
+
return cloneResponse(result.response);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return (await runPrerenderRegeneration(options, path)).response;
|
|
914
988
|
}
|
|
915
989
|
|
|
916
990
|
const task = runPrerenderRegeneration(options, path);
|
|
917
991
|
options.runtime.prerenderLocks.set(path, task);
|
|
918
992
|
|
|
919
993
|
try {
|
|
920
|
-
return cloneResponse(await task);
|
|
994
|
+
return cloneResponse((await task).response);
|
|
921
995
|
} finally {
|
|
922
996
|
options.runtime.prerenderLocks.delete(path);
|
|
923
997
|
}
|
|
@@ -926,27 +1000,55 @@ async function renderAndCachePrerenderWithLock(
|
|
|
926
1000
|
async function runPrerenderRegeneration(
|
|
927
1001
|
options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
|
|
928
1002
|
path: string,
|
|
929
|
-
): Promise<Response> {
|
|
1003
|
+
): Promise<{ response: Response; shareable: boolean }> {
|
|
930
1004
|
const regenerate = async () => {
|
|
931
1005
|
const stored = await readPrerenderedRoute(options.runtime, path, options.prerenderStore);
|
|
932
1006
|
|
|
933
1007
|
if (stored !== undefined) {
|
|
934
|
-
return
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1008
|
+
return {
|
|
1009
|
+
response: htmlResponse(stored.html, {
|
|
1010
|
+
headers: stored.headers,
|
|
1011
|
+
status: stored.status,
|
|
1012
|
+
}),
|
|
1013
|
+
shareable: true,
|
|
1014
|
+
};
|
|
938
1015
|
}
|
|
939
1016
|
|
|
940
|
-
|
|
1017
|
+
// A prerendered entry is replayed to every visitor by path alone, so a
|
|
1018
|
+
// render that depended on this visitor's request headers must not become
|
|
1019
|
+
// one; it is returned to the caller without being stored. The signal starts
|
|
1020
|
+
// closed so that a render which never reports back is not stored either.
|
|
1021
|
+
const renderSignals = { headerDependent: () => true };
|
|
1022
|
+
const response = await renderBuiltDynamicResponse({ ...options, renderSignals });
|
|
1023
|
+
|
|
1024
|
+
if (!response.ok) {
|
|
1025
|
+
return { response, shareable: false };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Draining the body finishes a streamed render, so the signal is only
|
|
1029
|
+
// final once the whole response has been read.
|
|
1030
|
+
const body = await response.text();
|
|
1031
|
+
|
|
1032
|
+
if (renderSignals.headerDependent() || isVisitorDependentResponse(response)) {
|
|
1033
|
+
return {
|
|
1034
|
+
response: htmlResponse(body, {
|
|
1035
|
+
headers: response.headers,
|
|
1036
|
+
status: response.status,
|
|
1037
|
+
}),
|
|
1038
|
+
shareable: false,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
941
1041
|
|
|
942
|
-
return
|
|
943
|
-
|
|
1042
|
+
return {
|
|
1043
|
+
response: await cacheRegeneratedPrerenderedRoute(
|
|
944
1044
|
options.runtime,
|
|
945
1045
|
path,
|
|
1046
|
+
body,
|
|
946
1047
|
response,
|
|
947
1048
|
options.prerenderStore,
|
|
948
|
-
)
|
|
949
|
-
:
|
|
1049
|
+
),
|
|
1050
|
+
shareable: true,
|
|
1051
|
+
};
|
|
950
1052
|
};
|
|
951
1053
|
|
|
952
1054
|
return options.prerenderStore?.withLock === undefined
|
|
@@ -975,18 +1077,19 @@ async function applyBuiltPrerenderInvalidations(
|
|
|
975
1077
|
async function cacheRegeneratedPrerenderedRoute(
|
|
976
1078
|
runtime: BuiltRuntime,
|
|
977
1079
|
path: string,
|
|
1080
|
+
body: string,
|
|
978
1081
|
response: Response,
|
|
979
1082
|
store: AppRouterPrerenderStore | undefined,
|
|
980
1083
|
): Promise<Response> {
|
|
981
|
-
const body = await response.text();
|
|
982
1084
|
const headers: Record<string, string> = {};
|
|
983
1085
|
|
|
984
1086
|
response.headers.forEach((value, key) => {
|
|
985
1087
|
headers[key] = value;
|
|
986
1088
|
});
|
|
987
|
-
const entry = {
|
|
1089
|
+
const entry: BuiltPrerenderedRoute = {
|
|
988
1090
|
headers,
|
|
989
1091
|
html: body,
|
|
1092
|
+
schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
|
|
990
1093
|
status: response.status,
|
|
991
1094
|
};
|
|
992
1095
|
runtime.prerenderedRoutes.set(path, entry);
|