@vinext/cloudflare 1.0.0-beta.6 → 1.0.0-beta.8

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 (58) hide show
  1. package/README.md +62 -12
  2. package/dist/cache/cdn-adapter-config.d.ts +30 -0
  3. package/dist/cache/cdn-adapter-config.js +87 -0
  4. package/dist/cache/cdn-adapter.d.ts +47 -15
  5. package/dist/cache/cdn-adapter.js +49 -13
  6. package/dist/cache/cdn-adapter.runtime.d.ts +26 -4
  7. package/dist/cache/cdn-adapter.runtime.js +131 -33
  8. package/dist/cache/cdn-adapter.worker.d.ts +30 -0
  9. package/dist/cache/cdn-adapter.worker.js +371 -0
  10. package/dist/cache/cdn-build-id.d.ts +6 -0
  11. package/dist/cache/cdn-build-id.js +9 -0
  12. package/dist/cache/kv-data-adapter.d.ts +21 -5
  13. package/dist/cache/kv-data-adapter.runtime.d.ts +38 -4
  14. package/dist/cache/kv-data-adapter.runtime.js +116 -42
  15. package/dist/cache/kv-key.d.ts +4 -5
  16. package/dist/cache/response-store-adapter.d.ts +37 -0
  17. package/dist/cache/response-store-adapter.js +39 -0
  18. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +17 -0
  19. package/dist/cache/response-store-adapter.self-contained.worker.js +10 -0
  20. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +15 -0
  21. package/dist/cache/response-store-adapter.service-binding.worker.js +9 -0
  22. package/dist/cache/response-store-adapter.worker.d.ts +14 -0
  23. package/dist/cache/response-store-adapter.worker.js +254 -0
  24. package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
  25. package/dist/cache/response-store-cdn.runtime.js +41 -0
  26. package/dist/cache/response-store-data.runtime.d.ts +34 -0
  27. package/dist/cache/response-store-data.runtime.js +289 -0
  28. package/dist/cacheability-artifact.d.ts +9 -0
  29. package/dist/cacheability-artifact.js +191 -0
  30. package/dist/cacheability-manifest-limits.d.ts +4 -0
  31. package/dist/cacheability-manifest-limits.js +7 -0
  32. package/dist/cacheability-probe.d.ts +49 -0
  33. package/dist/cacheability-probe.js +660 -0
  34. package/dist/cdn-warm.d.ts +110 -11
  35. package/dist/cdn-warm.js +864 -82
  36. package/dist/cli.js +15 -1
  37. package/dist/deploy-config.d.ts +13 -10
  38. package/dist/deploy-config.js +21 -3
  39. package/dist/deploy-help.d.ts +3 -4
  40. package/dist/deploy-help.js +43 -6
  41. package/dist/deploy.d.ts +77 -23
  42. package/dist/deploy.js +666 -84
  43. package/dist/images/images-optimizer.d.ts +3 -4
  44. package/dist/prerender-kv-populate.d.ts +3 -4
  45. package/dist/prerender-kv-populate.js +4 -3
  46. package/dist/tpr.d.ts +9 -10
  47. package/dist/tpr.js +3 -3
  48. package/dist/utils/cache-control-metadata.d.ts +3 -4
  49. package/dist/version-deploy.d.ts +19 -16
  50. package/dist/version-deploy.js +29 -12
  51. package/dist/version-headers.d.ts +2 -0
  52. package/dist/version-headers.js +2 -0
  53. package/dist/worker-deployment-url.d.ts +8 -3
  54. package/dist/worker-deployment-url.js +23 -1
  55. package/dist/workers-dev-url.d.ts +2 -3
  56. package/dist/wrangler-version-metadata.d.ts +7 -0
  57. package/dist/wrangler-version-metadata.js +9 -0
  58. package/package.json +14 -7
package/dist/cdn-warm.js CHANGED
@@ -1,9 +1,28 @@
1
- import path from "node:path";
1
+ import { VINEXT_CDN_BUILD_ID_HEADER } from "./cache/cdn-build-id.js";
2
2
  import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
5
+ import { randomUUID } from "node:crypto";
6
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
+ import { VINEXT_CACHE_HEADER, VINEXT_PRERENDER_READINESS_HEADER, VINEXT_PRERENDER_READINESS_PATH, VINEXT_PRERENDER_SECRET_HEADER } from "vinext/internal/server/headers";
3
8
  import { PRERENDER_PATHS_MANIFEST } from "vinext/internal/build/prerender-paths";
4
9
  import { getPrerenderedConcretePaths, readPrerenderManifest } from "vinext/internal/server/prerender-manifest";
10
+ import { VINEXT_RSC_BUILD_ID_HEADER, VINEXT_RSC_CONTENT_TYPE, VINEXT_RSC_VARY_HEADER as VINEXT_RSC_VARY_HEADER$1, createCanonicalLoadingShellRscRequestHeaders, createCanonicalRscRequestHeaders, createCanonicalRscRequestUrl, createRscRequestUrl } from "vinext/internal/server/app-rsc-cache-busting";
11
+ import { isNonCacheableCacheControl } from "vinext/shims/cdn-cache";
12
+ import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
5
13
  //#region src/cdn-warm.ts
