vinext 1.0.0-beta.2 → 1.0.0-beta.4

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 (174) hide show
  1. package/README.md +6 -9
  2. package/dist/build/client-build-config.js +7 -1
  3. package/dist/build/precompress.js +1 -1
  4. package/dist/build/prerender.d.ts +2 -1
  5. package/dist/build/prerender.js +12 -2
  6. package/dist/check.js +4 -0
  7. package/dist/cli-args.d.ts +2 -1
  8. package/dist/cli-args.js +10 -0
  9. package/dist/cli.js +14 -8
  10. package/dist/client/navigation-runtime.d.ts +6 -0
  11. package/dist/client/navigation-runtime.js +1 -1
  12. package/dist/config/config-matchers.js +4 -1
  13. package/dist/config/dotenv.d.ts +3 -4
  14. package/dist/config/dotenv.js +30 -22
  15. package/dist/config/next-config.d.ts +16 -5
  16. package/dist/config/next-config.js +7 -0
  17. package/dist/entries/app-browser-entry.d.ts +4 -10
  18. package/dist/entries/app-browser-entry.js +20 -4
  19. package/dist/entries/app-rsc-entry.js +21 -3
  20. package/dist/entries/app-rsc-manifest.js +19 -0
  21. package/dist/entries/pages-client-entry.js +0 -1
  22. package/dist/entries/pages-server-entry.js +13 -3
  23. package/dist/index.js +122 -49
  24. package/dist/init-cloudflare.d.ts +3 -1
  25. package/dist/init-cloudflare.js +59 -5
  26. package/dist/plugins/client-reference-dedup.js +7 -5
  27. package/dist/plugins/extensionless-dynamic-import.js +23 -18
  28. package/dist/plugins/ignore-dynamic-requests.js +3 -1
  29. package/dist/plugins/import-meta-url.js +17 -6
  30. package/dist/plugins/require-context.js +20 -15
  31. package/dist/plugins/sass.js +3 -4
  32. package/dist/plugins/transform-cache.d.ts +19 -0
  33. package/dist/plugins/transform-cache.js +36 -0
  34. package/dist/routing/app-route-graph.d.ts +14 -6
  35. package/dist/routing/app-route-graph.js +106 -16
  36. package/dist/server/api-handler.d.ts +2 -0
  37. package/dist/server/api-handler.js +7 -6
  38. package/dist/server/app-bfcache-identity.js +28 -2
  39. package/dist/server/app-browser-entry.js +175 -75
  40. package/dist/server/app-browser-navigation-controller.d.ts +23 -21
  41. package/dist/server/app-browser-navigation-controller.js +30 -5
  42. package/dist/server/app-browser-prefetch-response.d.ts +9 -0
  43. package/dist/server/app-browser-prefetch-response.js +32 -0
  44. package/dist/server/app-browser-state.d.ts +8 -3
  45. package/dist/server/app-browser-state.js +9 -3
  46. package/dist/server/app-page-cache-finalizer.js +1 -1
  47. package/dist/server/app-page-cache.d.ts +1 -1
  48. package/dist/server/app-page-cache.js +16 -2
  49. package/dist/server/app-page-dispatch.d.ts +9 -1
  50. package/dist/server/app-page-dispatch.js +37 -8
  51. package/dist/server/app-page-element-builder.d.ts +4 -1
  52. package/dist/server/app-page-element-builder.js +34 -7
  53. package/dist/server/app-page-head.d.ts +3 -3
  54. package/dist/server/app-page-head.js +58 -13
  55. package/dist/server/app-page-http-access-fallback-metadata.d.ts +2 -2
  56. package/dist/server/app-page-http-access-fallback-metadata.js +9 -1
  57. package/dist/server/app-page-probe.d.ts +4 -0
  58. package/dist/server/app-page-probe.js +6 -2
  59. package/dist/server/app-page-render.d.ts +1 -1
  60. package/dist/server/app-page-render.js +13 -3
  61. package/dist/server/app-page-request.d.ts +3 -1
  62. package/dist/server/app-page-request.js +4 -13
  63. package/dist/server/app-page-response.d.ts +2 -0
  64. package/dist/server/app-page-response.js +7 -2
  65. package/dist/server/app-page-route-wiring.d.ts +10 -2
  66. package/dist/server/app-page-route-wiring.js +269 -59
  67. package/dist/server/app-pages-bridge.d.ts +2 -1
  68. package/dist/server/app-pages-bridge.js +3 -1
  69. package/dist/server/app-route-handler-cache.js +4 -0
  70. package/dist/server/app-route-handler-dispatch.js +9 -3
  71. package/dist/server/app-route-handler-response.js +1 -1
  72. package/dist/server/app-route-module-loader.d.ts +6 -0
  73. package/dist/server/app-route-module-loader.js +3 -0
  74. package/dist/server/app-router-entry.js +6 -3
  75. package/dist/server/app-rsc-cache-busting.d.ts +7 -0
  76. package/dist/server/app-rsc-cache-busting.js +6 -0
  77. package/dist/server/app-rsc-errors.js +10 -0
  78. package/dist/server/app-rsc-handler.d.ts +2 -0
  79. package/dist/server/app-rsc-handler.js +30 -13
  80. package/dist/server/app-rsc-render-mode.d.ts +3 -2
  81. package/dist/server/app-rsc-render-mode.js +4 -1
  82. package/dist/server/app-rsc-request-normalization.js +65 -4
  83. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  84. package/dist/server/app-rsc-route-matching.js +6 -0
  85. package/dist/server/app-server-action-execution.d.ts +1 -1
  86. package/dist/server/app-ssr-entry.js +4 -4
  87. package/dist/server/app-visited-response-cache.d.ts +6 -1
  88. package/dist/server/app-visited-response-cache.js +49 -1
  89. package/dist/server/cache-control.d.ts +2 -2
  90. package/dist/server/cache-control.js +2 -1
  91. package/dist/server/dev-server.d.ts +3 -1
  92. package/dist/server/dev-server.js +204 -348
  93. package/dist/server/edge-api-runtime.d.ts +10 -1
  94. package/dist/server/edge-api-runtime.js +29 -1
  95. package/dist/server/headers.d.ts +4 -2
  96. package/dist/server/headers.js +6 -3
  97. package/dist/server/isr-cache.d.ts +10 -33
  98. package/dist/server/isr-cache.js +33 -43
  99. package/dist/server/isr-decision.d.ts +2 -2
  100. package/dist/server/pages-api-route.d.ts +4 -0
  101. package/dist/server/pages-api-route.js +6 -3
  102. package/dist/server/pages-asset-tags.d.ts +1 -0
  103. package/dist/server/pages-asset-tags.js +6 -4
  104. package/dist/server/pages-dev-hydration.js +0 -1
  105. package/dist/server/pages-document-asset-props.d.ts +26 -0
  106. package/dist/server/pages-document-asset-props.js +69 -0
  107. package/dist/server/pages-get-initial-props.d.ts +1 -1
  108. package/dist/server/pages-get-initial-props.js +1 -4
  109. package/dist/server/pages-i18n.js +64 -16
  110. package/dist/server/pages-node-compat.d.ts +2 -0
  111. package/dist/server/pages-node-compat.js +10 -6
  112. package/dist/server/pages-page-data.d.ts +61 -9
  113. package/dist/server/pages-page-data.js +334 -74
  114. package/dist/server/pages-page-handler.d.ts +6 -1
  115. package/dist/server/pages-page-handler.js +119 -20
  116. package/dist/server/pages-page-response.d.ts +7 -3
  117. package/dist/server/pages-page-response.js +41 -33
  118. package/dist/server/pages-request-pipeline.d.ts +7 -0
  119. package/dist/server/pages-request-pipeline.js +5 -1
  120. package/dist/server/pages-revalidate.d.ts +1 -1
  121. package/dist/server/pages-revalidate.js +54 -8
  122. package/dist/server/pages-router-entry.d.ts +1 -0
  123. package/dist/server/pages-router-entry.js +10 -4
  124. package/dist/server/prerender-manifest.d.ts +1 -0
  125. package/dist/server/prod-server.d.ts +2 -1
  126. package/dist/server/prod-server.js +38 -13
  127. package/dist/server/revalidation-host.d.ts +11 -0
  128. package/dist/server/revalidation-host.js +19 -0
  129. package/dist/server/seed-cache.js +1 -1
  130. package/dist/server/worker-revalidation-context.d.ts +13 -0
  131. package/dist/server/worker-revalidation-context.js +30 -0
  132. package/dist/shims/app-router-scroll-state.d.ts +2 -1
  133. package/dist/shims/app-router-scroll-state.js +7 -2
  134. package/dist/shims/cache-handler.d.ts +1 -1
  135. package/dist/shims/cache-handler.js +10 -7
  136. package/dist/shims/cache-runtime.d.ts +6 -0
  137. package/dist/shims/cache-runtime.js +4 -1
  138. package/dist/shims/cache.js +1 -1
  139. package/dist/shims/document.js +15 -2
  140. package/dist/shims/error.js +1 -1
  141. package/dist/shims/fetch-cache.js +1 -1
  142. package/dist/shims/font-google-base.js +17 -8
  143. package/dist/shims/font-local.js +41 -10
  144. package/dist/shims/hash-scroll.d.ts +1 -1
  145. package/dist/shims/hash-scroll.js +2 -1
  146. package/dist/shims/headers.d.ts +10 -13
  147. package/dist/shims/headers.js +85 -36
  148. package/dist/shims/internal/cookie-serialize.d.ts +10 -9
  149. package/dist/shims/internal/cookie-serialize.js +9 -8
  150. package/dist/shims/internal/interpolate-as.d.ts +15 -1
  151. package/dist/shims/internal/interpolate-as.js +27 -2
  152. package/dist/shims/link.d.ts +2 -0
  153. package/dist/shims/link.js +77 -24
  154. package/dist/shims/metadata.d.ts +18 -34
  155. package/dist/shims/metadata.js +98 -56
  156. package/dist/shims/navigation.d.ts +18 -2
  157. package/dist/shims/navigation.js +70 -21
  158. package/dist/shims/request-context.d.ts +6 -2
  159. package/dist/shims/router.js +18 -7
  160. package/dist/shims/script.js +1 -1
  161. package/dist/shims/server.d.ts +7 -6
  162. package/dist/shims/server.js +156 -57
  163. package/dist/shims/slot.d.ts +2 -1
  164. package/dist/shims/slot.js +6 -2
  165. package/dist/shims/streamed-icons.d.ts +9 -0
  166. package/dist/shims/streamed-icons.js +33 -0
  167. package/dist/shims/unified-request-context.d.ts +21 -2
  168. package/dist/shims/unified-request-context.js +82 -1
  169. package/dist/utils/cache-control-metadata.d.ts +2 -1
  170. package/dist/utils/cache-control-metadata.js +5 -1
  171. package/dist/utils/html-limited-bots.js +1 -1
  172. package/dist/utils/protocol-headers.d.ts +7 -1
  173. package/dist/utils/protocol-headers.js +7 -1
  174. package/package.json +1 -1
