vinext 1.0.0-beta.7 → 1.0.0-beta.9

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 (137) hide show
  1. package/README.md +43 -0
  2. package/dist/build/prerender-paths.d.ts +50 -1
  3. package/dist/build/prerender-paths.js +627 -88
  4. package/dist/build/prerender.d.ts +4 -2
  5. package/dist/build/prerender.js +40 -23
  6. package/dist/build/report.d.ts +16 -1
  7. package/dist/build/report.js +64 -3
  8. package/dist/build/static-export.d.ts +1 -1
  9. package/dist/cache/cache-adapters-virtual.d.ts +47 -1
  10. package/dist/cache/cache-adapters-virtual.js +10 -1
  11. package/dist/cli.js +7 -0
  12. package/dist/client/vinext-next-data.d.ts +2 -0
  13. package/dist/config/config-matchers.d.ts +3 -3
  14. package/dist/config/config-matchers.js +8 -3
  15. package/dist/config/prerender.d.ts +2 -2
  16. package/dist/config/prerender.js +2 -2
  17. package/dist/entries/app-browser-entry.d.ts +1 -1
  18. package/dist/entries/app-browser-entry.js +4 -1
  19. package/dist/entries/app-rsc-entry.d.ts +2 -0
  20. package/dist/entries/app-rsc-entry.js +28 -4
  21. package/dist/entries/app-rsc-manifest.js +1 -0
  22. package/dist/entries/pages-server-entry.d.ts +1 -1
  23. package/dist/entries/pages-server-entry.js +4 -1
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +146 -28
  26. package/dist/init-cloudflare.d.ts +5 -3
  27. package/dist/init-cloudflare.js +58 -25
  28. package/dist/plugins/import-meta-url.js +7 -6
  29. package/dist/routing/app-router.d.ts +3 -1
  30. package/dist/routing/app-router.js +5 -1
  31. package/dist/server/app-browser-entry.js +41 -13
  32. package/dist/server/app-browser-navigation-controller.js +1 -1
  33. package/dist/server/app-browser-state.js +1 -1
  34. package/dist/server/app-browser-visible-commit.js +1 -1
  35. package/dist/server/app-middleware.d.ts +8 -0
  36. package/dist/server/app-middleware.js +28 -2
  37. package/dist/server/app-optimistic-routing.d.ts +6 -1
  38. package/dist/server/app-optimistic-routing.js +13 -1
  39. package/dist/server/app-page-boundary.js +2 -1
  40. package/dist/server/app-page-cache-finalizer.d.ts +16 -1
  41. package/dist/server/app-page-cache-finalizer.js +73 -4
  42. package/dist/server/app-page-cache.js +2 -2
  43. package/dist/server/app-page-dispatch.d.ts +2 -0
  44. package/dist/server/app-page-dispatch.js +12 -4
  45. package/dist/server/app-page-execution.js +1 -1
  46. package/dist/server/app-page-render.d.ts +1 -0
  47. package/dist/server/app-page-render.js +27 -9
  48. package/dist/server/app-page-response.js +2 -2
  49. package/dist/server/app-page-stream.js +2 -1
  50. package/dist/server/app-pages-bridge.js +2 -0
  51. package/dist/server/app-prerender-endpoints.js +23 -11
  52. package/dist/server/app-prerender-static-params.js +12 -5
  53. package/dist/server/app-route-handler-dispatch.js +13 -9
  54. package/dist/server/app-route-handler-execution.d.ts +1 -0
  55. package/dist/server/app-route-handler-execution.js +127 -14
  56. package/dist/server/app-route-handler-policy.d.ts +15 -3
  57. package/dist/server/app-route-handler-policy.js +19 -4
  58. package/dist/server/app-route-tree-prefetch.js +2 -2
  59. package/dist/server/app-router-entry.js +52 -8
  60. package/dist/server/app-rsc-cache-busting.d.ts +22 -4
  61. package/dist/server/app-rsc-cache-busting.js +68 -18
  62. package/dist/server/app-rsc-handler.d.ts +2 -0
  63. package/dist/server/app-rsc-handler.js +95 -30
  64. package/dist/server/app-rsc-response-finalizer.js +24 -7
  65. package/dist/server/app-rsc-route-matching.d.ts +1 -0
  66. package/dist/server/app-rsc-route-matching.js +1 -0
  67. package/dist/server/app-segment-config.d.ts +3 -1
  68. package/dist/server/app-segment-config.js +23 -11
  69. package/dist/server/app-server-action-execution.js +4 -4
  70. package/dist/server/app-ssr-entry.js +4 -4
  71. package/dist/server/cache-control.d.ts +8 -2
  72. package/dist/server/cache-control.js +35 -4
  73. package/dist/server/cacheability-limits.d.ts +9 -0
  74. package/dist/server/cacheability-limits.js +9 -0
  75. package/dist/server/cacheability-manifest.d.ts +42 -0
  76. package/dist/server/cacheability-manifest.js +191 -0
  77. package/dist/server/cacheability-request.d.ts +20 -0
  78. package/dist/server/cacheability-request.js +457 -0
  79. package/dist/server/config-headers.js +23 -3
  80. package/dist/server/headers.d.ts +16 -3
  81. package/dist/server/headers.js +29 -3
  82. package/dist/server/middleware-matcher.d.ts +9 -1
  83. package/dist/server/middleware-matcher.js +22 -1
  84. package/dist/server/middleware-runtime.d.ts +6 -0
  85. package/dist/server/middleware-runtime.js +18 -3
  86. package/dist/server/normalize-path.js +1 -1
  87. package/dist/server/pages-api-route.js +1 -1
  88. package/dist/server/pages-i18n.js +3 -2
  89. package/dist/server/pages-page-handler.js +22 -9
  90. package/dist/server/pages-request-pipeline.d.ts +2 -0
  91. package/dist/server/pages-request-pipeline.js +17 -6
  92. package/dist/server/pages-router-entry.js +56 -9
  93. package/dist/server/prerender-route-params.js +1 -0
  94. package/dist/server/prerender-work-unit-setup.d.ts +3 -2
  95. package/dist/server/prerender-work-unit-setup.js +34 -11
  96. package/dist/server/prod-server.js +5 -5
  97. package/dist/server/request-pipeline.d.ts +1 -5
  98. package/dist/server/request-pipeline.js +2 -9
  99. package/dist/server/revalidation-host.js +1 -0
  100. package/dist/server/server-globals.js +13 -0
  101. package/dist/server/static-file-signal.d.ts +13 -0
  102. package/dist/server/static-file-signal.js +33 -0
  103. package/dist/server/worker-prerender-discovery.d.ts +19 -0
  104. package/dist/server/worker-prerender-discovery.js +55 -0
  105. package/dist/server/worker-utils.js +2 -3
  106. package/dist/shims/cache-runtime.js +140 -102
  107. package/dist/shims/cache.js +11 -1
  108. package/dist/shims/cacheability-classification.d.ts +85 -0
  109. package/dist/shims/cacheability-classification.js +146 -0
  110. package/dist/shims/cdn-cache.d.ts +30 -1
  111. package/dist/shims/cdn-cache.js +25 -1
  112. package/dist/shims/error.js +1 -1
  113. package/dist/shims/fetch-cache.js +8 -3
  114. package/dist/shims/headers.d.ts +3 -1
  115. package/dist/shims/headers.js +9 -5
  116. package/dist/shims/internal/app-prefetch-rsc-request.d.ts +25 -0
  117. package/dist/shims/internal/app-prefetch-rsc-request.js +21 -0
  118. package/dist/shims/internal/app-route-prefetch-policy.d.ts +2 -0
  119. package/dist/shims/internal/app-route-prefetch-policy.js +2 -0
  120. package/dist/shims/internal/hybrid-client-route-owner-direct.d.ts +2 -2
  121. package/dist/shims/internal/hybrid-client-route-owner-direct.js +29 -11
  122. package/dist/shims/internal/hybrid-client-route-owner.js +2 -2
  123. package/dist/shims/internal/make-hanging-promise.d.ts +7 -1
  124. package/dist/shims/internal/make-hanging-promise.js +13 -2
  125. package/dist/shims/internal/work-unit-async-storage.d.ts +2 -0
  126. package/dist/shims/link.js +25 -14
  127. package/dist/shims/navigation.js +31 -11
  128. package/dist/shims/navigation.react-server.js +1 -1
  129. package/dist/shims/request-context.d.ts +2 -0
  130. package/dist/shims/server.js +1 -1
  131. package/dist/shims/url-utils.d.ts +8 -1
  132. package/dist/shims/url-utils.js +10 -1
  133. package/dist/utils/prerender-output-paths.d.ts +7 -7
  134. package/dist/utils/prerender-output-paths.js +16 -9
  135. package/dist/utils/react-compiler-support.d.ts +20 -0
  136. package/dist/utils/react-compiler-support.js +29 -0
  137. package/package.json +10 -2