6
- const DEFAULT_CDN_WARM_TIMEOUT_MS = 5e3;
14
+ const DEFAULT_CDN_WARM_CONCURRENCY = 25;
15
+ const DEFAULT_CDN_WARM_TIMEOUT_MS = 1e4;
16
+ const DEFAULT_STAGED_READINESS_RETRIES = 60;
17
+ const DEFAULT_STAGED_READINESS_INTERVAL_MS = 1e3;
18
+ const DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS = 12e4;
19
+ const DEFAULT_STAGED_READINESS_SUCCESSES = 6;
20
+ const STAGED_READINESS_QUERY_PARAM = "__vinext_cdn_warm_readiness";
21
+ function applyWarmPathConfig(pathname, config) {
22
+ const withBasePath = config.basePath ? pathname === "/" ? config.basePath : `${config.basePath}${pathname}` : pathname;
23
+ const normalized = normalizePathTrailingSlash(withBasePath, config.trailingSlash === true);
24
+ return new URL(normalized, "http://vinext.local").pathname;
25
+ }
7
26
  function readBuiltBuildId(root) {
8
27
  try {
9
28
  const buildId = fs.readFileSync(path.join(root, "dist", "server", "BUILD_ID"), "utf-8").trim();
@@ -19,52 +38,83 @@ function readPrerenderPathManifest(manifestPath) {
19
38
  const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
20
39
  if (!parsed || typeof parsed !== "object") return null;
21
40
  const manifest = parsed;
22
- if (!Array.isArray(manifest.paths)) return null;
41
+ if (!Array.isArray(manifest.paths) || !manifest.paths.every((pathname) => typeof pathname === "string") || manifest.appPaths !== void 0 && (!Array.isArray(manifest.appPaths) || !manifest.appPaths.every((pathname) => typeof pathname === "string")) || manifest.pagesPaths !== void 0 && (!Array.isArray(manifest.pagesPaths) || !manifest.pagesPaths.every((pathname) => typeof pathname === "string")) || manifest.pagesDataPaths !== void 0 && (!Array.isArray(manifest.pagesDataPaths) || !manifest.pagesDataPaths.every((pathname) => typeof pathname === "string")) || manifest.excludedWarmPaths !== void 0 && (!Array.isArray(manifest.excludedWarmPaths) || !manifest.excludedWarmPaths.every((pathname) => typeof pathname === "string")) || manifest.fallbackRoutePatterns !== void 0 && (!Array.isArray(manifest.fallbackRoutePatterns) || !manifest.fallbackRoutePatterns.every((route) => route !== null && typeof route === "object" && !Array.isArray(route) && (route.kind === "app-page" || route.kind === "app-route" || route.kind === "pages-page") && typeof route.pattern === "string" && route.pattern.startsWith("/"))) || manifest.rscPaths !== void 0 && (!Array.isArray(manifest.rscPaths) || !manifest.rscPaths.every((pathname) => typeof pathname === "string")) || manifest.routeHandlerPaths !== void 0 && (!Array.isArray(manifest.routeHandlerPaths) || !manifest.routeHandlerPaths.every((pathname) => typeof pathname === "string")) || manifest.routePatterns !== void 0 && (!manifest.routePatterns || typeof manifest.routePatterns !== "object" || Array.isArray(manifest.routePatterns) || !Object.entries(manifest.routePatterns).every(([pathname, route]) => pathname.startsWith("/") && route !== null && typeof route === "object" && !Array.isArray(route) && (route.kind === "app-page" || route.kind === "app-route" || route.kind === "pages-page") && typeof route.pattern === "string" && route.pattern.startsWith("/") && (route.cacheabilityProbe === void 0 || route.cacheabilityProbe !== null && typeof route.cacheabilityProbe === "object" && !Array.isArray(route.cacheabilityProbe) && typeof route.cacheabilityProbe.canPrunePattern === "boolean" && (route.cacheabilityProbe.concretePathname === void 0 || typeof route.cacheabilityProbe.concretePathname === "string" && route.cacheabilityProbe.concretePathname.startsWith("/")) && (route.cacheabilityProbe.routeMayResolve === void 0 || typeof route.cacheabilityProbe.routeMayResolve === "boolean") && (route.cacheabilityProbe.requestStageMayTerminate === void 0 || typeof route.cacheabilityProbe.requestStageMayTerminate === "boolean")))) || manifest.loadingShellPaths !== void 0 && (!Array.isArray(manifest.loadingShellPaths) || !manifest.loadingShellPaths.every((pathname) => typeof pathname === "string")) || manifest.basePath !== void 0 && typeof manifest.basePath !== "string" || manifest.buildIdentity !== void 0 && typeof manifest.buildIdentity !== "string" || manifest.deploymentId !== void 0 && typeof manifest.deploymentId !== "string" || manifest.rscBuildId !== void 0 && typeof manifest.rscBuildId !== "string" || manifest.trailingSlash !== void 0 && typeof manifest.trailingSlash !== "boolean" || manifest.responseVary !== void 0 && manifest.responseVary !== "verbatim") return null;
23
42
  return manifest;
24
43
  } catch (error) {
25
44
  console.warn(`[vinext] Failed to read prerender path manifest at ${manifestPath}:`, error);
26
45
  return null;
27
46
  }
28
47
  }
29
- function readPrerenderPathWarmPaths(root, options) {
30
- const manifest = readPrerenderPathManifest(path.join(root, "dist", "server", PRERENDER_PATHS_MANIFEST));
31
- if (!manifest) return null;
48
+ function createPrerenderPathWarmPaths(root, manifest, options) {
32
49
  const builtBuildId = readBuiltBuildId(root);
33
50
  if (!manifest.buildId || !builtBuildId || manifest.buildId !== builtBuildId) {
34
51
  const message = "[vinext] CDN warmup skipped: prerender path manifest buildId does not match dist/server/BUILD_ID.";
35
52
  if (options?.strict) throw new Error(message);
36
53
  console.warn(message);
37
- return [];
54
+ return null;
38
55
  }
39
- return manifest.paths.filter((pathname) => pathname.startsWith("/"));
56
+ return {
57
+ manifest,
58
+ pagesPaths: (manifest.pagesPaths ?? []).filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map((pathname) => applyWarmPathConfig(pathname, manifest)),
59
+ paths: manifest.paths.filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map((pathname) => applyWarmPathConfig(pathname, manifest))
60
+ };
40
61
  }
41
- function readPrerenderWarmPaths(root, options) {
42
- const shouldPreferPrerenderManifest = options?.includeFallbackShells === true;
43
- if (!shouldPreferPrerenderManifest) {
44
- const pathManifestPaths = readPrerenderPathWarmPaths(root, options);
45
- if (pathManifestPaths !== null) return pathManifestPaths;
46
- }
47
- const manifest = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"));
48
- if (!manifest) {
49
- if (shouldPreferPrerenderManifest) {
50
- const pathManifestPaths = readPrerenderPathWarmPaths(root, options);
51
- if (pathManifestPaths !== null) {
52
- console.warn("[vinext] CDN warmup fallback shells requested, but prerender manifest not found; warming build-discovered paths only.");
53
- return pathManifestPaths;
54
- }
55
- }
56
- const message = "[vinext] CDN warmup skipped: prerender manifest not found.";
62
+ function createPrerenderWarmPlan(root, manifest, options) {
63
+ const pathPlan = createPrerenderPathWarmPaths(root, manifest, options);
64
+ if (!pathPlan) {
65
+ const message = "[vinext] CDN warmup skipped: prerender path discovery is invalid.";
57
66
  if (options?.strict) throw new Error(message);
58
- return [];
67
+ return {
68
+ loadingShellPaths: [],
69
+ paths: [],
70
+ rscPaths: []
71
+ };
59
72
  }
60
- const builtBuildId = readBuiltBuildId(root);
61
- if (!manifest.buildId || !builtBuildId || manifest.buildId !== builtBuildId) {
62
- const message = "[vinext] CDN warmup skipped: prerender manifest buildId does not match dist/server/BUILD_ID.";
73
+ const supportsCanonicalRsc = (manifest.responseVary === "verbatim" || options?.includeCanonicalRsc === true) && manifest.rscPaths !== void 0 && manifest.rscBuildId !== void 0;
74
+ const applyConfig = (pathname) => applyWarmPathConfig(pathname, manifest);
75
+ const routePatterns = manifest.routePatterns ? Object.fromEntries(Object.entries(manifest.routePatterns).map(([pathname, route]) => [pathname.includes("/_next/data/") ? pathname : applyConfig(pathname), route])) : void 0;
76
+ let htmlPaths = pathPlan.paths;
77
+ if (options?.includeFallbackShells === true) {
78
+ const prerenderManifest = readPrerenderManifest(path.join(root, "dist", "server", "vinext-prerender.json"));
79
+ if (!prerenderManifest) console.warn("[vinext] CDN warmup fallback shells requested, but prerender manifest not found; warming build-discovered paths only.");
80
+ else if (!prerenderManifest.buildId || prerenderManifest.buildId !== manifest.buildId) {
81
+ const message = "[vinext] CDN warmup skipped: prerender manifest buildId does not match dist/server/BUILD_ID.";
82
+ if (options.strict) throw new Error(message);
83
+ console.warn(message);
84
+ htmlPaths = [];
85
+ } else htmlPaths = getPrerenderedConcretePaths(prerenderManifest, { includeFallbackShells: true }).filter((pathname) => pathname.startsWith("/")).filter((pathname) => !manifest.excludedWarmPaths?.includes(pathname)).map(applyConfig);
86
+ }
87
+ return {
88
+ ...manifest.appPaths ? { appPaths: manifest.appPaths.map(applyConfig) } : {},
89
+ buildId: manifest.buildId,
90
+ ...manifest.buildIdentity ? { buildIdentity: manifest.buildIdentity } : {},
91
+ ...manifest.deploymentId ? { deploymentId: manifest.deploymentId } : {},
92
+ ...manifest.fallbackRoutePatterns ? { fallbackRoutePatterns: manifest.fallbackRoutePatterns } : {},
93
+ loadingShellPaths: supportsCanonicalRsc ? (manifest.loadingShellPaths ?? []).map(applyConfig) : [],
94
+ ...manifest.pagesDataPaths ? { pagesDataPaths: manifest.pagesDataPaths } : {},
95
+ ...manifest.pagesPaths ? { pagesPaths: manifest.pagesPaths.map(applyConfig) } : {},
96
+ paths: htmlPaths,
97
+ ...supportsCanonicalRsc ? { rscBuildId: manifest.rscBuildId } : {},
98
+ rscPaths: supportsCanonicalRsc ? manifest.rscPaths.map(applyConfig) : [],
99
+ ...manifest.routeHandlerPaths ? { routeHandlerPaths: manifest.routeHandlerPaths.map(applyConfig) } : {},
100
+ ...routePatterns ? { routePatterns } : {}
101
+ };
102
+ }
103
+ function readPrerenderWarmPlan(root, options) {
104
+ const manifest = readPrerenderPathManifest(path.join(root, "dist", "server", PRERENDER_PATHS_MANIFEST));
105
+ if (!manifest) {
106
+ const message = "[vinext] CDN warmup skipped: prerender path manifest not found.";
63
107
  if (options?.strict) throw new Error(message);
64
- console.warn(message);
65
- return [];
108
+ return {
109
+ loadingShellPaths: [],
110
+ paths: [],
111
+ rscPaths: []
112
+ };
66
113
  }
67
- return getPrerenderedConcretePaths(manifest, options);
114
+ return createPrerenderWarmPlan(root, manifest, options);
115
+ }
116
+ function readPrerenderWarmPaths(root, options) {
117
+ return readPrerenderWarmPlan(root, options).paths;
68
118
  }
69
119
  function getWarmPathsFromPrerenderManifest(manifest, options) {
70
120
  return getPrerenderedConcretePaths(manifest, options);
@@ -75,45 +125,660 @@ function normalizeWarmPath(pathname) {
75
125
  function buildWarmupUrl(targetUrl, pathname) {
76
126
  return new URL(normalizeWarmPath(pathname), targetUrl.endsWith("/") ? targetUrl : `${targetUrl}/`);
77
127
  }
78
- function isRetryableStatus(status) {
79
- return status === 408 || status === 429 || status >= 500;
128
+ function isRetryableStatus(status, retryNotFound) {
129
+ return status === 408 || status === 429 || status >= 500 || retryNotFound && status === 404;
80
130
  }
81
- async function fetchWithTimeout(fetchImpl, url, timeoutMs, headers) {
131
+ const WORKER_VERSION_OVERRIDE_HEADER = "Cloudflare-Workers-Version-Overrides";
132
+ async function fetchWithTimeout(fetchImpl, url, timeoutMs, headers, redirect) {
82
133
  const controller = new AbortController();
83
- const timer = setTimeout(() => controller.abort(), timeoutMs);
134
+ let response;
135
+ let timer;
84
136
  const requestHeaders = new Headers(headers);
85
137
  requestHeaders.set("User-Agent", "vinext-cloudflare-cdn-warm");
86
138
  try {
87
- return await fetchImpl(url, {
88
- method: "GET",
89
- redirect: "follow",
139
+ const timedOut = new Promise((_resolve, reject) => {
140
+ timer = setTimeout(() => {
141
+ controller.abort();
142
+ reject(new DOMException(`Timed out after ${timeoutMs}ms`, "AbortError"));
143
+ }, timeoutMs);
144
+ });
145
+ return await Promise.race([(async () => {
146
+ response = await fetchImpl(url, {
147
+ method: "GET",
148
+ redirect,
149
+ headers: requestHeaders,
150
+ signal: controller.signal
151
+ });
152
+ return {
153
+ body: await response.arrayBuffer(),
154
+ response
155
+ };
156
+ })(), timedOut]);
157
+ } finally {
158
+ if (timer !== void 0) clearTimeout(timer);
159
+ if (controller.signal.aborted && response?.body) response.body.cancel().catch(() => {});
160
+ }
161
+ }
162
+ async function fetchHeadersWithTimeout(fetchImpl, url, timeoutMs, headers, method = "GET") {
163
+ const controller = new AbortController();
164
+ const requestHeaders = new Headers(headers);
165
+ requestHeaders.set("User-Agent", "vinext-cloudflare-cdn-warm");
166
+ let timer;
167
+ try {
168
+ const timedOut = new Promise((_resolve, reject) => {
169
+ timer = setTimeout(() => {
170
+ controller.abort();
171
+ reject(new DOMException(`Timed out after ${timeoutMs}ms`, "AbortError"));
172
+ }, timeoutMs);
173
+ });
174
+ return await Promise.race([fetchImpl(url, {
175
+ method,
176
+ redirect: "manual",
90
177
  headers: requestHeaders,
91
178
  signal: controller.signal
92
- });
179
+ }), timedOut]);
93
180
  } finally {
94
- clearTimeout(timer);
95
- }
96
- }
97
- async function warmOnePath(pathname, options) {
98
- const url = buildWarmupUrl(options.targetUrl, pathname);
99
- let lastError = "unknown error";
100
- for (let attempt = 0; attempt <= options.retries; attempt++) try {
101
- const response = await fetchWithTimeout(options.fetchImpl, url, options.timeoutMs, options.headers);
102
- await response.arrayBuffer();
103
- if (response.status < 400) return {
104
- path: pathname,
105
- ok: true
181
+ if (timer !== void 0) clearTimeout(timer);
182
+ }
183
+ }
184
+ const MAX_CDN_WARM_REPORT_ROUTES = 10;
185
+ const MAX_CDN_WARM_REPORT_PATTERN_WIDTH = 48;
186
+ const CDN_WARM_ROUTE_KIND_LABELS = {
187
+ "app-page": "App page",
188
+ "app-route": "Route Handler",
189
+ "pages-page": "Pages page"
190
+ };
191
+ function formatCount(count, singular, plural = `${singular}s`) {
192
+ return `${count.toLocaleString("en-US")} ${count === 1 ? singular : plural}`;
193
+ }
194
+ function formatNumber(count) {
195
+ return count.toLocaleString("en-US");
196
+ }
197
+ function truncateMiddle(value, maxLength) {
198
+ if (value.length <= maxLength) return value;
199
+ const remaining = maxLength - 1;
200
+ const startLength = Math.ceil(remaining / 2);
201
+ return `${value.slice(0, startLength)}…${value.slice(-(remaining - startLength))}`;
202
+ }
203
+ function printCdnWarmTable(headings, rows, rightAlignedFrom) {
204
+ const widths = headings.map((heading, index) => Math.max(heading.length, ...rows.map((row) => row[index]?.length ?? 0)));
205
+ for (const row of [headings, ...rows]) console.log(` ${row.map((value, index) => index >= rightAlignedFrom ? value.padStart(widths[index]) : value.padEnd(widths[index])).join(" ")}`);
206
+ }
207
+ function createCdnWarmRouteReport(targets, outcomes) {
208
+ const routes = /* @__PURE__ */ new Map();
209
+ for (const [index, target] of targets.entries()) {
210
+ const key = target.route ? `${target.route.kind}\0${target.route.pattern}` : "unmatched";
211
+ const route = routes.get(key) ?? {
212
+ failed: 0,
213
+ kind: target.route ? CDN_WARM_ROUTE_KIND_LABELS[target.route.kind] : "Other",
214
+ key,
215
+ pattern: target.route?.pattern ?? "Other discovered requests",
216
+ paths: /* @__PURE__ */ new Set(),
217
+ skipped: 0,
218
+ total: 0,
219
+ warmed: 0
106
220
  };
107
- lastError = `HTTP ${response.status}`;
108
- if (!isRetryableStatus(response.status)) break;
109
- } catch (error) {
110
- if (error instanceof DOMException && error.name === "AbortError") lastError = `timed out after ${options.timeoutMs}ms`;
111
- else lastError = error instanceof Error ? error.message : String(error);
221
+ route.paths.add(target.route?.cacheabilityProbe?.concretePathname ?? cacheabilityRoutePathname(target.pathname, target.kind));
222
+ route.total++;
223
+ const outcome = outcomes?.[index];
224
+ if (outcome) route[outcome]++;
225
+ routes.set(key, route);
226
+ }
227
+ return [...routes.values()].sort((left, right) => right.total - left.total || left.key.localeCompare(right.key));
228
+ }
229
+ function printCdnWarmRouteReport(targets, outcomes) {
230
+ if (!targets.some((target) => target.route)) return;
231
+ const routes = createCdnWarmRouteReport(targets, outcomes);
232
+ if (routes.length === 0) return;
233
+ console.log(` CDN warmup ${outcomes ? "result" : "plan"} by route:`);
234
+ const visible = routes.slice(0, MAX_CDN_WARM_REPORT_ROUTES);
235
+ const routeColumns = (route) => [truncateMiddle(route.pattern, MAX_CDN_WARM_REPORT_PATTERN_WIDTH), route.kind];
236
+ if (outcomes) printCdnWarmTable([
237
+ "Route pattern",
238
+ "Kind",
239
+ "Warmed",
240
+ "Skipped",
241
+ "Failed"
242
+ ], visible.map((route) => [
243
+ ...routeColumns(route),
244
+ `${formatNumber(route.warmed)}/${formatNumber(route.total)}`,
245
+ formatNumber(route.skipped),
246
+ formatNumber(route.failed)
247
+ ]), 2);
248
+ else printCdnWarmTable([
249
+ "Route pattern",
250
+ "Kind",
251
+ "Paths",
252
+ "Entries"
253
+ ], visible.map((route) => [
254
+ ...routeColumns(route),
255
+ formatNumber(route.paths.size),
256
+ formatNumber(route.total)
257
+ ]), 2);
258
+ const omitted = routes.slice(MAX_CDN_WARM_REPORT_ROUTES);
259
+ if (omitted.length > 0) {
260
+ const omittedEntries = omitted.reduce((total, route) => total + route.total, 0);
261
+ console.log(` ... ${formatCount(omitted.length, "additional route pattern")} omitted (${formatCount(omittedEntries, "cache entry", "cache entries")})`);
262
+ }
263
+ }
264
+ function cdnWarmTargetKey(target) {
265
+ return `${target.kind}\0${target.sourcePathname}`;
266
+ }
267
+ async function createCdnWarmTargets(options) {
268
+ const requests = [];
269
+ const fullRscPaths = new Set(options.rscPaths ?? []);
270
+ const loadingShellPaths = new Set(options.loadingShellPaths ?? []);
271
+ const commonHeaders = new Headers(options.headers);
272
+ for (const pathname of /* @__PURE__ */ new Set([...fullRscPaths, ...loadingShellPaths])) {
273
+ if (fullRscPaths.has(pathname)) {
274
+ const rscHeaders = new Headers(commonHeaders);
275
+ for (const [name, value] of createCanonicalRscRequestHeaders(options.deploymentId)) rscHeaders.set(name, value);
276
+ requests.push({
277
+ headers: rscHeaders,
278
+ kind: "rsc-full",
279
+ label: `${pathname} (RSC full)`,
280
+ pathname: createCanonicalRscRequestUrl(pathname),
281
+ sourcePathname: pathname,
282
+ route: options.routePatterns?.[pathname]
283
+ });
284
+ }
285
+ if (loadingShellPaths.has(pathname)) {
286
+ const loadingHeaders = new Headers(commonHeaders);
287
+ for (const [name, value] of createCanonicalLoadingShellRscRequestHeaders(options.deploymentId)) loadingHeaders.set(name, value);
288
+ requests.push({
289
+ headers: loadingHeaders,
290
+ kind: "rsc-loading-shell",
291
+ label: `${pathname} (RSC loading shell)`,
292
+ pathname: await createRscRequestUrl(pathname, loadingHeaders),
293
+ sourcePathname: pathname,
294
+ route: options.routePatterns?.[pathname]
295
+ });
296
+ }
297
+ }
298
+ for (const pathname of new Set(options.paths)) {
299
+ const htmlHeaders = new Headers(commonHeaders);
300
+ htmlHeaders.set("Accept", "text/html");
301
+ requests.push({
302
+ headers: htmlHeaders,
303
+ kind: "html",
304
+ label: pathname,
305
+ pathname,
306
+ sourcePathname: pathname,
307
+ route: options.routePatterns?.[pathname]
308
+ });
309
+ }
310
+ for (const pathname of new Set(options.pagesDataPaths ?? [])) {
311
+ const dataHeaders = new Headers(commonHeaders);
312
+ dataHeaders.set("Accept", "application/json");
313
+ requests.push({
314
+ headers: dataHeaders,
315
+ kind: "pages-data",
316
+ label: `${pathname} (Pages data)`,
317
+ pathname,
318
+ sourcePathname: pathname,
319
+ route: options.routePatterns?.[pathname]
320
+ });
321
+ }
322
+ for (const pathname of new Set(options.routeHandlerPaths ?? [])) {
323
+ const routeHeaders = new Headers(commonHeaders);
324
+ routeHeaders.set("Accept", "*/*");
325
+ requests.push({
326
+ headers: routeHeaders,
327
+ kind: "app-route",
328
+ label: `${pathname} (Route Handler)`,
329
+ pathname,
330
+ sourcePathname: pathname,
331
+ route: options.routePatterns?.[pathname]
332
+ });
333
+ }
334
+ return requests;
335
+ }
336
+ var CdnOperationProgress = class {
337
+ isTTY = process.stderr.isTTY;
338
+ lastLineLength = 0;
339
+ update(completed, total, label, phase = "Warming CDN cache") {
340
+ if (!this.isTTY) return;
341
+ const percent = total > 0 ? Math.floor(completed / total * 100) : 0;
342
+ const filled = Math.floor(percent / 5);
343
+ const bar = `[${"█".repeat(filled)}${" ".repeat(20 - filled)}]`;
344
+ const shortLabel = label.length > 40 ? `…${label.slice(-39)}` : label;
345
+ const line = `${phase}... ${bar} ${String(completed).padStart(String(total).length)}/${total} ${shortLabel}`;
346
+ process.stderr.write(`\r${line.padEnd(this.lastLineLength)}`);
347
+ this.lastLineLength = line.length;
348
+ }
349
+ finish() {
350
+ if (!this.isTTY || this.lastLineLength === 0) return;
351
+ process.stderr.write(`\r${" ".repeat(this.lastLineLength)}\r`);
352
+ this.lastLineLength = 0;
353
+ }
354
+ };
355
+ const REQUIRED_RSC_VARY_HEADERS = VINEXT_RSC_VARY_HEADER$1.split(",").map((name) => name.trim().toLowerCase());
356
+ const ADMITTED_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
357
+ "HIT",
358
+ "MISS",
359
+ "EXPIRED",
360
+ "REVALIDATED",
361
+ "UPDATING"
362
+ ]);
363
+ const REUSABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set([
364
+ "HIT",
365
+ "REVALIDATED",
366
+ "UPDATING"
367
+ ]);
368
+ const NON_CACHEABLE_CF_CACHE_STATUSES = /* @__PURE__ */ new Set(["BYPASS"]);
369
+ const CDN_CACHE_POLICY_HEADERS = [
370
+ "Cloudflare-CDN-Cache-Control",
371
+ "CDN-Cache-Control",
372
+ "Cache-Control"
373
+ ];
374
+ function splitCacheControlDirectives(value) {
375
+ const directives = [];
376
+ let start = 0;
377
+ let quoted = false;
378
+ let escaped = false;
379
+ for (let index = 0; index < value.length; index++) {
380
+ const character = value[index];
381
+ if (escaped) escaped = false;
382
+ else if (quoted && character === "\\") escaped = true;
383
+ else if (character === "\"") quoted = !quoted;
384
+ else if (character === "," && !quoted) {
385
+ directives.push(value.slice(start, index));
386
+ start = index + 1;
387
+ }
388
+ }
389
+ directives.push(value.slice(start));
390
+ return directives;
391
+ }
392
+ function hasFieldQualifiedSetCookie(value) {
393
+ return splitCacheControlDirectives(value).some((directive) => {
394
+ const separator = directive.indexOf("=");
395
+ if (separator === -1) return false;
396
+ const name = directive.slice(0, separator).trim().toLowerCase();
397
+ if (name !== "private" && name !== "no-cache") return false;
398
+ const rawFields = directive.slice(separator + 1).trim();
399
+ return (rawFields.startsWith("\"") && rawFields.endsWith("\"") ? rawFields.slice(1, -1) : rawFields).split(",").some((field) => field.trim().toLowerCase() === "set-cookie");
400
+ });
401
+ }
402
+ function validateCachePolicy(response, requireCacheStatus, requireCacheHit = false) {
403
+ const effectivePolicy = CDN_CACHE_POLICY_HEADERS.map((name) => ({
404
+ name,
405
+ value: response.headers.get(name)
406
+ })).find((entry) => entry.value !== null);
407
+ const nonCacheableHeaders = effectivePolicy && isNonCacheableCacheControl(effectivePolicy.value) ? [effectivePolicy.name] : [];
408
+ const hasSetCookie = response.headers.has("Set-Cookie");
409
+ const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
410
+ if (requireCacheHit && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "")) return {
411
+ outcome: "failed",
412
+ error: `CF-Cache-Status is ${cacheStatus ?? "missing"}; the cache fill is not reusable`
413
+ };
414
+ if (cacheStatus && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus)) {
415
+ if (hasSetCookie && (!effectivePolicy || !hasFieldQualifiedSetCookie(effectivePolicy.value))) return {
416
+ outcome: "failed",
417
+ error: "response sets a cookie without an observable field-qualified cache policy"
418
+ };
419
+ return { outcome: "warmed" };
112
420
  }
421
+ if (cacheStatus && NON_CACHEABLE_CF_CACHE_STATUSES.has(cacheStatus)) return {
422
+ outcome: "skipped",
423
+ reason: nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : hasSetCookie ? "response sets a cookie" : `CF-Cache-Status is ${cacheStatus}`
424
+ };
425
+ if (cacheStatus === "DYNAMIC") {
426
+ if (nonCacheableHeaders.length > 0 || hasSetCookie) return {
427
+ outcome: "skipped",
428
+ reason: nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : "response sets a cookie"
429
+ };
430
+ return {
431
+ outcome: "failed",
432
+ error: "CF-Cache-Status is DYNAMIC"
433
+ };
434
+ }
435
+ if (nonCacheableHeaders.length > 0 || hasSetCookie) return {
436
+ outcome: "failed",
437
+ error: `${nonCacheableHeaders.length > 0 ? `${nonCacheableHeaders.join(", ")} opts out of caching` : "response sets a cookie"}, but CF-Cache-Status is ${cacheStatus ?? "missing"}`
438
+ };
439
+ if (!cacheStatus) return requireCacheStatus ? {
440
+ outcome: "failed",
441
+ error: "response is missing CF-Cache-Status"
442
+ } : { outcome: "warmed" };
113
443
  return {
114
- path: pathname,
444
+ outcome: "failed",
445
+ error: `CF-Cache-Status is ${cacheStatus}`
446
+ };
447
+ }
448
+ function validateVinextCacheStatus(response) {
449
+ const status = response.headers.get(VINEXT_CACHE_HEADER)?.trim().toUpperCase();
450
+ if (status === "MISS" || status === "HIT" || status === "UPDATING") return { outcome: "warmed" };
451
+ if (status === "BYPASS" || status === "DYNAMIC") return {
452
+ outcome: "skipped",
453
+ reason: `${VINEXT_CACHE_HEADER} is ${status}`
454
+ };
455
+ return {
456
+ outcome: "failed",
457
+ error: `response is missing ${VINEXT_CACHE_HEADER}`
458
+ };
459
+ }
460
+ function validateWarmAdmission(response, statusSource, requireCacheHit) {
461
+ return statusSource === "vinext" ? validateVinextCacheStatus(response) : validateCachePolicy(response, true, requireCacheHit);
462
+ }
463
+ function validateBuildIdentity(response, expectedBuildId) {
464
+ if (expectedBuildId !== void 0 && response.headers.get("X-Vinext-Build-Id") !== expectedBuildId) {
465
+ const actualBuildId = response.headers.get("X-Vinext-Build-Id") ?? "missing";
466
+ return {
467
+ outcome: "failed",
468
+ error: `response ${VINEXT_CDN_BUILD_ID_HEADER} does not match build ${expectedBuildId} (received ${actualBuildId})`
469
+ };
470
+ }
471
+ return null;
472
+ }
473
+ function isExpectedTerminalStatus(status) {
474
+ return status >= 300 && status < 400 || status === 404;
475
+ }
476
+ function validateRscWarmResponse(response, expectedBuildId, expectedRscBuildId, requireCacheHit = false, statusSource = "cloudflare") {
477
+ const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
478
+ if (buildIdentityValidation) return buildIdentityValidation;
479
+ if (expectedRscBuildId !== void 0 && response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) !== expectedRscBuildId) return {
480
+ outcome: "failed",
481
+ error: `response ${VINEXT_RSC_BUILD_ID_HEADER} does not match build ${expectedRscBuildId}`
482
+ };
483
+ if (response.redirected) return {
484
+ outcome: "failed",
485
+ error: "redirected response"
486
+ };
487
+ const terminalResponse = response.status < 200 || response.status >= 300;
488
+ if (terminalResponse) {
489
+ if (!isExpectedTerminalStatus(response.status) || expectedBuildId === void 0 && expectedRscBuildId === void 0) return {
490
+ outcome: "failed",
491
+ error: `HTTP ${response.status}`
492
+ };
493
+ } else if (!response.headers.get("Content-Type")?.toLowerCase().startsWith(VINEXT_RSC_CONTENT_TYPE)) return {
494
+ outcome: "failed",
495
+ error: `expected ${VINEXT_RSC_CONTENT_TYPE} response`
496
+ };
497
+ const cachePolicyValidation = validateWarmAdmission(response, statusSource, requireCacheHit);
498
+ if (cachePolicyValidation.outcome !== "warmed") return cachePolicyValidation;
499
+ if (terminalResponse && !response.headers.get("Content-Type")?.toLowerCase().startsWith(VINEXT_RSC_CONTENT_TYPE)) return {
500
+ outcome: "failed",
501
+ error: `expected ${VINEXT_RSC_CONTENT_TYPE} response`
502
+ };
503
+ const vary = new Set((response.headers.get("Vary") ?? "").split(",").map((name) => name.trim().toLowerCase()).filter(Boolean));
504
+ const missingVary = REQUIRED_RSC_VARY_HEADERS.find((name) => !vary.has(name));
505
+ if (missingVary) return {
506
+ outcome: "failed",
507
+ error: `response Vary is missing ${missingVary}`
508
+ };
509
+ return { outcome: "warmed" };
510
+ }
511
+ function validateHtmlWarmResponse(response, expectedBuildId, requireCacheHit = false, statusSource = "cloudflare") {
512
+ const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
513
+ if (buildIdentityValidation) return buildIdentityValidation;
514
+ if (response.redirected) return {
515
+ outcome: "failed",
516
+ error: "redirected response"
517
+ };
518
+ if (response.status < 200 || response.status >= 300) {
519
+ if (!isExpectedTerminalStatus(response.status) || expectedBuildId === void 0) return {
520
+ outcome: "failed",
521
+ error: `HTTP ${response.status}`
522
+ };
523
+ }
524
+ const cachePolicyValidation = validateWarmAdmission(response, statusSource, requireCacheHit);
525
+ if (cachePolicyValidation.outcome !== "warmed") return cachePolicyValidation;
526
+ return { outcome: "warmed" };
527
+ }
528
+ function validatePagesDataWarmResponse(response, expectedBuildId, requireCacheHit = false, statusSource = "cloudflare") {
529
+ const validation = validateHtmlWarmResponse(response, expectedBuildId, requireCacheHit, statusSource);
530
+ if (validation.outcome !== "warmed") return validation;
531
+ if (!response.headers.get("Content-Type")?.toLowerCase().startsWith("application/json")) return {
532
+ outcome: "failed",
533
+ error: "expected application/json response"
534
+ };
535
+ return validation;
536
+ }
537
+ function validateReadinessResponse(response, kind, expectedBuildId, expectedRscBuildId) {
538
+ const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
539
+ if (buildIdentityValidation?.outcome === "failed") return buildIdentityValidation.error;
540
+ if (kind === "rsc" && expectedRscBuildId !== void 0 && response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) !== expectedRscBuildId) return `response ${VINEXT_RSC_BUILD_ID_HEADER} does not match build ${expectedRscBuildId}`;
541
+ if (response.redirected) return "redirected response";
542
+ if (response.status >= 500) return `HTTP ${response.status}`;
543
+ if (response.status >= 200 && response.status < 300) {
544
+ const contentType = response.headers.get("Content-Type")?.toLowerCase();
545
+ if (kind === "rsc" && !contentType?.startsWith(VINEXT_RSC_CONTENT_TYPE)) return `expected ${VINEXT_RSC_CONTENT_TYPE} response`;
546
+ if (kind === "pages-data" && !contentType?.startsWith("application/json")) return "expected application/json response";
547
+ }
548
+ return null;
549
+ }
550
+ function validatePrerenderReadinessResponse(response, expectedBuildId) {
551
+ const buildIdentityValidation = validateBuildIdentity(response, expectedBuildId);
552
+ if (buildIdentityValidation?.outcome === "failed") return buildIdentityValidation.error;
553
+ if (response.redirected) return "redirected response";
554
+ if (response.status !== 204) return `expected readiness HTTP 204, received ${response.status}`;
555
+ if (response.headers.get(VINEXT_PRERENDER_READINESS_HEADER) !== "1") return `response is missing ${VINEXT_PRERENDER_READINESS_HEADER}: 1`;
556
+ const cacheControl = response.headers.get("Cache-Control");
557
+ if (!cacheControl || !/(?:^|,)\s*no-store\s*(?:,|$)/i.test(cacheControl)) return "readiness response is missing Cache-Control: no-store";
558
+ return null;
559
+ }
560
+ /**
561
+ * Wait until version-override requests consistently reach the uploaded build
562
+ * before any real cache key is filled. Every probe has a unique query key, so
563
+ * an early response cannot make a later readiness attempt pass from cache.
564
+ */
565
+ async function waitForCdnWarmTargetReadiness(options) {
566
+ const rscPath = options.plan.rscPaths[0] ?? options.plan.loadingShellPaths[0];
567
+ const htmlPath = options.plan.paths[0];
568
+ const pagesDataPath = options.plan.pagesDataPaths[0];
569
+ const routeHandlerPath = options.plan.routeHandlerPaths?.[0];
570
+ const kind = rscPath ? "rsc" : htmlPath ? "html" : pagesDataPath ? "pages-data" : "app-route";
571
+ const pathname = rscPath ?? htmlPath ?? pagesDataPath ?? routeHandlerPath;
572
+ if (!pathname) return { ready: true };
573
+ if (options.expectedBuildId === void 0 && options.expectedRscBuildId === void 0) return {
574
+ error: "no response build identity is available for a staged-version readiness probe",
575
+ ready: false
576
+ };
577
+ const headers = new Headers(options.headers);
578
+ const readinessSecret = options.prerenderSecret;
579
+ const useReadinessEndpoint = Boolean(readinessSecret);
580
+ const probePath = useReadinessEndpoint ? VINEXT_PRERENDER_READINESS_PATH : kind === "rsc" ? createCanonicalRscRequestUrl(pathname) : pathname;
581
+ if (readinessSecret) {
582
+ headers.set("Accept", "text/html");
583
+ headers.set(VINEXT_PRERENDER_SECRET_HEADER, readinessSecret);
584
+ } else if (kind === "rsc") for (const [name, value] of createCanonicalRscRequestHeaders(options.deploymentId)) headers.set(name, value);
585
+ else if (kind === "html") headers.set("Accept", "text/html");
586
+ else if (kind === "pages-data") headers.set("Accept", "application/json");
587
+ else headers.set("Accept", "*/*");
588
+ headers.set("Cache-Control", "no-cache");
589
+ headers.set("Pragma", "no-cache");
590
+ const fetchImpl = options.fetchImpl ?? fetch;
591
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 1e4);
592
+ const probeIntervalMs = Math.max(0, options.probeIntervalMs ?? 1e3);
593
+ const requiredConsecutiveSuccesses = Math.max(1, options.requiredConsecutiveSuccesses ?? DEFAULT_STAGED_READINESS_SUCCESSES);
594
+ const readinessRetries = Math.max(0, options.retries ?? 60);
595
+ const maxAttempts = Math.max(requiredConsecutiveSuccesses, options.maxAttempts ?? requiredConsecutiveSuccesses + readinessRetries);
596
+ const phaseTimeoutMs = Math.max(1, options.phaseTimeoutMs ?? 12e4);
597
+ const deadlineAt = Date.now() + phaseTimeoutMs;
598
+ const probeId = randomUUID();
599
+ let consecutiveSuccesses = 0;
600
+ let lastError = "readiness probe did not run";
601
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
602
+ const remainingMs = deadlineAt - Date.now();
603
+ if (remainingMs <= 0) {
604
+ lastError = `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline`;
605
+ break;
606
+ }
607
+ const url = buildWarmupUrl(options.targetUrl, probePath);
608
+ url.searchParams.set(STAGED_READINESS_QUERY_PARAM, `${probeId}-${attempt}`);
609
+ let response;
610
+ try {
611
+ response = await fetchHeadersWithTimeout(fetchImpl, url, Math.min(timeoutMs, remainingMs), headers, useReadinessEndpoint ? "POST" : "GET");
612
+ const validationError = useReadinessEndpoint ? validatePrerenderReadinessResponse(response, options.expectedBuildId) : validateReadinessResponse(response, kind, options.expectedBuildId, options.expectedRscBuildId);
613
+ if (process.env.VINEXT_CDN_WARM_DEBUG === "1") console.log(` CDN warm readiness attempt ${attempt + 1}: ${url.pathname}${url.search} HTTP ${response.status} build=${response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) ?? response.headers.get("X-Vinext-Build-Id") ?? "missing"}`);
614
+ if (validationError === null) {
615
+ consecutiveSuccesses++;
616
+ if (consecutiveSuccesses >= requiredConsecutiveSuccesses) return { ready: true };
617
+ } else {
618
+ consecutiveSuccesses = 0;
619
+ lastError = validationError;
620
+ }
621
+ } catch (error) {
622
+ consecutiveSuccesses = 0;
623
+ lastError = Date.now() >= deadlineAt ? `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline` : error instanceof DOMException && error.name === "AbortError" ? `timed out after ${Math.min(timeoutMs, remainingMs)}ms` : error instanceof Error ? error.message : String(error);
624
+ } finally {
625
+ if (response?.body) response.body.cancel().catch(() => {});
626
+ }
627
+ if (attempt + 1 < maxAttempts && probeIntervalMs > 0) {
628
+ const delayMs = Math.min(probeIntervalMs, Math.max(0, deadlineAt - Date.now()));
629
+ if (delayMs <= 0) {
630
+ lastError = `staged readiness exceeded its ${phaseTimeoutMs}ms phase deadline`;
631
+ break;
632
+ }
633
+ await setTimeout$1(delayMs);
634
+ }
635
+ }
636
+ return {
637
+ error: `${lastError}; uploaded build was not stable for ${requiredConsecutiveSuccesses} consecutive probe(s)`,
638
+ ready: false
639
+ };
640
+ }
641
+ function shouldRetryValidationFailure(response, target, options) {
642
+ if (!options.retryPropagationFailures) return isRetryableStatus(response.status, options.retryNotFound);
643
+ const expectedIdentities = [options.expectedBuildId === void 0 ? null : response.headers.get(VINEXT_CDN_BUILD_ID_HEADER) === options.expectedBuildId, !target.kind.startsWith("rsc-") || options.expectedRscBuildId === void 0 ? null : response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) === options.expectedRscBuildId].filter((matches) => matches !== null);
644
+ if (expectedIdentities.some((matches) => matches)) return isRetryableStatus(response.status, false);
645
+ if (expectedIdentities.some((matches) => !matches)) return true;
646
+ return isRetryableStatus(response.status, options.retryNotFound);
647
+ }
648
+ async function warmOnePath(target, options) {
649
+ const url = buildWarmupUrl(options.targetUrl, target.pathname);
650
+ let lastError = "request failed before the first attempt";
651
+ let lastRetryable = true;
652
+ let lastSkippedReason = null;
653
+ const phaseDeadlineError = () => `CDN warmup exceeded its ${options.phaseTimeoutMs}ms phase deadline`;
654
+ const remainingPhaseMs = () => options.deadlineAt === void 0 ? options.timeoutMs : options.deadlineAt - Date.now();
655
+ const canRetry = (attempt) => attempt < options.retries;
656
+ const waitBeforeRetry = async () => {
657
+ if (options.retryDelayMs <= 0) return remainingPhaseMs() > 0;
658
+ const delayMs = Math.min(options.retryDelayMs, Math.max(0, remainingPhaseMs()));
659
+ if (delayMs <= 0) return false;
660
+ await setTimeout$1(delayMs);
661
+ return remainingPhaseMs() > 0;
662
+ };
663
+ for (let attempt = 0; attempt <= options.retries; attempt++) {
664
+ const remainingMs = remainingPhaseMs();
665
+ if (remainingMs <= 0) return {
666
+ path: target.label,
667
+ ok: false,
668
+ error: phaseDeadlineError(),
669
+ retryable: false
670
+ };
671
+ const attemptTimeoutMs = Math.min(options.timeoutMs, remainingMs);
672
+ const phaseLimitedAttempt = options.deadlineAt !== void 0 && remainingMs <= options.timeoutMs;
673
+ try {
674
+ const response = options.requireCacheHit ? await fetchHeadersWithTimeout(options.fetchImpl, url, attemptTimeoutMs, target.headers ?? options.headers) : (await fetchWithTimeout(options.fetchImpl, url, attemptTimeoutMs, target.headers ?? options.headers, "manual")).response;
675
+ if (options.deadlineAt !== void 0 && Date.now() >= options.deadlineAt) return {
676
+ path: target.label,
677
+ ok: false,
678
+ error: phaseDeadlineError(),
679
+ retryable: false
680
+ };
681
+ if (options.requireCacheHit) response.body?.cancel().catch(() => {});
682
+ if (process.env.VINEXT_CDN_WARM_DEBUG === "1") console.log(` CDN warm debug ${target.label} attempt ${attempt + 1}: ${url.pathname}${url.search} HTTP ${response.status} cache=${response.headers.get("CF-Cache-Status") ?? "missing"} ray=${response.headers.get("CF-Ray") ?? "missing"} encoding=${response.headers.get("Content-Encoding") ?? "identity"} rscBuild=${response.headers.get(VINEXT_RSC_BUILD_ID_HEADER) ?? "missing"}`);
683
+ if (target.kind.startsWith("rsc-")) {
684
+ const validation = validateRscWarmResponse(response, options.expectedBuildId, options.expectedRscBuildId, options.requireCacheHit, options.statusSource);
685
+ if (validation.outcome === "warmed") return {
686
+ path: target.label,
687
+ ok: true,
688
+ skipped: false
689
+ };
690
+ if (validation.outcome === "skipped") {
691
+ if (!options.retrySkipped) return {
692
+ path: target.label,
693
+ ok: true,
694
+ skipped: true,
695
+ reason: validation.reason
696
+ };
697
+ lastSkippedReason = validation.reason;
698
+ lastRetryable = true;
699
+ if (!canRetry(attempt)) break;
700
+ if (!await waitBeforeRetry()) return {
701
+ path: target.label,
702
+ ok: false,
703
+ error: phaseDeadlineError(),
704
+ retryable: false
705
+ };
706
+ continue;
707
+ }
708
+ lastSkippedReason = null;
709
+ lastError = validation.error;
710
+ const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
711
+ lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
712
+ if (!lastRetryable) break;
713
+ if (!canRetry(attempt)) break;
714
+ if (!await waitBeforeRetry()) return {
715
+ path: target.label,
716
+ ok: false,
717
+ error: phaseDeadlineError(),
718
+ retryable: false
719
+ };
720
+ continue;
721
+ }
722
+ const validation = target.kind === "pages-data" ? validatePagesDataWarmResponse(response, options.expectedBuildId, options.requireCacheHit, options.statusSource) : validateHtmlWarmResponse(response, options.expectedBuildId, options.requireCacheHit, options.statusSource);
723
+ if (validation.outcome === "warmed") return {
724
+ path: target.label,
725
+ ok: true,
726
+ skipped: false
727
+ };
728
+ if (validation.outcome === "skipped") {
729
+ if (!options.retrySkipped) return {
730
+ path: target.label,
731
+ ok: true,
732
+ skipped: true,
733
+ reason: validation.reason
734
+ };
735
+ lastSkippedReason = validation.reason;
736
+ lastRetryable = true;
737
+ if (!canRetry(attempt)) break;
738
+ if (!await waitBeforeRetry()) return {
739
+ path: target.label,
740
+ ok: false,
741
+ error: phaseDeadlineError(),
742
+ retryable: false
743
+ };
744
+ continue;
745
+ }
746
+ lastSkippedReason = null;
747
+ lastError = validation.error;
748
+ const cacheStatus = response.headers.get("CF-Cache-Status")?.trim().toUpperCase();
749
+ lastRetryable = options.requireCacheHit && ADMITTED_CF_CACHE_STATUSES.has(cacheStatus ?? "") && !REUSABLE_CF_CACHE_STATUSES.has(cacheStatus ?? "") || shouldRetryValidationFailure(response, target, options);
750
+ if (!lastRetryable) break;
751
+ } catch (error) {
752
+ lastSkippedReason = null;
753
+ lastRetryable = true;
754
+ if (error instanceof DOMException && error.name === "AbortError") {
755
+ if (phaseLimitedAttempt) return {
756
+ path: target.label,
757
+ ok: false,
758
+ error: phaseDeadlineError(),
759
+ retryable: false
760
+ };
761
+ lastError = `timed out after ${attemptTimeoutMs}ms`;
762
+ } else lastError = error instanceof Error ? error.message : String(error);
763
+ }
764
+ if (!canRetry(attempt)) break;
765
+ if (!await waitBeforeRetry()) return {
766
+ path: target.label,
767
+ ok: false,
768
+ error: phaseDeadlineError(),
769
+ retryable: false
770
+ };
771
+ }
772
+ return lastSkippedReason === null ? {
773
+ path: target.label,
115
774
  ok: false,
116
- error: lastError
775
+ error: lastError,
776
+ retryable: lastRetryable
777
+ } : {
778
+ path: target.label,
779
+ ok: true,
780
+ skipped: true,
781
+ reason: lastSkippedReason
117
782
  };
118
783
  }
119
784
  async function runWithConcurrency(items, concurrency, fn) {
@@ -131,53 +796,170 @@ async function runWithConcurrency(items, concurrency, fn) {
131
796
  return results;
132
797
  }
133
798
  async function warmCdnCache(options) {
134
- const paths = options.paths;
135
- const concurrency = Math.max(1, options.concurrency ?? 10);
136
- const timeoutMs = Math.max(1, options.timeoutMs ?? 5e3);
137
- const retries = Math.max(0, options.retries ?? 1);
799
+ const targets = await createCdnWarmTargets(options);
800
+ const requests = targets.filter((target) => target.kind !== "html");
801
+ const htmlRequests = targets.filter((target) => target.kind === "html");
802
+ requests.push(...htmlRequests);
803
+ const timeoutMs = Math.max(1, options.timeoutMs ?? 1e4);
804
+ const hasVersionOverride = new Headers(options.headers).has(WORKER_VERSION_OVERRIDE_HEADER);
805
+ const propagatingTarget = options.propagatingTarget ?? hasVersionOverride;
806
+ const concurrency = Math.max(1, options.concurrency ?? 25);
807
+ const normalRetries = Math.max(0, options.retries ?? 1);
808
+ const propagationRetries = Math.max(0, options.retries ?? 60);
809
+ const normalRetryDelayMs = Math.max(0, options.retryDelayMs ?? 0);
810
+ const propagationRetryDelayMs = Math.max(0, options.retryDelayMs ?? 1e3);
138
811
  const fetchImpl = options.fetchImpl ?? fetch;
139
- if (paths.length === 0) return {
812
+ const phaseTimeoutMs = options.phaseTimeoutMs === void 0 ? void 0 : Math.max(1, options.phaseTimeoutMs);
813
+ const deadlineAt = phaseTimeoutMs === void 0 ? void 0 : Date.now() + phaseTimeoutMs;
814
+ if (requests.length === 0) return {
140
815
  total: 0,
141
816
  warmed: 0,
817
+ skipped: 0,
142
818
  failed: 0,
143
- failures: []
144
- };
145
- console.log(`\n Warming CDN cache for ${paths.length} build-discovered path(s)...`);
146
- const results = await runWithConcurrency(paths, concurrency, (pathname) => warmOnePath(pathname, {
147
- targetUrl: options.targetUrl,
148
- timeoutMs,
149
- retries,
150
- fetchImpl,
151
- headers: options.headers
152
- }));
153
- const failures = results.filter((result) => !result.ok).map(({ path, error }) => ({
819
+ failures: [],
820
+ skippedTargets: [],
821
+ warmedPlan: {
822
+ loadingShellPaths: [],
823
+ pagesDataPaths: [],
824
+ paths: [],
825
+ rscPaths: []
826
+ },
827
+ retryPlan: {
828
+ loadingShellPaths: [],
829
+ pagesDataPaths: [],
830
+ paths: [],
831
+ rscPaths: []
832
+ }
833
+ };
834
+ console.log(`\n Warming ${formatCount(requests.length, "CDN cache entry", "CDN cache entries")}...`);
835
+ printCdnWarmRouteReport(requests);
836
+ const progress = new CdnOperationProgress();
837
+ let completedRequests = 0;
838
+ progress.update(0, requests.length, "starting warmup");
839
+ const warmTarget = (target, retryMode = "normal") => {
840
+ const isPropagationRequest = retryMode !== "normal";
841
+ const retries = retryMode === "propagation-retry" ? Math.max(0, propagationRetries - 1) : retryMode === "propagation-pass" ? 0 : normalRetries;
842
+ return warmOnePath(target, {
843
+ targetUrl: options.targetUrl,
844
+ timeoutMs,
845
+ retries,
846
+ deadlineAt,
847
+ fetchImpl,
848
+ headers: options.headers,
849
+ expectedBuildId: options.expectedBuildId,
850
+ expectedRscBuildId: options.expectedRscBuildId,
851
+ retryPropagationFailures: isPropagationRequest,
852
+ retryDelayMs: isPropagationRequest ? propagationRetryDelayMs : normalRetryDelayMs,
853
+ retryNotFound: isPropagationRequest,
854
+ phaseTimeoutMs,
855
+ requireCacheHit: options.requireCacheHit === true,
856
+ statusSource: options.statusSource ?? "cloudflare",
857
+ retrySkipped: isPropagationRequest && options.retrySkippedTargetKeys?.has(cdnWarmTargetKey(target)) === true
858
+ });
859
+ };
860
+ const warmRequest = async (target, retryMode = "normal") => {
861
+ const result = await warmTarget(target, retryMode);
862
+ completedRequests++;
863
+ progress.update(completedRequests, requests.length, target.label);
864
+ return result;
865
+ };
866
+ const warmPropagatingPass = async (targets) => {
867
+ const results = await runWithConcurrency(targets, concurrency, (target) => warmRequest(target, "propagation-pass"));
868
+ const retryable = targets.map((target, index) => ({
869
+ index,
870
+ result: results[index],
871
+ target
872
+ })).filter(({ result, target }) => result.ok ? result.skipped && options.retrySkippedTargetKeys?.has(cdnWarmTargetKey(target)) === true : result.retryable);
873
+ if (retryable.length === 0 || propagationRetries === 0) return results;
874
+ progress.finish();
875
+ console.log(` CDN warmup: retrying ${retryable.length} unresolved request(s) after completing the initial pass...`);
876
+ let completedRetries = 0;
877
+ progress.update(0, retryable.length, "starting retry pass", "Retrying CDN cache");
878
+ const retried = await runWithConcurrency(retryable.map(({ target }) => target), concurrency, async (target) => {
879
+ const result = await warmTarget(target, "propagation-retry");
880
+ completedRetries++;
881
+ progress.update(completedRetries, retryable.length, target.label, "Retrying CDN cache");
882
+ return result;
883
+ });
884
+ progress.finish();
885
+ for (const [retryIndex, { index }] of retryable.entries()) results[index] = retried[retryIndex];
886
+ return results;
887
+ };
888
+ let results;
889
+ if (propagatingTarget) results = await warmPropagatingPass(requests);
890
+ else results = await runWithConcurrency(requests, concurrency, (target) => warmRequest(target));
891
+ progress.finish();
892
+ const failedRequests = requests.map((target, index) => ({
893
+ result: results[index],
894
+ target
895
+ })).filter((entry) => !entry.result.ok);
896
+ const failures = failedRequests.map(({ result: { path, error } }) => ({
154
897
  path,
155
898
  error
156
899
  }));
157
- const warmed = results.length - failures.length;
158
- console.log(` CDN warmup: ${warmed}/${paths.length} path(s) warmed.`);
900
+ const skippedResults = results.filter((result) => result.ok && result.skipped);
901
+ const skippedTargets = requests.filter((_target, index) => {
902
+ const result = results[index];
903
+ return result.ok && result.skipped;
904
+ });
905
+ const warmedRequests = requests.filter((_target, index) => {
906
+ const result = results[index];
907
+ return result.ok && !result.skipped;
908
+ });
909
+ const warmed = results.length - failures.length - skippedResults.length;
910
+ const warmedRouteHandlerPaths = warmedRequests.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname);
911
+ const failedRouteHandlerPaths = failedRequests.filter(({ target }) => target.kind === "app-route").map(({ target }) => target.sourcePathname);
912
+ printCdnWarmRouteReport(requests, results.map((result) => !result.ok ? "failed" : result.skipped ? "skipped" : "warmed"));
913
+ console.log(` CDN warmup: ${warmed} warmed, ${skippedResults.length} skipped, ${failures.length} failed.`);
914
+ for (const skipped of skippedResults.slice(0, 5)) console.log(` CDN warmup skipped ${skipped.path}: ${skipped.reason}`);
159
915
  if (failures.length > 0) {
160
916
  for (const failure of failures.slice(0, 5)) console.warn(` CDN warmup failed for ${failure.path}: ${failure.error}`);
161
917
  if (failures.length > 5) console.warn(` CDN warmup: ${failures.length - 5} additional failure(s) omitted.`);
162
918
  }
163
919
  const result = {
164
- total: paths.length,
920
+ total: requests.length,
165
921
  warmed,
922
+ skipped: skippedResults.length,
166
923
  failed: failures.length,
167
- failures
924
+ failures,
925
+ skippedTargets,
926
+ warmedPlan: {
927
+ loadingShellPaths: warmedRequests.filter((target) => target.kind === "rsc-loading-shell").map((target) => target.sourcePathname),
928
+ pagesDataPaths: warmedRequests.filter((target) => target.kind === "pages-data").map((target) => target.sourcePathname),
929
+ paths: warmedRequests.filter((target) => target.kind === "html").map((target) => target.sourcePathname),
930
+ rscPaths: warmedRequests.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname),
931
+ ...warmedRouteHandlerPaths.length > 0 ? { routeHandlerPaths: warmedRouteHandlerPaths } : {}
932
+ },
933
+ retryPlan: {
934
+ loadingShellPaths: failedRequests.filter(({ target }) => target.kind === "rsc-loading-shell").map(({ target }) => target.sourcePathname),
935
+ pagesDataPaths: failedRequests.filter(({ target }) => target.kind === "pages-data").map(({ target }) => target.sourcePathname),
936
+ paths: failedRequests.filter(({ target }) => target.kind === "html").map(({ target }) => target.sourcePathname),
937
+ rscPaths: failedRequests.filter(({ target }) => target.kind === "rsc-full").map(({ target }) => target.sourcePathname),
938
+ ...failedRouteHandlerPaths.length > 0 ? { routeHandlerPaths: failedRouteHandlerPaths } : {}
939
+ }
168
940
  };
169
- if (options.strict && failures.length > 0) throw new Error(`CDN warmup failed for ${failures.length}/${paths.length} path(s). First failure: ${failures[0].path}: ${failures[0].error}`);
941
+ if (options.strict && failures.length > 0) throw new Error(`CDN warmup failed for ${failures.length}/${requests.length} request(s). First failure: ${failures[0].path}: ${failures[0].error}`);
170
942
  return result;
171
943
  }
172
944
  async function warmCdnCacheFromPrerender(options) {
173
- const paths = readPrerenderWarmPaths(options.root, {
945
+ const plan = readPrerenderWarmPlan(options.root, {
174
946
  includeFallbackShells: options.includeFallbackShells,
175
947
  strict: options.strict
176
948
  });
949
+ const warmPlan = {
950
+ deploymentId: plan.deploymentId,
951
+ loadingShellPaths: plan.loadingShellPaths,
952
+ pagesDataPaths: plan.pagesDataPaths,
953
+ paths: plan.paths,
954
+ routeHandlerPaths: plan.routeHandlerPaths,
955
+ rscPaths: plan.rscPaths
956
+ };
177
957
  return warmCdnCache({
178
958
  ...options,
179
- paths
959
+ ...warmPlan,
960
+ expectedBuildId: options.expectedBuildId ?? (options.validateBuildIdentity === false ? void 0 : plan.buildIdentity),
961
+ expectedRscBuildId: options.expectedRscBuildId ?? plan.rscBuildId
180
962
  });
181
963
  }
182
964
  //#endregion
183
- export { DEFAULT_CDN_WARM_TIMEOUT_MS, buildWarmupUrl, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, warmCdnCache, warmCdnCacheFromPrerender };
965
+ export { CdnOperationProgress, DEFAULT_CDN_WARM_CONCURRENCY, DEFAULT_CDN_WARM_TIMEOUT_MS, DEFAULT_STAGED_READINESS_INTERVAL_MS, DEFAULT_STAGED_READINESS_PHASE_TIMEOUT_MS, DEFAULT_STAGED_READINESS_RETRIES, buildWarmupUrl, createCdnWarmTargets, createPrerenderWarmPlan, getWarmPathsFromPrerenderManifest, readPrerenderWarmPaths, readPrerenderWarmPlan, waitForCdnWarmTargetReadiness, warmCdnCache, warmCdnCacheFromPrerender };