@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/build.ts CHANGED
@@ -16,6 +16,7 @@ import { builtinModules } from "node:module";
16
16
  import { availableParallelism } from "node:os";
17
17
  import { dirname, join, relative, resolve, sep } from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
+ import type { DehydrateOptions } from "@reckona/mreact-query";
19
20
  import {
20
21
  collectStaticImportReferences,
21
22
  collectTopLevelValueExportNames,
@@ -46,6 +47,7 @@ import {
46
47
  buildNavigationRuntimeBundle,
47
48
  clientScriptForPath,
48
49
  routeIdForPath,
50
+ routeMarkerParts,
49
51
  type BuildClientRouteOutputOptions,
50
52
  } from "./navigation-runtime.js";
51
53
  import {
@@ -56,6 +58,7 @@ import {
56
58
  resolveCompatVendorEntryFiles,
57
59
  sourceReferencesCompatVendorSpecifier,
58
60
  } from "./module-runner.js";
61
+ import { dehydrateOptionsFromModule } from "./dehydrate-policy.js";
59
62
  import { compileRouteMatcherArtifact, scanAppRoutes } from "./routes.js";
60
63
  import type { AppRoute, CompiledRouteMatcherArtifact } from "./routes.js";
61
64
  import { appFileConventionForRootFilename } from "./file-conventions.js";
@@ -72,6 +75,7 @@ import type { RouteCachePolicy } from "./cache.js";
72
75
  import { routeCachePolicyFromSource } from "./cache.js";
73
76
  import {
74
77
  bundleMiddlewareModuleCode,
78
+ prerenderVariantMarkerParts,
75
79
  renderAppRequest,
76
80
  type RenderAppRequestRuntimeOptions,
77
81
  } from "./render.js";
@@ -105,6 +109,7 @@ import { collectBuildInferredServerActions } from "./server-action-inference.js"
105
109
  import {
106
110
  isVisitorDependentResponse,
107
111
  PRERENDERED_ROUTE_SCHEMA_VERSION,
112
+ storedPrerenderedRouteHeaders,
108
113
  } from "./prerender-entry.js";
109
114
  import { prepareRouteServerActionPlaceholders } from "./actions.js";
110
115
  import { viteDefineCacheKey, vitePluginsCacheKey } from "./vite-plugin-cache-key.js";
@@ -246,10 +251,11 @@ export interface BuildAppResult {
246
251
 
247
252
  interface IncrementalBuildCacheManifest {
248
253
  fingerprint: string;
249
- version: 1;
254
+ version: 2;
250
255
  }
251
256
 
252
257
  interface IncrementalBuildServerManifestOutputs {
258
+ dehydratePolicyModule?: string;
253
259
  serverModuleFiles?: Record<string, string>;
254
260
  serverModuleRenderFiles?: Record<string, string>;
255
261
  serverModuleRequestFiles?: Record<string, string>;
@@ -323,6 +329,7 @@ export interface PackageCloudflarePagesArtifactOptions {
323
329
  export interface BuiltServerManifest {
324
330
  allowedSourceDirs?: readonly string[];
325
331
  assetBaseUrl?: string;
332
+ dehydratePolicyModule?: string;
326
333
  version: 1;
327
334
  files: Record<string, string>;
328
335
  prerenderedRoutes?: Record<string, BuiltPrerenderedRoute>;
@@ -423,9 +430,11 @@ export interface BuiltServerModuleOutput {
423
430
  export interface BuiltPrerenderedRoute {
424
431
  headers: Record<string, string>;
425
432
  html: string;
433
+ navigationHtml?: string | undefined;
426
434
  /** Identifies entries that satisfy the complete current prerender contract. */
427
- schemaVersion?: 2 | undefined;
435
+ schemaVersion?: 4 | undefined;
428
436
  status: number;
437
+ strictTransportSecurity?: string | undefined;
429
438
  }
430
439
 
431
440
  type StaticParams = Record<string, string | number | boolean | readonly string[]>;
@@ -451,6 +460,13 @@ async function buildAppWithResolvedProject(
451
460
  options: BuildAppOptions,
452
461
  project: ResolvedAppRouterProject,
453
462
  ): Promise<BuildAppResult> {
463
+ if (
464
+ (options as BuildAppOptions & { dehydrateOptions?: unknown }).dehydrateOptions !== undefined
465
+ ) {
466
+ throw new Error(
467
+ "buildApp dehydration policies must use dehydratePolicyModule so the same executable policy is available to prerendering and built runtimes.",
468
+ );
469
+ }
454
470
  const timingSink = options.onBuildPhaseTiming;
455
471
  const progressSink = options.onBuildProgress;
456
472
  const shouldTrackBuildPhases = timingSink !== undefined || progressSink !== undefined;
@@ -567,6 +583,24 @@ async function buildAppWithResolvedProject(
567
583
  });
568
584
  }
569
585
 
586
+ const builtDehydratePolicy =
587
+ project.dehydratePolicyModule === undefined
588
+ ? undefined
589
+ : await buildDehydratePolicyModule({
590
+ define: viteDefine,
591
+ file: project.dehydratePolicyModule,
592
+ projectRoot: project.projectRoot,
593
+ vitePlugins,
594
+ });
595
+ const dehydratePolicyArtifact =
596
+ builtDehydratePolicy === undefined
597
+ ? undefined
598
+ : `dehydrate-policy.${hashText(builtDehydratePolicy.code).slice(0, 16)}.mjs`;
599
+ if (builtDehydratePolicy !== undefined && dehydratePolicyArtifact !== undefined) {
600
+ await writeFile(join(serverDir, dehydratePolicyArtifact), builtDehydratePolicy.code);
601
+ }
602
+ const dehydrateOptions = builtDehydratePolicy?.dehydrateOptions;
603
+
570
604
  const publicAssets =
571
605
  shouldTrackBuildPhases === false
572
606
  ? await buildPublicAssetManifest(project, clientDir)
@@ -724,6 +758,7 @@ async function buildAppWithResolvedProject(
724
758
  assetBaseUrl: project.assetBaseUrl,
725
759
  clientRoutes: clientManifestRoutes,
726
760
  define: viteDefine,
761
+ dehydrateOptions,
727
762
  project,
728
763
  routes,
729
764
  serverModules,
@@ -736,6 +771,7 @@ async function buildAppWithResolvedProject(
736
771
  assetBaseUrl: project.assetBaseUrl,
737
772
  clientRoutes: clientManifestRoutes,
738
773
  define: viteDefine,
774
+ dehydrateOptions,
739
775
  project,
740
776
  routes,
741
777
  serverModules,
@@ -785,6 +821,9 @@ async function buildAppWithResolvedProject(
785
821
  relative(project.projectRoot, directory),
786
822
  ),
787
823
  ...(project.assetBaseUrl === undefined ? {} : { assetBaseUrl: project.assetBaseUrl }),
824
+ ...(dehydratePolicyArtifact === undefined
825
+ ? {}
826
+ : { dehydratePolicyModule: dehydratePolicyArtifact }),
788
827
  version: 1,
789
828
  routes: serverRoutes,
790
829
  routeMatcher: compileRouteMatcherArtifact(serverRoutes),
@@ -898,7 +937,7 @@ async function buildAppWithResolvedProject(
898
937
  }
899
938
 
900
939
  const incrementalBuildCacheFilename = "build-cache.json";
901
- const incrementalBuildCacheVersion = 1;
940
+ const incrementalBuildCacheVersion = 2;
902
941
 
903
942
  async function createIncrementalBuildCacheFingerprint(options: {
904
943
  buildTargets: readonly AppRouterBuildTarget[];
@@ -944,6 +983,12 @@ async function createIncrementalBuildCacheFingerprint(options: {
944
983
  assetBaseUrl: options.project.assetBaseUrl ?? null,
945
984
  clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
946
985
  clientSourceMaps: options.project.clientSourceMaps,
986
+ dehydratePolicyModule:
987
+ options.project.dehydratePolicyModule === undefined
988
+ ? null
989
+ : normalizeBuildInputPath(
990
+ relative(options.project.projectRoot, options.project.dehydratePolicyModule),
991
+ ),
947
992
  define,
948
993
  nodeEnv: process.env.NODE_ENV ?? null,
949
994
  publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
@@ -1010,6 +1055,9 @@ async function hasRequiredIncrementalBuildOutputs(
1010
1055
  }
1011
1056
 
1012
1057
  for (const file of [
1058
+ ...(serverManifest.dehydratePolicyModule === undefined
1059
+ ? []
1060
+ : [serverManifest.dehydratePolicyModule]),
1013
1061
  ...Object.values(serverManifest.serverModuleFiles ?? {}),
1014
1062
  ...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
1015
1063
  ...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
@@ -1461,7 +1509,7 @@ async function buildRouteUsesRequestInput(options: {
1461
1509
  files: options.files,
1462
1510
  projectRoot: options.projectRoot,
1463
1511
  source: entry.source,
1464
- })
1512
+ }),
1465
1513
  );
1466
1514
  }
1467
1515
 
@@ -2555,6 +2603,38 @@ function isReservedPublicAssetPath(pathname: string): boolean {
2555
2603
  return pathname === "/_mreact" || pathname.startsWith("/_mreact/");
2556
2604
  }
2557
2605
 
2606
+ async function buildDehydratePolicyModule(options: {
2607
+ define?: UserConfig["define"] | undefined;
2608
+ file: string;
2609
+ projectRoot: string;
2610
+ vitePlugins?: readonly PluginOption[] | undefined;
2611
+ }): Promise<{ code: string; dehydrateOptions: DehydrateOptions }> {
2612
+ const source = await readFile(options.file, "utf8");
2613
+ const code = await bundleAppRouterSourceModule({
2614
+ code: source,
2615
+ define: options.define,
2616
+ label: `dehydrate-policy:${options.file}`,
2617
+ plugins: [fileImportMetaUrlPlugin()],
2618
+ resolveDir: dirname(options.file),
2619
+ root: options.projectRoot,
2620
+ sourcefile: options.file,
2621
+ vitePlugins: options.vitePlugins,
2622
+ });
2623
+ const policyModule = await importAppRouterSourceModule<unknown>({
2624
+ code,
2625
+ label: `dehydrate-policy-runtime:${options.file}`,
2626
+ sourcefile: options.file,
2627
+ });
2628
+
2629
+ return {
2630
+ code,
2631
+ dehydrateOptions: dehydrateOptionsFromModule(
2632
+ policyModule,
2633
+ `mreactRouter dehydratePolicyModule ${options.file}`,
2634
+ ),
2635
+ };
2636
+ }
2637
+
2558
2638
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
2559
2639
  return error instanceof Error && "code" in error;
2560
2640
  }
@@ -2563,6 +2643,7 @@ async function prerenderStaticRoutes(options: {
2563
2643
  appDir: string;
2564
2644
  assetBaseUrl?: string | undefined;
2565
2645
  clientRoutes: readonly ClientRouteManifestEntry[];
2646
+ dehydrateOptions?: DehydrateOptions | undefined;
2566
2647
  define?: UserConfig["define"] | undefined;
2567
2648
  project: ResolvedAppRouterProject;
2568
2649
  routes: readonly AppRoute[];
@@ -2620,6 +2701,7 @@ async function prerenderStaticRoutes(options: {
2620
2701
  for (const pathname of await prerenderPathsForRoute(route, analysis, options.vitePlugins)) {
2621
2702
  const renderSignals = {
2622
2703
  headerDependent: () => true,
2704
+ strictTransportSecurity: () => undefined as string | undefined,
2623
2705
  };
2624
2706
  const renderOptions = {
2625
2707
  appDir: options.appDir,
@@ -2627,33 +2709,51 @@ async function prerenderStaticRoutes(options: {
2627
2709
  clientScripts,
2628
2710
  clientStyles,
2629
2711
  define: options.define,
2712
+ dehydrateOptions: options.dehydrateOptions,
2630
2713
  importPolicy,
2631
2714
  navigationScripts,
2632
2715
  request: new Request(`http://mreact.local${pathname}`),
2716
+ prerenderVariantCapture: !analysis.clientRoute,
2633
2717
  renderSignals,
2634
2718
  serverModuleCacheVersion,
2635
2719
  serverModules: serverModuleMap,
2636
2720
  vitePlugins: options.vitePlugins,
2637
2721
  } satisfies RenderAppRequestRuntimeOptions;
2638
2722
  const response = await renderAppRequest(renderOptions);
2639
- const html = await response.text();
2723
+ const renderedHtml = await response.text();
2640
2724
 
2641
2725
  if (renderSignals.headerDependent() || isVisitorDependentResponse(response)) {
2642
2726
  continue;
2643
2727
  }
2644
2728
 
2645
- const headers: Record<string, string> = {};
2729
+ let html = renderedHtml;
2730
+ let navigationHtml = renderedHtml;
2731
+ if (!analysis.clientRoute) {
2732
+ const capture = prerenderVariantMarkerParts(route.path);
2733
+ const start = renderedHtml.indexOf(capture.prefix);
2734
+ const end = renderedHtml.lastIndexOf(capture.suffix);
2735
+ if (start === -1 || end < start + capture.prefix.length) {
2736
+ throw new Error(`Failed to capture prerender response variants for ${pathname}.`);
2737
+ }
2738
+ const before = renderedHtml.slice(0, start);
2739
+ const content = renderedHtml.slice(start + capture.prefix.length, end);
2740
+ const after = renderedHtml.slice(end + capture.suffix.length);
2741
+ const navigationMarker = routeMarkerParts(route.path);
2742
+ html = `${before}${content}${after}`;
2743
+ navigationHtml = `${before}${navigationMarker.prefix}${content}${navigationMarker.suffix}${after}`;
2744
+ }
2646
2745
 
2647
- response.headers.forEach((value, key) => {
2648
- headers[key] = value;
2649
- });
2746
+ const headers = storedPrerenderedRouteHeaders(response.headers);
2747
+ const strictTransportSecurity = renderSignals.strictTransportSecurity();
2650
2748
  entries.push([
2651
2749
  pathname,
2652
2750
  {
2653
2751
  headers,
2654
2752
  html,
2753
+ navigationHtml,
2655
2754
  schemaVersion: PRERENDERED_ROUTE_SCHEMA_VERSION,
2656
2755
  status: response.status,
2756
+ ...(strictTransportSecurity === undefined ? {} : { strictTransportSecurity }),
2657
2757
  },
2658
2758
  ]);
2659
2759
  }
@@ -3954,6 +4054,7 @@ function cloudflarePageRouteFacadeModuleSource(componentImport: string): string
3954
4054
  const componentSlots = readComponentModuleExport(componentModule, "slots");
3955
4055
  const componentGenerateMetadata = readComponentModuleExport(componentModule, "generateMetadata");
3956
4056
  const componentMetadata = readComponentModuleExport(componentModule, "metadata");
4057
+ const componentSecurityHeadersApplied = readComponentModuleExport(componentModule, "__mreactSecurityHeadersApplied");
3957
4058
 
3958
4059
  export function App(props) {
3959
4060
  return renderCloudflareRouteComponent(props);
@@ -3968,6 +4069,7 @@ export const slots = componentSlots === undefined ? undefined : { ...componentSl
3968
4069
  export const generateMetadata =
3969
4070
  typeof componentGenerateMetadata === "function" ? componentGenerateMetadata : undefined;
3970
4071
  export const metadata = componentMetadata;
4072
+ export const __mreactSecurityHeadersApplied = componentSecurityHeadersApplied === true;
3971
4073
 
3972
4074
  function renderCloudflareRouteComponent(props) {
3973
4075
  const routeComponent = resolveCloudflareRouteComponent();
@@ -4714,7 +4816,9 @@ ${cloudflareShellRuntimeSource()}`;
4714
4816
  }
4715
4817
 
4716
4818
  function cloudflareShellRuntimeSource(): string {
4717
- return `async function renderLayoutShells(shells, props, namedSlots) {
4819
+ return `export const __mreactSecurityHeadersApplied = true;
4820
+
4821
+ async function renderLayoutShells(shells, props, namedSlots) {
4718
4822
  const slotContext = { consumedSlots: new Set(), namedSlots };
4719
4823
  const rendered = [];
4720
4824
  for (const shell of shells) {
@@ -4793,7 +4897,7 @@ function cloudflareHydrationMarkerParts(props) {
4793
4897
  const escapedRouteId = escapeHtmlAttribute(routeId);
4794
4898
  const propsJson = escapeScriptJson(JSON.stringify({
4795
4899
  params: props.params,
4796
- request: { url: props.request.url },
4900
+ request: { url: new URL(props.request.url).pathname },
4797
4901
  data: props.data,
4798
4902
  }));
4799
4903
  const clientReferencesJson = route.clientReferenceManifest === undefined || route.clientReferenceManifest.length === 0
@@ -4975,9 +5079,43 @@ function validateOptionalHeadMetadata(value, path) {
4975
5079
  }
4976
5080
  }
4977
5081
  }
5082
+ validateMetaRefreshHeadDescriptor(descriptor, descriptorPath);
4978
5083
  });
4979
5084
  }
4980
5085
 
5086
+ function validateMetaRefreshHeadDescriptor(descriptor, path) {
5087
+ if (descriptor.tag !== "meta" || descriptor.attrs === undefined) {
5088
+ return;
5089
+ }
5090
+ const attrs = Object.entries(descriptor.attrs);
5091
+ const hasRefresh = attrs.some(([name, value]) => {
5092
+ const canonicalName = name.toLowerCase();
5093
+ return (canonicalName === "http-equiv" || canonicalName === "httpequiv")
5094
+ && typeof value === "string"
5095
+ && value.toLowerCase() === "refresh";
5096
+ });
5097
+ if (!hasRefresh) {
5098
+ return;
5099
+ }
5100
+ for (const [name, value] of attrs) {
5101
+ if (name.toLowerCase() !== "content" || typeof value !== "string") {
5102
+ continue;
5103
+ }
5104
+ const match = /^[^;]*;\\s*url\\s*=\\s*([\\s\\S]+)$/iu.exec(value);
5105
+ if (match === null || match[1] === undefined) {
5106
+ continue;
5107
+ }
5108
+ let target = match[1].trim();
5109
+ const quote = target[0];
5110
+ if ((quote === String.fromCharCode(34) || quote === "'") && target[target.length - 1] === quote) {
5111
+ target = target.slice(1, -1).trim();
5112
+ }
5113
+ if (isUnsafeUrlValueForName("href", target)) {
5114
+ throw new Error(\`Invalid metadata field \${path}.attrs.\${name}: unsafe refresh URL value.\`);
5115
+ }
5116
+ }
5117
+ }
5118
+
4981
5119
  function validateHeadAttribute(name, value, path) {
4982
5120
  if (!isSafeHeadAttributeName(name)) {
4983
5121
  throw new Error(\`Invalid metadata field \${path}: expected safe HTML attribute name.\`);
@@ -5039,7 +5177,7 @@ function isUnsafeUrlValueForName(name, value) {
5039
5177
  }
5040
5178
  const scheme = match[1].toLowerCase();
5041
5179
  if (scheme === "data" && (name === "src" || name === "poster")) {
5042
- return !/^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(canonical);
5180
+ return !/^data:image\\/(?!svg\\+xml\\s*(?:[;,]|$))/i.test(canonical);
5043
5181
  }
5044
5182
  return scheme === "javascript" || scheme === "data" || scheme === "vbscript" || scheme === "livescript" || scheme === "mhtml" || scheme === "file";
5045
5183
  }
@@ -5216,17 +5354,63 @@ function routeSecurityHeaders(security, request) {
5216
5354
  } else {
5217
5355
  headers["referrer-policy"] = validateHeaderValue(security?.referrerPolicy ?? "strict-origin-when-cross-origin");
5218
5356
  }
5357
+ if (security?.permissionsPolicy === null) {
5358
+ delete headers["permissions-policy"];
5359
+ } else {
5360
+ const permissionsPolicy = serializePermissionsPolicy(security?.permissionsPolicy);
5361
+ if (permissionsPolicy === undefined) {
5362
+ delete headers["permissions-policy"];
5363
+ } else {
5364
+ headers["permissions-policy"] = permissionsPolicy;
5365
+ }
5366
+ }
5219
5367
  if (security?.frameOptions === null) {
5220
5368
  delete headers["x-frame-options"];
5221
5369
  } else if (security?.frameOptions !== undefined) {
5222
5370
  headers["x-frame-options"] = validateHeaderValue(security.frameOptions);
5223
5371
  }
5224
5372
  if (request.url.startsWith("https://") && security?.hsts !== undefined && security.hsts !== false && security.hsts !== null) {
5225
- headers["strict-transport-security"] = \`max-age=\${Math.trunc(security.hsts.maxAge)}\${security.hsts.includeSubDomains === true ? "; includeSubDomains" : ""}\${security.hsts.preload === true ? "; preload" : ""}\`;
5373
+ headers["strict-transport-security"] = serializeHsts(security.hsts);
5226
5374
  }
5227
5375
  return headers;
5228
5376
  }
5229
5377
 
5378
+ function serializeHsts(hsts) {
5379
+ if (hsts === false) {
5380
+ throw new TypeError("Invalid security header value for hsts.");
5381
+ }
5382
+ const maxAge = Math.trunc(hsts.maxAge);
5383
+ if (!Number.isFinite(maxAge) || maxAge < 0) {
5384
+ throw new TypeError("Invalid security header value for hsts.maxAge.");
5385
+ }
5386
+ const parts = [\`max-age=\${maxAge}\`];
5387
+ if (hsts.includeSubDomains === true) {
5388
+ parts.push("includeSubDomains");
5389
+ }
5390
+ if (hsts.preload === true) {
5391
+ parts.push("preload");
5392
+ }
5393
+ return parts.join("; ");
5394
+ }
5395
+
5396
+ function serializePermissionsPolicy(policy) {
5397
+ if (policy === undefined) {
5398
+ return "camera=(), microphone=(), geolocation=()";
5399
+ }
5400
+ const directives = [];
5401
+ for (const [directive, allowlist] of Object.entries(policy)) {
5402
+ if (allowlist === null || allowlist === undefined) {
5403
+ continue;
5404
+ }
5405
+ validateToken(directive, "permissionsPolicy directive");
5406
+ for (const value of allowlist) {
5407
+ validatePermissionAllowlistValue(value);
5408
+ }
5409
+ directives.push(\`\${directive}=(\${allowlist.join(" ")})\`);
5410
+ }
5411
+ return directives.length === 0 ? undefined : directives.join(", ");
5412
+ }
5413
+
5230
5414
  function validateHeaderValue(value) {
5231
5415
  for (let index = 0; index < value.length; index += 1) {
5232
5416
  const code = value.charCodeAt(index);
@@ -5237,6 +5421,22 @@ function validateHeaderValue(value) {
5237
5421
  return value;
5238
5422
  }
5239
5423
 
5424
+ function validateToken(value, label) {
5425
+ if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(value)) {
5426
+ throw new TypeError(\`Invalid security header value for \${label}: \${JSON.stringify(value)}\`);
5427
+ }
5428
+ }
5429
+
5430
+ function validatePermissionAllowlistValue(value) {
5431
+ if (value === "self" || value === "*" || /^[A-Za-z][A-Za-z0-9+.-]*:$/.test(value)) {
5432
+ return;
5433
+ }
5434
+ if (/^https:\\/\\/[A-Za-z0-9.-]+(?::[0-9]+)?$/.test(value)) {
5435
+ return;
5436
+ }
5437
+ throw new TypeError(\`Invalid security header value for permissionsPolicy allowlist: \${JSON.stringify(value)}\`);
5438
+ }
5439
+
5240
5440
  function mergeRouteMetadata(metadata) {
5241
5441
  if (metadata.length === 0) {
5242
5442
  return undefined;
@@ -6216,6 +6416,10 @@ async function writeClientRouteBundles(options: {
6216
6416
  options.sourceAnalysis.byRouteFile.get(
6217
6417
  relative(options.projectRoot, route.file).split(sep).join("/"),
6218
6418
  )?.streamRoute === true,
6419
+ restoreRequestUrl:
6420
+ options.sourceAnalysis.byRouteFile.get(
6421
+ relative(options.projectRoot, route.file).split(sep).join("/"),
6422
+ )?.usesRequestInput === true,
6219
6423
  cacheDir: options.cacheDir,
6220
6424
  dropConsoleFunctions: options.clientConsolePureFunctions,
6221
6425
  filename: route.file,
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, normalize } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import type { DehydrateOptions } from "@reckona/mreact-query";
4
6
  import type {
5
7
  BuiltPrerenderedRoute,
6
8
  BuiltServerManifest,
@@ -10,6 +12,7 @@ import { builtClientAssetPaths } from "./built-assets.js";
10
12
  import type { BuiltServerModuleArtifactRuntime } from "./built-server-module-artifacts.js";
11
13
  import type { ClientRouteManifestEntry } from "./client-route-inference.js";
12
14
  import type { AppRouterImportPolicy } from "./import-policy.js";
15
+ import { dehydrateOptionsFromModule } from "./dehydrate-policy.js";
13
16
  import { createRouteMatcher, type AppRoute, type RouteMatcher } from "./routes.js";
14
17
 
15
18
  export interface BuiltRuntime extends BuiltServerModuleArtifactRuntime {
@@ -19,6 +22,7 @@ export interface BuiltRuntime extends BuiltServerModuleArtifactRuntime {
19
22
  clientScripts: ReadonlyMap<string, string>;
20
23
  clientStylesByFile: ReadonlyMap<string, readonly string[]>;
21
24
  clientStyles: ReadonlyMap<string, readonly string[]>;
25
+ dehydrateOptions?: DehydrateOptions | undefined;
22
26
  generatedImportPolicy?: AppRouterImportPolicy | undefined;
23
27
  hasMiddleware: boolean;
24
28
  navigationScripts: ReadonlyMap<string, string>;
@@ -160,6 +164,13 @@ export async function materializeBuiltRuntime(options: {
160
164
  options.importPolicyText,
161
165
  options.importPolicyPath,
162
166
  );
167
+ const dehydrateOptions =
168
+ serverManifest.dehydratePolicyModule === undefined
169
+ ? undefined
170
+ : await loadBuiltDehydratePolicy(
171
+ join(options.outDir, "server", safeManifestFilePath(serverManifest.dehydratePolicyModule)),
172
+ serverManifest.dehydratePolicyModule,
173
+ );
163
174
 
164
175
  return {
165
176
  appDir: routesDir,
@@ -171,6 +182,7 @@ export async function materializeBuiltRuntime(options: {
171
182
  clientScripts,
172
183
  clientStylesByFile,
173
184
  clientStyles,
185
+ ...(dehydrateOptions === undefined ? {} : { dehydrateOptions }),
174
186
  ...(generatedImportPolicy === undefined ? {} : { generatedImportPolicy }),
175
187
  hasMiddleware,
176
188
  navigationScripts,
@@ -198,6 +210,22 @@ export async function materializeBuiltRuntime(options: {
198
210
  };
199
211
  }
200
212
 
213
+ async function loadBuiltDehydratePolicy(
214
+ policyPath: string,
215
+ manifestPath: string,
216
+ ): Promise<DehydrateOptions> {
217
+ let policyModule: unknown;
218
+ try {
219
+ policyModule = await import(pathToFileURL(policyPath).href);
220
+ } catch (error) {
221
+ throw builtArtifactReadError("built app dehydration policy", policyPath, error);
222
+ }
223
+ return dehydrateOptionsFromModule(
224
+ policyModule,
225
+ `Built dehydration policy ${manifestPath}`,
226
+ );
227
+ }
228
+
201
229
  export async function readBuiltImportPolicyText(outDir: string): Promise<string | undefined> {
202
230
  const policyPath = join(outDir, "server", "import-policy.json");
203
231