@@ -181,7 +181,9 @@ type StaticParamsMap = Record<string, ((opts: {
181
181
  * Uses the `staticParamsMap` (pattern → generateStaticParams) exported from
182
182
  * the production bundle.
183
183
  */
184
- declare function resolveParentParams(childRoute: AppRoute, staticParamsMap: StaticParamsMap): Promise<Record<string, string | string[]>[]>;
184
+ declare function resolveParentParams(childRoute: AppRoute, staticParamsMap: StaticParamsMap, options?: {
185
+ includeLastDynamicSegment?: boolean;
186
+ }): Promise<Record<string, string | string[]>[]>;
185
187
  /**
186
188
  * Run the prerender phase for Pages Router.
187
189
  *
@@ -202,7 +204,7 @@ declare function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, m
202
204
  * Starts a local production server and fetches every static/ISR route via HTTP.
203
205
  * Works for both plain Node and Cloudflare Workers builds — the CF Workers bundle
204
206
  * (`dist/server/index.js`) is a standard Node-compatible server entry, so no
205
- * wrangler/miniflare is needed. Writes HTML files, `.rsc` files, and
207
+ * wrangler/miniflare is needed. Writes HTML files, Flight payloads, and
206
208
  * `vinext-prerender.json` to `outDir`.
207
209
  *
208
210
  * If the bundle does not exist, an error is thrown directing the user to run
@@ -181,16 +181,28 @@ function metadataOutputPath(servedUrl) {
181
181
  if (segments.length === 0 || segments.some((segment) => segment === "." || segment === "..")) return null;
182
182
  return segments.join("/");
183
183
  }
184
- function emitStaticMetadataFiles(metadataRoutes, outDir) {
184
+ function emitStaticMetadataFiles(metadataRoutes, outDir, basePath = "") {
185
185
  const outputFiles = [];
186
+ const outputPrefix = basePath.replace(/^\//, "").replace(/\/$/, "");
186
187
  for (const route of metadataRoutes) {
187
188
  if (route.isDynamic) continue;
188
189
  const outputPath = metadataOutputPath(route.servedUrl);
189
190
  if (!outputPath) continue;
190
- const fullPath = path.join(outDir, ...outputPath.split("/"));
191
+ const namespacedOutputPath = outputPrefix ? `${outputPrefix}/${outputPath}` : outputPath;
192
+ const fullPath = path.join(outDir, ...namespacedOutputPath.split("/"));
191
193
  fs.mkdirSync(path.dirname(fullPath), { recursive: true });
192
194
  fs.copyFileSync(route.filePath, fullPath);
193
- outputFiles.push(outputPath);
195
+ outputFiles.push(namespacedOutputPath);
196
+ }
197
+ return outputFiles;
198
+ }
199
+ function emitStatic404Files(outDir, html, trailingSlash) {
200
+ const outputFiles = ["404.html"];
201
+ if (trailingSlash) outputFiles.push("404/index.html");
202
+ for (const outputFile of outputFiles) {
203
+ const fullPath = path.join(outDir, ...outputFile.split("/"));
204
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
205
+ fs.writeFileSync(fullPath, html, "utf-8");
194
206
  }
195
207
  return outputFiles;
196
208
  }
@@ -201,7 +213,7 @@ function emitStaticMetadataFiles(metadataRoutes, outDir) {
201
213
  * Uses the `staticParamsMap` (pattern → generateStaticParams) exported from
202
214
  * the production bundle.
203
215
  */
204
- async function resolveParentParams(childRoute, staticParamsMap) {
216
+ async function resolveParentParams(childRoute, staticParamsMap, options = {}) {
205
217
  const { patternParts } = childRoute;
206
218
  let lastDynamicIdx = -1;
207
219
  for (let i = patternParts.length - 1; i >= 0; i--) if (patternParts[i].startsWith(":")) {
@@ -210,7 +222,8 @@ async function resolveParentParams(childRoute, staticParamsMap) {
210
222
  }
211
223
  const parentSegments = [];
212
224
  let prefixPattern = "";
213
- for (let i = 0; i < lastDynamicIdx; i++) {
225
+ const prefixEnd = options.includeLastDynamicSegment ? lastDynamicIdx + 1 : lastDynamicIdx;
226
+ for (let i = 0; i < prefixEnd; i++) {
214
227
  const part = patternParts[i];
215
228
  prefixPattern += "/" + part;
216
229
  if (!part.startsWith(":")) continue;
@@ -292,7 +305,9 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
292
305
  let renderRoundRobin = 0;
293
306
  const renderPage = (urlPath) => {
294
307
  const port = renderPorts[renderRoundRobin++ % renderPorts.length];
295
- return fetch(`http://127.0.0.1:${port}${urlPath}`, {
308
+ const routeRequestPath = config.trailingSlash && !urlPath.endsWith("/") ? `${urlPath}/` : urlPath;
309
+ const requestPath = config.basePath && routeRequestPath === "/" && !config.trailingSlash ? config.basePath : `${config.basePath ?? ""}${routeRequestPath}`;
310
+ return fetch(`http://127.0.0.1:${port}${requestPath}`, {
296
311
  headers: secretHeaders,
297
312
  redirect: "manual"
298
313
  });
@@ -316,7 +331,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
316
331
  fallback: false
317
332
  };
318
333
  }
319
- if (text === "null") return {
334
+ if (res.status === 204 || text === "null") return {
320
335
  paths: [],
321
336
  fallback: false
322
337
  };
@@ -430,7 +445,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
430
445
  try {
431
446
  const response = await renderPage(urlPath);
432
447
  const outputFiles = [];
433
- const htmlOutputPath = getOutputPath(urlPath, config.trailingSlash);
448
+ const htmlOutputPath = getOutputPath(urlPath, config.trailingSlash, mode === "export" ? config.basePath : "");
434
449
  const htmlFullPath = path.join(outDir, htmlOutputPath);
435
450
  if (response.status >= 300 && response.status < 400) {
436
451
  const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;url=${(response.headers.get("location") ?? "/").replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}" /></head><body></body></html>`;
@@ -479,12 +494,10 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
479
494
  const contentType = notFoundRes.headers.get("content-type") ?? "";
480
495
  if (notFoundRes.status === 404 && contentType.includes("text/html")) {
481
496
  const html404 = await notFoundRes.text();
482
- const fullPath = path.join(outDir, "404.html");
483
- fs.writeFileSync(fullPath, html404, "utf-8");
484
497
  results.push({
485
498
  route: "/404",
486
499
  status: "rendered",
487
- outputFiles: ["404.html"],
500
+ outputFiles: emitStatic404Files(outDir, html404, config.trailingSlash),
488
501
  revalidate: false,
489
502
  router: "pages"
490
503
  });
@@ -514,7 +527,7 @@ async function prerenderPages({ routes, apiRoutes, pagesDir, outDir, config, mod
514
527
  * Starts a local production server and fetches every static/ISR route via HTTP.
515
528
  * Works for both plain Node and Cloudflare Workers builds — the CF Workers bundle
516
529
  * (`dist/server/index.js`) is a standard Node-compatible server entry, so no
517
- * wrangler/miniflare is needed. Writes HTML files, `.rsc` files, and
530
+ * wrangler/miniflare is needed. Writes HTML files, Flight payloads, and
518
531
  * `vinext-prerender.json` to `outDir`.
519
532
  *
520
533
  * If the bundle does not exist, an error is thrown directing the user to run
@@ -589,7 +602,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
589
602
  console.warn(`[vinext] Warning: /__vinext/prerender/static-params returned ${res.status} for ${pattern}. Static params will be skipped. This may indicate a stale or missing prerender secret.`);
590
603
  return null;
591
604
  }
592
- if (text === "null") return null;
605
+ if (res.status === 204 || text === "null") return null;
593
606
  return JSON.parse(text);
594
607
  })();
595
608
  request.catch(() => staticParamsCache.delete(cacheKey));
@@ -829,7 +842,9 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
829
842
  const htmlHeaders = new Headers();
830
843
  if (prerenderRouteParamsHeader !== null) htmlHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
831
844
  if (isSpeculative) htmlHeaders.set(VINEXT_PRERENDER_SPECULATIVE_HEADER, "1");
832
- const htmlRequest = new Request(`http://localhost${urlPath}`, { headers: htmlHeaders });
845
+ const routeRequestPath = config.trailingSlash && !urlPath.endsWith("/") ? `${urlPath}/` : urlPath;
846
+ const requestPath = config.basePath && routeRequestPath === "/" && !config.trailingSlash ? config.basePath : `${config.basePath ?? ""}${routeRequestPath}`;
847
+ const htmlRequest = new Request(`http://localhost${requestPath}`, { headers: htmlHeaders });
833
848
  const htmlRender = await runWithHeadersContext(headersContextFromRequest(htmlRequest), async () => {
834
849
  const response = await rscHandler(htmlRequest);
835
850
  const cacheControl = response.headers.get("cache-control") ?? "";
@@ -896,7 +911,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
896
911
  });
897
912
  if (prerenderRouteParamsHeader !== null) rscHeaders.set(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, prerenderRouteParamsHeader);
898
913
  if (isSpeculative) rscHeaders.set(VINEXT_PRERENDER_SPECULATIVE_HEADER, "1");
899
- const rscRequest = new Request(`http://localhost${urlPath}`, { headers: rscHeaders });
914
+ const rscRequest = new Request(`http://localhost${requestPath}`, { headers: rscHeaders });
900
915
  const rscRes = await runWithHeadersContext(headersContextFromRequest(rscRequest), () => rscHandler(rscRequest));
901
916
  if (!rscRes.ok) {
902
917
  await rscRes.body?.cancel();
@@ -905,12 +920,16 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
905
920
  rscData = new Uint8Array(await rscRes.arrayBuffer());
906
921
  }
907
922
  const outputFiles = [];
908
- const htmlOutputPath = getOutputPath(urlPath, config.trailingSlash);
923
+ const htmlOutputPath = getOutputPath(urlPath, config.trailingSlash, mode === "export" ? config.basePath : "");
909
924
  const htmlFullPath = path.join(outDir, htmlOutputPath);
910
925
  fs.mkdirSync(path.dirname(htmlFullPath), { recursive: true });
911
926
  fs.writeFileSync(htmlFullPath, html, "utf-8");
912
927
  outputFiles.push(htmlOutputPath);
913
- const rscOutputPath = getRscOutputPath(urlPath);
928
+ const rscOutputPath = getRscOutputPath(urlPath, {
929
+ mode,
930
+ trailingSlash: config.trailingSlash,
931
+ basePath: mode === "export" ? config.basePath : ""
932
+ });
914
933
  const rscFullPath = path.join(outDir, rscOutputPath);
915
934
  fs.mkdirSync(path.dirname(rscFullPath), { recursive: true });
916
935
  fs.writeFileSync(rscFullPath, rscData);
@@ -967,19 +986,17 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
967
986
  });
968
987
  results.push(...appResults);
969
988
  renderPool?.assertHealthy();
970
- const outputFiles = mode === "export" && metadataRoutes.length > 0 ? emitStaticMetadataFiles(metadataRoutes, outDir) : [];
989
+ const outputFiles = mode === "export" && metadataRoutes.length > 0 ? emitStaticMetadataFiles(metadataRoutes, outDir, config.basePath) : [];
971
990
  try {
972
- const notFoundRequest = new Request(`http://localhost${NOT_FOUND_SENTINEL_PATH}`);
991
+ const notFoundPath = config.trailingSlash && !NOT_FOUND_SENTINEL_PATH.endsWith("/") ? `${NOT_FOUND_SENTINEL_PATH}/` : NOT_FOUND_SENTINEL_PATH;
992
+ const notFoundRequest = new Request(`http://localhost${config.basePath ?? ""}${notFoundPath}`);
973
993
  const notFoundRes = await runWithHeadersContext(headersContextFromRequest(notFoundRequest), () => rscHandler(notFoundRequest));
974
994
  if (notFoundRes.status === 404) {
975
995
  const html404 = await notFoundRes.text();
976
- const fullPath = path.join(outDir, "404.html");
977
- fs.mkdirSync(path.dirname(fullPath), { recursive: true });
978
- fs.writeFileSync(fullPath, html404, "utf-8");
979
996
  results.push({
980
997
  route: "/404",
981
998
  status: "rendered",
982
- outputFiles: ["404.html"],
999
+ outputFiles: emitStatic404Files(outDir, html404, config.trailingSlash),
983
1000
  revalidate: false,
984
1001
  router: "app"
985
1002
  });
@@ -92,6 +92,8 @@ declare function classifyLayoutSegmentConfig(code: string): LayoutBuildClassific
92
92
  * API routes (files under pages/api/) are always `api`.
93
93
  */
94
94
  declare function classifyPagesRoute(filePath: string): {
95
+ hasStaticProps?: boolean;
96
+ hasServerSideProps?: boolean;
95
97
  type: RouteType;
96
98
  revalidate?: number;
97
99
  };
@@ -106,6 +108,19 @@ declare function classifyAppRoute(pagePath: string | null, routePath: string | n
106
108
  type: RouteType;
107
109
  revalidate?: number;
108
110
  };
111
+ /**
112
+ * Return whether a Route Handler has a GET contract that Next.js considers
113
+ * eligible for static generation. This intentionally examines only the route
114
+ * module's direct segment config; request-time dynamic usage is decided by the
115
+ * staged Worker probe after the module has executed to completion.
116
+ *
117
+ * Ported from Next.js:
118
+ * packages/next/src/server/route-modules/app-route/helpers/is-static-gen-enabled.ts
119
+ */
120
+ declare function classifyAppRouteHandler(filePath: string): {
121
+ hasGet: boolean;
122
+ staticGenerationEnabled: boolean;
123
+ };
109
124
  /**
110
125
  * Builds a sorted list of RouteRow objects from the discovered routes.
111
126
  * Routes are sorted alphabetically by path, matching filesystem order.
@@ -144,4 +159,4 @@ declare function printBuildReport(options: {
144
159
  prerenderResult?: PrerenderResult;
145
160
  }): Promise<void>;
146
161
  //#endregion
147
- export { RouteRow, RouteType, StaticMiddlewareMatcher, buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
162
+ export { RouteRow, RouteType, StaticMiddlewareMatcher, buildReportRows, classifyAppRoute, classifyAppRouteHandler, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
@@ -97,6 +97,20 @@ function hasNamedExportInProgram(program, name) {
97
97
  }
98
98
  return false;
99
99
  }
100
+ function hasRuntimeExportedNameInProgram(program, name) {
101
+ for (const node of program.body) {
102
+ if (node.type !== "ExportNamedDeclaration" || node.exportKind === "type") continue;
103
+ if (declarationHasBindingName(node.declaration, name)) return true;
104
+ for (const specifier of node.specifiers) {
105
+ if (specifier.exportKind === "type") continue;
106
+ if (moduleExportNameValue(specifier.exported ?? specifier.local) === name) return true;
107
+ }
108
+ }
109
+ return false;
110
+ }
111
+ function hasRuntimeExportAllInProgram(program) {
112
+ return program.body.some((node) => node.type === "ExportAllDeclaration" && node.exportKind !== "type");
113
+ }
100
114
  function unwrapStaticExpression(expression) {
101
115
  let current = expression;
102
116
  while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
@@ -434,12 +448,19 @@ function classifyPagesRoute(filePath) {
434
448
  return { type: "unknown" };
435
449
  }
436
450
  const program = parseRouteModule(code);
437
- if (program && hasNamedExportInProgram(program, "getServerSideProps")) return { type: "ssr" };
451
+ if (program && hasNamedExportInProgram(program, "getServerSideProps")) return {
452
+ hasServerSideProps: true,
453
+ type: "ssr"
454
+ };
438
455
  if (program && hasNamedExportInProgram(program, "getStaticProps")) {
439
456
  const revalidate = extractGetStaticPropsRevalidateFromProgram(program, code);
440
- if (revalidate === null || revalidate === false || revalidate === Infinity) return { type: "static" };
457
+ if (revalidate === null || revalidate === false || revalidate === Infinity) return {
458
+ hasStaticProps: true,
459
+ type: "static"
460
+ };
441
461
  if (revalidate === 0) return { type: "ssr" };
442
462
  return {
463
+ hasStaticProps: true,
443
464
  type: "isr",
444
465
  revalidate
445
466
  };
@@ -480,6 +501,46 @@ function classifyAppRoute(pagePath, routePath, isDynamic) {
480
501
  return { type: "unknown" };
481
502
  }
482
503
  /**
504
+ * Return whether a Route Handler has a GET contract that Next.js considers
505
+ * eligible for static generation. This intentionally examines only the route
506
+ * module's direct segment config; request-time dynamic usage is decided by the
507
+ * staged Worker probe after the module has executed to completion.
508
+ *
509
+ * Ported from Next.js:
510
+ * packages/next/src/server/route-modules/app-route/helpers/is-static-gen-enabled.ts
511
+ */
512
+ function classifyAppRouteHandler(filePath) {
513
+ let code;
514
+ try {
515
+ code = fs.readFileSync(filePath, "utf8");
516
+ } catch {
517
+ return {
518
+ hasGet: false,
519
+ staticGenerationEnabled: false
520
+ };
521
+ }
522
+ const program = parseRouteModule(code);
523
+ if (!program) return {
524
+ hasGet: false,
525
+ staticGenerationEnabled: false
526
+ };
527
+ const dynamicValue = extractExportConstStringFromProgram(program, "dynamic");
528
+ const revalidateValue = extractExportConstNumberFromProgram(program, "revalidate");
529
+ const hasGet = hasRuntimeExportedNameInProgram(program, "GET");
530
+ const hasNonStaticMethod = [
531
+ "POST",
532
+ "PUT",
533
+ "DELETE",
534
+ "PATCH",
535
+ "OPTIONS"
536
+ ].some((method) => hasRuntimeExportedNameInProgram(program, method));
537
+ const hasUnknownRuntimeExports = hasRuntimeExportAllInProgram(program);
538
+ return {
539
+ hasGet,
540
+ staticGenerationEnabled: !hasNonStaticMethod && !hasUnknownRuntimeExports && (dynamicValue === "force-static" || dynamicValue === "error" || revalidateValue === Infinity || revalidateValue !== null && revalidateValue > 0 || hasRuntimeExportedNameInProgram(program, "generateStaticParams"))
541
+ };
542
+ }
543
+ /**
483
544
  * Builds a sorted list of RouteRow objects from the discovered routes.
484
545
  * Routes are sorted alphabetically by path, matching filesystem order.
485
546
  *
@@ -606,4 +667,4 @@ async function printBuildReport(options) {
606
667
  }
607
668
  }
608
669
  //#endregion
609
- export { buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
670
+ export { buildReportRows, classifyAppRoute, classifyAppRouteHandler, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
@@ -20,7 +20,7 @@ type StaticExportOptions = {
20
20
  config: ResolvedNextConfig;
21
21
  };
22
22
  type StaticExportResult = {
23
- /** Number of HTML files generated */
23
+ /** Number of rendered route entries */
24
24
  pageCount: number;
25
25
  /** Generated file paths (relative to outDir) */
26
26
  files: string[];
@@ -6,6 +6,45 @@
6
6
  * by hand. `options` must be JSON-serializable: it is inlined into the generated
7
7
  * registration module and forwarded to the adapter factory at runtime.
8
8
  */
9
+ type CdnCacheAdapterCapabilities = {
10
+ /**
11
+ * Page responses include the current application build identity in the
12
+ * framework-owned `X-Vinext-Build-Id` response header, including responses
13
+ * that are ultimately marked non-cacheable.
14
+ *
15
+ * Deploy adapters use this guarantee to distinguish the newly uploaded
16
+ * Worker from an older version while staged traffic is propagating.
17
+ */
18
+ buildIdentity?: "response-header";
19
+ /**
20
+ * The shared cache selects response variants using every request header
21
+ * named by `Vary`, comparing the header values verbatim.
22
+ *
23
+ * Vinext only uses the canonical deploy-warmed RSC request shape when this
24
+ * guarantee is present. URL-only caches retain the contextual `_rsc` digest.
25
+ */
26
+ responseVary?: "verbatim";
27
+ /**
28
+ * Cacheable App Page responses require a build-bound probe manifest before
29
+ * the adapter may emit public CDN cache policy. Cloudflare deploy tooling
30
+ * carries that manifest as a module in a second Worker version.
31
+ */
32
+ routeCacheability?: "probe-manifest";
33
+ };
34
+ type CacheAdapterBuildOutput = {
35
+ /** Whether this adapter owns output for the resolved build platform. */
36
+ matchesBuild?: (build: {
37
+ plugins: readonly {
38
+ name?: string;
39
+ }[];
40
+ }) => boolean;
41
+ /** Finalize an emitted directory after other platform output hooks. */
42
+ finalizeBuildOutput?: (output: {
43
+ root: string;
44
+ outDir: string;
45
+ isPrimaryServerOutput: boolean;
46
+ }) => Promise<void> | void;
47
+ };
9
48
  type CacheAdapterDescriptor<O extends Record<string, unknown> = Record<string, unknown>> = {
10
49
  /**
11
50
  * Module specifier (or absolute path, e.g. from `require.resolve(...)`) whose
@@ -14,7 +53,14 @@ type CacheAdapterDescriptor<O extends Record<string, unknown> = Record<string, u
14
53
  adapter: string;
15
54
  /** JSON-serializable options forwarded to the factory at runtime. */
16
55
  options?: O;
56
+ /** Optional adapter-owned finalization for platform-generated build output. */
57
+ output?: CacheAdapterBuildOutput;
58
+ /** Build-time cache semantics used by shared request protocol code. */
59
+ capabilities?: CdnCacheAdapterCapabilities;
17
60
  };
61
+ declare function hasVerbatimResponseVary(cache?: VinextCacheConfig | null): boolean;
62
+ declare function hasBuildIdentityResponseHeader(cache?: VinextCacheConfig | null): boolean;
63
+ declare function requiresRouteCacheabilityProbeManifest(cache?: VinextCacheConfig | null): boolean;
18
64
  /**
19
65
  * The `cache` option of the vinext() plugin: declaratively register cache
20
66
  * handlers instead of calling `setDataCacheHandler()` / `setCdnCacheAdapter()`
@@ -40,4 +86,4 @@ declare function loadVinextCacheConfigFromViteConfig(vite: ViteConfigLoader, roo
40
86
  */
41
87
  declare function generateCacheAdaptersModule(cache?: VinextCacheConfig): string;
42
88
  //#endregion
43
- export { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VIRTUAL_CACHE_ADAPTERS, VinextCacheConfig, findVinextCacheConfigInPlugins, generateCacheAdaptersModule, loadVinextCacheConfigFromViteConfig };
89
+ export { CacheAdapterBuildOutput, CacheAdapterDescriptor, CdnCacheAdapterCapabilities, VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VIRTUAL_CACHE_ADAPTERS, VinextCacheConfig, findVinextCacheConfigInPlugins, generateCacheAdaptersModule, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, requiresRouteCacheabilityProbeManifest };
@@ -16,6 +16,15 @@ import { flattenPluginOptions } from "../utils/plugin-options.js";
16
16
  * never touches the Workers runtime — instantiation is deferred to the first
17
17
  * request.
18
18
  */
19
+ function hasVerbatimResponseVary(cache) {
20
+ return cache?.cdn?.capabilities?.responseVary === "verbatim";
21
+ }
22
+ function hasBuildIdentityResponseHeader(cache) {
23
+ return cache?.cdn?.capabilities?.buildIdentity === "response-header";
24
+ }
25
+ function requiresRouteCacheabilityProbeManifest(cache) {
26
+ return cache?.cdn?.capabilities?.routeCacheability === "probe-manifest";
27
+ }
19
28
  /** Public virtual module id imported by the server entries. */
20
29
  const VIRTUAL_CACHE_ADAPTERS = "virtual:vinext-cache-adapters";
21
30
  const VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY = "__vinextCacheConfig";
@@ -75,4 +84,4 @@ function generateCacheAdaptersModule(cache) {
75
84
  return lines.join("\n");
76
85
  }
77
86
  //#endregion
78
- export { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VIRTUAL_CACHE_ADAPTERS, findVinextCacheConfigInPlugins, generateCacheAdaptersModule, loadVinextCacheConfigFromViteConfig };
87
+ export { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VIRTUAL_CACHE_ADAPTERS, findVinextCacheConfigInPlugins, generateCacheAdaptersModule, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, requiresRouteCacheabilityProbeManifest };
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ import { detectPackageManager, ensureViteConfigCompatibility, hasAppDir, hasVite
4
4
  import { formatReport, runCheck } from "./check.js";
5
5
  import { parseArgs } from "./cli-args.js";
6
6
  import { createDevServerConfigPlugin, normalizeDevServerHostname } from "./cli-dev-config.js";
7
+ import { findVinextCacheConfigInPlugins, hasBuildIdentityResponseHeader, hasVerbatimResponseVary } from "./cache/cache-adapters-virtual.js";
7
8
  import { PHASE_PRODUCTION_BUILD } from "./shims/constants.js";
8
9
  import { createRscCompatibilityId, findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "./config/next-config.js";
9
10
  import { loadDotenv } from "./config/dotenv.js";
@@ -127,6 +128,7 @@ function hasPagesDir() {
127
128
  }
128
129
  async function loadBuildViteConfigMetadata(vite, root, mode) {
129
130
  if (!hasViteConfig(root)) return {
131
+ cacheConfig: null,
130
132
  nextConfig: null,
131
133
  prerenderConfig: null,
132
134
  routeRootConfig: null
@@ -137,6 +139,7 @@ async function loadBuildViteConfigMetadata(vite, root, mode) {
137
139
  }, void 0, root);
138
140
  const emptyOutDir = loaded?.config.build?.emptyOutDir;
139
141
  return {
142
+ cacheConfig: await findVinextCacheConfigInPlugins(loaded?.config.plugins),
140
143
  emptyOutDir: typeof emptyOutDir === "boolean" ? emptyOutDir : void 0,
141
144
  nextConfig: await findVinextNextConfigInPlugins(loaded?.config.plugins),
142
145
  prerenderConfig: await findVinextPrerenderConfigInPlugins(loaded?.config.plugins),
@@ -293,7 +296,9 @@ async function buildApp() {
293
296
  const resolvedNextConfig = await resolveNextConfig(buildConfigMetadata.nextConfig ? await resolveNextConfigInput(buildConfigMetadata.nextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root);
294
297
  process.env.__VINEXT_SHARED_BUILD_ID = resolvedNextConfig.buildId;
295
298
  process.env.__VINEXT_SHARED_RSC_COMPATIBILITY_ID = createRscCompatibilityId(resolvedNextConfig);
299
+ process.env.__VINEXT_SHARED_RSC_BUILD_IDENTITY = randomBytes(16).toString("hex");
296
300
  if (!process.env.__VINEXT_SHARED_REVALIDATE_SECRET) process.env.__VINEXT_SHARED_REVALIDATE_SECRET = randomBytes(32).toString("hex");
301
+ if (!process.env.__VINEXT_SHARED_PRERENDER_SECRET) process.env.__VINEXT_SHARED_PRERENDER_SECRET = randomBytes(32).toString("hex");
297
302
  const outputMode = resolvedNextConfig.output;
298
303
  const distDir = path.resolve(root, "dist");
299
304
  if (outputMode === "standalone") {
@@ -397,6 +402,8 @@ async function buildApp() {
397
402
  await emitPrerenderPathManifest({
398
403
  root,
399
404
  nextConfig: resolvedNextConfig,
405
+ buildIdentity: hasBuildIdentityResponseHeader(buildConfigMetadata.cacheConfig) ? "response-header" : void 0,
406
+ responseVary: hasVerbatimResponseVary(buildConfigMetadata.cacheConfig) ? "verbatim" : void 0,
400
407
  routeRootConfig: buildConfigMetadata.routeRootConfig
401
408
  });
402
409
  }
@@ -2,6 +2,8 @@ import { NEXT_DATA } from "../shims/internal/utils.js";
2
2
  //#region src/client/vinext-next-data.d.ts
3
3
  type VinextLinkPrefetchRoute = {
4
4
  canPrefetchLoadingShell: boolean;
5
+ /** The loading shell is an ordinary main-tree variant shared with deploy warmup. */
6
+ canUseCanonicalLoadingShell?: true;
5
7
  documentOnly?: boolean;
6
8
  isDynamic: boolean;
7
9
  patternParts: string[];
@@ -110,7 +110,7 @@ declare function matchConfigPattern(pathname: string, pattern: string): Record<s
110
110
  * an original index < N are checked via matchConfigPattern first — they are
111
111
  * few in practice (typically zero) so this is not a hot-path concern.
112
112
  */
113
- declare function matchRedirect(pathname: string, redirects: NextRedirect[], ctx: RequestContext, basePathState?: BasePathMatchState): {
113
+ declare function matchRedirect(pathname: string, redirects: NextRedirect[], ctx: RequestContext, basePathState?: BasePathMatchState, onRuleSourceMatch?: (rule: NextRedirect) => void): {
114
114
  destination: string;
115
115
  permanent: boolean;
116
116
  } | null;
@@ -122,7 +122,7 @@ declare function matchRedirect(pathname: string, redirects: NextRedirect[], ctx:
122
122
  * to evaluate has/missing conditions. Next.js always has request context
123
123
  * when evaluating rewrites, so this parameter is required.
124
124
  */
125
- declare function matchRewrite(pathname: string, rewrites: NextRewrite[], ctx: RequestContext, basePathState?: BasePathMatchState, paramsPathname?: string): string | null;
125
+ declare function matchRewrite(pathname: string, rewrites: NextRewrite[], ctx: RequestContext, basePathState?: BasePathMatchState, paramsPathname?: string, onRuleSourceMatch?: (rule: NextRewrite) => void): string | null;
126
126
  /**
127
127
  * Check whether a rewrite source can match a pathname without evaluating its
128
128
  * request-dependent `has` / `missing` conditions.
@@ -187,7 +187,7 @@ declare function proxyExternalRequest(request: Request, externalUrl: string): Pr
187
187
  * to evaluate has/missing conditions. Next.js always has request context
188
188
  * when evaluating headers, so this parameter is required.
189
189
  */
190
- declare function matchHeaders(pathname: string, headers: NextHeader[], ctx: RequestContext, basePathState?: BasePathMatchState): Array<{
190
+ declare function matchHeaders(pathname: string, headers: NextHeader[], ctx: RequestContext, basePathState?: BasePathMatchState, onRuleSourceMatch?: (rule: NextHeader) => void): Array<{
191
191
  key: string;
192
192
  value: string;
193
193
  }>;
@@ -546,7 +546,7 @@ function matchConfigPattern(pathname, pattern) {
546
546
  * an original index < N are checked via matchConfigPattern first — they are
547
547
  * few in practice (typically zero) so this is not a hot-path concern.
548
548
  */
549
- function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAULT) {
549
+ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAULT, onRuleSourceMatch) {
550
550
  if (redirects.length === 0) return null;
551
551
  const normalizedPathname = stripTrailingSlashForConfigMatch(pathname);
552
552
  const index = _getRedirectIndex(redirects);
@@ -559,6 +559,7 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
559
559
  if (entry.originalIndex >= localeMatchIndex) continue;
560
560
  const redirect = entry.redirect;
561
561
  if (!shouldEvaluateRule(redirect.basePath, basePathState)) continue;
562
+ onRuleSourceMatch?.(redirect);
562
563
  const conditionParams = redirect.has || redirect.missing ? collectConditionParams(redirect.has, redirect.missing, ctx) : _emptyParams();
563
564
  if (!conditionParams) continue;
564
565
  localeMatch = {
@@ -581,6 +582,7 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
581
582
  if (!entry.altRe.test(localePart)) continue;
582
583
  const redirect = entry.redirect;
583
584
  if (!shouldEvaluateRule(redirect.basePath, basePathState)) continue;
585
+ onRuleSourceMatch?.(redirect);
584
586
  const conditionParams = redirect.has || redirect.missing ? collectConditionParams(redirect.has, redirect.missing, ctx) : _emptyParams();
585
587
  if (!conditionParams) continue;
586
588
  localeMatch = {
@@ -600,6 +602,7 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
600
602
  if (!shouldEvaluateRule(redirect.basePath, basePathState)) continue;
601
603
  const params = matchConfigPattern(pathname, redirect.source);
602
604
  if (params) {
605
+ onRuleSourceMatch?.(redirect);
603
606
  const conditionParams = redirect.has || redirect.missing ? collectConditionParams(redirect.has, redirect.missing, ctx) : _emptyParams();
604
607
  if (!conditionParams) continue;
605
608
  return {
@@ -621,11 +624,12 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
621
624
  * to evaluate has/missing conditions. Next.js always has request context
622
625
  * when evaluating rewrites, so this parameter is required.
623
626
  */
624
- function matchRewrite(pathname, rewrites, ctx, basePathState = _BASEPATH_DEFAULT, paramsPathname = pathname) {
627
+ function matchRewrite(pathname, rewrites, ctx, basePathState = _BASEPATH_DEFAULT, paramsPathname = pathname, onRuleSourceMatch) {
625
628
  for (const rewrite of rewrites) {
626
629
  if (!shouldEvaluateRule(rewrite.basePath, basePathState)) continue;
627
630
  const matchedParams = matchConfigPattern(pathname, rewrite.source);
628
631
  if (matchedParams) {
632
+ onRuleSourceMatch?.(rewrite);
629
633
  const params = paramsPathname === pathname ? matchedParams : matchConfigPattern(paramsPathname, rewrite.source) ?? matchedParams;
630
634
  const conditionParams = rewrite.has || rewrite.missing ? collectConditionParams(rewrite.has, rewrite.missing, ctx) : _emptyParams();
631
635
  if (!conditionParams) continue;
@@ -891,7 +895,7 @@ async function proxyExternalRequest(request, externalUrl) {
891
895
  * to evaluate has/missing conditions. Next.js always has request context
892
896
  * when evaluating headers, so this parameter is required.
893
897
  */
894
- function matchHeaders(pathname, headers, ctx, basePathState = _BASEPATH_DEFAULT) {
898
+ function matchHeaders(pathname, headers, ctx, basePathState = _BASEPATH_DEFAULT, onRuleSourceMatch) {
895
899
  const pathnameHadTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
896
900
  pathname = stripTrailingSlashForConfigMatch(pathname);
897
901
  const result = [];
@@ -900,6 +904,7 @@ function matchHeaders(pathname, headers, ctx, basePathState = _BASEPATH_DEFAULT)
900
904
  const source = pathnameHadTrailingSlash ? stripTrailingSlashForConfigMatch(rule.source) : rule.source;
901
905
  const sourceRegex = getCachedRegex(_compiledHeaderSourceCache, source, () => safeRegExp("^" + escapeHeaderSource(source) + "$", "i"));
902
906
  if (sourceRegex && sourceRegex.test(pathname)) {
907
+ onRuleSourceMatch?.(rule);
903
908
  if (rule.has || rule.missing) {
904
909
  if (!checkHasConditions(rule.has, rule.missing, ctx)) continue;
905
910
  }
@@ -1,4 +1,4 @@
1
- import { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VinextCacheConfig, findVinextCacheConfigInPlugins, loadVinextCacheConfigFromViteConfig } from "../cache/cache-adapters-virtual.js";
1
+ import { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VinextCacheConfig, findVinextCacheConfigInPlugins, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, requiresRouteCacheabilityProbeManifest } from "../cache/cache-adapters-virtual.js";
2
2
  import { PluginOption } from "vite";
3
3
  //#region src/config/prerender.d.ts
4
4
  type VinextPrerenderConfig = true | {
@@ -40,4 +40,4 @@ declare function resolveVinextPrerenderDecision(options: {
40
40
  }): VinextPrerenderDecision | null;
41
41
  declare function formatVinextPrerenderLabel(decision: VinextPrerenderDecision): string;
42
42
  //#endregion
43
- export { ResolvedVinextPrerenderConfig, VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY, VINEXT_ROUTE_ROOT_CONFIG_PLUGIN_PROPERTY, type VinextCacheConfig, VinextPrerenderConfig, VinextPrerenderDecision, VinextPrerenderDecisionReason, VinextRouteRootConfig, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, normalizeVinextPrerenderConfig, resolveVinextPrerenderDecision };
43
+ export { ResolvedVinextPrerenderConfig, VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY, VINEXT_ROUTE_ROOT_CONFIG_PLUGIN_PROPERTY, type VinextCacheConfig, VinextPrerenderConfig, VinextPrerenderDecision, VinextPrerenderDecisionReason, VinextRouteRootConfig, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, normalizeVinextPrerenderConfig, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision };
@@ -1,6 +1,6 @@
1
1
  import { isUnknownRecord } from "../utils/record.js";
2
2
  import { flattenPluginOptions } from "../utils/plugin-options.js";
3
- import { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, findVinextCacheConfigInPlugins, loadVinextCacheConfigFromViteConfig } from "../cache/cache-adapters-virtual.js";
3
+ import { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, findVinextCacheConfigInPlugins, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, requiresRouteCacheabilityProbeManifest } from "../cache/cache-adapters-virtual.js";
4
4
  //#region src/config/prerender.ts
5
5
  const VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY = "__vinextPrerenderConfig";
6
6
  const VINEXT_ROUTE_ROOT_CONFIG_PLUGIN_PROPERTY = "__vinextRouteRootConfig";
@@ -62,4 +62,4 @@ function formatVinextPrerenderLabel(decision) {
62
62
  return "Pre-rendering all routes...";
63
63
  }
64
64
  //#endregion
65
- export { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY, VINEXT_ROUTE_ROOT_CONFIG_PLUGIN_PROPERTY, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, normalizeVinextPrerenderConfig, resolveVinextPrerenderDecision };
65
+ export { VINEXT_CACHE_CONFIG_PLUGIN_PROPERTY, VINEXT_PRERENDER_CONFIG_PLUGIN_PROPERTY, VINEXT_ROUTE_ROOT_CONFIG_PLUGIN_PROPERTY, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasVerbatimResponseVary, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, normalizeVinextPrerenderConfig, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision };
@@ -13,7 +13,7 @@ declare function generateBrowserEntry(routes?: readonly AppRoute[], routeManifes
13
13
  afterFiles: NextRewrite[];
14
14
  beforeFiles: NextRewrite[];
15
15
  fallback: NextRewrite[];
16
- }): string;
16
+ }, locales?: readonly string[]): string;
17
17
  /** Project an `AppRoute` down to the public `VinextLinkPrefetchRoute` shape. */
18
18
  declare function toLinkPrefetchRoute(route: AppRoute, hasSiblingInterceptLoading?: boolean): VinextLinkPrefetchRoute;
19
19
  /** Project App routes together so sibling-intercept loading is applied to its target route. */
@@ -1,6 +1,7 @@
1
1
  import { resolveClientRuntimeModule, resolveRuntimeEntryModule } from "./runtime-entry-module.js";
2
2
  import { toClientRewrites } from "../client/client-rewrites.js";
3
3
  import { patternsStructurallyEquivalent } from "../routing/app-route-graph.js";
4
+ import { appRouteHasMainTreeLoadingBoundary } from "../routing/app-router.js";
4
5
  //#region src/entries/app-browser-entry.ts
5
6
  /**
6
7
  * Generate the virtual browser entry module.
@@ -13,7 +14,7 @@ function generateBrowserEntry(routes = [], routeManifest = null, pagesPrefetchRo
13
14
  afterFiles: [],
14
15
  beforeFiles: [],
15
16
  fallback: []
16
- }) {
17
+ }, locales = []) {
17
18
  const entryPath = resolveRuntimeEntryModule("app-browser-entry");
18
19
  const reactInstanceBootstrapPath = resolveClientRuntimeModule("react-instance-bootstrap");
19
20
  const navigationRuntimePath = resolveClientRuntimeModule("navigation-runtime");
@@ -29,6 +30,7 @@ window.__VINEXT_LINK_PREFETCH_ROUTES__ = ${JSON.stringify(prefetchRoutes)};
29
30
  // the same — whichever entry runs first emits both globals).
30
31
  window.__VINEXT_PAGES_LINK_PREFETCH_ROUTES__ = ${JSON.stringify(pagesPrefetchRoutes)};
31
32
  window.__VINEXT_CLIENT_REWRITES__ = ${JSON.stringify(clientRewrites)};
33
+ window.__VINEXT_LOCALES__ = ${JSON.stringify(locales)};
32
34
  registerNavigationRuntimeBootstrap({
33
35
  routeManifest: ${buildRouteManifestExpression(routeManifest)}
34
36
  });
@@ -68,6 +70,7 @@ function hasLoadingBoundary(route, hasSiblingInterceptLoading) {
68
70
  function toLinkPrefetchRoute(route, hasSiblingInterceptLoading = route.siblingIntercepts.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) {
69
71
  return {
70
72
  canPrefetchLoadingShell: hasLoadingBoundary(route, hasSiblingInterceptLoading),
73
+ ...appRouteHasMainTreeLoadingBoundary(route) ? { canUseCanonicalLoadingShell: true } : {},
71
74
  patternParts: [...route.patternParts],
72
75
  isDynamic: route.isDynamic,
73
76
  ...requiresDynamicNavigationRequest(route) ? { requiresDynamicNavigationRequest: true } : {},