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
@@ -1,21 +1,42 @@
1
1
  import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { findDir } from "../utils/project.js";
3
- import { apiRouter, pagesRouter } from "../routing/pages-router.js";
3
+ import { apiRouter, matchRoute, pagesRouter } from "../routing/pages-router.js";
4
4
  import { VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
5
- import { classifyAppRoute, classifyPagesRoute, getAppRouteRenderEntryPath, hasNamedExport } from "./report.js";
6
- import { appRouter } from "../routing/app-router.js";
7
- import { normalizeStaticPathsEntry } from "../routing/route-pattern.js";
5
+ import "../server/headers.js";
6
+ import { matchHeaders, matchesRewriteSource } from "../config/config-matchers.js";
7
+ import { extractLocaleFromUrl, normalizeDefaultLocalePathname } from "../server/pages-i18n.js";
8
+ import { classifyAppRoute, classifyAppRouteHandler, classifyPagesRoute, extractExportConstString, getAppRouteRenderEntryPath } from "./report.js";
9
+ import { appRouteHasMainTreeLoadingBoundary, appRouter, matchAppRoute } from "../routing/app-router.js";
10
+ import { normalizeStaticPathname, normalizeStaticPathsEntry } from "../routing/route-pattern.js";
11
+ import { buildPagesDataHref } from "../shims/internal/pages-data-url.js";
12
+ import { normalizePathTrailingSlash } from "../shims/url-utils.js";
8
13
  import { BLOCKED_PAGES, PHASE_PRODUCTION_BUILD } from "../shims/constants.js";
9
14
  import { loadNextConfig, resolveNextConfig } from "../config/next-config.js";
15
+ import { CACHEABILITY_POLICY_HEADERS } from "../shims/cacheability-classification.js";
16
+ import { pagesRouteHasPriorityOverAppRoute } from "../server/hybrid-route-priority.js";
10
17
  import { readPrerenderSecret } from "./server-manifest.js";
11
18
  import { startProdServer } from "../server/prod-server.js";
12
19
  import { enterPrerenderPhase } from "./prerender-phase.js";
13
20
  import { buildUrlFromParams, resolveParentParams } from "./prerender.js";
21
+ import { resolveAppPageDynamicConfig } from "../server/app-segment-config.js";
14
22
  import fs from "node:fs";
15
23
  //#region src/build/prerender-paths.ts
16
24
  const PRERENDER_PATH_DISCOVERY_ENV = "__VINEXT_PRERENDER_PATH_DISCOVERY";
17
25
  const PRERENDER_PATHS_MANIFEST = "vinext-prerender-paths.json";
18
26
  const PATH_DISCOVERY_FETCH_TIMEOUT_MS = 3e4;
27
+ const DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS = 1e3;
28
+ const DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS = 12e4;
29
+ function readDiscoveryUserFailure(response, text) {
30
+ if (response.status !== 500 || !response.headers.get("content-type")?.includes("application/json")) return null;
31
+ try {
32
+ const value = JSON.parse(text);
33
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
34
+ const error = value.error;
35
+ return typeof error === "string" && error.length > 0 ? error : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
19
40
  function readBuiltBuildId(serverDir) {
20
41
  try {
21
42
  const buildId = fs.readFileSync(path.join(serverDir, "BUILD_ID"), "utf-8").trim();
@@ -25,40 +46,153 @@ function readBuiltBuildId(serverDir) {
25
46
  throw error;
26
47
  }
27
48
  }
49
+ function readBuiltRscBuildId(serverDir) {
50
+ try {
51
+ const buildId = fs.readFileSync(path.join(serverDir, "RSC_BUILD_ID"), "utf-8").trim();
52
+ return buildId.length > 0 ? buildId : null;
53
+ } catch (error) {
54
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return null;
55
+ throw error;
56
+ }
57
+ }
28
58
  function addPath(paths, seen, pathname) {
29
59
  if (seen.has(pathname)) return;
30
60
  seen.add(pathname);
31
61
  paths.push(pathname);
32
62
  }
33
- function warnDiscoveryFailure(route, error) {
63
+ function throwDiscoveryFailure(route, error) {
34
64
  const message = error instanceof Error ? error.message : String(error);
35
- console.warn(`[vinext] Warning: failed to discover warmup path(s) for ${route}: ${message}`);
65
+ if (/cloudflare:|ERR_UNSUPPORTED_ESM_URL_SCHEME/i.test(message)) throw new Error(`Failed to discover warmup path(s) for ${route}: Cloudflare runtime bindings cannot execute in the local Node prerender server. Use \`vinext-cloudflare deploy --experimental-warm-cdn-cache\` so path discovery runs against the staged Worker version.`, { cause: error });
66
+ throw new Error(`Failed to discover warmup path(s) for ${route}: ${message}`, { cause: error });
36
67
  }
37
- async function fetchDiscoveryEndpoint(url, headers) {
38
- const controller = new AbortController();
39
- const timeout = setTimeout(() => controller.abort(), PATH_DISCOVERY_FETCH_TIMEOUT_MS);
40
- try {
41
- const res = await fetch(url, {
42
- headers,
43
- signal: controller.signal
44
- });
45
- const text = await res.text();
46
- if (!res.ok || text === "null") return null;
47
- return text;
48
- } catch (error) {
49
- if (error instanceof Error && error.name === "AbortError") throw new Error(`path discovery timed out after ${PATH_DISCOVERY_FETCH_TIMEOUT_MS}ms`);
50
- throw error;
51
- } finally {
52
- clearTimeout(timeout);
68
+ function validatePagesStaticPathsResult(value, route) {
69
+ const expected = "Expected { paths: [], fallback: boolean | 'blocking' }.";
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid value returned from getStaticPaths for ${route}. ${expected}`);
71
+ const result = value;
72
+ const extraKeys = Object.keys(result).filter((key) => key !== "paths" && key !== "fallback");
73
+ if (extraKeys.length > 0) throw new Error(`Extra key(s) returned from getStaticPaths for ${route}: ${extraKeys.join(", ")}. ${expected}`);
74
+ if (typeof result.fallback !== "boolean" && result.fallback !== "blocking") throw new Error(`Invalid fallback returned from getStaticPaths for ${route}. ${expected}`);
75
+ if (!Array.isArray(result.paths)) throw new Error(`Invalid paths returned from getStaticPaths for ${route}; paths must be an array.`);
76
+ return {
77
+ fallback: result.fallback,
78
+ paths: result.paths
79
+ };
80
+ }
81
+ function hasUnsafeRawUrlPathCharacter(value) {
82
+ for (let index = 0; index < value.length; index++) {
83
+ const code = value.charCodeAt(index);
84
+ if (code === 92 || code <= 31 || code === 127) return true;
53
85
  }
86
+ return false;
54
87
  }
55
- function fileHasNamedExport(filePath, name) {
56
- if (!filePath) return false;
57
- try {
58
- return hasNamedExport(fs.readFileSync(filePath, "utf-8"), name);
59
- } catch {
60
- return false;
88
+ function getDynamicPatternParams(pattern) {
89
+ return pattern.split("/").filter((segment) => segment.startsWith(":")).map((segment) => ({
90
+ name: segment.slice(1, segment.endsWith("+") || segment.endsWith("*") ? -1 : void 0),
91
+ optional: segment.endsWith("*"),
92
+ repeat: segment.endsWith("+") || segment.endsWith("*")
93
+ }));
94
+ }
95
+ function validateDiscoveredParams(value, pattern, source) {
96
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${source} must return parameter objects for ${pattern}.`);
97
+ const params = { ...value };
98
+ for (const { name, optional, repeat } of getDynamicPatternParams(pattern)) {
99
+ const hasValue = Object.prototype.hasOwnProperty.call(params, name);
100
+ let paramValue = params[name];
101
+ if (optional && hasValue && (paramValue === null || paramValue === void 0 || paramValue === false)) {
102
+ paramValue = [];
103
+ params[name] = paramValue;
104
+ }
105
+ if (!(repeat ? Array.isArray(paramValue) && paramValue.every((entry) => typeof entry === "string") : typeof paramValue === "string")) throw new Error(`Parameter ${name} from ${source} for ${pattern} must be ${repeat ? "an array of strings" : "a string"}.`);
106
+ if ((Array.isArray(paramValue) ? paramValue : [paramValue]).some((entry) => entry === "." || entry === "..")) throw new Error(`Parameter ${name} from ${source} for ${pattern} must not contain dot path segments.`);
107
+ }
108
+ return params;
109
+ }
110
+ function validatePagesStaticPathsEntry(entry, pattern) {
111
+ if (typeof entry === "string") {
112
+ if (!entry.startsWith("/") || entry.includes("//") || entry.includes("?") || entry.includes("#") || hasUnsafeRawUrlPathCharacter(entry)) throw new Error(`The provided path \`${entry}\` from getStaticPaths does not match the route pattern \`${pattern}\`.`);
113
+ let decodedSegments;
114
+ try {
115
+ decodedSegments = entry.split("/").map((segment) => decodeURIComponent(segment));
116
+ } catch {
117
+ throw new Error(`The provided path \`${entry}\` from getStaticPaths contains malformed percent-encoding.`);
118
+ }
119
+ if (decodedSegments.some((segment) => segment === "." || segment === "..")) throw new Error(`The provided path \`${entry}\` from getStaticPaths contains a dot path segment.`);
120
+ return entry;
61
121
  }
122
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return entry;
123
+ const extraKeys = Object.keys(entry).filter((key) => key !== "params" && key !== "locale");
124
+ if (extraKeys.length > 0) throw new Error(`Additional key(s) returned from getStaticPaths for ${pattern}: ${extraKeys.join(", ")}.`);
125
+ if (entry.locale !== void 0 && typeof entry.locale !== "string") throw new Error(`Invalid locale returned from getStaticPaths for ${pattern}.`);
126
+ return {
127
+ ...entry,
128
+ params: validateDiscoveredParams(entry.params, pattern, "getStaticPaths")
129
+ };
130
+ }
131
+ async function fetchDiscoveryEndpoint(url, headers, retryOptions = {}) {
132
+ const hasSharedPhaseDeadline = retryOptions.deadlineAt !== void 0;
133
+ const retryDelayMs = Math.max(0, retryOptions.retryDelayMs ?? (hasSharedPhaseDeadline ? 1e3 : 0));
134
+ const phaseTimeoutMs = Math.max(1, retryOptions.phaseTimeoutMs ?? 12e4);
135
+ const retryBudgetDelayMs = retryDelayMs > 0 ? retryDelayMs : DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS;
136
+ const retries = Math.max(0, retryOptions.retries ?? (hasSharedPhaseDeadline ? Math.ceil(phaseTimeoutMs / retryBudgetDelayMs) : 0));
137
+ const deadlineAt = retryOptions.deadlineAt ?? Date.now() + phaseTimeoutMs;
138
+ let lastError;
139
+ let attemptsRun = 0;
140
+ let lastTransientStatus;
141
+ const attemptSummary = () => ` after ${attemptsRun} attempt(s)${lastTransientStatus === void 0 ? "" : `; last transient status was HTTP ${lastTransientStatus}`}`;
142
+ const phaseTimeoutError = () => /* @__PURE__ */ new Error(`remote path discovery exceeded its ${phaseTimeoutMs}ms phase deadline${attemptSummary()}`);
143
+ for (let attempt = 0; attempt <= retries; attempt++) {
144
+ const remainingMs = deadlineAt - Date.now();
145
+ if (remainingMs <= 0) throw phaseTimeoutError();
146
+ attemptsRun++;
147
+ const controller = new AbortController();
148
+ const attemptTimeoutMs = Math.min(PATH_DISCOVERY_FETCH_TIMEOUT_MS, remainingMs);
149
+ let timeout;
150
+ let shouldRetry = true;
151
+ try {
152
+ const request = (async () => {
153
+ const res = await fetch(url, {
154
+ headers,
155
+ signal: controller.signal
156
+ });
157
+ return {
158
+ res,
159
+ text: await res.text()
160
+ };
161
+ })();
162
+ const timedOut = new Promise((_resolve, reject) => {
163
+ timeout = setTimeout(() => {
164
+ controller.abort();
165
+ reject(new DOMException(`Timed out after ${attemptTimeoutMs}ms`, "AbortError"));
166
+ }, attemptTimeoutMs);
167
+ });
168
+ const { res, text } = await Promise.race([request, timedOut]);
169
+ if (res.ok) {
170
+ if (res.status === 204) return null;
171
+ return text;
172
+ }
173
+ const userFailure = readDiscoveryUserFailure(res, text);
174
+ const detail = userFailure ?? (/cloudflare:|ERR_UNSUPPORTED_ESM_URL_SCHEME/i.test(text) ? text.trim() : "");
175
+ lastError = /* @__PURE__ */ new Error(`path discovery returned HTTP ${res.status}${detail ? `: ${detail}` : ""}`);
176
+ if (!(res.status === 404 || res.status >= 500 && res.status <= 599 && userFailure === null)) {
177
+ shouldRetry = false;
178
+ throw lastError;
179
+ }
180
+ lastTransientStatus = res.status;
181
+ } catch (error) {
182
+ if (Date.now() >= deadlineAt) throw phaseTimeoutError();
183
+ lastError = error instanceof Error && error.name === "AbortError" ? /* @__PURE__ */ new Error(`path discovery timed out after ${attemptTimeoutMs}ms`) : error;
184
+ if (!shouldRetry) throw lastError;
185
+ } finally {
186
+ if (timeout !== void 0) clearTimeout(timeout);
187
+ }
188
+ if (attempt < retries && retryDelayMs > 0) {
189
+ const delayMs = Math.min(retryDelayMs, Math.max(0, deadlineAt - Date.now()));
190
+ if (delayMs <= 0) throw phaseTimeoutError();
191
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
192
+ }
193
+ }
194
+ const message = lastError instanceof Error ? lastError.message : String(lastError);
195
+ throw new Error(`${message}${attemptSummary()}`, { cause: lastError });
62
196
  }
63
197
  function resolveConfiguredRouteDirs(root, routeRootConfig) {
64
198
  if (!routeRootConfig) return {
@@ -83,16 +217,12 @@ function resolveConfiguredRouteDirs(root, routeRootConfig) {
83
217
  pagesDir: fs.existsSync(pagesDir) ? pagesDir : null
84
218
  };
85
219
  }
86
- function appRouteMayHaveGenerateStaticParams(route) {
87
- if (fileHasNamedExport(route.pagePath, "generateStaticParams")) return true;
88
- return route.layouts.some((layoutPath) => fileHasNamedExport(layoutPath, "generateStaticParams"));
89
- }
90
220
  async function shouldStartPathDiscoveryServer(options) {
91
221
  if (options.appDir) {
92
- if ((await appRouter(options.appDir, options.pageExtensions)).some((route) => route.isDynamic && appRouteMayHaveGenerateStaticParams(route))) return true;
222
+ if ((await appRouter(options.appDir, options.pageExtensions)).some((route) => route.isDynamic)) return true;
93
223
  }
94
224
  if (options.pagesDir) {
95
- if ((await pagesRouter(options.pagesDir, options.pageExtensions)).some((route) => route.isDynamic && fileHasNamedExport(route.filePath, "getStaticPaths"))) return true;
225
+ if ((await pagesRouter(options.pagesDir, options.pageExtensions)).some((route) => route.isDynamic)) return true;
96
226
  }
97
227
  return false;
98
228
  }
@@ -113,56 +243,154 @@ async function collectPagesPaths(options) {
113
243
  const apiPatterns = new Set(apiRoutes.map((route) => route.pattern));
114
244
  const paths = [];
115
245
  const seen = /* @__PURE__ */ new Set();
246
+ const dataPaths = [];
247
+ const seenDataPaths = /* @__PURE__ */ new Set();
248
+ const fallbackRoutePatterns = [];
116
249
  for (const route of pageRoutes) {
117
250
  if (apiPatterns.has(route.pattern)) continue;
118
251
  if (BLOCKED_PAGES.includes(route.pattern)) continue;
119
252
  if (route.pattern === "/404" || route.pattern === "/500" || route.pattern === "/_error") continue;
120
- const { type } = classifyPagesRoute(route.filePath);
121
- if (type === "api" || type === "ssr") continue;
253
+ const { hasServerSideProps, hasStaticProps, type } = classifyPagesRoute(route.filePath);
254
+ if (type === "api") continue;
122
255
  if (!route.isDynamic) {
123
- addPath(paths, seen, route.pattern);
256
+ if (options.i18n) for (const locale of options.i18n.locales) {
257
+ const pathname = localizePagesPath(route.pattern, locale, options.i18n);
258
+ addPath(paths, seen, pathname);
259
+ if (hasStaticProps || hasServerSideProps) addPath(dataPaths, seenDataPaths, pathname);
260
+ }
261
+ else {
262
+ addPath(paths, seen, route.pattern);
263
+ if (hasStaticProps || hasServerSideProps) addPath(dataPaths, seenDataPaths, route.pattern);
264
+ }
124
265
  continue;
125
266
  }
126
- if (!fileHasNamedExport(route.filePath, "getStaticPaths")) continue;
267
+ if (type === "ssr") continue;
127
268
  if (!options.baseUrl) continue;
128
269
  try {
129
270
  const search = new URLSearchParams({ pattern: route.pattern });
130
- const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/pages-static-paths?${search}`, options.secretHeaders);
271
+ if (options.i18n) {
272
+ search.set("locales", JSON.stringify(options.i18n.locales));
273
+ search.set("defaultLocale", options.i18n.defaultLocale);
274
+ }
275
+ const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/pages-static-paths?${search}`, options.secretHeaders, options.retryOptions);
131
276
  if (text === null) continue;
132
- const pathsResult = JSON.parse(text);
133
- for (const item of pathsResult.paths ?? []) {
134
- const normalized = normalizeStaticPathsEntry(item, route.pattern);
277
+ const pathsResult = validatePagesStaticPathsResult(JSON.parse(text), route.pattern);
278
+ if (pathsResult.fallback !== false) fallbackRoutePatterns.push({
279
+ kind: "pages-page",
280
+ pattern: route.pattern
281
+ });
282
+ for (const item of pathsResult.paths) {
283
+ const validatedItem = validatePagesStaticPathsEntry(item, route.pattern);
284
+ let itemToNormalize = validatedItem;
285
+ let locale = options.i18n?.defaultLocale;
286
+ if (options.i18n && typeof validatedItem === "string") itemToNormalize = extractPagesStaticPathLocale(validatedItem, options.i18n).url;
287
+ else if (options.i18n && validatedItem && typeof validatedItem === "object" && validatedItem.locale) {
288
+ if (!options.i18n.locales.includes(validatedItem.locale)) throw new Error(`Invalid locale returned from getStaticPaths for ${route.pattern}: ${validatedItem.locale}`);
289
+ locale = validatedItem.locale;
290
+ }
291
+ const normalized = normalizeStaticPathsEntry(itemToNormalize, route.pattern);
135
292
  if ("error" in normalized) throw new Error(normalized.error);
136
- addPath(paths, seen, buildUrlFromParams(route.pattern, normalized.params));
293
+ const pathname = typeof validatedItem === "string" ? normalizeStaticPathname(validatedItem) : localizePagesPath(buildUrlFromParams(route.pattern, normalized.params), locale, options.i18n);
294
+ addPath(paths, seen, pathname);
295
+ if (hasStaticProps) addPath(dataPaths, seenDataPaths, pathname);
137
296
  }
138
297
  } catch (error) {
139
- warnDiscoveryFailure(route.pattern, error);
298
+ throwDiscoveryFailure(route.pattern, error);
140
299
  }
141
300
  }
142
- return paths;
301
+ return {
302
+ dataPaths,
303
+ fallbackRoutePatterns,
304
+ paths
305
+ };
306
+ }
307
+ async function excludePagesApiWarmPaths(options) {
308
+ const apiRoutes = await apiRouter(options.pagesDir, options.pageExtensions);
309
+ return options.paths.filter((pathname) => {
310
+ const pagesPathname = options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname;
311
+ if (pagesPathname !== "/api" && !pagesPathname.startsWith("/api/")) return true;
312
+ return matchRoute(pagesPathname, apiRoutes) === null;
313
+ });
314
+ }
315
+ async function resolvePagesWarmRoutePatterns(options) {
316
+ const pageRoutes = await pagesRouter(options.pagesDir, options.pageExtensions);
317
+ return Object.fromEntries(options.paths.flatMap((pathname) => {
318
+ const match = matchRoute(options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname, pageRoutes);
319
+ return match ? [[pathname, {
320
+ kind: "pages-page",
321
+ pattern: match.route.pattern
322
+ }]] : [];
323
+ }));
324
+ }
325
+ function localizePagesPath(pathname, locale, i18n) {
326
+ if (!i18n || !locale || locale === i18n.defaultLocale) return pathname;
327
+ return pathname === "/" ? `/${locale}` : `/${locale}${pathname}`;
328
+ }
329
+ /**
330
+ * Next.js data URLs always carry the locale segment, including the default
331
+ * locale that is omitted from the corresponding public HTML pathname.
332
+ *
333
+ * Ported from Next.js:
334
+ * packages/next/src/shared/lib/router/utils/format-next-pathname-info.ts
335
+ */
336
+ function localizePagesDataPath(pathname, i18n) {
337
+ if (!i18n) return pathname;
338
+ if (extractPagesStaticPathLocale(pathname, i18n).explicitLocalePrefix) return pathname;
339
+ return pathname === "/" ? `/${i18n.defaultLocale}` : `/${i18n.defaultLocale}${pathname}`;
340
+ }
341
+ function extractPagesStaticPathLocale(url, i18n) {
342
+ const queryIndex = url.indexOf("?");
343
+ const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);
344
+ const query = queryIndex === -1 ? "" : url.slice(queryIndex);
345
+ const parts = pathname.split("/").filter(Boolean);
346
+ const locale = parts.length > 0 ? i18n.locales.find((candidate) => candidate.toLowerCase() === parts[0].toLowerCase()) : void 0;
347
+ if (!locale) return extractLocaleFromUrl(url, i18n);
348
+ const rest = `/${parts.slice(1).join("/")}`;
349
+ return {
350
+ explicitLocalePrefix: parts[0],
351
+ locale,
352
+ url: `${rest || "/"}${query}`
353
+ };
143
354
  }
144
355
  async function collectAppPaths(options) {
145
356
  const routes = await appRouter(options.appDir, options.pageExtensions);
146
357
  const paths = [];
147
358
  const seen = /* @__PURE__ */ new Set();
359
+ const loadingShellPaths = [];
360
+ const seenLoadingShellPaths = /* @__PURE__ */ new Set();
361
+ const routeHandlerPaths = [];
362
+ const seenRouteHandlerPaths = /* @__PURE__ */ new Set();
363
+ const fallbackRoutePatterns = [];
148
364
  const staticParamsCache = /* @__PURE__ */ new Map();
365
+ let requireNonEmptyStaticParams = false;
149
366
  const staticParamsMap = new Proxy({}, {
150
367
  get(_target, pattern) {
151
368
  return async ({ params }) => {
152
369
  if (!options.baseUrl) return null;
153
370
  const cacheKey = `${pattern}\0${JSON.stringify(params)}`;
154
- const cached = staticParamsCache.get(cacheKey);
155
- if (cached !== void 0) return cached;
156
- const request = (async () => {
157
- const search = new URLSearchParams({ pattern });
158
- if (Object.keys(params).length > 0) search.set("parentParams", JSON.stringify(params));
159
- const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/static-params?${search}`, options.secretHeaders);
160
- if (text === null) return null;
161
- return JSON.parse(text);
162
- })();
163
- request.catch(() => staticParamsCache.delete(cacheKey));
164
- staticParamsCache.set(cacheKey, request);
165
- return request;
371
+ let request = staticParamsCache.get(cacheKey);
372
+ if (request === void 0) {
373
+ request = (async () => {
374
+ const search = new URLSearchParams({ pattern });
375
+ if (Object.keys(params).length > 0) search.set("parentParams", JSON.stringify(params));
376
+ const text = await fetchDiscoveryEndpoint(`${options.baseUrl}/__vinext/prerender/static-params?${search}`, options.secretHeaders, options.retryOptions);
377
+ if (text === null) return null;
378
+ const value = JSON.parse(text);
379
+ if (!Array.isArray(value)) throw new Error(`generateStaticParams must return an array for ${pattern}.`);
380
+ return value.map((entry) => {
381
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`generateStaticParams must return parameter objects for ${pattern}.`);
382
+ return validateDiscoveredParams({
383
+ ...params,
384
+ ...entry
385
+ }, pattern, "generateStaticParams");
386
+ });
387
+ })();
388
+ request.catch(() => staticParamsCache.delete(cacheKey));
389
+ staticParamsCache.set(cacheKey, request);
390
+ }
391
+ const value = await request;
392
+ if (requireNonEmptyStaticParams && value?.length === 0) throw new Error("When using Cache Components, all `generateStaticParams` functions must return at least one result. This is to ensure that we can perform build-time validation that there is no other dynamic accesses that would cause a runtime error.\n\nLearn more: https://nextjs.org/docs/messages/empty-generate-static-params");
393
+ return value;
166
394
  };
167
395
  },
168
396
  has() {
@@ -170,16 +398,30 @@ async function collectAppPaths(options) {
170
398
  }
171
399
  });
172
400
  for (const route of routes) {
173
- const renderEntryPath = getAppRouteRenderEntryPath(route);
401
+ const isRouteHandler = route.routePath !== null && route.pagePath === null;
402
+ const renderEntryPath = isRouteHandler ? route.routePath : getAppRouteRenderEntryPath(route);
174
403
  if (!renderEntryPath) continue;
175
- const { type } = classifyAppRoute(renderEntryPath, route.routePath, route.isDynamic);
176
- if (type === "api") continue;
177
- if (type === "ssr" && !route.isDynamic) continue;
404
+ if (isRouteHandler) {
405
+ const classification = classifyAppRouteHandler(route.routePath);
406
+ if (!classification.hasGet || !classification.staticGenerationEnabled) continue;
407
+ } else {
408
+ const { type } = classifyAppRoute(renderEntryPath, route.routePath, route.isDynamic);
409
+ if (type === "api") continue;
410
+ }
411
+ const hasMainTreeLoadingBoundary = appRouteHasMainTreeLoadingBoundary(route);
412
+ const addDiscoveredPath = (pathname) => {
413
+ if (isRouteHandler) {
414
+ addPath(routeHandlerPaths, seenRouteHandlerPaths, pathname);
415
+ return;
416
+ }
417
+ addPath(paths, seen, pathname);
418
+ if (hasMainTreeLoadingBoundary) addPath(loadingShellPaths, seenLoadingShellPaths, pathname);
419
+ };
178
420
  if (!route.isDynamic) {
179
- addPath(paths, seen, route.pattern);
421
+ addDiscoveredPath(route.pattern);
180
422
  continue;
181
423
  }
182
- if (!appRouteMayHaveGenerateStaticParams(route)) continue;
424
+ requireNonEmptyStaticParams = options.cacheComponents && !isRouteHandler;
183
425
  try {
184
426
  const generateStaticParams = staticParamsMap[route.pattern];
185
427
  if (typeof generateStaticParams !== "function") continue;
@@ -200,18 +442,208 @@ async function collectAppPaths(options) {
200
442
  }
201
443
  } else {
202
444
  const results = await generateStaticParams({ params: {} });
203
- paramSets = Array.isArray(results) || results === null ? results : [];
445
+ if (results === null) {
446
+ const layoutParamSets = await resolveParentParams(route, staticParamsMap, { includeLastDynamicSegment: true });
447
+ paramSets = layoutParamSets.length > 0 ? layoutParamSets : null;
448
+ } else paramSets = Array.isArray(results) ? results : [];
449
+ }
450
+ if (!paramSets?.length) {
451
+ if (isRouteHandler) {
452
+ const dynamicConfig = extractExportConstString(fs.readFileSync(renderEntryPath, "utf8"), "dynamic");
453
+ if (paramSets !== null || dynamicConfig === "force-static" || dynamicConfig === "error") fallbackRoutePatterns.push({
454
+ kind: "app-route",
455
+ pattern: route.pattern
456
+ });
457
+ continue;
458
+ }
459
+ const parallelSegments = route.parallelSlots.flatMap((slot) => [
460
+ slot.layoutPath,
461
+ ...slot.configLayoutPaths ?? [],
462
+ slot.pagePath ?? slot.defaultPath
463
+ ].filter((filePath) => typeof filePath === "string"));
464
+ const hasDynamicSegment = [
465
+ ...route.layouts,
466
+ renderEntryPath,
467
+ ...parallelSegments
468
+ ].map((filePath) => classifyAppRoute(filePath, null, false)).some((classification) => classification.type === "ssr");
469
+ const readDynamicConfig = (filePath) => {
470
+ const dynamic = extractExportConstString(fs.readFileSync(filePath, "utf8"), "dynamic");
471
+ return dynamic === null ? {} : { dynamic };
472
+ };
473
+ const dynamicConfig = resolveAppPageDynamicConfig({
474
+ layouts: route.layouts.map(readDynamicConfig),
475
+ page: readDynamicConfig(renderEntryPath),
476
+ parallelSegments: parallelSegments.map(readDynamicConfig)
477
+ });
478
+ if ((paramSets !== null || dynamicConfig === "force-static" || dynamicConfig === "error") && !hasDynamicSegment) fallbackRoutePatterns.push({
479
+ kind: "app-page",
480
+ pattern: route.pattern
481
+ });
482
+ continue;
204
483
  }
205
- if (!paramSets?.length) continue;
206
484
  for (const params of paramSets) {
207
485
  if (params === null || params === void 0) continue;
208
- addPath(paths, seen, buildUrlFromParams(route.pattern, params));
486
+ addDiscoveredPath(buildUrlFromParams(route.pattern, params));
209
487
  }
210
488
  } catch (error) {
211
- warnDiscoveryFailure(route.pattern, error);
489
+ throwDiscoveryFailure(route.pattern, error);
490
+ }
491
+ }
492
+ return {
493
+ fallbackRoutePatterns,
494
+ loadingShellPaths,
495
+ paths,
496
+ routeHandlerPaths
497
+ };
498
+ }
499
+ async function resolveAppWarmPaths(options) {
500
+ const appRoutes = await appRouter(options.appDir, options.pageExtensions);
501
+ const routeHandlerClassifications = new Map(appRoutes.flatMap((route) => route.routePath && !route.pagePath ? [[route.routePath, classifyAppRouteHandler(route.routePath)]] : []));
502
+ const [pageRoutes, apiRoutes] = options.pagesDir ? await Promise.all([pagesRouter(options.pagesDir, options.pageExtensions), apiRouter(options.pagesDir, options.pageExtensions)]) : [[], []];
503
+ const rscPaths = [];
504
+ const appPaths = [];
505
+ const appRoutePaths = [];
506
+ const htmlPaths = [];
507
+ const loadingShellPaths = [];
508
+ const pagesPaths = [];
509
+ const routePatterns = {};
510
+ for (const pathname of options.paths) {
511
+ const appMatch = matchAppRoute(pathname, appRoutes);
512
+ const pagesPathname = options.i18n ? extractLocaleFromUrl(pathname, options.i18n).url : pathname;
513
+ const isPagesApiRequest = pathname === "/api" || pathname.startsWith("/api/");
514
+ const pagesMatch = matchRoute(pagesPathname, isPagesApiRequest ? apiRoutes : pageRoutes);
515
+ if (pagesMatch && (!appMatch || pagesRouteHasPriorityOverAppRoute(pagesMatch.route, appMatch.route))) {
516
+ if (!isPagesApiRequest) {
517
+ htmlPaths.push(pathname);
518
+ pagesPaths.push(pathname);
519
+ routePatterns[pathname] = {
520
+ kind: "pages-page",
521
+ pattern: pagesMatch.route.pattern
522
+ };
523
+ }
524
+ continue;
525
+ }
526
+ if (!appMatch) continue;
527
+ const matchedAppRoute = appMatch.route;
528
+ if (matchedAppRoute.routePath && !matchedAppRoute.pagePath) {
529
+ const classification = routeHandlerClassifications.get(matchedAppRoute.routePath);
530
+ if (classification?.hasGet && classification.staticGenerationEnabled) {
531
+ appRoutePaths.push(pathname);
532
+ routePatterns[pathname] = {
533
+ kind: "app-route",
534
+ pattern: matchedAppRoute.pattern
535
+ };
536
+ }
537
+ continue;
212
538
  }
539
+ const appRenderEntryPath = getAppRouteRenderEntryPath(matchedAppRoute);
540
+ if (!appRenderEntryPath) continue;
541
+ if (classifyAppRoute(appRenderEntryPath, matchedAppRoute.routePath, matchedAppRoute.isDynamic).type === "api") continue;
542
+ appPaths.push(pathname);
543
+ htmlPaths.push(pathname);
544
+ rscPaths.push(pathname);
545
+ routePatterns[pathname] = {
546
+ kind: "app-page",
547
+ pattern: matchedAppRoute.pattern
548
+ };
549
+ if (appRouteHasMainTreeLoadingBoundary(matchedAppRoute)) loadingShellPaths.push(pathname);
213
550
  }
214
- return paths;
551
+ return {
552
+ appPaths,
553
+ appRoutePaths,
554
+ htmlPaths,
555
+ loadingShellPaths,
556
+ pagesPaths,
557
+ routePatterns,
558
+ rscPaths
559
+ };
560
+ }
561
+ const CACHEABILITY_POLICY_HEADER_NAMES = new Set(CACHEABILITY_POLICY_HEADERS);
562
+ function cachePolicyRuleMatchesWarmPath(pathname, rule, config) {
563
+ const canonicalPathname = normalizePathTrailingSlash(pathname, config.trailingSlash);
564
+ return [void 0, ...config.i18n?.domains?.map((domain) => domain.domain) ?? []].some((hostname) => {
565
+ const matchPathname = normalizeDefaultLocalePathname(canonicalPathname, config.i18n, { hostname });
566
+ let sourceMatched = false;
567
+ matchHeaders(matchPathname, [rule], {
568
+ cookies: {},
569
+ headers: new Headers(),
570
+ host: hostname ?? "",
571
+ query: new URLSearchParams()
572
+ }, {
573
+ basePath: config.basePath,
574
+ hadBasePath: true
575
+ }, () => {
576
+ sourceMatched = true;
577
+ });
578
+ return sourceMatched;
579
+ });
580
+ }
581
+ function cachePolicyRuleSourceMatchesWarmPath(pathname, rule, config) {
582
+ return cachePolicyRuleMatchesWarmPath(pathname, {
583
+ ...rule,
584
+ has: void 0,
585
+ missing: void 0
586
+ }, config);
587
+ }
588
+ function staticConfigPatternSegments(pattern) {
589
+ const segments = [];
590
+ for (const segment of pattern.split("/").filter(Boolean)) {
591
+ if (/[:*()[\]{}]/.test(segment)) break;
592
+ segments.push(segment);
593
+ }
594
+ return segments;
595
+ }
596
+ function routePatternCouldIntersectCachePolicyRule(routePattern, rule, basePath) {
597
+ let ruleSource = rule.source;
598
+ if (rule.basePath !== false && basePath && (ruleSource === basePath || ruleSource.startsWith(`${basePath}/`))) ruleSource = ruleSource.slice(basePath.length) || "/";
599
+ const routeSegments = staticConfigPatternSegments(routePattern);
600
+ const ruleSegments = staticConfigPatternSegments(ruleSource);
601
+ const sharedLength = Math.min(routeSegments.length, ruleSegments.length);
602
+ for (let index = 0; index < sharedLength; index++) if (routeSegments[index] !== ruleSegments[index]) return false;
603
+ return true;
604
+ }
605
+ /**
606
+ * Certify only route-config bailouts that cannot hide a path- or
607
+ * request-specific next.config cache policy. The final Worker still evaluates
608
+ * every concrete response before emitting public cache headers.
609
+ */
610
+ function annotateCacheabilityProbeSafety(routePatterns, config) {
611
+ const cachePolicyRules = config.headers.filter((rule) => rule.headers.some((header) => CACHEABILITY_POLICY_HEADER_NAMES.has(header.key.toLowerCase())));
612
+ const matchingPolicyRules = new Map(Object.keys(routePatterns).map((pathname) => [pathname, new Set(cachePolicyRules.filter((rule) => cachePolicyRuleMatchesWarmPath(pathname, rule, config)))]));
613
+ const pathsByPattern = /* @__PURE__ */ new Map();
614
+ for (const [pathname, route] of Object.entries(routePatterns)) {
615
+ const key = `${route.kind}\0${route.pattern}`;
616
+ const paths = pathsByPattern.get(key) ?? [];
617
+ paths.push(pathname);
618
+ pathsByPattern.set(key, paths);
619
+ }
620
+ const canPrunePatterns = /* @__PURE__ */ new Map();
621
+ for (const [patternKey, patternPaths] of pathsByPattern) {
622
+ const routePattern = routePatterns[patternPaths[0]].pattern;
623
+ const relevantRules = cachePolicyRules.filter((rule) => patternPaths.some((path) => cachePolicyRuleSourceMatchesWarmPath(path, rule, config)) || routePatternCouldIntersectCachePolicyRule(routePattern, rule, config.basePath));
624
+ canPrunePatterns.set(patternKey, relevantRules.every((rule) => !rule.has?.length && !rule.missing?.length && patternPaths.every((path) => matchingPolicyRules.get(path)?.has(rule) === true)));
625
+ }
626
+ return Object.fromEntries(Object.entries(routePatterns).map(([pathname, route]) => {
627
+ const patternKey = `${route.kind}\0${route.pattern}`;
628
+ const canPrunePattern = canPrunePatterns.get(patternKey) ?? false;
629
+ return [pathname, {
630
+ ...route,
631
+ cacheabilityProbe: { canPrunePattern }
632
+ }];
633
+ }));
634
+ }
635
+ function configuredRouteAffectsWarmPath(pathname, config) {
636
+ const canonicalPathname = normalizePathTrailingSlash(pathname, config.trailingSlash);
637
+ const hostnames = [void 0, ...config.i18n?.domains?.map((domain) => domain.domain) ?? []];
638
+ const matchPathnames = new Set(hostnames.map((hostname) => normalizeDefaultLocalePathname(canonicalPathname, config.i18n, { hostname })));
639
+ return [...[
640
+ ...config.rewrites.beforeFiles,
641
+ ...config.rewrites.afterFiles,
642
+ ...config.rewrites.fallback
643
+ ], ...config.redirects].some((rule) => Array.from(matchPathnames).some((matchPathname) => matchesRewriteSource(matchPathname, rule, {
644
+ basePath: config.basePath,
645
+ hadBasePath: true
646
+ })));
215
647
  }
216
648
  async function startPathDiscoveryServer(options) {
217
649
  return startProdServer({
@@ -237,16 +669,29 @@ async function emitPrerenderPathManifest(options) {
237
669
  const manifestDir = path.join(root, "dist", "server");
238
670
  const config = options.nextConfig ? { ...options.nextConfig } : { ...await resolveNextConfig(await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root) };
239
671
  const builtBuildId = readBuiltBuildId(manifestDir) ?? readBuiltBuildId(bundleServerDir);
672
+ const rscBuildId = readBuiltRscBuildId(manifestDir) ?? readBuiltRscBuildId(bundleServerDir);
240
673
  if (builtBuildId) config.buildId = builtBuildId;
241
674
  const paths = [];
242
675
  const seen = /* @__PURE__ */ new Set();
676
+ const discoveredPagesPaths = [];
677
+ const seenPagesPaths = /* @__PURE__ */ new Set();
678
+ const discoveredPagesDataPaths = [];
679
+ const seenPagesDataPaths = /* @__PURE__ */ new Set();
680
+ const discoveredAppPaths = [];
681
+ const seenAppPaths = /* @__PURE__ */ new Set();
682
+ const discoveredRouteHandlerPaths = [];
683
+ const seenRouteHandlerPaths = /* @__PURE__ */ new Set();
684
+ const discoveredLoadingShellPaths = [];
685
+ const seenLoadingShellPaths = /* @__PURE__ */ new Set();
686
+ const fallbackRoutePatterns = [];
243
687
  await withPrerenderEndpoints(async () => {
244
688
  let prodServer = null;
245
- if (await shouldStartPathDiscoveryServer({
689
+ const needsServer = await shouldStartPathDiscoveryServer({
246
690
  appDir,
247
691
  pagesDir,
248
692
  pageExtensions: config.pageExtensions
249
- })) try {
693
+ });
694
+ if (needsServer && !options.pathDiscoveryTarget) try {
250
695
  prodServer = await startPathDiscoveryServer({
251
696
  serverDir: bundleServerDir,
252
697
  pagesBundlePath: !appDir && pagesDir ? pagesBundlePath : void 0,
@@ -254,37 +699,131 @@ async function emitPrerenderPathManifest(options) {
254
699
  });
255
700
  } catch (error) {
256
701
  const message = error instanceof Error ? error.message : String(error);
257
- console.warn(`[vinext] Warning: failed to start prerender path discovery server: ${message}`);
702
+ throw new Error(`Failed to start prerender path discovery server: ${message}`, { cause: error });
258
703
  }
259
- const baseUrl = prodServer ? `http://127.0.0.1:${prodServer.port}` : null;
704
+ const baseUrl = options.pathDiscoveryTarget?.baseUrl ? new URL(options.pathDiscoveryTarget.baseUrl).origin : prodServer ? `http://127.0.0.1:${prodServer.port}` : null;
260
705
  const prerenderSecret = readPrerenderSecret(bundleServerDir) ?? readPrerenderSecret(manifestDir);
261
- const secretHeaders = prerenderSecret ? { [VINEXT_PRERENDER_SECRET_HEADER]: prerenderSecret } : {};
706
+ if (needsServer && options.pathDiscoveryTarget && !prerenderSecret) throw new Error("Cannot discover warmup paths from the staged Worker because dist/server/vinext-server.json does not contain a prerender secret. Rebuild the app before deploying.");
707
+ const secretHeaders = Object.fromEntries(new Headers(options.pathDiscoveryTarget?.headers));
708
+ if (prerenderSecret) secretHeaders[VINEXT_PRERENDER_SECRET_HEADER] = prerenderSecret;
709
+ const pathDiscoveryRetryOptions = options.pathDiscoveryTarget ? {
710
+ deadlineAt: Date.now() + Math.max(1, options.pathDiscoveryTarget.phaseTimeoutMs ?? 12e4),
711
+ phaseTimeoutMs: options.pathDiscoveryTarget.phaseTimeoutMs,
712
+ retries: options.pathDiscoveryTarget.retries,
713
+ retryDelayMs: options.pathDiscoveryTarget.retryDelayMs
714
+ } : void 0;
262
715
  try {
263
- if (appDir) for (const pathname of await collectAppPaths({
264
- appDir,
265
- baseUrl,
266
- pageExtensions: config.pageExtensions,
267
- secretHeaders
268
- })) addPath(paths, seen, pathname);
269
- if (pagesDir) for (const pathname of await collectPagesPaths({
270
- baseUrl,
271
- pagesDir,
272
- pageExtensions: config.pageExtensions,
273
- secretHeaders
274
- })) addPath(paths, seen, pathname);
716
+ if (appDir) {
717
+ const appPathResult = await collectAppPaths({
718
+ appDir,
719
+ baseUrl,
720
+ cacheComponents: config.cacheComponents,
721
+ pageExtensions: config.pageExtensions,
722
+ retryOptions: pathDiscoveryRetryOptions,
723
+ secretHeaders
724
+ });
725
+ for (const pathname of appPathResult.paths) {
726
+ addPath(paths, seen, pathname);
727
+ addPath(discoveredAppPaths, seenAppPaths, pathname);
728
+ }
729
+ for (const pathname of appPathResult.loadingShellPaths) addPath(discoveredLoadingShellPaths, seenLoadingShellPaths, pathname);
730
+ for (const pathname of appPathResult.routeHandlerPaths) addPath(discoveredRouteHandlerPaths, seenRouteHandlerPaths, pathname);
731
+ fallbackRoutePatterns.push(...appPathResult.fallbackRoutePatterns);
732
+ }
733
+ if (pagesDir) {
734
+ const pagesPathResult = await collectPagesPaths({
735
+ baseUrl,
736
+ i18n: config.i18n,
737
+ pagesDir,
738
+ pageExtensions: config.pageExtensions,
739
+ retryOptions: pathDiscoveryRetryOptions,
740
+ secretHeaders
741
+ });
742
+ for (const pathname of pagesPathResult.paths) {
743
+ addPath(paths, seen, pathname);
744
+ addPath(discoveredPagesPaths, seenPagesPaths, pathname);
745
+ }
746
+ for (const pathname of pagesPathResult.dataPaths) addPath(discoveredPagesDataPaths, seenPagesDataPaths, pathname);
747
+ fallbackRoutePatterns.push(...pagesPathResult.fallbackRoutePatterns);
748
+ }
275
749
  } finally {
276
750
  if (prodServer) await new Promise((resolve) => prodServer.server.close(() => resolve()));
277
751
  }
278
752
  });
753
+ const excludedWarmPathSet = new Set(options.responseVary ? [...paths, ...discoveredRouteHandlerPaths].filter((pathname) => configuredRouteAffectsWarmPath(pathname, config)) : []);
754
+ const configuredPagesWarmPaths = discoveredPagesPaths.filter((pathname) => !excludedWarmPathSet.has(pathname));
755
+ const resolvedPagesWarmPaths = !appDir && pagesDir ? await excludePagesApiWarmPaths({
756
+ i18n: config.i18n,
757
+ pagesDir,
758
+ pageExtensions: config.pageExtensions,
759
+ paths: configuredPagesWarmPaths
760
+ }) : configuredPagesWarmPaths;
761
+ const discoveredPagesDataPathSet = new Set(discoveredPagesDataPaths);
762
+ const configuredCandidatePaths = paths.filter((pathname) => !excludedWarmPathSet.has(pathname));
763
+ const configuredRouteHandlerPaths = discoveredRouteHandlerPaths.filter((pathname) => !excludedWarmPathSet.has(pathname));
764
+ const appOwnedWarmPaths = appDir ? await resolveAppWarmPaths({
765
+ appDir,
766
+ i18n: config.i18n,
767
+ pagesDir,
768
+ pageExtensions: config.pageExtensions,
769
+ paths: [...configuredCandidatePaths, ...configuredRouteHandlerPaths]
770
+ }) : {
771
+ appPaths: [],
772
+ appRoutePaths: [],
773
+ htmlPaths: discoveredAppPaths,
774
+ loadingShellPaths: discoveredLoadingShellPaths,
775
+ pagesPaths: resolvedPagesWarmPaths,
776
+ routePatterns: pagesDir ? await resolvePagesWarmRoutePatterns({
777
+ i18n: config.i18n,
778
+ pagesDir,
779
+ pageExtensions: config.pageExtensions,
780
+ paths: resolvedPagesWarmPaths
781
+ }) : {},
782
+ rscPaths: discoveredAppPaths
783
+ };
784
+ const warmPaths = appDir ? appOwnedWarmPaths.htmlPaths : resolvedPagesWarmPaths;
785
+ const pagesOwnedWarmPaths = appDir ? appOwnedWarmPaths.pagesPaths : resolvedPagesWarmPaths;
786
+ const resolvedPagesDataWarmPaths = pagesOwnedWarmPaths.filter((pathname) => discoveredPagesDataPathSet.has(pathname));
787
+ const pagesDataPaths = resolvedPagesDataWarmPaths.map((pathname) => buildPagesDataHref(config.basePath, config.buildId, localizePagesDataPath(pathname, config.i18n), ""));
788
+ const routePatterns = annotateCacheabilityProbeSafety(appOwnedWarmPaths.routePatterns, config);
789
+ for (let index = 0; index < resolvedPagesDataWarmPaths.length; index++) {
790
+ const route = routePatterns[resolvedPagesDataWarmPaths[index]];
791
+ if (route) {
792
+ const htmlPathname = resolvedPagesDataWarmPaths[index] === "/" ? config.basePath || "/" : `${config.basePath}${resolvedPagesDataWarmPaths[index]}`;
793
+ routePatterns[pagesDataPaths[index]] = {
794
+ ...route,
795
+ cacheabilityProbe: {
796
+ ...route.cacheabilityProbe,
797
+ concretePathname: htmlPathname
798
+ }
799
+ };
800
+ }
801
+ }
279
802
  const manifest = {
803
+ ...appDir ? { appPaths: appOwnedWarmPaths.appPaths } : {},
804
+ ...config.basePath ? { basePath: config.basePath } : {},
280
805
  buildId: config.buildId,
806
+ ...rscBuildId && options.buildIdentity === "response-header" ? { buildIdentity: rscBuildId } : {},
807
+ ...config.deploymentId ? { deploymentId: config.deploymentId } : {},
808
+ ...pagesDir ? {
809
+ pagesDataPaths,
810
+ pagesPaths: pagesOwnedWarmPaths
811
+ } : {},
812
+ ...excludedWarmPathSet.size > 0 ? { excludedWarmPaths: Array.from(excludedWarmPathSet) } : {},
813
+ ...fallbackRoutePatterns.length > 0 ? { fallbackRoutePatterns } : {},
814
+ ...rscBuildId ? { rscBuildId } : {},
815
+ ...options.responseVary ? { responseVary: options.responseVary } : {},
816
+ ...options.responseVary ? { rscPaths: appOwnedWarmPaths.rscPaths } : {},
817
+ ...Object.keys(routePatterns).length > 0 ? { routePatterns } : {},
818
+ ...appOwnedWarmPaths.appRoutePaths.length > 0 ? { routeHandlerPaths: appOwnedWarmPaths.appRoutePaths } : {},
819
+ ...options.responseVary ? { loadingShellPaths: appOwnedWarmPaths.loadingShellPaths } : {},
281
820
  trailingSlash: config.trailingSlash,
282
- paths
821
+ paths: warmPaths
283
822
  };
284
823
  fs.mkdirSync(manifestDir, { recursive: true });
285
824
  fs.writeFileSync(path.join(manifestDir, PRERENDER_PATHS_MANIFEST), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
286
- console.log(` Discovered ${paths.length} CDN warmup path(s).`);
825
+ console.log(` Discovered ${warmPaths.length + appOwnedWarmPaths.appRoutePaths.length} CDN warmup path(s).`);
287
826
  return manifest;
288
827
  }
289
828
  //#endregion
290
- export { PRERENDER_PATHS_MANIFEST, PRERENDER_PATH_DISCOVERY_ENV, emitPrerenderPathManifest };
829
+ export { DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS, DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS, PRERENDER_PATHS_MANIFEST, PRERENDER_PATH_DISCOVERY_ENV, emitPrerenderPathManifest };