@reckona/mreact-router 0.0.203 → 0.0.205

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 (140) hide show
  1. package/README.md +53 -9
  2. package/dist/actions.d.ts +6 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +70 -10
  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 +2 -1
  11. package/dist/adapters/cloudflare.d.ts.map +1 -1
  12. package/dist/adapters/cloudflare.js +18 -10
  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 +1 -0
  25. package/dist/build.d.ts.map +1 -1
  26. package/dist/build.js +91 -5
  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 +3 -1
  33. package/dist/cache.d.ts.map +1 -1
  34. package/dist/cache.js +211 -13
  35. package/dist/cache.js.map +1 -1
  36. package/dist/cli.js +12 -1
  37. package/dist/cli.js.map +1 -1
  38. package/dist/client.d.ts.map +1 -1
  39. package/dist/client.js +32 -1
  40. package/dist/client.js.map +1 -1
  41. package/dist/config.d.ts +3 -0
  42. package/dist/config.d.ts.map +1 -1
  43. package/dist/config.js +21 -2
  44. package/dist/config.js.map +1 -1
  45. package/dist/csp.d.ts +1 -0
  46. package/dist/csp.d.ts.map +1 -1
  47. package/dist/csp.js +11 -6
  48. package/dist/csp.js.map +1 -1
  49. package/dist/dehydrate-policy.d.ts +3 -0
  50. package/dist/dehydrate-policy.d.ts.map +1 -0
  51. package/dist/dehydrate-policy.js +16 -0
  52. package/dist/dehydrate-policy.js.map +1 -0
  53. package/dist/dev-server.d.ts +7 -2
  54. package/dist/dev-server.d.ts.map +1 -1
  55. package/dist/dev-server.js +120 -11
  56. package/dist/dev-server.js.map +1 -1
  57. package/dist/http.d.ts +1 -1
  58. package/dist/http.d.ts.map +1 -1
  59. package/dist/http.js +147 -7
  60. package/dist/http.js.map +1 -1
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/logger.d.ts +22 -1
  66. package/dist/logger.d.ts.map +1 -1
  67. package/dist/logger.js +22 -8
  68. package/dist/logger.js.map +1 -1
  69. package/dist/metadata.d.ts +8 -0
  70. package/dist/metadata.d.ts.map +1 -1
  71. package/dist/metadata.js +60 -22
  72. package/dist/metadata.js.map +1 -1
  73. package/dist/navigation-marker.d.ts.map +1 -1
  74. package/dist/navigation-marker.js +72 -9
  75. package/dist/navigation-marker.js.map +1 -1
  76. package/dist/navigation.d.ts +1 -0
  77. package/dist/navigation.d.ts.map +1 -1
  78. package/dist/navigation.js +1 -1
  79. package/dist/navigation.js.map +1 -1
  80. package/dist/node-server.d.ts +5 -2
  81. package/dist/node-server.d.ts.map +1 -1
  82. package/dist/node-server.js +75 -6
  83. package/dist/node-server.js.map +1 -1
  84. package/dist/prerender-entry.d.ts +14 -0
  85. package/dist/prerender-entry.d.ts.map +1 -1
  86. package/dist/prerender-entry.js +40 -3
  87. package/dist/prerender-entry.js.map +1 -1
  88. package/dist/render.d.ts +9 -1
  89. package/dist/render.d.ts.map +1 -1
  90. package/dist/render.js +102 -123
  91. package/dist/render.js.map +1 -1
  92. package/dist/serve.d.ts +8 -2
  93. package/dist/serve.d.ts.map +1 -1
  94. package/dist/serve.js +22 -20
  95. package/dist/serve.js.map +1 -1
  96. package/dist/session.d.ts.map +1 -1
  97. package/dist/session.js +52 -19
  98. package/dist/session.js.map +1 -1
  99. package/dist/upgrade.d.ts +59 -2
  100. package/dist/upgrade.d.ts.map +1 -1
  101. package/dist/upgrade.js +282 -1
  102. package/dist/upgrade.js.map +1 -1
  103. package/dist/vite-config.d.ts +2 -0
  104. package/dist/vite-config.d.ts.map +1 -1
  105. package/dist/vite-config.js +2 -0
  106. package/dist/vite-config.js.map +1 -1
  107. package/dist/vite.d.ts +4 -1
  108. package/dist/vite.d.ts.map +1 -1
  109. package/dist/vite.js +117 -6
  110. package/dist/vite.js.map +1 -1
  111. package/package.json +11 -11
  112. package/src/actions.ts +81 -11
  113. package/src/adapters/aws-lambda.ts +426 -115
  114. package/src/adapters/cloudflare.ts +29 -13
  115. package/src/adapters/edge.ts +2 -0
  116. package/src/adapters/node.ts +8 -10
  117. package/src/adapters/static.ts +31 -2
  118. package/src/build.ts +117 -10
  119. package/src/built-runtime.ts +28 -0
  120. package/src/cache.ts +262 -15
  121. package/src/cli.ts +14 -1
  122. package/src/client.ts +32 -1
  123. package/src/config.ts +43 -6
  124. package/src/csp.ts +14 -11
  125. package/src/dehydrate-policy.ts +23 -0
  126. package/src/dev-server.ts +188 -15
  127. package/src/http.ts +172 -6
  128. package/src/index.ts +14 -1
  129. package/src/logger.ts +47 -10
  130. package/src/metadata.ts +125 -60
  131. package/src/navigation-marker.ts +79 -9
  132. package/src/navigation.ts +1 -1
  133. package/src/node-server.ts +128 -22
  134. package/src/prerender-entry.ts +52 -3
  135. package/src/render.ts +807 -753
  136. package/src/serve.ts +49 -56
  137. package/src/session.ts +56 -25
  138. package/src/upgrade.ts +402 -3
  139. package/src/vite-config.ts +6 -0
  140. package/src/vite.ts +157 -5