package/README.md CHANGED
@@ -345,24 +345,16 @@ Requires a custom domain (zone analytics are unavailable on `*.workers.dev`) and
345
345
 
346
346
  #### Custom Vite configuration
347
347
 
348
- If you need to customize the Vite config, create a `vite.config.ts`. vinext will merge its config with yours. This is required for Cloudflare Workers deployment with the App Router (RSC needs explicit plugin configuration):
348
+ If you need to customize the Vite config, create a `vite.config.ts`. vinext will merge its config with yours. For Cloudflare Workers deployment with the App Router, configure `@cloudflare/vite-plugin` so the RSC environment runs in workerd:
349
349
 
350
350
  ```ts
351
351
  import { defineConfig } from "vite";
352
352
  import vinext from "vinext";
353
- import rsc from "@vitejs/plugin-rsc";
354
353
  import { cloudflare } from "@cloudflare/vite-plugin";
355
354
 
356
355
  export default defineConfig({
357
356
  plugins: [
358
357
  vinext(),
359
- rsc({
360
- entries: {
361
- rsc: "virtual:vinext-rsc-entry",
362
- ssr: "virtual:vinext-app-ssr-entry",
363
- client: "virtual:vinext-app-browser-entry",
364
- },
365
- }),
366
358
  cloudflare({
367
359
  viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
368
360
  }),
@@ -370,6 +362,11 @@ export default defineConfig({
370
362
  });
371
363
  ```
