@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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/cloudflare.d.ts.map +1 -1
  3. package/dist/adapters/cloudflare.js +51 -4
  4. package/dist/adapters/cloudflare.js.map +1 -1
  5. package/dist/adapters/node.d.ts +7 -0
  6. package/dist/adapters/node.d.ts.map +1 -1
  7. package/dist/adapters/node.js +7 -1
  8. package/dist/adapters/node.js.map +1 -1
  9. package/dist/adapters/static.d.ts.map +1 -1
  10. package/dist/adapters/static.js +14 -3
  11. package/dist/adapters/static.js.map +1 -1
  12. package/dist/build.d.ts +3 -0
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +42 -5
  15. package/dist/build.js.map +1 -1
  16. package/dist/built-runtime.d.ts +4 -1
  17. package/dist/built-runtime.d.ts.map +1 -1
  18. package/dist/built-runtime.js.map +1 -1
  19. package/dist/cache.d.ts +26 -0
  20. package/dist/cache.d.ts.map +1 -1
  21. package/dist/cache.js +78 -12
  22. package/dist/cache.js.map +1 -1
  23. package/dist/cli-options.d.ts +4 -0
  24. package/dist/cli-options.d.ts.map +1 -1
  25. package/dist/cli-options.js +17 -0
  26. package/dist/cli-options.js.map +1 -1
  27. package/dist/cli.js +2 -1
  28. package/dist/cli.js.map +1 -1
  29. package/dist/node-server.d.ts +6 -0
  30. package/dist/node-server.d.ts.map +1 -1
  31. package/dist/node-server.js +19 -1
  32. package/dist/node-server.js.map +1 -1
  33. package/dist/prerender-entry.d.ts +5 -0
  34. package/dist/prerender-entry.d.ts.map +1 -0
  35. package/dist/prerender-entry.js +41 -0
  36. package/dist/prerender-entry.js.map +1 -0
  37. package/dist/render.d.ts +13 -0
  38. package/dist/render.d.ts.map +1 -1
  39. package/dist/render.js +139 -87
  40. package/dist/render.js.map +1 -1
  41. package/dist/request-header-tracking.d.ts +44 -0
  42. package/dist/request-header-tracking.d.ts.map +1 -0
  43. package/dist/request-header-tracking.js +116 -0
  44. package/dist/request-header-tracking.js.map +1 -0
  45. package/dist/route-source.d.ts +16 -0
  46. package/dist/route-source.d.ts.map +1 -1
  47. package/dist/route-source.js +55 -1
  48. package/dist/route-source.js.map +1 -1
  49. package/dist/security-headers.d.ts +16 -0
  50. package/dist/security-headers.d.ts.map +1 -1
  51. package/dist/security-headers.js +37 -0
  52. package/dist/security-headers.js.map +1 -1
  53. package/dist/serve.d.ts +7 -0
  54. package/dist/serve.d.ts.map +1 -1
  55. package/dist/serve.js +94 -28
  56. package/dist/serve.js.map +1 -1
  57. package/package.json +11 -11
  58. package/src/adapters/cloudflare.ts +81 -9
  59. package/src/adapters/node.ts +14 -1
  60. package/src/adapters/static.ts +18 -5
  61. package/src/build.ts +71 -4
  62. package/src/built-runtime.ts +2 -2
  63. package/src/cache.ts +117 -11
  64. package/src/cli-options.ts +24 -0
  65. package/src/cli.ts +5 -0
  66. package/src/node-server.ts +27 -1
  67. package/src/prerender-entry.ts +56 -0
  68. package/src/render.ts +188 -98
  69. package/src/request-header-tracking.ts +161 -0
  70. package/src/route-source.ts +71 -0
  71. package/src/security-headers.ts +41 -0
  72. 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
+ }
@@ -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
  }
@@ -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
- if (options.request.method === "GET" || options.request.method === "HEAD") {
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 (prerendered !== undefined) {
466
- if (options.request.method === "HEAD") {
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
- runtime.prerenderedRoutes.set(path, stored);
819
- return stored;
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 !== undefined && store !== undefined) {
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(builtRenderAppRequestOptions(options));
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
- return cloneResponse(await existing);
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 htmlResponse(stored.html, {
935
- headers: stored.headers,
936
- status: stored.status,
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
- const response = await renderBuiltDynamicResponse(options);
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 response.ok
943
- ? await cacheRegeneratedPrerenderedRoute(
1042
+ return {
1043
+ response: await cacheRegeneratedPrerenderedRoute(
944
1044
  options.runtime,
945
1045
  path,
1046
+ body,
946
1047
  response,
947
1048
  options.prerenderStore,
948
- )
949
- : response;
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);