@@ -33,14 +33,17 @@ import { middlewareMatches, type MiddlewareModule } from "../middleware.js";
33
33
  import { normalizeRoutePath } from "../route-path.js";
34
34
  import type { AppRoute } from "../routes.js";
35
35
  import { contentSecurityPolicy } from "../csp.js";
36
- import { isNotFoundError, rewriteLocation } from "../navigation.js";
36
+ import { isNotFoundError, isRedirectError, rewriteLocation } from "../navigation.js";
37
37
  import { validateRouteMetadata } from "../metadata.js";
38
38
  import { routeSecurityHeaders } from "../security-headers.js";
39
39
  import type { AppRouterPrerenderStore } from "../serve.js";
40
40
  import { emitRouterDevtoolsEvent } from "./devtools.js";
41
41
  import { escapeHtmlAttribute, escapeHtmlText } from "@reckona/mreact-shared/html-escape";
42
- import { isCurrentPrerenderedRoute, replayedPrerenderedRouteHeaders } from "../prerender-entry.js";
43
- import { hasNavigationRouteMarker } from "../navigation-marker.js";
42
+ import {
43
+ isCurrentPrerenderedRoute,
44
+ replayedPrerenderedRouteHeaders,
45
+ validatedPrerenderedNavigationHtml,
46
+ } from "../prerender-entry.js";
44
47
 
45
48
  /** Re-exports build manifest contracts used by Cloudflare handlers. */