372
364
 
365
+ > **Do not register `@vitejs/plugin-rsc` yourself.** It is an optional peer dependency, so it must be
366
+ > _installed_ in your project, but vinext auto-registers it whenever an `app/` directory is detected.
367
+ > Adding an explicit `rsc()` call fails the build with `[vinext] Duplicate @vitejs/plugin-rsc detected`.
368
+ > Pass `rsc: false` to `vinext()` only if you want to own that registration.
369
+
373
370
  See the [examples](#live-examples) for complete working configurations.
374
371
 
375
372
  ### Other platforms (via Nitro)
@@ -89,7 +89,13 @@ function createClientManualChunks(shimsDir, preserveRouteBoundaries = false) {
89
89
  if (id.includes("node_modules")) {
90
90
  const pkg = getPackageName(id);
91
91
  if (!pkg) return void 0;
92
- if (pkg === "react" || pkg === "react-dom" || pkg === "scheduler") return "framework";
92
+ if (pkg === "react-dom") {
93
+ const slashedId = toSlash(id);
94
+ const sub = slashedId.slice(slashedId.lastIndexOf("react-dom/") + 10);
95
+ if (sub.startsWith("server.") || sub.startsWith("static.") || sub.startsWith("cjs/react-dom-server")) return "react-dom-server";
96
+ return "framework";
97
+ }
98
+ if (pkg === "react" || pkg === "scheduler") return "framework";
93
99
  return;
94
100
  }
95
101
  const slashedId = toSlash(id);
@@ -1,9 +1,9 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { ASSET_PREFIX_URL_DIR } from "../utils/asset-prefix.js";
3
3
  import fsp from "node:fs/promises";
4
+ import { promisify } from "node:util";
4
5
  import os from "node:os";
5
6
  import zlib from "node:zlib";
6
- import { promisify } from "node:util";
7
7
  //#region src/build/precompress.ts
8
8
  /**
9
9
  * Build-time precompression for hashed static assets.
@@ -23,7 +23,8 @@ type PrerenderRouteResult = {
23
23
  */
24
24
  path?: string; /** Which router produced this route. Used by cache seeding. */
25
25
  router: "app" | "pages"; /** Response headers that must be replayed with the prerendered artifact. */
26
- headers?: Record<string, string>; /** Set to true when this is a PPR fallback shell. */
26
+ headers?: Record<string, string>; /** Cache tags collected while rendering this route. */
27
+ tags?: string[]; /** Set to true when this is a PPR fallback shell. */
27
28
  fallback?: boolean;
28
29
  } | {
29
30
  route: string;
@@ -1,7 +1,7 @@
1
1
  import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { createValidFileMatcher, findFileWithExtensions } from "../routing/file-matcher.js";
3
3
  import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_PRERENDER_SPECULATIVE_HEADER } from "../utils/protocol-headers.js";
4
- import { VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
4
+ import { NEXT_CACHE_TAGS_HEADER, VINEXT_PRERENDER_CACHE_LIFE_HEADER } from "../server/headers.js";
5
5
  import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath } from "./report.js";
6
6
  import { NoOpCacheHandler, getCacheHandler, setCacheHandler } from "../shims/cache-handler.js";
7
7
  import { headersContextFromRequest, runWithHeadersContext } from "../shims/headers.js";
@@ -760,6 +760,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
760
760
  const cacheControl = response.headers.get("cache-control") ?? "";
761
761
  const linkHeader = response.headers.get("link");
762
762
  const responseCacheLife = readPrerenderCacheLifeHeader(response.headers);
763
+ const cacheTags = readPrerenderCacheTagsHeader(response.headers);
763
764
  if (!response.ok || cacheControl.includes("no-store")) {
764
765
  await response.body?.cancel();
765
766
  return {
@@ -768,6 +769,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
768
769
  html: null,
769
770
  ok: response.ok,
770
771
  requestCacheLife: null,
772
+ tags: [],
771
773
  status: response.status
772
774
  };
773
775
  }
@@ -779,7 +781,8 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
779
781
  html,
780
782
  ok: true,
781
783
  requestCacheLife: responseCacheLife ?? processCacheLife,
782
- status: response.status
784
+ status: response.status,
785
+ tags: cacheTags
783
786
  };
784
787
  });
785
788
  const htmlCacheControl = htmlRender.cacheControl;
