@reckona/mreact-router 0.0.202 → 0.0.204

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 (152) hide show
  1. package/README.md +58 -11
  2. package/dist/actions.d.ts +8 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +125 -45
  5. package/dist/actions.js.map +1 -1
  6. package/dist/adapters/aws-lambda.d.ts +15 -4
  7. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  8. package/dist/adapters/aws-lambda.js +301 -63
  9. package/dist/adapters/aws-lambda.js.map +1 -1
  10. package/dist/adapters/cloudflare.d.ts +5 -2
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +35 -11
  13. package/dist/adapters/cloudflare.js.map +1 -1
  14. package/dist/adapters/edge.d.ts +1 -1
  15. package/dist/adapters/edge.d.ts.map +1 -1
  16. package/dist/adapters/edge.js.map +1 -1
  17. package/dist/adapters/node.d.ts +5 -3
  18. package/dist/adapters/node.d.ts.map +1 -1
  19. package/dist/adapters/node.js +2 -1
  20. package/dist/adapters/node.js.map +1 -1
  21. package/dist/adapters/static.d.ts.map +1 -1
  22. package/dist/adapters/static.js +24 -2
  23. package/dist/adapters/static.js.map +1 -1
  24. package/dist/build.d.ts +4 -1
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +185 -13
  27. package/dist/build.js.map +1 -1
  28. package/dist/built-runtime.d.ts +2 -0
  29. package/dist/built-runtime.d.ts.map +1 -1
  30. package/dist/built-runtime.js +16 -0
  31. package/dist/built-runtime.js.map +1 -1
  32. package/dist/cache.d.ts +4 -2
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +216 -17
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli-options.d.ts +3 -0
  37. package/dist/cli-options.d.ts.map +1 -1
  38. package/dist/cli-options.js +10 -2
  39. package/dist/cli-options.js.map +1 -1
  40. package/dist/cli.js +14 -3
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +5 -0
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +43 -3
  45. package/dist/client.js.map +1 -1
  46. package/dist/config.d.ts +3 -0
  47. package/dist/config.d.ts.map +1 -1
  48. package/dist/config.js +21 -2
  49. package/dist/config.js.map +1 -1
  50. package/dist/csp.d.ts +1 -0
  51. package/dist/csp.d.ts.map +1 -1
  52. package/dist/csp.js +9 -0
  53. package/dist/csp.js.map +1 -1
  54. package/dist/dehydrate-policy.d.ts +3 -0
  55. package/dist/dehydrate-policy.d.ts.map +1 -0
  56. package/dist/dehydrate-policy.js +16 -0
  57. package/dist/dehydrate-policy.js.map +1 -0
  58. package/dist/dev-server.d.ts +7 -2
  59. package/dist/dev-server.d.ts.map +1 -1
  60. package/dist/dev-server.js +120 -11
  61. package/dist/dev-server.js.map +1 -1
  62. package/dist/http.d.ts +1 -1
  63. package/dist/http.d.ts.map +1 -1
  64. package/dist/http.js +147 -7
  65. package/dist/http.js.map +1 -1
  66. package/dist/index.d.ts +4 -3
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +1 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/logger.d.ts +22 -1
  71. package/dist/logger.d.ts.map +1 -1
  72. package/dist/logger.js +22 -8
  73. package/dist/logger.js.map +1 -1
  74. package/dist/metadata.d.ts +8 -0
  75. package/dist/metadata.d.ts.map +1 -1
  76. package/dist/metadata.js +60 -22
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/navigation-marker.d.ts +2 -0
  79. package/dist/navigation-marker.d.ts.map +1 -0
  80. package/dist/navigation-marker.js +357 -0
  81. package/dist/navigation-marker.js.map +1 -0
  82. package/dist/navigation-runtime.d.ts +1 -1
  83. package/dist/navigation-runtime.d.ts.map +1 -1
  84. package/dist/navigation-runtime.js +1 -1
  85. package/dist/navigation-runtime.js.map +1 -1
  86. package/dist/navigation.d.ts +1 -0
  87. package/dist/navigation.d.ts.map +1 -1
  88. package/dist/navigation.js +1 -1
  89. package/dist/navigation.js.map +1 -1
  90. package/dist/node-server.d.ts +5 -2
  91. package/dist/node-server.d.ts.map +1 -1
  92. package/dist/node-server.js +75 -6
  93. package/dist/node-server.js.map +1 -1
  94. package/dist/prerender-entry.d.ts +17 -1
  95. package/dist/prerender-entry.d.ts.map +1 -1
  96. package/dist/prerender-entry.js +98 -3
  97. package/dist/prerender-entry.js.map +1 -1
  98. package/dist/render.d.ts +16 -2
  99. package/dist/render.d.ts.map +1 -1
  100. package/dist/render.js +142 -135
  101. package/dist/render.js.map +1 -1
  102. package/dist/serve.d.ts +11 -2
  103. package/dist/serve.d.ts.map +1 -1
  104. package/dist/serve.js +67 -41
  105. package/dist/serve.js.map +1 -1
  106. package/dist/session.d.ts.map +1 -1
  107. package/dist/session.js +52 -19
  108. package/dist/session.js.map +1 -1
  109. package/dist/upgrade.d.ts +59 -2
  110. package/dist/upgrade.d.ts.map +1 -1
  111. package/dist/upgrade.js +282 -1
  112. package/dist/upgrade.js.map +1 -1
  113. package/dist/vite-config.d.ts +2 -0
  114. package/dist/vite-config.d.ts.map +1 -1
  115. package/dist/vite-config.js +2 -0
  116. package/dist/vite-config.js.map +1 -1
  117. package/dist/vite.d.ts +4 -1
  118. package/dist/vite.d.ts.map +1 -1
  119. package/dist/vite.js +117 -6
  120. package/dist/vite.js.map +1 -1
  121. package/package.json +11 -11
  122. package/src/actions.ts +146 -60
  123. package/src/adapters/aws-lambda.ts +426 -115
  124. package/src/adapters/cloudflare.ts +58 -12
  125. package/src/adapters/edge.ts +2 -0
  126. package/src/adapters/node.ts +8 -10
  127. package/src/adapters/static.ts +31 -2
  128. package/src/build.ts +217 -13
  129. package/src/built-runtime.ts +28 -0
  130. package/src/cache.ts +272 -19
  131. package/src/cli-options.ts +15 -2
  132. package/src/cli.ts +16 -2
  133. package/src/client.ts +47 -3
  134. package/src/config.ts +43 -6
  135. package/src/csp.ts +11 -0
  136. package/src/dehydrate-policy.ts +23 -0
  137. package/src/dev-server.ts +188 -15
  138. package/src/http.ts +172 -6
  139. package/src/index.ts +14 -1
  140. package/src/logger.ts +47 -10
  141. package/src/metadata.ts +125 -60
  142. package/src/navigation-marker.ts +423 -0
  143. package/src/navigation-runtime.ts +1 -0
  144. package/src/navigation.ts +1 -1
  145. package/src/node-server.ts +128 -22
  146. package/src/prerender-entry.ts +127 -5
  147. package/src/render.ts +859 -749
  148. package/src/serve.ts +112 -82
  149. package/src/session.ts +56 -25
  150. package/src/upgrade.ts +402 -3
  151. package/src/vite-config.ts +6 -0
  152. package/src/vite.ts +157 -5
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";
@@ -22,10 +23,7 @@ import {
22
23
  loadBuiltServerModuleArtifacts,
23
24
  loadBuiltServerModuleArtifactsForRequest,
24
25
  } from "./built-server-module-artifacts.js";