46
49
  export type {
@@ -68,6 +71,8 @@ export type {
68
71
  AppRouterRequestStartLogEvent,
69
72
  AppRouterRequestTimingLogEvent,
70
73
  AppRouterRuntime,
74
+ AppRouterUpgradeErrorLogEvent,
75
+ AppRouterUpgradeRejectedLogEvent,
71
76
  } from "../logger.js";
72
77
  /** Re-exports response hook contracts used by Cloudflare handlers. */
73
78
  export type { AppRouterResponseHook, AppRouterResponseHookContext } from "../render.js";
@@ -214,6 +219,7 @@ export type CloudflareRouteModuleComponent<Data = unknown, Env = unknown> = (
214
219
  * Defines the exports accepted from a Cloudflare page route module.
215
220
  */
216
221
  export interface CloudflareRouteModule<Data = unknown, Env = unknown> {
222
+ [exportName: string]: unknown;
217
223
  App?: CloudflareRouteModuleComponent<Data, Env> | undefined;
218
224
  CloudflareRouteComponent?: CloudflareRouteModuleComponent<Data, Env> | undefined;
219
225
  default?: CloudflareRouteModuleComponent<Data, Env> | undefined;
@@ -573,12 +579,24 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
573
579
  return error;
574
580
  }
575
581
 
582
+ if (isRedirectError(error)) {
583
+ return new Response(null, {
584
+ headers: { location: error.location },
585
+ status: error.status,
586
+ });
587
+ }
588
+
576
589
  if (isNotFoundError(error)) {
577
590
  return cloudflareNotFoundResponse(request);
578
591
  }
579
592
 
580
593
  throw error;
581
594
  }
595
+
596
+ if (data instanceof Response) {
597
+ return data;
598
+ }
599
+
582
600
  const props = {
583
601
  ...context,
584
602
  data,
@@ -590,6 +608,13 @@ export function createCloudflareRouteModuleRenderer<Env = unknown>(
590
608
  try {
591
609
  rendered = await runWithCloudflareQueryClient(queryClient, () => component(props));
592
610
  } catch (error) {
611
+ if (isRedirectError(error)) {
612
+ return new Response(null, {
613
+ headers: { location: error.location },
614
+ status: error.status,
615
+ });
616
+ }
617
+
593
618
  if (isNotFoundError(error)) {
594
619
  return cloudflareNotFoundResponse(request);
595
620
  }
@@ -1406,20 +1431,11 @@ function prerenderedResponse(
1406
1431
  }
1407
1432
 
1408
1433
  const prerendered = prerenderedRoutes?.[path];
1409
- const candidateNavigationHtml = prerendered?.navigationHtml;
1410
-
1411
1434
  if (!isCurrentPrerenderedRoute(prerendered)) {
1412
- if (
1413
- isNavigation &&
1414
- typeof candidateNavigationHtml === "string" &&
1415
- !hasNavigationRouteMarker(candidateNavigationHtml)
1416
- ) {
1417
- return cloudflareDocumentReloadNavigationResponse();
1418
- }
1419
1435
  return undefined;
1420
1436
  }
1421
1437
 
1422
- const html = isNavigation ? prerendered.navigationHtml : prerendered.html;
1438
+ const html = isNavigation ? validatedPrerenderedNavigationHtml(prerendered) : prerendered.html;
1423
1439
  if (html === undefined) {
1424
1440
  return cloudflareDocumentReloadNavigationResponse();
1425
1441
  }
@@ -20,6 +20,8 @@ export type {
20
20
  AppRouterRequestStartLogEvent,
21
21
  AppRouterRequestTimingLogEvent,
22
22
  AppRouterRuntime,
23
+ AppRouterUpgradeErrorLogEvent,
24
+ AppRouterUpgradeRejectedLogEvent,
23
25
  } from "../logger.js";
24
26
 
25
27
  /**
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DehydrateOptions } from "@reckona/mreact-query";
2
3
  import type { AppRouterCache } from "../cache.js";
3
4
  import { nodeRequestToWebRequest, sendResponse } from "../http.js";
4
5
  import type { AppRouterServerActionOptions } from "../actions.js";
@@ -19,10 +20,7 @@ import { resolveNodeRequestProtocol } from "../node-server.js";
19
20
  /** Re-exports cache contracts used by Node handlers. */
20
21
  export type { AppRouterCache, AppRouterCacheEntry } from "../cache.js";
21
22
  /** Re-exports server action contracts used by Node handlers. */
22
- export type {
23
- AppRouterAllowedServerAction,
24
- AppRouterServerActionOptions,
25
- } from "../actions.js";
23
+ export type { AppRouterAllowedServerAction, AppRouterServerActionOptions } from "../actions.js";
26
24
  /** Re-exports import policy contracts used by Node handlers. */
27
25
  export type { AppRouterImportPolicy } from "../import-policy.js";
28
26
  /** Re-exports logger contracts used by Node handlers. */
@@ -37,6 +35,8 @@ export type {
37
35
  AppRouterRequestStartLogEvent,
38
36
  AppRouterRequestTimingLogEvent,
39
37
  AppRouterRuntime,
38
+ AppRouterUpgradeErrorLogEvent,
39
+ AppRouterUpgradeRejectedLogEvent,
40
40
  } from "../logger.js";
41
41
  /** Re-exports response hook contracts used by Node handlers. */
42
42
  export type { AppRouterResponseHook, AppRouterResponseHookContext } from "../render.js";
@@ -63,17 +63,14 @@ import {
63
63
  } from "../serve.js";
64
64
 
65
65
  /** Re-exports request and rendering contracts used by Node handlers. */
66
- export type {
67
- AppRouterPrerenderStore,
68
- RequestHostPolicy,
69
- ResponseSinkStrategy,
70
- } from "../serve.js";
66
+ export type { AppRouterPrerenderStore, RequestHostPolicy, ResponseSinkStrategy } from "../serve.js";
71
67
 
72
68
  /**
73
69
  * Configures a Node request handler for built app-router output.
74
70
  */
75
71
  export interface NodeRequestHandlerOptions {
76
72
  allowedHosts?: readonly string[] | undefined;
73
+ dehydrateOptions?: DehydrateOptions | undefined;
77
74
  errorHandler?:
78
75
  | ((error: unknown) => {
79
76
  body: string;
@@ -139,13 +136,14 @@ export function createNodeRequestHandler(options: NodeRequestHandlerOptions): No
139
136
  forwardedProto: incoming.headers["x-forwarded-proto"],
140
137
  trustForwardedProto: options.trustForwardedProto,
141
138
  });
142
- const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`);
139
+ const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`, outgoing);
143
140
  const logFields = requestLogFields(request, "node");
144
141
  emitRouterLog(options.logger, "info", {
145
142
  ...logFields,
146
143
  type: "router:request:start",
147
144
  });
148
145
  const response = await renderBuiltAppRequest({
146
+ dehydrateOptions: options.dehydrateOptions,
149
147
  outDir: options.outDir,
150
148
  importPolicy: options.importPolicy,
151
149
  instrumentation: options.instrumentation,
@@ -1,7 +1,13 @@
1
1
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve, sep } from "node:path";
3
3
  import type { BuiltPrerenderedRoute, BuiltServerManifest } from "../build.js";
4
- import { isCurrentPrerenderedRoute } from "../prerender-entry.js";
4
+ import {
5
+ isCurrentPrerenderedRoute,
6
+ validatedPrerenderedNavigationHtml,
7
+ } from "../prerender-entry.js";
8
+
9
+ const staticNavigationBase = "/_mreact/navigation";
10
+ const staticNavigationMetadata = `<meta name="mreact-static-navigation" content="${staticNavigationBase}">`;
5
11
 
6
12
  /**
7
13
  * Configures static export from a built app-router output directory.
@@ -104,8 +110,31 @@ async function writePrerenderedRoute(
104
110
  }
105
111
 
106
112
  const file = routeToHtmlFile(exportDir, route);
113
+ const navigationHtml = validatedPrerenderedNavigationHtml(entry);
114
+ if (navigationHtml === undefined) {
115
+ throw new Error(`Cannot export route ${route} without a valid navigation variant.`);
116
+ }
117
+ const navigationFile = routeToHtmlFile(join(exportDir, staticNavigationBase), route);
107
118
  await mkdir(dirname(file), { recursive: true });
108
- await writeFile(file, entry.html);
119
+ await mkdir(dirname(navigationFile), { recursive: true });
120
+ await writeFile(file, withStaticNavigationMetadata(entry.html));
121
+ await writeFile(navigationFile, navigationHtml);
122
+ }
123
+
124
+ function withStaticNavigationMetadata(html: string): string {
125
+ const head = /<head(?:\s[^>]*)?>/i.exec(html);
126
+ if (head?.index !== undefined) {
127
+ const insertion = head.index + head[0].length;
128
+ return `${html.slice(0, insertion)}${staticNavigationMetadata}${html.slice(insertion)}`;
129
+ }
130
+
131
+ const doctype = /^\s*<!doctype\s+html\s*>/i.exec(html);
132
+ if (doctype !== null) {
133
+ const insertion = doctype[0].length;
134
+ return `${html.slice(0, insertion)}${staticNavigationMetadata}${html.slice(insertion)}`;
135
+ }
136
+
137
+ return `${staticNavigationMetadata}${html}`;
109
138
  }
110
139
 
111
140
  function routeToHtmlFile(exportDir: string, route: string): string {
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,
@@ -57,6 +58,7 @@ import {
57
58
  resolveCompatVendorEntryFiles,
58
59
  sourceReferencesCompatVendorSpecifier,
59
60
  } from "./module-runner.js";
61
+ import { dehydrateOptionsFromModule } from "./dehydrate-policy.js";
60
62
  import { compileRouteMatcherArtifact, scanAppRoutes } from "./routes.js";
61
63
  import type { AppRoute, CompiledRouteMatcherArtifact } from "./routes.js";
62
64
  import { appFileConventionForRootFilename } from "./file-conventions.js";
@@ -249,10 +251,11 @@ export interface BuildAppResult {
249
251
 
250
252
  interface IncrementalBuildCacheManifest {
251
253
  fingerprint: string;
252
- version: 1;
254
+ version: 2;
253
255
  }
254
256
 
255
257
  interface IncrementalBuildServerManifestOutputs {
258
+ dehydratePolicyModule?: string;
256
259
  serverModuleFiles?: Record<string, string>;
257
260
  serverModuleRenderFiles?: Record<string, string>;
258
261
  serverModuleRequestFiles?: Record<string, string>;
@@ -326,6 +329,7 @@ export interface PackageCloudflarePagesArtifactOptions {
326
329
  export interface BuiltServerManifest {
327
330
  allowedSourceDirs?: readonly string[];
328
331
  assetBaseUrl?: string;
332
+ dehydratePolicyModule?: string;
329
333
  version: 1;
330
334
  files: Record<string, string>;
331
335
  prerenderedRoutes?: Record<string, BuiltPrerenderedRoute>;
@@ -456,6 +460,13 @@ async function buildAppWithResolvedProject(
456
460
  options: BuildAppOptions,
457
461
  project: ResolvedAppRouterProject,
458
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
+ }
459
470
  const timingSink = options.onBuildPhaseTiming;
460
471
  const progressSink = options.onBuildProgress;
461
472
  const shouldTrackBuildPhases = timingSink !== undefined || progressSink !== undefined;
@@ -572,6 +583,24 @@ async function buildAppWithResolvedProject(
572
583
  });
573
584
  }
574
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
+
575
604
  const publicAssets =
576
605
  shouldTrackBuildPhases === false
577
606
  ? await buildPublicAssetManifest(project, clientDir)
@@ -729,6 +758,7 @@ async function buildAppWithResolvedProject(
729
758
  assetBaseUrl: project.assetBaseUrl,
730
759
  clientRoutes: clientManifestRoutes,
731
760
  define: viteDefine,
761
+ dehydrateOptions,
732
762
  project,
733
763
  routes,
734
764
  serverModules,
@@ -741,6 +771,7 @@ async function buildAppWithResolvedProject(
741
771
  assetBaseUrl: project.assetBaseUrl,
742
772
  clientRoutes: clientManifestRoutes,
743
773
  define: viteDefine,
774
+ dehydrateOptions,
744
775
  project,
745
776
  routes,
746
777
  serverModules,
@@ -790,6 +821,9 @@ async function buildAppWithResolvedProject(
790
821
  relative(project.projectRoot, directory),
791
822
  ),
792
823
  ...(project.assetBaseUrl === undefined ? {} : { assetBaseUrl: project.assetBaseUrl }),
824
+ ...(dehydratePolicyArtifact === undefined
825
+ ? {}
826
+ : { dehydratePolicyModule: dehydratePolicyArtifact }),
793
827
  version: 1,
794
828
  routes: serverRoutes,
795
829
  routeMatcher: compileRouteMatcherArtifact(serverRoutes),
@@ -903,7 +937,7 @@ async function buildAppWithResolvedProject(
903
937
  }
904
938
 
905
939
  const incrementalBuildCacheFilename = "build-cache.json";
906
- const incrementalBuildCacheVersion = 1;
940
+ const incrementalBuildCacheVersion = 2;
907
941
 
908
942
  async function createIncrementalBuildCacheFingerprint(options: {
909
943
  buildTargets: readonly AppRouterBuildTarget[];
@@ -949,6 +983,12 @@ async function createIncrementalBuildCacheFingerprint(options: {
949
983
  assetBaseUrl: options.project.assetBaseUrl ?? null,
950
984
  clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
951
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
+ ),
952
992
  define,
953
993
  nodeEnv: process.env.NODE_ENV ?? null,
954
994
  publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
@@ -1015,6 +1055,9 @@ async function hasRequiredIncrementalBuildOutputs(
1015
1055
  }
1016
1056
 
1017
1057
  for (const file of [
1058
+ ...(serverManifest.dehydratePolicyModule === undefined
1059
+ ? []
1060
+ : [serverManifest.dehydratePolicyModule]),
1018
1061
  ...Object.values(serverManifest.serverModuleFiles ?? {}),
1019
1062
  ...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
1020
1063
  ...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
@@ -1466,7 +1509,7 @@ async function buildRouteUsesRequestInput(options: {
1466
1509
  files: options.files,
1467
1510
  projectRoot: options.projectRoot,
1468
1511
  source: entry.source,
1469
- })
1512
+ }),
1470
1513
  );
1471
1514
  }
1472
1515
 
@@ -2560,6 +2603,38 @@ function isReservedPublicAssetPath(pathname: string): boolean {
2560
2603
  return pathname === "/_mreact" || pathname.startsWith("/_mreact/");
2561
2604
  }
2562
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
+
2563
2638
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
2564
2639
  return error instanceof Error && "code" in error;
2565
2640
  }
@@ -2568,6 +2643,7 @@ async function prerenderStaticRoutes(options: {
2568
2643
  appDir: string;
2569
2644
  assetBaseUrl?: string | undefined;
2570
2645
  clientRoutes: readonly ClientRouteManifestEntry[];
2646
+ dehydrateOptions?: DehydrateOptions | undefined;
2571
2647
  define?: UserConfig["define"] | undefined;
2572
2648
  project: ResolvedAppRouterProject;
2573
2649
  routes: readonly AppRoute[];
@@ -2633,14 +2709,11 @@ async function prerenderStaticRoutes(options: {
2633
2709
  clientScripts,
2634
2710
  clientStyles,
2635
2711
  define: options.define,
2712
+ dehydrateOptions: options.dehydrateOptions,
2636
2713
  importPolicy,
2637
2714
  navigationScripts,
2638
- request: new Request(
2639
- `http://mreact.local${pathname}`,
2640
- analysis.clientRoute
2641
- ? undefined
2642
- : { headers: { "x-mreact-prerender-variant-capture": "1" } },
2643
- ),
2715
+ request: new Request(`http://mreact.local${pathname}`),
2716
+ prerenderVariantCapture: !analysis.clientRoute,
2644
2717
  renderSignals,
2645
2718
  serverModuleCacheVersion,
2646
2719
  serverModules: serverModuleMap,
@@ -5006,9 +5079,43 @@ function validateOptionalHeadMetadata(value, path) {
5006
5079
  }
5007
5080
  }
5008
5081
  }
5082
+ validateMetaRefreshHeadDescriptor(descriptor, descriptorPath);
5009
5083
  });
5010
5084
  }
5011
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
+
5012
5119
  function validateHeadAttribute(name, value, path) {
5013
5120
  if (!isSafeHeadAttributeName(name)) {
5014
5121
  throw new Error(\`Invalid metadata field \${path}: expected safe HTML attribute name.\`);
@@ -5070,7 +5177,7 @@ function isUnsafeUrlValueForName(name, value) {
5070
5177
  }
5071
5178
  const scheme = match[1].toLowerCase();
5072
5179
  if (scheme === "data" && (name === "src" || name === "poster")) {
5073
- return !/^data:image\\/(?!svg\\+xml(?:[;,]|$))/i.test(canonical);
5180
+ return !/^data:image\\/(?!svg\\+xml\\s*(?:[;,]|$))/i.test(canonical);
5074
5181
  }
5075
5182
  return scheme === "javascript" || scheme === "data" || scheme === "vbscript" || scheme === "livescript" || scheme === "mhtml" || scheme === "file";
5076
5183
  }
@@ -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