@@ -842,6 +845,7 @@ async function prerenderApp({ routes, metadataRoutes = [], outDir, config, mode,
842
845
  revalidate: renderedRevalidate,
843
846
  ...typeof renderedRevalidate === "number" ? { expire: renderedCacheControl.expire } : {},
844
847
  router: "app",
848
+ ...htmlRender.tags.length > 0 ? { tags: htmlRender.tags } : {},
845
849
  ...htmlRender.linkHeader ? { headers: { link: htmlRender.linkHeader } } : {},
846
850
  ...urlPath !== routePattern ? { path: urlPath } : {},
847
851
  ...isFallback ? { fallback: true } : {}
@@ -945,6 +949,11 @@ function readPrerenderCacheLifeHeader(headers) {
945
949
  return null;
946
950
  }
947
951
  }
952
+ function readPrerenderCacheTagsHeader(headers) {
953
+ const value = headers.get(NEXT_CACHE_TAGS_HEADER);
954
+ if (!value) return [];
955
+ return [...new Set(value.split(",").filter(Boolean))];
956
+ }
948
957
  function resolveRenderedExpireSeconds(options) {
949
958
  const { fallbackExpireSeconds, sMaxage, staleWhileRevalidate } = options;
950
959
  if (sMaxage === void 0 || staleWhileRevalidate === void 0) return fallbackExpireSeconds;
@@ -976,6 +985,7 @@ function writePrerenderIndex(routes, outDir, options) {
976
985
  revalidate: r.revalidate,
977
986
  ...typeof r.revalidate === "number" ? { expire: r.expire } : {},
978
987
  router: r.router,
988
+ ...r.tags && r.tags.length > 0 ? { tags: r.tags } : {},
979
989
  ...r.headers ? { headers: r.headers } : {},
980
990
  ...r.path ? { path: r.path } : {},
981
991
  ...r.fallback ? { fallback: true } : {}
package/dist/check.js CHANGED
@@ -230,6 +230,10 @@ const CONFIG_SUPPORT = {
230
230
  status: "supported",
231
231
  detail: "server actions via 'use server' directive"
232
232
  },
233
+ "experimental.allowedRevalidateHeaderKeys": {
234
+ status: "supported",
235
+ detail: "forwards explicitly allowed request headers during Pages Router revalidation"
236
+ },
233
237
  "experimental.prefetchInlining": {
234
238
  status: "partial",
235
239
  detail: "config recognized; Link prefetch preserves pending/dedup semantics, but vinext does not implement per-segment cache storage"
@@ -3,12 +3,13 @@
3
3
  * CLI argument parser for the vinext CLI.
4
4
  *
5
5
  * Parses flags for `vinext dev`, `vinext start`, `vinext build`, etc.
6
- * Validates that value-taking flags (`--port`, `--hostname`) have actual values
6
+ * Validates that value-taking flags (`--port`, `--hostname`, `--mode`) have actual values
7
7
  * rather than silently consuming the next flag or returning NaN/undefined.
8
8
  */
9
9
  type ParsedArgs = {
10
10
  port?: number;
11
11
  hostname?: string;
12
+ mode?: string;
12
13
  help?: boolean;
13
14
  verbose?: boolean;
14
15
  turbopack?: boolean;
package/dist/cli-args.js CHANGED
@@ -96,6 +96,10 @@ function parseArgs(args) {
96
96
  result.hostname = takeValue(arg, args, i);
97
97
  i++;
98
98
  break;
99
+ case "--mode":
100
+ result.mode = takeValue(arg, args, i);
101
+ i++;
102
+ break;
99
103
  default: {
100
104
  const eqRaw = tryEqualsForm(arg, "port");
101
105
  if (eqRaw !== null) {
@@ -108,6 +112,12 @@ function parseArgs(args) {
108
112
  result.hostname = hostRaw;
109
113
  break;
110
114
  }
115
+ const modeRaw = tryEqualsForm(arg, "mode");
116
+ if (modeRaw !== null) {
117
+ if (modeRaw === "") throw new Error(`--mode requires a value, but none was provided.`);
118
+ result.mode = modeRaw;
119
+ break;
120
+ }
111
121
  const prerenderConcurrencyRaw = tryEqualsForm(arg, "prerender-concurrency");
112
122
  if (prerenderConcurrencyRaw !== null) {
113
123
  result.prerenderConcurrency = parsePositiveIntegerArg(prerenderConcurrencyRaw, "--prerender-concurrency");
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { parseArgs } from "./cli-args.js";
6
6
  import { createDevServerConfigPlugin, normalizeDevServerHostname } from "./cli-dev-config.js";
7
7
  import { PHASE_PRODUCTION_BUILD } from "./shims/constants.js";
8
8
  import { createRscCompatibilityId, findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "./config/next-config.js";
9
+ import { loadDotenv } from "./config/dotenv.js";
9
10
  import { generateRouteTypes } from "./typegen.js";
10
11
  import { clearPagesClientAssetsBuildMetadata } from "./build/pages-client-assets-module.js";
11
12
  import { runWithPreviewBuildCredentials } from "./build/preview-credentials.js";
@@ -16,7 +17,6 @@ import { emitPrerenderPathManifest } from "./build/prerender-paths.js";
16
17
  import { getReactUpgradeDeps } from "./utils/react-version.js";
17
18
  import { init } from "./init.js";
18
19
  import { resolveInitOptions } from "./init-platform.js";
19
- import { loadDotenv } from "./config/dotenv.js";
20
20
  import { resolveVinextPackageRoot } from "./utils/vinext-root.js";
21
21
  import { emitStandaloneOutput } from "./build/standalone.js";
22
22
  import { cleanBuildOutput } from "./build/clean-output.js";
@@ -125,7 +125,7 @@ function createBuildLogger(vite) {
125
125
  function hasPagesDir() {
126
126
  return fs.existsSync(path.join(process.cwd(), "pages")) || fs.existsSync(path.join(process.cwd(), "src", "pages"));
127
127
  }
128
- async function loadBuildViteConfigMetadata(vite, root) {
128
+ async function loadBuildViteConfigMetadata(vite, root, mode) {
129
129
  if (!hasViteConfig(root)) return {
130
130
  nextConfig: null,
131
131
  prerenderConfig: null,
@@ -133,7 +133,7 @@ async function loadBuildViteConfigMetadata(vite, root) {
133
133
  };
134
134
  const loaded = await vite.loadConfigFromFile({
135
135
  command: "build",
136
- mode: "production"
136
+ mode
137
137
  }, void 0, root);
138
138
  const emptyOutDir = loaded?.config.build?.emptyOutDir;
139
139
  return {
@@ -148,14 +148,16 @@ async function loadBuildViteConfigMetadata(vite, root) {
148
148
  * project, Vite will merge our config with it (theirs takes precedence).
149
149
  * If there's no vite.config, this provides everything needed.
150
150
  */
151
- function buildViteConfig(overrides = {}, logger) {
151
+ function buildViteConfig(overrides = {}, logger, mode) {
152
152
  if (hasViteConfig(process.cwd())) return {
153
153
  root: process.cwd(),
154
+ ...mode ? { mode } : {},
154
155
  ...logger ? { customLogger: logger } : {},
155
156
  ...overrides
156
157
  };
157
158
  return {
158
159
  root: process.cwd(),
160
+ ...mode ? { mode } : {},
159
161
  configFile: false,
160
162
  plugins: [vinext()],
161
163
  resolve: { dedupe: [
@@ -274,10 +276,12 @@ async function dev() {
274
276
  async function buildApp() {
275
277
  const parsed = parseArgs(rawArgs);
276
278
  if (parsed.help) return printHelp("build");
279
+ const buildMode = parsed.mode ?? "production";
280
+ if (!process.env.NODE_ENV) Reflect.set(process.env, "NODE_ENV", "production");
277
281
  if (parsed.precompress) process.env.VINEXT_PRECOMPRESS = "1";
278
282
  loadDotenv({
279
283
  root: process.cwd(),
280
- mode: "production"
284
+ mode: buildMode
281
285
  });
282
286
  applyViteConfigCompatibility(process.cwd());
283
287
  const vite = await loadVite();
@@ -285,7 +289,7 @@ async function buildApp() {
285
289
  console.log(`\n vinext build (Vite ${getViteVersion()})\n`);
286
290
  const root = toSlash(process.cwd());
287
291
  const isApp = hasAppDir(root);
288
- const buildConfigMetadata = await loadBuildViteConfigMetadata(vite, root);
292
+ const buildConfigMetadata = await loadBuildViteConfigMetadata(vite, root, buildMode);
289
293
  const resolvedNextConfig = await resolveNextConfig(buildConfigMetadata.nextConfig ? await resolveNextConfigInput(buildConfigMetadata.nextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root);
290
294
  process.env.__VINEXT_SHARED_BUILD_ID = resolvedNextConfig.buildId;
291
295
  process.env.__VINEXT_SHARED_RSC_COMPATIBILITY_ID = createRscCompatibilityId(resolvedNextConfig);
@@ -322,7 +326,7 @@ async function buildApp() {
322
326
  if (pagesClientAssetsBuildSession) process.env.__VINEXT_PAGES_CLIENT_ASSETS_BUILD_SESSION = pagesClientAssetsBuildSession;
323
327
  await runWithPreviewBuildCredentials(async () => {
324
328
  try {
325
- const config = buildViteConfig({}, logger);
329
+ const config = buildViteConfig({}, logger, buildMode);
326
330
  await (await vite.createBuilder(config)).buildApp();
327
331
  if (isHybrid) {
328
332
  console.log(" Building Pages Router server (hybrid)...");
@@ -331,13 +335,14 @@ async function buildApp() {
331
335
  if (hasViteConfig(process.cwd())) {
332
336
  const loaded = await vite.loadConfigFromFile({
333
337
  command: "build",
334
- mode: "production",
338
+ mode: buildMode,
335
339
  isSsrBuild: true
336
340
  }, void 0, root);
337
341
  if (loaded?.config.plugins) userTransformPlugins = loaded.config.plugins.flat(Infinity).filter((p) => !!p && typeof p.name === "string" && !p.name.startsWith("vinext:") && !p.name.startsWith("vite:react") && !p.name.startsWith("rsc:") && p.name !== "vite-rsc-load-module-dev-proxy" && !p.name.startsWith("vite-plugin-cloudflare"));
338
342
  }
339
343
  await vite.build({
340
344
  root,
345
+ mode: buildMode,
341
346
  configFile: false,
342
347
  plugins: [...userTransformPlugins, vinext({ disableAppRouter: true })],
343
348
  resolve: { dedupe: [
@@ -536,6 +541,7 @@ function printHelp(cmd) {
536
541
  to vinext({ prerender: true }) and takes priority.
537
542
  --prerender-concurrency <count>
538
543
  Maximum number of routes to pre-render in parallel
544
+ --mode <mode> Load .env.<mode>* files and pass mode to Vite
539
545
  --precompress Precompress static assets at build time (.br, .gz, .zst)
540
546
  -h, --help Show this help
541
547
  `);
@@ -18,6 +18,10 @@ type NavigationRuntimeRscBootstrap = {
18
18
  type NavigationRuntimeKind = "navigate" | "traverse" | "refresh";
19
19
  type NavigationRuntimeHistoryUpdateMode = "push" | "replace";
20
20
  type NavigationRuntimeVisibleCommitMode = "transition" | "synchronous";
21
+ type NavigationRuntimePrefetchRouterState = {
22
+ pathAndSearch: string;
23
+ routeId: string;
24
+ };
21
25
  type NavigationRuntimeTraversalIntent = {
22
26
  direction: "back" | "forward" | "unknown";
23
27
  historyState: unknown;
@@ -29,6 +33,7 @@ type NavigationRuntimeFunctions = {
29
33
  commitHashNavigation?: (href: string, historyUpdateMode: NavigationRuntimeHistoryUpdateMode, scroll: boolean) => void;
30
34
  navigateExternal?: (href: string, historyUpdateMode: NavigationRuntimeHistoryUpdateMode) => Promise<void>;
31
35
  navigate?: NavigationRuntimeNavigate;
36
+ getPrefetchRouterState?: () => NavigationRuntimePrefetchRouterState | null;
32
37
  /**
33
38
  * Called at the start of every App Router navigation so the <Link> shim can
34
39
  * reset any link that is still showing a `useLinkStatus()` pending state but
@@ -38,6 +43,7 @@ type NavigationRuntimeFunctions = {
38
43
  */
39
44
  notifyLinkNavigationStart?: () => void;
40
45
  pingVisibleLinks?: () => void;
46
+ preparePrefetchResponse?: (response: Response) => Promise<unknown>;
41
47
  };
42
48
  type NavigationRuntimeBootstrap = {
43
49
  routeManifest: RouteManifest | null;
@@ -31,7 +31,7 @@ function readRuntimeWindow() {
31
31
  }
32
32
  function isNavigationRuntimeFunctions(value) {
33
33
  if (!isUnknownRecord(value)) return false;
34
- return isOptionalRuntimeFunction(Reflect.get(value, "clearNavigationCaches")) && isOptionalRuntimeFunction(Reflect.get(value, "commitHashNavigation")) && isOptionalRuntimeFunction(Reflect.get(value, "navigateExternal")) && isOptionalRuntimeFunction(Reflect.get(value, "navigate")) && isOptionalRuntimeFunction(Reflect.get(value, "notifyLinkNavigationStart")) && isOptionalRuntimeFunction(Reflect.get(value, "pingVisibleLinks"));
34
+ return isOptionalRuntimeFunction(Reflect.get(value, "clearNavigationCaches")) && isOptionalRuntimeFunction(Reflect.get(value, "commitHashNavigation")) && isOptionalRuntimeFunction(Reflect.get(value, "navigateExternal")) && isOptionalRuntimeFunction(Reflect.get(value, "navigate")) && isOptionalRuntimeFunction(Reflect.get(value, "getPrefetchRouterState")) && isOptionalRuntimeFunction(Reflect.get(value, "notifyLinkNavigationStart")) && isOptionalRuntimeFunction(Reflect.get(value, "pingVisibleLinks")) && isOptionalRuntimeFunction(Reflect.get(value, "preparePrefetchResponse"));
35
35
  }
36
36
  function isNavigationRuntimeRscChunk(value) {
37
37
  if (typeof value === "string") return true;
@@ -1,4 +1,4 @@
1
- import { VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
1
+ import { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HOST_HEADER } from "../utils/protocol-headers.js";
2
2
  import { buildRequestHeadersFromMiddlewareResponse } from "../utils/middleware-request-headers.js";
3
3
  import { analyzeRegexSafety } from "../utils/regex-safety.js";
4
4
  import { normalizeHost, parseCookies, requestContextFromRequest } from "./request-context.js";
@@ -835,6 +835,9 @@ async function proxyExternalRequest(request, externalUrl) {
835
835
  for (const key of keysToDelete) headers.delete(key);
836
836
  headers.delete(VINEXT_PRERENDER_SECRET_HEADER);
837
837
  headers.delete(VINEXT_PRERENDER_ROUTE_PARAMS_HEADER);
838
+ headers.delete(PRERENDER_REVALIDATE_HEADER);
839
+ headers.delete(PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER);
840
+ headers.delete(VINEXT_REVALIDATE_HOST_HEADER);
838
841
  headers.delete(VINEXT_MW_CTX_HEADER);
839
842
  const method = request.method;
840
843
  const hasBody = method !== "GET" && method !== "HEAD";
@@ -1,5 +1,4 @@
1
1
  //#region src/config/dotenv.d.ts
2
- type VinextEnvMode = "development" | "production" | "test";
3
2
  /**
4
3
  * Environment-variable bag accepted by {@link loadDotenv}.
5
4
  *
@@ -12,18 +11,18 @@ type VinextEnvMode = "development" | "production" | "test";
12
11
  type EnvBag = Record<string, string | undefined>;
13
12
  type LoadDotenvOptions = {
14
13
  root: string;
15
- mode: VinextEnvMode;
14
+ mode: string;
16
15
  processEnv?: EnvBag;
17
16
  };
18
17
  type LoadDotenvResult = {
19
- mode: VinextEnvMode;
18
+ mode: string;
20
19
  loadedFiles: string[];
21
20
  loadedEnv: Record<string, string>;
22
21
  };
23
22
  /**
24
23
  * Next.js-compatible dotenv lookup order (highest priority first).
25
24
  */
26
- declare function getDotenvFiles(mode: VinextEnvMode): string[];
25
+ declare function getDotenvFiles(mode: string): string[];
27
26
  /**
28
27
  * Load .env files into processEnv with Next.js-like precedence:
29
28
  * process.env > .env.<mode>.local > .env.local > .env.<mode> > .env.
@@ -56,33 +56,41 @@ function loadDotenv({ root, mode, processEnv = process.env }) {
56
56
  loadedEnv
57
57
  };
58
58
  }
59
- const ENV_REF_RE = /(\\)?\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
60
59
  function expandEnv(parsed, processEnv) {
61
- const expanded = {};
62
- const resolving = /* @__PURE__ */ new Set();
63
- const context = {
60
+ const expanded = { ...parsed };
61
+ for (const key of Object.keys(expanded)) {
62
+ const processValue = processEnv[key];
63
+ expanded[key] = (processValue && processValue !== expanded[key] ? processValue : expandValue(expanded[key], processEnv, expanded)).replace(/\\\$/g, "$");
64
+ }
65
+ return expanded;
66
+ }
67
+ function expandValue(value, processEnv, parsed) {
68
+ const env = {
64
69
  ...parsed,
65
70
  ...processEnv
66
71
  };
67
- function resolveValue(key) {
68
- const cached = expanded[key];
69
- if (cached !== void 0) return cached;
70
- if (resolving.has(key)) return context[key] ?? "";
71
- const raw = context[key];
72
- if (raw === void 0) return "";
73
- resolving.add(key);
74
- let value = raw.replace(ENV_REF_RE, (match, escaped, braced, bare) => {
75
- if (escaped) return match.slice(1);
76
- return resolveValue(braced || bare);
77
- });
78
- value = value.replace(/\\\$/g, "$");
79
- resolving.delete(key);
80
- expanded[key] = value;
81
- context[key] = value;
82
- return value;
72
+ const envRefRe = /(?<!\\)\${([^{}]+)}|(?<!\\)\$([A-Za-z_][A-Za-z0-9_]*)/g;
73
+ const seen = /* @__PURE__ */ new Set();
74
+ let result = value;
75
+ let match;
76
+ while ((match = envRefRe.exec(result)) !== null) {
77
+ seen.add(result);
78
+ const [template, braced, bare] = match;
79
+ const expression = braced || bare;
80
+ const operator = expression.match(/(:\+|\+|:-|-)/)?.[0];
81
+ const parts = operator ? expression.split(operator) : [expression];
82
+ const refKey = parts.shift();
83
+ const operand = parts.join(operator ?? "");
84
+ const refValue = env[refKey];
85
+ let replacement;
86
+ if (operator === ":+" || operator === "+") replacement = refValue ? operand : "";
87
+ else if (refValue) replacement = seen.has(refValue) ? operand : refValue;
88
+ else replacement = operand;
89
+ result = result.replace(template, replacement);
90
+ if (result === parsed[refKey]) break;
91
+ envRefRe.lastIndex = 0;
83
92
  }
84
- for (const key of Object.keys(parsed)) resolveValue(key);
85
- return expanded;
93
+ return result;
86
94
  }
87
95
  //#endregion
88
96
  export { getDotenvFiles, loadDotenv };
@@ -99,8 +99,13 @@ type NextConfig = {
99
99
  * `basePath` affects route URLs; `assetPrefix` only affects asset URLs.
100
100
  * @see https://nextjs.org/docs/app/api-reference/config/next-config-js/assetPrefix
101
101
  */
102
- assetPrefix?: string; /** Whether to add trailing slashes */
103
- trailingSlash?: boolean; /** Internationalization routing config */
102
+ assetPrefix?: string; /** Cross-origin mode applied to framework scripts and preload links. */
103
+ crossOrigin?: "anonymous" | "use-credentials"; /** Whether to add trailing slashes */
104
+ trailingSlash?: boolean; /** TypeScript build settings. */
105
+ typescript?: {
106
+ /** Project-relative path to the TypeScript configuration file. */tsconfigPath?: string;
107
+ [key: string]: unknown;
108
+ }; /** Internationalization routing config */
104
109
  i18n?: NextI18nConfig; /** URL redirect rules */
105
110
  redirects?: () => Promise<NextRedirect[]> | NextRedirect[]; /** URL rewrite rules */
106
111
  rewrites?: () => Promise<NextRewrite[] | {
@@ -217,7 +222,8 @@ type NextConfig = {
217
222
  prefetchInlining?: boolean | {
218
223
  maxBundleSize?: number;
219
224
  maxSize?: number;
220
- };
225
+ }; /** Header names forwarded by Pages Router `res.revalidate()` internal requests. */
226
+ allowedRevalidateHeaderKeys?: string[];
221
227
  [key: string]: unknown;
222
228
  };
223
229
  /**
@@ -266,6 +272,9 @@ type ResolvedNextConfig = {
266
272
  */
267
273
  assetPrefix: string;
268
274
  trailingSlash: boolean;
275
+ typescript: {
276
+ tsconfigPath?: string;
277
+ };
269
278
  output: "" | "export" | "standalone";
270
279
  pageExtensions: string[];
271
280
  resolveExtensions: string[] | null;
@@ -295,7 +304,8 @@ type ResolvedNextConfig = {
295
304
  mdx: MdxOptions | null; /** Explicit module aliases preserved from wrapped next.config plugins. */
296
305
  aliases: Record<string, string>; /** Extra allowed origins for dev server access (from allowedDevOrigins). */
297
306
  allowedDevOrigins: string[]; /** Extra allowed origins for server action CSRF validation (from experimental.serverActions.allowedOrigins). */
298
- serverActionsAllowedOrigins: string[]; /** Packages whose barrel imports should be optimized (from experimental.optimizePackageImports). */
307
+ serverActionsAllowedOrigins: string[]; /** Header names forwarded by Pages Router `res.revalidate()` internal requests. */
308
+ allowedRevalidateHeaderKeys: string[]; /** Packages whose barrel imports should be optimized (from experimental.optimizePackageImports). */
299
309
  optimizePackageImports: string[]; /** Packages explicitly requested for server/client transpilation. */
300
310
  transpilePackages: string[]; /** Packages treated as application code by Turbopack's foreign-code condition. */
301
311
  turbopackTranspilePackages: string[]; /** Inline app CSS into production HTML (from experimental.inlineCss). */
@@ -373,7 +383,8 @@ type ResolvedNextConfig = {
373
383
  * `defer={!disableOptimizedLoading}`) and the upstream
374
384
  * `test/e2e/optimized-loading` test fixture.
375
385
  */
376
- disableOptimizedLoading: boolean;
386
+ disableOptimizedLoading: boolean; /** Cross-origin mode applied to framework scripts and preload links. */
387
+ crossOrigin: "anonymous" | "use-credentials" | undefined;
377
388
  /**
378
389
  * Resolved `reactStrictMode` from next.config, preserved as `boolean | null`
379
390
  * so each router can apply its own default (Next.js resolves `null` to OFF
@@ -658,6 +658,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
658
658
  basePath: "",
659
659
  assetPrefix: "",
660
660
  trailingSlash: false,
661
+ typescript: {},
661
662
  output: "",
662
663
  pageExtensions: normalizePageExtensions(),
663
664
  resolveExtensions: null,
@@ -679,6 +680,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
679
680
  aliases: {},
680
681
  allowedDevOrigins: [],
681
682
  serverActionsAllowedOrigins: [],
683
+ allowedRevalidateHeaderKeys: [],
682
684
  optimizePackageImports: [],
683
685
  transpilePackages: [],
684
686
  turbopackTranspilePackages: [...DEFAULT_TRANSPILED_PACKAGES],
@@ -700,6 +702,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
700
702
  sassOptions: null,
701
703
  removeConsole: false,
702
704
  disableOptimizedLoading: false,
705
+ crossOrigin: void 0,
703
706
  reactStrictMode: null,
704
707
  scrollRestoration: false,
705
708
  compilerDefine: {},
@@ -716,6 +719,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
716
719
  detectNextIntlConfig(root, resolved);
717
720
  return resolved;
718
721
  }
722
+ if (config.crossOrigin !== void 0 && config.crossOrigin !== "anonymous" && config.crossOrigin !== "use-credentials") console.warn("Invalid next.config options detected:\n Invalid option at \"crossOrigin\": expected \"anonymous\" or \"use-credentials\"\nSee more info here: https://nextjs.org/docs/messages/invalid-next-config");
719
723
  warnDeprecatedConfigOptions(config, root);
720
724
  const i18n = normalizeI18nConfig(config.i18n);
721
725
  let redirects = [];
@@ -833,6 +837,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
833
837
  basePath: config.basePath ?? "",
834
838
  assetPrefix: normalizeAssetPrefix(config.assetPrefix),
835
839
  trailingSlash: config.trailingSlash ?? false,
840
+ typescript: typeof config.typescript?.tsconfigPath === "string" ? { tsconfigPath: config.typescript.tsconfigPath } : {},
836
841
  output: output === "export" || output === "standalone" ? output : "",
837
842
  pageExtensions,
838
843
  resolveExtensions: resolveExtensions ?? webpackProbe.resolveExtensions,
@@ -851,6 +856,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
851
856
  aliases,
852
857
  allowedDevOrigins,
853
858
  serverActionsAllowedOrigins,
859
+ allowedRevalidateHeaderKeys: Array.isArray(experimental?.allowedRevalidateHeaderKeys) ? experimental.allowedRevalidateHeaderKeys.filter((value) => typeof value === "string").map((value) => value.toLowerCase()) : [],
854
860
  optimizePackageImports,
855
861
  transpilePackages,
856
862
  turbopackTranspilePackages,
@@ -872,6 +878,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
872
878
  sassOptions: readOptionalRecord(config.sassOptions) ?? null,
873
879
  removeConsole: config.compiler?.removeConsole === true ? true : isUnknownRecord(config.compiler?.removeConsole) ? { exclude: readStringArray(config.compiler.removeConsole.exclude) } : false,
874
880
  disableOptimizedLoading: experimental?.disableOptimizedLoading === true,
881
+ crossOrigin: config.crossOrigin === "anonymous" || config.crossOrigin === "use-credentials" ? config.crossOrigin : void 0,
875
882
  reactStrictMode: typeof config.reactStrictMode === "boolean" ? config.reactStrictMode : null,
876
883
  scrollRestoration: experimental?.scrollRestoration === true,
877
884
  compilerDefine: serializeCompilerDefine(config.compiler?.define),
@@ -15,15 +15,9 @@ declare function generateBrowserEntry(routes?: readonly AppRoute[], routeManifes
15
15
  beforeFiles: NextRewrite[];
16
16
  fallback: NextRewrite[];
17
17
  }): string;
18
- /**
19
- * Filter for routes that should appear in the `__VINEXT_LINK_PREFETCH_ROUTES__`
20
- * manifest. Exported so the Pages Router client entry can reuse it when
21
- * emitting the same manifest for hybrid builds — see issue #1526 and
22
- * `pages-client-entry.ts`.
23
- */
24
- declare function isLinkPrefetchRoute(route: AppRoute): boolean;
25
- declare function toDocumentOnlyAppRoute(route: AppRoute): VinextLinkPrefetchRoute;
26
18
  /** Project an `AppRoute` down to the public `VinextLinkPrefetchRoute` shape. */
27
- declare function toLinkPrefetchRoute(route: AppRoute): VinextLinkPrefetchRoute;
19
+ declare function toLinkPrefetchRoute(route: AppRoute, hasSiblingInterceptLoading?: boolean): VinextLinkPrefetchRoute;
20
+ /** Project App routes together so sibling-intercept loading is applied to its target route. */
21
+ declare function toLinkPrefetchRoutes(routes: readonly AppRoute[]): VinextLinkPrefetchRoute[];
28
22
  //#endregion
29
- export { generateBrowserEntry, isLinkPrefetchRoute, toDocumentOnlyAppRoute, toLinkPrefetchRoute };
23
+ export { generateBrowserEntry, toLinkPrefetchRoute, toLinkPrefetchRoutes };
@@ -1,4 +1,5 @@
1
1
  import { resolveClientRuntimeModule, resolveRuntimeEntryModule } from "./runtime-entry-module.js";
2
+ import { patternsStructurallyEquivalent } from "../routing/app-route-graph.js";
2
3
  //#region src/entries/app-browser-entry.ts
3
4
  /**
4
5
  * Generate the virtual browser entry module.
@@ -14,7 +15,7 @@ function generateBrowserEntry(routes = [], routeManifest = null, pagesPrefetchRo
14
15
  }) {
15
16
  const entryPath = resolveRuntimeEntryModule("app-browser-entry");
16
17
  const navigationRuntimePath = resolveClientRuntimeModule("navigation-runtime");
17
- const prefetchRoutes = routes.map((route) => isLinkPrefetchRoute(route) ? toLinkPrefetchRoute(route) : toDocumentOnlyAppRoute(route));
18
+ const prefetchRoutes = toLinkPrefetchRoutes(routes);
18
19
  return `import { registerNavigationRuntimeBootstrap } from ${JSON.stringify(navigationRuntimePath)};
19
20
 
20
21
  window.__VINEXT_LINK_PREFETCH_ROUTES__ = ${JSON.stringify(prefetchRoutes)};
@@ -50,15 +51,30 @@ function toDocumentOnlyAppRoute(route) {
50
51
  function requiresDynamicNavigationRequest(route) {
51
52
  return route.isDynamic && route.parallelSlots.length > 0;
52
53
  }
54
+ function splitPatternParts(pattern) {
55
+ return pattern.split("/").filter(Boolean);
56
+ }
57
+ function interceptTargetsRoute(interceptTargetPattern, route) {
58
+ return patternsStructurallyEquivalent(splitPatternParts(interceptTargetPattern), route.patternParts);
59
+ }
60
+ function hasLoadingBoundary(route, hasSiblingInterceptLoading) {
61
+ return route.loadingPath !== null || (route.loadingPaths?.some((_, index) => (route.loadingTreePositions?.[index] ?? 0) > 0) ?? false) || route.parallelSlots.some((slot) => slot.loadingPath !== null || (slot.loadingPaths?.length ?? 0) > 0 || slot.interceptingRoutes.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) || hasSiblingInterceptLoading;
62
+ }
53
63
  /** Project an `AppRoute` down to the public `VinextLinkPrefetchRoute` shape. */
54
- function toLinkPrefetchRoute(route) {
64
+ function toLinkPrefetchRoute(route, hasSiblingInterceptLoading = route.siblingIntercepts.some((intercept) => interceptTargetsRoute(intercept.targetPattern, route) && (intercept.loadingPaths?.length ?? 0) > 0)) {
55
65
  return {
56
- canPrefetchLoadingShell: route.loadingPath !== null,
66
+ canPrefetchLoadingShell: hasLoadingBoundary(route, hasSiblingInterceptLoading),
57
67
  patternParts: [...route.patternParts],
58
68
  isDynamic: route.isDynamic,
59
69
  ...requiresDynamicNavigationRequest(route) ? { requiresDynamicNavigationRequest: true } : {}
60
70
  };
61
71
  }
72
+ /** Project App routes together so sibling-intercept loading is applied to its target route. */
73
+ function toLinkPrefetchRoutes(routes) {
74
+ const siblingInterceptLoadingTargets = [];
75
+ for (const route of routes) for (const intercept of route.siblingIntercepts) if ((intercept.loadingPaths?.length ?? 0) > 0) siblingInterceptLoadingTargets.push(splitPatternParts(intercept.targetPattern));
76
+ return routes.map((route) => isLinkPrefetchRoute(route) ? toLinkPrefetchRoute(route, siblingInterceptLoadingTargets.some((targetParts) => patternsStructurallyEquivalent(targetParts, route.patternParts))) : toDocumentOnlyAppRoute(route));
77
+ }
62
78
  function buildRouteManifestExpression(routeManifest) {
63
79
  if (routeManifest === null) return "null";
64
80
  const graph = routeManifest.segmentGraph;
@@ -84,4 +100,4 @@ function buildMapExpression(map) {
84
100
  return `new Map(${JSON.stringify(Array.from(map.entries()))})`;
85
101
  }
86
102
  //#endregion
87
- export { generateBrowserEntry, isLinkPrefetchRoute, toDocumentOnlyAppRoute, toLinkPrefetchRoute };
103
+ export { generateBrowserEntry, toLinkPrefetchRoute, toLinkPrefetchRoutes };