25
- import {
26
- type AppRoute,
27
- type MatchedRoute,
28
- } from "./routes.js";
26
+ import { type AppRoute, type MatchedRoute } from "./routes.js";
29
27
  import type { AppRouterServerActionOptions } from "./actions.js";
30
28
  import type { AppRouterImportPolicy } from "./import-policy.js";
31
29
  import {
@@ -43,24 +41,21 @@ import {
43
41
  traceContextFromRequest,
44
42
  type RouterInstrumentation,
45
43
  } from "./trace.js";
46
- import {
47
- bytesResponse,
48
- htmlResponse,
49
- } from "./http.js";
50
- import {
51
- emitRouterLog,
52
- logDurationMs,
53
- logNow,
54
- type AppRouterLogger,
55
- } from "./logger.js";
44
+ import { bytesResponse, htmlResponse } from "./http.js";
45
+ import { emitRouterLog, logDurationMs, logNow, type AppRouterLogger } from "./logger.js";
56
46
  import { startNodeRequestServer } from "./node-server.js";
57
47
  import { builtAppRuntimePreloadPlan } from "./preload-policy.js";
58
48
  import { normalizeRoutePath } from "./route-path.js";
59
- import type { HttpUpgradeHandler } from "./upgrade.js";
49
+ import type { HttpUpgradeOriginPolicy, ManagedHttpUpgradeHandler } from "./upgrade.js";
60
50
  import {
61
51
  isCurrentPrerenderedRoute,
62
52
  isVisitorDependentResponse,
53
+ mergePrerenderedNavigationHtml,
54
+ protectNonceBearingResponse,
63
55
  PRERENDERED_ROUTE_SCHEMA_VERSION,
56
+ replayedPrerenderedRouteHeaders,
57
+ storedPrerenderedRouteHeaders,
58
+ validatedPrerenderedNavigationHtml,
64
59
  } from "./prerender-entry.js";
65
60
 
66
61
  interface BuiltRuntimeCacheEntry {
@@ -99,6 +94,7 @@ let warnedImplicitHostTrust = false;
99
94
  * Configures rendering a request against a built app-router output directory.
100
95
  */
101
96
  export interface RenderBuiltAppRequestOptions {
97
+ dehydrateOptions?: DehydrateOptions | undefined;
102
98
  outDir: string;
103
99
  importPolicy?: AppRouterImportPolicy | undefined;
104
100
  instrumentation?: RouterInstrumentation | undefined;
@@ -156,6 +152,7 @@ export interface BuiltAppRuntimePreloadStrategy {
156
152
  * Configures the Node HTTP server used to serve a built app-router output.
157
153
  */
158
154
  export interface StartServerOptions {
155
+ dehydrateOptions?: DehydrateOptions | undefined;
159
156
  outDir: string;
160
157
  port: number;
161
158
  hostname?: string;
@@ -163,7 +160,11 @@ export interface StartServerOptions {
163
160
  // a generic "Internal Server Error" body and logs the stack to stderr
164
161
  // via console.error. Issue 071: stack traces must never end up in
165
162
  // production responses.
166
- errorHandler?: (error: unknown) => { body: string; status: number; headers?: Record<string, string> };
163
+ errorHandler?: (error: unknown) => {
164
+ body: string;
165
+ status: number;
166
+ headers?: Record<string, string>;
167
+ };
167
168
  // When set, an incoming Host header that does not exactly match one of
168
169
  // the listed values is replaced with the configured hostname/port for
169
170
  // origin reconstruction. Use this in front of public deployments to
@@ -180,7 +181,13 @@ export interface StartServerOptions {
180
181
  routeCache?: AppRouterCache | undefined;
181
182
  serverActions?: AppRouterServerActionOptions | undefined;
182
183
  sinkStrategy?: ResponseSinkStrategy;
183
- onUpgrade?: HttpUpgradeHandler | undefined;
184
+ onUpgrade?: ManagedHttpUpgradeHandler | undefined;
185
+ /** Maximum time to wait for application-owned upgrade sockets during shutdown. */
186
+ upgradeCloseTimeoutMs?: number | undefined;
187
+ /** Maximum time an asynchronous upgrade handler may leave ownership undecided. */
188
+ upgradeDecisionTimeoutMs?: number | undefined;
189
+ /** Origin policy for application HTTP upgrades. Defaults to exact same-origin validation. */
190
+ upgradeOriginPolicy?: HttpUpgradeOriginPolicy | undefined;
184
191
  /**
185
192
  * Trusts the first `X-Forwarded-Proto` value when the socket is not TLS.
186
193
  *
@@ -219,7 +226,7 @@ export function warnIfImplicitHostTrust(options: {
219
226
 
220
227
  warnedImplicitHostTrust = true;
221
228
  console.error(
222
- "[mreact] Host header trust is implicit because neither allowedHosts nor hostPolicy is configured. Set allowedHosts for public deployments, hostPolicy: \"strict\" to reject unlisted Host headers, or hostPolicy: \"trusted-proxy\" when a trusted reverse proxy normalizes Host.",
229
+ '[mreact] Host header trust is implicit because neither allowedHosts nor hostPolicy is configured. Set allowedHosts for public deployments, hostPolicy: "strict" to reject unlisted Host headers, or hostPolicy: "trusted-proxy" when a trusted reverse proxy normalizes Host.',
223
230
  );
224
231
  }
225
232
 
@@ -255,6 +262,7 @@ export async function createBuiltRequestRuntime(
255
262
  emitBuiltRequestStart(renderOptions.instrumentation, request);
256
263
  const response = await renderBuiltAppRequestWithRuntime({
257
264
  ...renderOptions,
265
+ dehydrateOptions: renderOptions.dehydrateOptions ?? runtime.dehydrateOptions,
258
266
  instrumentation: withoutBuiltRequestInstrumentation(renderOptions.instrumentation),
259
267
  importPolicy:
260
268
  renderOptions.importPolicy === undefined
@@ -400,8 +408,11 @@ function withoutBuiltRequestInstrumentation(
400
408
  return undefined;
401
409
  }
402
410
 
403
- const { onRequestEnd: _onRequestEnd, onRequestStart: _onRequestStart, ...remaining } =
404
- instrumentation;
411
+ const {
412
+ onRequestEnd: _onRequestEnd,
413
+ onRequestStart: _onRequestStart,
414
+ ...remaining
415
+ } = instrumentation;
405
416
  return remaining;
406
417
  }
407
418
 
@@ -465,22 +476,15 @@ async function renderBuiltAppRequestWithRuntime(
465
476
  // no middleware at all. Otherwise a middleware that gates access (auth,
466
477
  // geo blocking, maintenance mode) would be bypassed for exactly the routes
467
478
  // it is most often used to protect.
468
- if (
469
- !options.runtime.hasMiddleware &&
470
- (request.method === "GET" || request.method === "HEAD")
471
- ) {
479
+ if (!options.runtime.hasMiddleware && (request.method === "GET" || request.method === "HEAD")) {
472
480
  // Sync fast path when no external prerender store is configured (the
473
481
  // common case): skip the Promise wrap that `readPrerenderedRoute`
474
482
  // would otherwise introduce just to satisfy the async signature.
475
483
  const prerendered =
476
484
  options.prerenderStore === undefined
477
485
  ? options.runtime.prerenderedRoutes.get(normalizedPath)
478
- : await readPrerenderedRoute(
479
- options.runtime,
480
- normalizedPath,
481
- options.prerenderStore,
482
- );
483
- const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
486
+ : await readPrerenderedRoute(options.runtime, normalizedPath, options.prerenderStore);
487
+ const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
484
488
 
485
489
  if (prerenderedResponse !== undefined) {
486
490
  return prerenderedResponse;
@@ -503,19 +507,12 @@ async function renderBuiltAppRequestWithRuntime(
503
507
  // Middleware apps resolve prerendered HTML here instead, so that a
504
508
  // middleware response or rewrite is honoured before the stored HTML is
505
509
  // served. The lookup uses the post-middleware path.
506
- if (
507
- options.runtime.hasMiddleware &&
508
- (request.method === "GET" || request.method === "HEAD")
509
- ) {
510
+ if (options.runtime.hasMiddleware && (request.method === "GET" || request.method === "HEAD")) {
510
511
  const prerendered =
511
512
  options.prerenderStore === undefined
512
513
  ? options.runtime.prerenderedRoutes.get(normalizedPath)
513
- : await readPrerenderedRoute(
514
- options.runtime,
515
- normalizedPath,
516
- options.prerenderStore,
517
- );
518
- const prerenderedResponse = prerenderedRouteResponse(prerendered, request.method);
514
+ : await readPrerenderedRoute(options.runtime, normalizedPath, options.prerenderStore);
515
+ const prerenderedResponse = prerenderedRouteResponse(prerendered, request);
519
516
 
520
517
  if (prerenderedResponse !== undefined) {
521
518
  emitBuiltRenderTiming(options, request, timing, prerenderedResponse.status);
@@ -540,34 +537,34 @@ async function renderBuiltAppRequestWithRuntime(
540
537
  requestUrl: new URL(request.url),
541
538
  });
542
539
 
543
- await applyBuiltPrerenderInvalidations(
544
- options.runtime,
545
- response,
546
- options.prerenderStore,
547
- );
540
+ await applyBuiltPrerenderInvalidations(options.runtime, response, options.prerenderStore);
548
541
 
549
- return options.sinkStrategy === "buffer"
550
- ? await materializeResponseAsBuffer(response)
551
- : response;
542
+ return options.sinkStrategy === "buffer" ? await materializeResponseAsBuffer(response) : response;
552
543
  }
553
544
 
554
545
  function prerenderedRouteResponse(
555
546
  prerendered: BuiltPrerenderedRoute | undefined,
556
- method: string,
547
+ request: Request,
557
548
  ): Response | undefined {
558
549
  if (!isCurrentPrerenderedRoute(prerendered)) {
559
550
  return undefined;
560
551
  }
561
552
 
562
- if (method === "HEAD") {
553
+ const navigation = request.headers.get("x-mreact-navigation") === "1";
554
+ const html = navigation ? validatedPrerenderedNavigationHtml(prerendered) : prerendered.html;
555
+ if (html === undefined) {
556
+ return undefined;
557
+ }
558
+
559
+ if (request.method === "HEAD") {
563
560
  return new Response(null, {
564
- headers: prerendered.headers,
561
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
565
562
  status: prerendered.status,
566
563
  });
567
564
  }
568
565
 
569
- return htmlResponse(prerendered.html, {
570
- headers: prerendered.headers,
566
+ return htmlResponse(html, {
567
+ headers: replayedPrerenderedRouteHeaders(prerendered, request),
571
568
  status: prerendered.status,
572
569
  });
573
570
  }
@@ -662,7 +659,7 @@ async function applyBuiltAppResponseHook(
662
659
  request: options.request,
663
660
  });
664
661
 
665
- return hooked instanceof Response ? hooked : response;
662
+ return protectNonceBearingResponse(hooked instanceof Response ? hooked : response);
666
663
  }
667
664
 
668
665
  async function materializeResponseAsBuffer(response: Response): Promise<Response> {
@@ -707,8 +704,12 @@ export async function startServer(
707
704
  port: options.port,
708
705
  resolveHost: resolveRequestHost,
709
706
  trustForwardedProto: options.trustForwardedProto,
707
+ upgradeCloseTimeoutMs: options.upgradeCloseTimeoutMs,
708
+ upgradeDecisionTimeoutMs: options.upgradeDecisionTimeoutMs,
709
+ upgradeOriginPolicy: options.upgradeOriginPolicy,
710
710
  render: (request) =>
711
711
  runtime.render(request, {
712
+ dehydrateOptions: options.dehydrateOptions,
712
713
  instrumentation: options.instrumentation,
713
714
  logger: options.logger,
714
715
  onResponse: options.onResponse,
@@ -927,6 +928,7 @@ function builtRenderAppRequestOptions(
927
928
  clientScripts: options.runtime.clientScripts,
928
929
  clientStylesByFile: options.runtime.clientStylesByFile,
929
930
  clientStyles: options.runtime.clientStyles,
931
+ dehydrateOptions: options.dehydrateOptions,
930
932
  importPolicy: {
931
933
  ...options.importPolicy,
932
934
  allowedSourceDirs: options.runtime.allowedSourceDirs,
@@ -960,7 +962,9 @@ function builtRenderAppRequestOptions(
960
962
 
961
963
  function mergeBuiltServerActionOptions(
962
964
  options: AppRouterServerActionOptions | undefined,
963
- allowedActions: readonly { moduleId: string; exportName: string; inferred?: boolean }[] | undefined,
965
+ allowedActions:
966
+ | readonly { moduleId: string; exportName: string; inferred?: boolean }[]
967
+ | undefined,
964
968
  ): AppRouterServerActionOptions | undefined {
965
969
  if (allowedActions === undefined) {
966
970
  return options;
@@ -976,7 +980,9 @@ async function renderAndCachePrerenderWithLock(
976
980
  options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
977
981
  path: string,
978
982
  ): Promise<Response> {
979
- const existing = options.runtime.prerenderLocks.get(path);
983
+ const lockKey =
984
+ options.request.headers.get("x-mreact-navigation") === "1" ? `${path}\0navigation` : path;
985
+ const existing = options.runtime.prerenderLocks.get(lockKey);
980
986
 
981
987
  if (existing !== undefined) {
982
988
  const result = await existing;
@@ -988,12 +994,12 @@ async function renderAndCachePrerenderWithLock(
988
994
  }
989
995
 
990
996
  const task = runPrerenderRegeneration(options, path);
991
- options.runtime.prerenderLocks.set(path, task);
997
+ options.runtime.prerenderLocks.set(lockKey, task);
992
998
 
993
999
  try {
994
1000
  return cloneResponse((await task).response);
995
1001
  } finally {
996
- options.runtime.prerenderLocks.delete(path);
1002
+ options.runtime.prerenderLocks.delete(lockKey);
997
1003
  }
998
1004
  }
999
1005
 
@@ -1001,15 +1007,15 @@ async function runPrerenderRegeneration(
1001
1007
  options: RenderBuiltAppRequestOptions & { runtime: BuiltRuntime },
1002
1008
  path: string,
1003
1009
  ): Promise<{ response: Response; shareable: boolean }> {
1010
+ const lockKey =
1011
+ options.request.headers.get("x-mreact-navigation") === "1" ? `${path}\0navigation` : path;
1004
1012
  const regenerate = async () => {
1005
1013
  const stored = await readPrerenderedRoute(options.runtime, path, options.prerenderStore);
1014
+ const storedResponse = prerenderedRouteResponse(stored, options.request);
1006
1015
 
1007
- if (stored !== undefined) {
1016
+ if (storedResponse !== undefined) {
1008
1017
  return {
1009
- response: htmlResponse(stored.html, {
1010
- headers: stored.headers,
1011
- status: stored.status,
1012
- }),
1018
+ response: storedResponse,
1013
1019
  shareable: true,
1014
1020
  };
1015
1021
  }
@@ -1018,7 +1024,10 @@ async function runPrerenderRegeneration(
1018
1024
  // render that depended on this visitor's request headers must not become
1019
1025
  // one; it is returned to the caller without being stored. The signal starts
1020
1026
  // closed so that a render which never reports back is not stored either.
1021
- const renderSignals = { headerDependent: () => true };
1027
+ const renderSignals = {
1028
+ headerDependent: () => true,
1029
+ strictTransportSecurity: () => undefined as string | undefined,
1030
+ };
1022
1031
  const response = await renderBuiltDynamicResponse({ ...options, renderSignals });
1023
1032
 
1024
1033
  if (!response.ok) {
@@ -1039,21 +1048,34 @@ async function runPrerenderRegeneration(
1039
1048
  };
1040
1049
  }
1041
1050
 
1051
+ const navigation = options.request.headers.get("x-mreact-navigation") === "1";
1052
+ if (navigation && stored === undefined) {
1053
+ return {
1054
+ response: htmlResponse(body, {
1055
+ headers: response.headers,
1056
+ status: response.status,
1057
+ }),
1058
+ shareable: false,
1059
+ };
1060
+ }
1061
+
1042
1062
  return {
1043
1063
  response: await cacheRegeneratedPrerenderedRoute(
1044
- options.runtime,
1045
- path,
1046
- body,
1047
- response,
1048
- options.prerenderStore,
1049
- ),
1064
+ options.runtime,
1065
+ path,
1066
+ body,
1067
+ response,
1068
+ renderSignals.strictTransportSecurity(),
1069
+ options.prerenderStore,
1070
+ navigation ? stored : undefined,
1071
+ ),
1050
1072
  shareable: true,
1051
1073
  };
1052
1074
  };
1053
1075
 
1054
1076
  return options.prerenderStore?.withLock === undefined
1055
1077
  ? await regenerate()
1056
- : await options.prerenderStore.withLock(path, regenerate);
1078
+ : await options.prerenderStore.withLock(lockKey, regenerate);
1057
1079
  }
1058
1080
 
1059
1081
  async function applyBuiltPrerenderInvalidations(
@@ -1079,19 +1101,27 @@ async function cacheRegeneratedPrerenderedRoute(
1079
1101
  path: string,
1080
1102
  body: string,
1081
1103
  response: Response,
1104
+ strictTransportSecurity: string | undefined,
1082
1105
  store: AppRouterPrerenderStore | undefined,
1106
+ existing?: BuiltPrerenderedRoute | undefined,
1083
1107
  ): Promise<Response> {
1084
- const headers: Record<string, string> = {};
1085
-
1086
- response.headers.forEach((value, key) => {
1087
- headers[key] = value;
1088
- });
1089
- const entry: BuiltPrerenderedRoute = {
1090
- headers,
1091
- html: body,
1092
- schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
1093
- status: response.status,
1094
- };
1108
+ const headers = storedPrerenderedRouteHeaders(response.headers);
1109
+ const entry: BuiltPrerenderedRoute =
1110
+ existing === undefined
1111
+ ? {
1112
+ headers,
1113
+ html: body,
1114
+ schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
1115
+ status: response.status,
1116
+ ...(strictTransportSecurity === undefined ? {} : { strictTransportSecurity }),
1117
+ }
1118
+ : mergePrerenderedNavigationHtml(existing, body);
1119
+ if (entry === existing) {
1120
+ return htmlResponse(body, {
1121
+ headers: response.headers,
1122
+ status: response.status,
1123
+ });
1124
+ }
1095
1125
  runtime.prerenderedRoutes.set(path, entry);
1096
1126
  await store?.set(path, entry);
1097
1127
 
package/src/session.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { deleteCookie, parseCookieHeader, setCookie } from "./cookies.js";
1
+ import { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from "./cookies.js";
2
2
 
3
3
  /**
4
4
  * Stores session data and expiration metadata for one session id.
@@ -43,32 +43,54 @@ const DEFAULT_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
43
43
  const SESSION_COOKIE_DEV = "mreact.session";
44
44
  const SESSION_COOKIE_PROD = "__Host-mreact.session";
45
45
 
46
- function isProduction(): boolean {
47
- return process.env.NODE_ENV === "production";
46
+ function usesHardenedCookieDefaults(): boolean {
47
+ const nodeEnv = typeof process === "undefined" ? undefined : process["env"]?.["NODE_ENV"];
48
+ return nodeEnv !== "development" && nodeEnv !== "test";
48
49
  }
49
50
 
50
- function defaultCookieName(): string {
51
- return isProduction() ? SESSION_COOKIE_PROD : SESSION_COOKIE_DEV;
52
- }
53
-
54
- function sessionCookieOptions(options: SessionCookieOptions = {}) {
55
- const production = isProduction();
56
-
57
- return {
51
+ function resolveSessionCookie(options: SessionCookieOptions = {}) {
52
+ const hardened = usesHardenedCookieDefaults();
53
+ const cookieOptions = {
58
54
  httpOnly: true,
59
55
  maxAge: options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS,
60
- path: production ? "/" : options.path ?? "/",
56
+ path: options.path ?? "/",
61
57
  sameSite: options.sameSite ?? "Lax",
62
- secure: options.secure ?? production,
58
+ secure: options.secure ?? hardened,
63
59
  } as const;
60
+ const canUseHostPrefix = cookieOptions.secure && cookieOptions.path === "/";
61
+
62
+ return {
63
+ name:
64
+ options.cookieName ??
65
+ (hardened && canUseHostPrefix ? SESSION_COOKIE_PROD : SESSION_COOKIE_DEV),
66
+ options: cookieOptions,
67
+ };
64
68
  }
65
69
 
66
- function readSessionId(
67
- request: Request,
68
- options: SessionCookieOptions = {},
69
- ): string | undefined {
70
+ function validateResolvedSessionCookie(cookie: ReturnType<typeof resolveSessionCookie>): void {
71
+ serializeCookie(cookie.name, "", cookie.options);
72
+ }
73
+
74
+ function assertMutableResponseHeaders(response: Response): void {
75
+ const probeName = "x-mreact-session-header-probe";
76
+ const previous = response.headers.get(probeName);
77
+
78
+ try {
79
+ response.headers.set(probeName, "1");
80
+ if (previous === null) {
81
+ response.headers.delete(probeName);
82
+ } else {
83
+ response.headers.set(probeName, previous);
84
+ }
85
+ } catch (cause) {
86
+ throw new TypeError("Session helpers require a Response with mutable headers.", { cause });
87
+ }
88
+ }
89
+
90
+ function readSessionId(request: Request, options: SessionCookieOptions = {}): string | undefined {
91
+ const cookie = resolveSessionCookie(options);
70
92
  const values = parseCookieHeader(request.headers.get("cookie"));
71
- return values.get(options.cookieName ?? defaultCookieName());
93
+ return values.get(cookie.name);
72
94
  }
73
95
 
74
96
  function createSessionId(): string {
@@ -208,6 +230,9 @@ export async function createSession<TData>(
208
230
  ): Promise<SessionRecord<TData>> {
209
231
  const now = Date.now();
210
232
  const maxAgeSeconds = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;
233
+ const cookie = resolveSessionCookie({ ...options, maxAgeSeconds });
234
+ validateResolvedSessionCookie(cookie);
235
+ assertMutableResponseHeaders(response);
211
236
  const record: SessionRecord<TData> = {
212
237
  createdAt: now,
213
238
  data,
@@ -216,9 +241,7 @@ export async function createSession<TData>(
216
241
  };
217
242
 
218
243
  await store.set(record);
219
- setCookie(response, options.cookieName ?? defaultCookieName(), record.id, {
220
- ...sessionCookieOptions({ ...options, maxAgeSeconds }),
221
- });
244
+ setCookie(response, cookie.name, record.id, cookie.options);
222
245
 
223
246
  return record;
224
247
  }
@@ -232,16 +255,22 @@ export async function destroySession<TData>(
232
255
  store: SessionStore<TData>,
233
256
  options: SessionCookieOptions = {},
234
257
  ): Promise<void> {
258
+ const cookie = resolveSessionCookie(options);
259
+ validateResolvedSessionCookie({
260
+ name: cookie.name,
261
+ options: { ...cookie.options, maxAge: 0 },
262
+ });
263
+ assertMutableResponseHeaders(response);
235
264
  const id = readSessionId(request, options);
236
265
 
237
266
  if (id !== undefined) {
238
267
  await store.delete(id);
239
268
  }
240
269
 
241
- deleteCookie(response, options.cookieName ?? defaultCookieName(), {
242
- path: isProduction() ? "/" : options.path ?? "/",
243
- sameSite: options.sameSite ?? "Lax",
244
- secure: options.secure ?? isProduction(),
270
+ deleteCookie(response, cookie.name, {
271
+ path: cookie.options.path,
272
+ sameSite: cookie.options.sameSite,
273
+ secure: cookie.options.secure,
245
274
  });
246
275
  }
247
276
 
@@ -256,6 +285,8 @@ export async function rotateSession<TData>(
256
285
  store: SessionStore<TData>,
257
286
  options: SessionCookieOptions = {},
258
287
  ): Promise<SessionRecord<TData> | undefined> {
288
+ validateResolvedSessionCookie(resolveSessionCookie(options));
289
+ assertMutableResponseHeaders(response);
259
290
  const current = await getSession(request, store, options);
260
291
 
261
292
  if (current === undefined) {