@vinext/cloudflare 1.0.0-beta.0 → 1.0.0-beta.10

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 (64) hide show
  1. package/README.md +94 -1
  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 +150 -37
  8. package/dist/cache/cdn-adapter.worker.d.ts +31 -0
  9. package/dist/cache/cdn-adapter.worker.js +373 -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 +32 -8
  13. package/dist/cache/kv-data-adapter.js +5 -1
  14. package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
  15. package/dist/cache/kv-data-adapter.runtime.js +133 -53
  16. package/dist/cache/kv-key.d.ts +15 -0
  17. package/dist/cache/kv-key.js +43 -0
  18. package/dist/cache/response-store-adapter.d.ts +73 -0
  19. package/dist/cache/response-store-adapter.js +64 -0
  20. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
  21. package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
  22. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
  23. package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
  24. package/dist/cache/response-store-adapter.worker.d.ts +14 -0
  25. package/dist/cache/response-store-adapter.worker.js +278 -0
  26. package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
  27. package/dist/cache/response-store-cdn.runtime.js +41 -0
  28. package/dist/cache/response-store-data.runtime.d.ts +34 -0
  29. package/dist/cache/response-store-data.runtime.js +300 -0
  30. package/dist/cacheability-artifact.d.ts +9 -0
  31. package/dist/cacheability-artifact.js +191 -0
  32. package/dist/cacheability-manifest-limits.d.ts +4 -0
  33. package/dist/cacheability-manifest-limits.js +7 -0
  34. package/dist/cacheability-probe.d.ts +49 -0
  35. package/dist/cacheability-probe.js +660 -0
  36. package/dist/cdn-warm.d.ts +110 -12
  37. package/dist/cdn-warm.js +880 -82
  38. package/dist/cli.d.ts +1 -1
  39. package/dist/cli.js +15 -1
  40. package/dist/deploy-config.d.ts +13 -11
  41. package/dist/deploy-config.js +21 -3
  42. package/dist/deploy-help.d.ts +3 -4
  43. package/dist/deploy-help.js +61 -18
  44. package/dist/deploy.d.ts +121 -41
  45. package/dist/deploy.js +787 -121
  46. package/dist/images/images-optimizer.d.ts +5 -5
  47. package/dist/images/images-optimizer.runtime.d.ts +1 -5
  48. package/dist/prerender-kv-populate.d.ts +3 -4
  49. package/dist/prerender-kv-populate.js +51 -18
  50. package/dist/tpr.d.ts +32 -70
  51. package/dist/tpr.js +45 -602
  52. package/dist/utils/cache-control-metadata.d.ts +3 -4
  53. package/dist/version-deploy.d.ts +19 -17
  54. package/dist/version-deploy.js +32 -13
  55. package/dist/version-headers.d.ts +2 -0
  56. package/dist/version-headers.js +2 -0
  57. package/dist/worker-deployment-url.d.ts +9 -0
  58. package/dist/worker-deployment-url.js +45 -0
  59. package/dist/workers-dev-url.d.ts +2 -3
  60. package/dist/wrangler-config.d.ts +19 -0
  61. package/dist/wrangler-config.js +294 -0
  62. package/dist/wrangler-version-metadata.d.ts +7 -0
  63. package/dist/wrangler-version-metadata.js +9 -0
  64. package/package.json +14 -10
package/dist/deploy.js CHANGED
@@ -1,21 +1,29 @@
1
- import { readPrerenderWarmPaths, warmCdnCache } from "./cdn-warm.js";
2
- import { parseWranglerConfig, runTPR } from "./tpr.js";
3
- import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
4
- import { parseWorkersDevUrl } from "./workers-dev-url.js";
1
+ import { writeCacheabilityManifestArtifact } from "./cacheability-artifact.js";
2
+ import { DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS, probeStagedWorkerCacheability, readPrerenderSecret } from "./cacheability-probe.js";
3
+ import { CdnOperationProgress, createCdnWarmTargets, createPrerenderWarmPlan, waitForCdnWarmTargetReadiness, warmCdnCache } from "./cdn-warm.js";
4
+ import { parseWranglerConfig } from "./wrangler-config.js";
5
+ import { resolveTPRRoutes, selectRoutes } from "./tpr.js";
6
+ import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "./version-headers.js";
7
+ import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveCdnAdapterConfig, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
8
+ import { assertCdnVersionMetadataConfig } from "./wrangler-version-metadata.js";
9
+ import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
5
10
  import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
6
11
  import { buildPrerenderKVPairs } from "./prerender-kv-populate.js";
7
12
  import { createRequire } from "node:module";
8
- import path from "node:path";
9
13
  import fs from "node:fs";
10
- import { emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
14
+ import path from "node:path";
15
+ import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
16
+ import { setTimeout } from "node:timers/promises";
17
+ import { DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS, DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS, discoverPrerenderPathManifest, emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
18
+ import { normalizePathTrailingSlash } from "vinext/shims/url-utils";
11
19
  import os from "node:os";
12
20
  import { spawn } from "node:child_process";
13
21
  import { parseArgs } from "node:util";
14
22
  import { pathToFileURL } from "node:url";
15
23
  import { runPrerender } from "vinext/internal/build/run-prerender";
16
24
  import { loadDotenv } from "vinext/internal/config/dotenv";
17
- import { loadNextConfig, resolveNextConfig } from "vinext/internal/config/next-config";
18
- import { formatVinextPrerenderLabel, loadVinextCacheConfigFromViteConfig, loadVinextPrerenderConfigFromViteConfig, loadVinextRouteRootConfigFromViteConfig, resolveVinextPrerenderDecision } from "vinext/internal/config/prerender";
25
+ import { findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "vinext/internal/config/next-config";
26
+ import { cacheWarmupStatusSource, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasUncachedRequestRouting, hasVerbatimResponseVary, isConfiguredCdnResponsePolicyHeader, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision, supportsCanonicalRscWarmup } from "vinext/internal/config/prerender";
19
27
  import { detectProject, findInNodeModules, formatMissingCloudflarePluginError, getMissingDeps } from "vinext/internal/utils/project";
20
28
  import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
21
29
  //#region src/deploy.ts
@@ -28,6 +36,7 @@ import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
28
36
  * 2. Runs the Vite build
29
37
  * 3. Deploys to Cloudflare Workers via Wrangler
30
38
  */
39
+ const DEFAULT_CDN_WARM_PROMOTION_DELAY_MS = 15e3;
31
40
  function parsePositiveIntegerArg(raw, flag) {
32
41
  if (raw === "") throw new Error(`${flag} requires a value, but none was provided.`);
33
42
  const parsed = Number(raw);
@@ -40,10 +49,31 @@ function parseNonNegativeIntegerArg(raw, flag) {
40
49
  if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
41
50
  return parsed;
42
51
  }
52
+ const MAX_NODE_TIMER_DELAY_MS = 2147483647;
53
+ function validateTimerDelay(value, flag, raw = String(value)) {
54
+ if (!Number.isInteger(value) || value < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
55
+ if (value > MAX_NODE_TIMER_DELAY_MS) throw new Error(`${flag} must not exceed ${MAX_NODE_TIMER_DELAY_MS} milliseconds, but got "${raw}".`);
56
+ return value;
57
+ }
58
+ function validatePromotionDelay(value, raw = String(value)) {
59
+ return validateTimerDelay(value, "--warm-cdn-promotion-delay", raw);
60
+ }
61
+ function validateCdnWarmTarget(raw) {
62
+ const value = raw.trim();
63
+ let url;
64
+ try {
65
+ url = new URL(value);
66
+ } catch {
67
+ throw new Error(`--warm-cdn-target expects an HTTPS origin, but got "${raw}".`);
68
+ }
69
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.port !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error(`--warm-cdn-target expects an HTTPS origin without a path, query, credentials, or port, but got "${raw}".`);
70
+ return url.origin;
71
+ }
43
72
  function formatUnknownError(error) {
44
73
  if (error instanceof Error && error.message) return error.message;
45
74
  return String(error);
46
75
  }
76
+ var StagedWarmupError = class extends Error {};
47
77
  /** Deploy command flag definitions for util.parseArgs. */
48
78
  const deployArgOptions = {
49
79
  help: {
@@ -66,6 +96,10 @@ const deployArgOptions = {
66
96
  type: "boolean",
67
97
  default: false
68
98
  },
99
+ verbose: {
100
+ type: "boolean",
101
+ default: false
102
+ },
69
103
  "prerender-all": {
70
104
  type: "boolean",
71
105
  default: false
@@ -75,17 +109,46 @@ const deployArgOptions = {
75
109
  type: "boolean",
76
110
  default: false
77
111
  },
112
+ "warm-cdn-target": { type: "string" },
78
113
  "warm-cdn-concurrency": { type: "string" },
79
114
  "warm-cdn-timeout": { type: "string" },
80
115
  "warm-cdn-retries": { type: "string" },
81
- "warm-cdn-strict": {
116
+ "warm-cdn-discovery-timeout": { type: "string" },
117
+ "warm-cdn-discovery-retries": { type: "string" },
118
+ "warm-cdn-probe-timeout": { type: "string" },
119
+ "warm-cdn-probe-retries": { type: "string" },
120
+ "warm-cdn-certify": {
121
+ type: "boolean",
122
+ default: false
123
+ },
124
+ "warm-cdn-readiness-timeout": { type: "string" },
125
+ "warm-cdn-readiness-retries": { type: "string" },
126
+ "warm-cdn-readiness-probes": { type: "string" },
127
+ "warm-cdn-readiness-probe-delay": { type: "string" },
128
+ "dangerously-promote-on-cdn-warm-error": {
129
+ type: "boolean",
130
+ default: false
131
+ },
132
+ "no-promote": {
133
+ type: "boolean",
134
+ default: false
135
+ },
136
+ "warm-cdn-no-promote": {
82
137
  type: "boolean",
83
138
  default: false
84
139
  },
140
+ "warm-cdn-promotion-delay": { type: "string" },
85
141
  "warm-cdn-include-fallbacks": {
86
142
  type: "boolean",
87
143
  default: false
88
144
  },
145
+ "experimental-traffic-aware-warm-cache": {
146
+ type: "boolean",
147
+ default: false
148
+ },
149
+ "traffic-aware-coverage": { type: "string" },
150
+ "traffic-aware-limit": { type: "string" },
151
+ "traffic-aware-window": { type: "string" },
89
152
  "experimental-tpr": {
90
153
  type: "boolean",
91
154
  default: false
@@ -100,6 +163,8 @@ function parseDeployArgs(args) {
100
163
  options: deployArgOptions,
101
164
  strict: true
102
165
  });
166
+ if (values["warm-cdn-certify"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-certify requires --experimental-warm-cdn-cache.");
167
+ if (values["warm-cdn-target"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
103
168
  function parseIntArg(name, raw) {
104
169
  if (!raw) return void 0;
105
170
  const n = parseInt(raw, 10);
@@ -117,18 +182,31 @@ function parseDeployArgs(args) {
117
182
  config: values.config?.trim() || void 0,
118
183
  skipBuild: values["skip-build"],
119
184
  dryRun: values["dry-run"],
185
+ verbose: values.verbose,
120
186
  prerenderAll: values["prerender-all"],
121
187
  prerenderConcurrency: values["prerender-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["prerender-concurrency"], "--prerender-concurrency"),
122
188
  warmCdnCache: values["experimental-warm-cdn-cache"],
189
+ warmCdnTarget: values["warm-cdn-target"] === void 0 ? void 0 : validateCdnWarmTarget(values["warm-cdn-target"]),
123
190
  warmCdnConcurrency: values["warm-cdn-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-concurrency"], "--warm-cdn-concurrency"),
124
191
  warmCdnTimeout: values["warm-cdn-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-timeout"], "--warm-cdn-timeout"),
125
192
  warmCdnRetries: values["warm-cdn-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-retries"], "--warm-cdn-retries"),
126
- warmCdnStrict: values["warm-cdn-strict"],
193
+ warmCdnDiscoveryTimeout: values["warm-cdn-discovery-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-discovery-timeout"], "--warm-cdn-discovery-timeout"),
194
+ warmCdnDiscoveryRetries: values["warm-cdn-discovery-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-discovery-retries"], "--warm-cdn-discovery-retries"),
195
+ warmCdnProbeTimeout: values["warm-cdn-probe-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-probe-timeout"], "--warm-cdn-probe-timeout"),
196
+ warmCdnProbeRetries: values["warm-cdn-probe-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-probe-retries"], "--warm-cdn-probe-retries"),
197
+ warmCdnCertify: values["warm-cdn-certify"],
198
+ warmCdnReadinessTimeout: values["warm-cdn-readiness-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-timeout"], "--warm-cdn-readiness-timeout"),
199
+ warmCdnReadinessRetries: values["warm-cdn-readiness-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-readiness-retries"], "--warm-cdn-readiness-retries"),
200
+ warmCdnReadinessProbes: values["warm-cdn-readiness-probes"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-probes"], "--warm-cdn-readiness-probes"),
201
+ warmCdnReadinessProbeDelay: values["warm-cdn-readiness-probe-delay"] === void 0 ? void 0 : validateTimerDelay(parseNonNegativeIntegerArg(values["warm-cdn-readiness-probe-delay"], "--warm-cdn-readiness-probe-delay"), "--warm-cdn-readiness-probe-delay", values["warm-cdn-readiness-probe-delay"]),
202
+ dangerouslyPromoteOnCdnWarmError: values["dangerously-promote-on-cdn-warm-error"],
203
+ warmCdnPromote: !values["no-promote"] && !values["warm-cdn-no-promote"],
204
+ warmCdnPromotionDelay: values["warm-cdn-promotion-delay"] === void 0 ? void 0 : validatePromotionDelay(parseNonNegativeIntegerArg(values["warm-cdn-promotion-delay"], "--warm-cdn-promotion-delay"), values["warm-cdn-promotion-delay"]),
127
205
  warmCdnIncludeFallbacks: values["warm-cdn-include-fallbacks"],
128
- experimentalTPR: values["experimental-tpr"],
129
- tprCoverage: parseIntArg("tpr-coverage", values["tpr-coverage"]),
130
- tprLimit: parseIntArg("tpr-limit", values["tpr-limit"]),
131
- tprWindow: parseIntArg("tpr-window", values["tpr-window"])
206
+ experimentalTPR: values["experimental-traffic-aware-warm-cache"] || values["experimental-tpr"],
207
+ tprCoverage: parseIntArg("traffic-aware-coverage", values["traffic-aware-coverage"] ?? values["tpr-coverage"]),
208
+ tprLimit: parseIntArg("traffic-aware-limit", values["traffic-aware-limit"] ?? values["tpr-limit"]),
209
+ tprWindow: parseIntArg("traffic-aware-window", values["traffic-aware-window"] ?? values["tpr-window"])
132
210
  };
133
211
  }
134
212
  /**
@@ -164,6 +242,32 @@ async function loadProjectViteApi(root) {
164
242
  }
165
243
  return await (vitePath === "vite" ? import(vitePath) : import(pathToFileURL(vitePath).href));
166
244
  }
245
+ async function loadProjectWranglerApi(root) {
246
+ const req = createRequire(path.join(root, "package.json"));
247
+ let wranglerPath;
248
+ try {
249
+ wranglerPath = req.resolve("wrangler");
250
+ } catch {
251
+ const cloudflarePluginPath = req.resolve("@cloudflare/vite-plugin");
252
+ wranglerPath = createRequire(cloudflarePluginPath).resolve("wrangler");
253
+ }
254
+ return await import(
255
+ /* @vite-ignore */
256
+ pathToFileURL(wranglerPath).href
257
+ );
258
+ }
259
+ async function loadDeployViteConfigMetadata(root) {
260
+ const plugins = (await (await loadProjectViteApi(root)).loadConfigFromFile({
261
+ command: "build",
262
+ mode: "production"
263
+ }, void 0, root))?.config.plugins;
264
+ return {
265
+ cacheConfig: await findVinextCacheConfigInPlugins(plugins),
266
+ nextConfig: await findVinextNextConfigInPlugins(plugins),
267
+ prerenderConfig: await findVinextPrerenderConfigInPlugins(plugins),
268
+ routeRootConfig: await findVinextRouteRootConfigInPlugins(plugins)
269
+ };
270
+ }
167
271
  async function runBuild(info, env) {
168
272
  console.log("\n Building for Cloudflare Workers...\n");
169
273
  const { createBuilder } = await loadProjectViteApi(info.root);
@@ -171,10 +275,8 @@ async function runBuild(info, env) {
171
275
  await (await createBuilder({ root: info.root })).buildApp();
172
276
  });
173
277
  }
174
- async function populateKVCacheFromPrerenderedArtifacts(root, buildEnv, wranglerEnv) {
175
- if (!viteConfigHasCacheAdapter(root)) return;
176
- const vite = await loadProjectViteApi(root);
177
- const kvConfig = resolveKvDataAdapterConfig(await withCloudflareEnv(buildEnv, () => loadVinextCacheConfigFromViteConfig(vite, root)));
278
+ async function populateKVCacheFromPrerenderedArtifacts(root, wranglerEnv, cacheConfig) {
279
+ const kvConfig = resolveKvDataAdapterConfig(cacheConfig);
178
280
  if (!kvConfig) return;
179
281
  const { routeCount, pairs } = buildPrerenderKVPairs(path.join(root, "dist", "server"), {
180
282
  appPrefix: kvConfig.appPrefix,
@@ -299,6 +401,7 @@ async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutab
299
401
  }
300
402
  }
301
403
  async function runWranglerDeploy(root, options, execute = spawn) {
404
+ if (options.promote === false) return runWranglerVersionUpload(root, options).previewUrl ?? "(Preview URL not detected in wrangler output)";
302
405
  const spawnOptions = {
303
406
  cwd: root,
304
407
  stdio: [
@@ -334,90 +437,548 @@ async function runWranglerDeploy(root, options, execute = spawn) {
334
437
  reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
335
438
  });
336
439
  });
337
- return parseWorkersDevUrl(output) ?? "(URL not detected in wrangler output)";
440
+ return parseWorkerDeploymentUrl(output) ?? "(URL not detected in wrangler output)";
441
+ }
442
+ function hasCdnWarmRequests(plan) {
443
+ return plan.paths.length + (plan.pagesDataPaths?.length ?? 0) + (plan.routeHandlerPaths?.length ?? 0) + plan.rscPaths.length + plan.loadingShellPaths.length > 0;
444
+ }
445
+ function selectTPRWarmPlan(plan, traffic, coverage, limit) {
446
+ const canonical = (pathname) => normalizePathTrailingSlash(pathname, false);
447
+ const resolved = new Set(Object.entries(plan.routePatterns ?? {}).map(([pathname, route]) => canonical(route.cacheabilityProbe?.concretePathname ?? pathname)));
448
+ const requestsByPath = /* @__PURE__ */ new Map();
449
+ for (const { path, requests } of traffic) {
450
+ const pathname = canonical(path);
451
+ if (!resolved.has(pathname)) continue;
452
+ requestsByPath.set(pathname, (requestsByPath.get(pathname) ?? 0) + requests);
453
+ }
454
+ const selected = new Set(selectRoutes(Array.from(requestsByPath, ([path, requests]) => ({
455
+ path,
456
+ requests
457
+ })).sort((a, b) => b.requests - a.requests), coverage, limit).routes.map(({ path }) => path));
458
+ const includes = (pathname) => selected.has(canonical(plan.routePatterns?.[pathname]?.cacheabilityProbe?.concretePathname ?? pathname));
459
+ const filter = (pathnames) => pathnames?.filter(includes);
460
+ return {
461
+ ...plan,
462
+ appPaths: filter(plan.appPaths),
463
+ loadingShellPaths: filter(plan.loadingShellPaths) ?? [],
464
+ pagesDataPaths: filter(plan.pagesDataPaths),
465
+ pagesPaths: filter(plan.pagesPaths),
466
+ paths: filter(plan.paths) ?? [],
467
+ routeHandlerPaths: filter(plan.routeHandlerPaths),
468
+ routePatterns: plan.routePatterns ? Object.fromEntries(Object.entries(plan.routePatterns).filter(([pathname]) => includes(pathname))) : void 0,
469
+ rscPaths: filter(plan.rscPaths) ?? []
470
+ };
471
+ }
472
+ function projectRequiresRouteCacheabilityProbeManifest(project, cacheConfig) {
473
+ return (project.isAppRouter || project.isPagesRouter) && requiresRouteCacheabilityProbeManifest(cacheConfig);
474
+ }
475
+ function cdnWarmTargetKey(target) {
476
+ return `${target.kind}\0${target.sourcePathname}`;
477
+ }
478
+ function requiredCdnWarmTargetKeys(plan, optionalTargetKeys) {
479
+ const keys = /* @__PURE__ */ new Set([
480
+ ...plan.paths.map((pathname) => `html\0${pathname}`),
481
+ ...plan.pagesDataPaths.map((pathname) => `pages-data\0${pathname}`),
482
+ ...(plan.routeHandlerPaths ?? []).map((pathname) => `app-route\0${pathname}`),
483
+ ...plan.rscPaths.map((pathname) => `rsc-full\0${pathname}`),
484
+ ...plan.loadingShellPaths.map((pathname) => `rsc-loading-shell\0${pathname}`)
485
+ ]);
486
+ if (optionalTargetKeys) for (const key of optionalTargetKeys) keys.delete(key);
487
+ return keys;
338
488
  }
339
489
  async function deployWithCdnWarmup(root, paths, options) {
340
- const upload = runWranglerVersionUpload(root, options);
490
+ if (options.warmCdnTarget !== void 0) options = {
491
+ ...options,
492
+ warmCdnTarget: validateCdnWarmTarget(options.warmCdnTarget)
493
+ };
494
+ if (options.warmCdnDiscoveryTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnDiscoveryTimeout), "--warm-cdn-discovery-timeout");
495
+ if (options.warmCdnDiscoveryRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnDiscoveryRetries), "--warm-cdn-discovery-retries");
496
+ if (options.warmCdnProbeTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnProbeTimeout), "--warm-cdn-probe-timeout");
497
+ if (options.warmCdnProbeRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnProbeRetries), "--warm-cdn-probe-retries");
498
+ if (options.warmCdnReadinessTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessTimeout), "--warm-cdn-readiness-timeout");
499
+ if (options.warmCdnReadinessRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnReadinessRetries), "--warm-cdn-readiness-retries");
500
+ if (options.warmCdnReadinessProbes !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessProbes), "--warm-cdn-readiness-probes");
501
+ if (options.warmCdnReadinessProbeDelay !== void 0) validateTimerDelay(options.warmCdnReadinessProbeDelay, "--warm-cdn-readiness-probe-delay");
502
+ if (options.warmCdnPromotionDelay !== void 0) validatePromotionDelay(options.warmCdnPromotionDelay);
503
+ if (options.cacheabilityProbe) return deployWithCacheabilityProbe(root, options);
504
+ return deployUploadedVersionWithCdnWarmup(root, paths, options);
505
+ }
506
+ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
507
+ const allowUnverifiedPromotion = options.dangerouslyPromoteOnCdnWarmError === true && options.warmCdnCertify !== true;
508
+ let deploymentId = options.deploymentId;
509
+ let expectedBuildId = options.expectedBuildId;
510
+ let expectedRscBuildId = options.expectedRscBuildId;
511
+ const hasPreparedWarmPlan = options.uploadedVersion !== void 0;
512
+ let warmPlanDiscovered = hasPreparedWarmPlan || options.discoverWarmPlan === void 0;
513
+ let remainingWarmPlan = {
514
+ loadingShellPaths: [...options.loadingShellPaths ?? []],
515
+ pagesDataPaths: [...options.pagesDataPaths ?? []],
516
+ paths: [...paths],
517
+ routeHandlerPaths: [...options.routeHandlerPaths ?? []],
518
+ routePatterns: options.routePatterns ? { ...options.routePatterns } : void 0,
519
+ rscPaths: [...options.rscPaths ?? []]
520
+ };
521
+ let discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
522
+ const prepareWarmPlan = (plan) => {
523
+ if (plan.paths.length === 0 && plan.pagesDataPaths.length === 0 && (plan.routeHandlerPaths?.length ?? 0) === 0 || expectedBuildId !== void 0) return plan;
524
+ if (!allowUnverifiedPromotion) {
525
+ const warmupKind = plan.paths.length > 0 ? "CDN HTML warmup" : plan.pagesDataPaths.length > 0 ? "CDN Pages data warmup" : "CDN Route Handler warmup";
526
+ throw new Error(`${warmupKind} requires a CDN adapter that declares build-identity response headers. Configure that adapter capability or deploy without --experimental-warm-cdn-cache.`);
527
+ }
528
+ console.warn(` CDN warmup: skipping ${plan.paths.length} HTML, ${plan.pagesDataPaths.length} Pages data, and ${plan.routeHandlerPaths?.length ?? 0} Route Handler request(s) because the CDN adapter does not declare build-identity response headers.`);
529
+ return {
530
+ ...plan,
531
+ pagesDataPaths: [],
532
+ paths: [],
533
+ routeHandlerPaths: []
534
+ };
535
+ };
536
+ const discoverWarmPlan = async (targetUrl, headers) => {
537
+ if (!options.discoverWarmPlan) return;
538
+ const discovered = await options.discoverWarmPlan({
539
+ headers,
540
+ targetUrl
541
+ });
542
+ const plan = options.selectWarmPlan?.(discovered) ?? discovered;
543
+ deploymentId = plan.deploymentId;
544
+ expectedBuildId = plan.buildIdentity;
545
+ expectedRscBuildId = plan.rscBuildId;
546
+ remainingWarmPlan = {
547
+ loadingShellPaths: [...plan.loadingShellPaths],
548
+ pagesDataPaths: [...plan.pagesDataPaths ?? []],
549
+ paths: [...plan.paths],
550
+ routeHandlerPaths: [...plan.routeHandlerPaths ?? []],
551
+ routePatterns: plan.routePatterns ? { ...plan.routePatterns } : void 0,
552
+ rscPaths: [...plan.rscPaths]
553
+ };
554
+ discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
555
+ warmPlanDiscovered = true;
556
+ };
557
+ if (!options.discoverWarmPlan || hasPreparedWarmPlan) remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
558
+ const upload = options.uploadedVersion ?? runWranglerVersionUpload(root, options);
559
+ const warmUploadedVersion = (targetUrl, headers, propagatingTarget = false, plan = {
560
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
561
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
562
+ paths: remainingWarmPlan.paths,
563
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
564
+ routePatterns: remainingWarmPlan.routePatterns,
565
+ rscPaths: remainingWarmPlan.rscPaths
566
+ }, requireCacheHit = false) => warmCdnCache({
567
+ targetUrl,
568
+ paths: plan.paths,
569
+ headers,
570
+ propagatingTarget,
571
+ deploymentId,
572
+ expectedBuildId,
573
+ expectedRscBuildId,
574
+ loadingShellPaths: plan.loadingShellPaths,
575
+ pagesDataPaths: plan.pagesDataPaths,
576
+ routeHandlerPaths: plan.routeHandlerPaths,
577
+ routePatterns: plan.routePatterns,
578
+ rscPaths: plan.rscPaths,
579
+ concurrency: options.warmCdnConcurrency,
580
+ timeoutMs: options.warmCdnTimeout,
581
+ retries: options.warmCdnRetries,
582
+ retrySkippedTargetKeys: propagatingTarget && hasPreparedWarmPlan ? requiredCdnWarmTargetKeys(plan, options.optionalWarmTargetKeys) : void 0,
583
+ requireCacheHit,
584
+ strict: requireCacheHit || !allowUnverifiedPromotion,
585
+ statusSource: options.statusSource
586
+ });
341
587
  const wranglerConfig = parseWranglerConfig(root, options.config);
342
- const stagingTraffic = getZeroPercentStagingTraffic(readWranglerDeploymentStatus(root, options), upload.versionId);
588
+ let deploymentStatus;
589
+ try {
590
+ deploymentStatus = runWranglerDeploymentStatus(root, options);
591
+ } catch (error) {
592
+ if (!hasPreparedWarmPlan) throw error;
593
+ throw new StagedWarmupError(formatUnknownError(error), { cause: error });
594
+ }
595
+ if (options.expectedDeploymentState && !deploymentStateEquals(deploymentStatus, options.expectedDeploymentState)) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed before the final version could be staged. No final version was promoted.");
596
+ const stagingTraffic = getZeroPercentStagingTraffic(deploymentStatus, upload.versionId);
597
+ if (hasPreparedWarmPlan && !stagingTraffic) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic changed before the final version could be staged. No final version was promoted.");
343
598
  let staged = null;
344
- let triggersDeployedUrl = null;
345
- let warmedBeforePromotion = false;
346
- let triggersApplied = false;
599
+ let triggersDeployedUrl = options.triggersDeployedUrl ?? null;
600
+ let stagedCacheFilled = false;
601
+ let stagedDeploymentState = null;
602
+ const initialWarmRequests = options.discoverWarmPlan === void 0 || hasPreparedWarmPlan ? remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length : 1;
603
+ let triggersApplied = options.triggersAlreadyApplied === true;
347
604
  function applyTriggers() {
348
605
  if (triggersApplied) return;
349
606
  triggersDeployedUrl = runWranglerTriggersDeploy(root, options).deployedUrl;
350
607
  triggersApplied = true;
351
608
  }
352
609
  if (stagingTraffic) {
353
- staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
354
610
  try {
611
+ staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
612
+ } catch (error) {
613
+ throw reconcileVersionDeployFailure(root, options, error, {
614
+ desiredTraffic: stagingTraffic,
615
+ desiredDescription: "The uploaded version is staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes were not changed.",
616
+ priorState: deploymentStatus,
617
+ priorDescription: "Worker deployment traffic was not changed and Worker triggers/routes were not changed."
618
+ });
619
+ }
620
+ try {
621
+ if (hasPreparedWarmPlan) {
622
+ stagedDeploymentState = runWranglerDeploymentStatus(root, options);
623
+ if (!deploymentTrafficEquals(stagedDeploymentState.versions, stagingTraffic)) throw new Error("Two-stage CDN warming stopped because Worker deployment traffic changed before production triggers could be applied. No final version was promoted.");
624
+ }
355
625
  applyTriggers();
356
626
  } catch (error) {
357
627
  throw withStagedVersionCleanupNote(error);
358
628
  }
359
- const targetUrl = resolveCdnWarmupTargetUrl(root, staged.deployedUrl ?? triggersDeployedUrl, options);
360
- const headers = buildVersionOverrideHeaders(resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
361
- if (targetUrl && headers) {
362
- try {
363
- await warmCdnCache({
629
+ const targetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? staged.deployedUrl;
630
+ const headers = buildVersionOverrideHeaders(options.name ?? upload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
631
+ if (targetUrl && headers) try {
632
+ if (!warmPlanDiscovered) {
633
+ console.log(" CDN warmup: discovering paths from the staged Worker version...");
634
+ await discoverWarmPlan(targetUrl, headers);
635
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
636
+ console.log(` CDN warmup: discovered ${remainingWarmPlan.paths.length} HTML, ${remainingWarmPlan.pagesDataPaths.length} Pages data, ${remainingWarmPlan.routeHandlerPaths?.length ?? 0} Route Handler, ${remainingWarmPlan.rscPaths.length} RSC, and ${remainingWarmPlan.loadingShellPaths.length} loading-shell request(s).`);
637
+ }
638
+ const stagedWarmPlan = {
639
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
640
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
641
+ paths: remainingWarmPlan.paths,
642
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
643
+ routePatterns: remainingWarmPlan.routePatterns,
644
+ rscPaths: remainingWarmPlan.rscPaths
645
+ };
646
+ const stagedWarmRequests = stagedWarmPlan.paths.length + stagedWarmPlan.pagesDataPaths.length + (stagedWarmPlan.routeHandlerPaths?.length ?? 0) + stagedWarmPlan.rscPaths.length + stagedWarmPlan.loadingShellPaths.length;
647
+ if (stagedWarmRequests > 0) {
648
+ console.log(" CDN warmup: waiting for the staged Worker version to become stable...");
649
+ const readiness = await waitForCdnWarmTargetReadiness({
364
650
  targetUrl,
365
- paths,
366
651
  headers,
367
- concurrency: options.warmCdnConcurrency,
368
- timeoutMs: options.warmCdnTimeout,
369
- retries: options.warmCdnRetries,
370
- strict: options.warmCdnStrict
652
+ plan: stagedWarmPlan,
653
+ prerenderSecret: options.prerenderSecret,
654
+ deploymentId,
655
+ expectedBuildId,
656
+ expectedRscBuildId,
657
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
658
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
659
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
660
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
661
+ timeoutMs: options.warmCdnTimeout
371
662
  });
663
+ if (!readiness.ready) {
664
+ const message = `CDN warmup could not verify staged Worker readiness: ${readiness.error}.`;
665
+ const noPromoteNote = options.warmCdnPromote === false ? " CDN warmup cannot continue because promotion is disabled and the staged version was not warmed." : "";
666
+ if (!allowUnverifiedPromotion || options.warmCdnPromote === false) throw new Error(`${message}${noPromoteNote}`);
667
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
668
+ } else {
669
+ console.log(" CDN warmup: staged Worker version is stable.");
670
+ const warmResult = await warmUploadedVersion(targetUrl, headers, true, stagedWarmPlan);
671
+ const optionalSkipped = options.optionalWarmTargetKeys ? warmResult.skippedTargets.filter((target) => options.optionalWarmTargetKeys.has(cdnWarmTargetKey(target))).length : 0;
672
+ if (hasPreparedWarmPlan && options.warmCdnCertify) {
673
+ if (warmResult.warmed + optionalSkipped !== stagedWarmRequests) throw new Error(`CDN warmup cannot certify the staged cache because only ${warmResult.warmed}/${stagedWarmRequests - optionalSkipped} cacheable entries completed their initial fill.`);
674
+ }
675
+ const requiredSkipped = warmResult.skipped - optionalSkipped;
676
+ if (hasPreparedWarmPlan && requiredSkipped > 0) {
677
+ const message = `Two-stage CDN warming could not fill ${requiredSkipped}/${warmResult.total} planned cache entries because Cloudflare refused cache admission.`;
678
+ if (!allowUnverifiedPromotion) throw new Error(message);
679
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
680
+ }
681
+ if (optionalSkipped > 0) console.log(` CDN warmup: ${optionalSkipped} paired representation${optionalSkipped === 1 ? " remained" : "s remained"} private after ${optionalSkipped === 1 ? "its" : "their"} final render and will not be cached.`);
682
+ remainingWarmPlan = {
683
+ loadingShellPaths: warmResult.retryPlan.loadingShellPaths,
684
+ pagesDataPaths: warmResult.retryPlan.pagesDataPaths,
685
+ paths: warmResult.retryPlan.paths,
686
+ routeHandlerPaths: warmResult.retryPlan.routeHandlerPaths,
687
+ rscPaths: warmResult.retryPlan.rscPaths
688
+ };
689
+ if (options.warmCdnCertify && warmResult.warmed > 0) {
690
+ console.log(` CDN warmup: certifying ${warmResult.warmed} staged cache entr${warmResult.warmed === 1 ? "y" : "ies"} before promotion...`);
691
+ const certification = await warmUploadedVersion(targetUrl, headers, true, warmResult.warmedPlan, true);
692
+ if (certification.warmed !== warmResult.warmed) throw new Error(`CDN warmup certified ${certification.warmed}/${warmResult.warmed} staged cache entries as reusable.`);
693
+ stagedCacheFilled = true;
694
+ } else stagedCacheFilled = warmResult.warmed > 0;
695
+ }
696
+ }
697
+ } catch (error) {
698
+ throw withStagedVersionCleanupNote(error);
699
+ }
700
+ else if (initialWarmRequests > 0) {
701
+ const message = "CDN warmup failed: pre-traffic warmup needs a production URL and Worker name for version overrides. Configure a route/custom domain and Worker name, or deploy without --experimental-warm-cdn-cache.";
702
+ if (!allowUnverifiedPromotion) throw new StagedWarmupError(`${message} ${getStagedVersionCleanupNote()}`);
703
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
704
+ }
705
+ } else {
706
+ if (initialWarmRequests > 0) {
707
+ const message = "CDN warmup cannot stage the uploaded Worker at 0% because the current deployment is not exactly one version serving 100% traffic.";
708
+ if (!allowUnverifiedPromotion) throw new Error(`${message} No traffic or triggers were changed.`);
709
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
710
+ }
711
+ console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not one version serving 100% traffic.");
712
+ }
713
+ const countRemainingWarmRequests = () => remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
714
+ if (options.warmCdnPromote === false) {
715
+ if (!staged) throw new Error("CDN warmup cannot skip promotion because the uploaded Worker version could not be staged at 0% traffic. The current deployment must have exactly one version serving 100% traffic.");
716
+ const remainingWarmRequests = countRemainingWarmRequests();
717
+ if (!options.allowEmptyWarmPlan && !hasPreparedWarmPlan && options.discoverWarmPlan && discoveredWarmRequests === 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error("CDN warmup cannot skip promotion because no build-discovered requests were found to warm."));
718
+ if (remainingWarmRequests > 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error(`CDN warmup cannot skip promotion because ${remainingWarmRequests} request(s) remain unwarmed.`));
719
+ if (hasPreparedWarmPlan && stagedDeploymentState) {
720
+ let currentDeployment;
721
+ try {
722
+ currentDeployment = runWranglerDeploymentStatus(root, options);
372
723
  } catch (error) {
373
- throw withStagedVersionCleanupNote(error);
724
+ throw new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} CDN warmup cannot confirm that the uploaded Worker remains staged at 0% traffic; production Worker triggers/routes remain applied. Inspect \`wrangler deployments status\` before continuing.`, { cause: error });
374
725
  }
375
- warmedBeforePromotion = true;
376
- } else if (options.warmCdnStrict) throw new Error("CDN warmup failed: pre-traffic warmup needs a production URL and Worker name for version overrides. Configure a route/custom domain and Worker name, or rerun without --warm-cdn-strict. " + getStagedVersionCleanupNote());
377
- } else console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not a single 100% version.");
378
- const deployed = runWranglerVersionDeploy(root, [{
726
+ if (!deploymentStateEquals(currentDeployment, stagedDeploymentState)) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed before the no-promote handoff. The uploaded Worker is not confirmed staged at 0% traffic; production Worker triggers/routes remain applied.");
727
+ }
728
+ console.log(` CDN warmup: promotion disabled; uploaded Worker version ${upload.versionId} remains staged at 0% traffic and production Worker triggers/routes remain applied.`);
729
+ return staged.deployedUrl ?? triggersDeployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
730
+ }
731
+ let deployed;
732
+ let prePromotionState = null;
733
+ const promotionTraffic = [{
379
734
  versionId: upload.versionId,
380
735
  percentage: 100
381
- }], options, warmedBeforePromotion ? "promote-warmed" : "promote-uploaded");
382
- if (!warmedBeforePromotion) {
383
- try {
384
- applyTriggers();
385
- } catch (error) {
386
- throw withPromotedVersionTriggerNote(error);
736
+ }];
737
+ let promotionAttempted = false;
738
+ try {
739
+ if (stagedCacheFilled && options.statusSource !== "vinext") {
740
+ const promotionDelay = options.warmCdnPromotionDelay ?? 15e3;
741
+ if (promotionDelay > 0) {
742
+ console.log(` CDN warmup: waiting ${promotionDelay / 1e3} seconds for cache propagation before promotion...`);
743
+ await setTimeout(promotionDelay);
744
+ }
387
745
  }
388
- const targetUrl = resolveCdnWarmupTargetUrl(root, deployed.deployedUrl ?? triggersDeployedUrl, options);
389
- if (targetUrl) await warmCdnCache({
390
- targetUrl,
391
- paths,
392
- concurrency: options.warmCdnConcurrency,
393
- timeoutMs: options.warmCdnTimeout,
394
- retries: options.warmCdnRetries,
395
- strict: options.warmCdnStrict
746
+ if (hasPreparedWarmPlan && stagingTraffic) {
747
+ prePromotionState = runWranglerDeploymentStatus(root, options);
748
+ if (!deploymentTrafficEquals(prePromotionState.versions, stagingTraffic) || stagedDeploymentState && !deploymentStateEquals(prePromotionState, stagedDeploymentState)) throw new Error("Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed before the final version could be promoted. No final version was promoted.");
749
+ }
750
+ promotionAttempted = true;
751
+ deployed = runWranglerVersionDeploy(root, promotionTraffic, options, stagedCacheFilled ? "promote-warmed" : "promote-uploaded");
752
+ } catch (error) {
753
+ if (promotionAttempted) throw reconcileVersionDeployFailure(root, options, error, {
754
+ desiredTraffic: promotionTraffic,
755
+ desiredDescription: "The uploaded version is already promoted to 100%; Worker triggers/routes may already have changed.",
756
+ priorState: prePromotionState ?? (stagingTraffic ? {
757
+ deploymentId: null,
758
+ output: "",
759
+ versions: stagingTraffic
760
+ } : deploymentStatus),
761
+ priorDescription: "The uploaded version remains staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes may already have changed."
396
762
  });
397
- else if (options.warmCdnStrict) throw new Error("CDN warmup failed: no production URL could be inferred from wrangler config or output. Configure a route/custom domain, ensure Wrangler prints a workers.dev URL, or rerun without --warm-cdn-strict.");
398
- else console.warn(" CDN warmup skipped: no production URL could be inferred from wrangler config or output.");
763
+ throw staged ? withStagedVersionCleanupNote(error) : error;
764
+ }
765
+ try {
766
+ applyTriggers();
767
+ } catch (error) {
768
+ throw withPromotedVersionTriggerNote(error);
769
+ }
770
+ let postPromotionTargetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? deployed.deployedUrl;
771
+ if (!warmPlanDiscovered && postPromotionTargetUrl) try {
772
+ console.log(" CDN warmup: discovering paths from the promoted Worker version...");
773
+ await discoverWarmPlan(postPromotionTargetUrl);
774
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
775
+ } catch (error) {
776
+ throw withPromotedVersionWarmupNote(error);
777
+ }
778
+ if (countRemainingWarmRequests() > 0) {
779
+ const targetUrl = postPromotionTargetUrl;
780
+ if (targetUrl) try {
781
+ await warmUploadedVersion(targetUrl, void 0, true, remainingWarmPlan);
782
+ } catch (error) {
783
+ throw withPromotedVersionWarmupNote(error);
784
+ }
785
+ else if (!allowUnverifiedPromotion) throw withPromotedVersionWarmupNote(/* @__PURE__ */ new Error("CDN warmup failed: no production URL could be inferred from wrangler config or output. Configure a route/custom domain, ensure Wrangler prints a workers.dev URL, or deploy without --experimental-warm-cdn-cache."));
786
+ else console.warn(" CDN warmup skipped: no production URL could be inferred after promotion; the dangerous override was enabled.");
399
787
  }
400
788
  return deployed.deployedUrl ?? triggersDeployedUrl ?? staged?.deployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
401
789
  }
402
- function resolveCdnWarmupTargetUrl(root, deployedUrl, options) {
403
- const config = parseWranglerConfig(root, options?.config);
404
- const env = getWranglerTargetEnv(options ?? {});
405
- const customDomain = (env ? config?.env?.[env]?.customDomain : void 0) ?? config?.customDomain;
406
- if (customDomain) return `https://${customDomain}`;
407
- return deployedUrl;
790
+ function deploymentTrafficEquals(actual, expected) {
791
+ const normalize = (traffic) => [...traffic].sort((a, b) => a.versionId.localeCompare(b.versionId));
792
+ const normalizedActual = normalize(actual);
793
+ const normalizedExpected = normalize(expected);
794
+ return normalizedActual.length === normalizedExpected.length && normalizedActual.every((version, index) => version.versionId === normalizedExpected[index]?.versionId && version.percentage === normalizedExpected[index]?.percentage);
408
795
  }
409
- function readWranglerDeploymentStatus(root, options) {
796
+ function deploymentStateEquals(actual, expected) {
797
+ if (expected.deploymentId !== null && actual.deploymentId !== expected.deploymentId) return false;
798
+ return deploymentTrafficEquals(actual.versions, expected.versions);
799
+ }
800
+ function reconcileVersionDeployFailure(root, options, error, expected) {
801
+ const message = error instanceof Error ? error.message : String(error);
802
+ let reconciliation;
803
+ let mayNeedCleanup = true;
410
804
  try {
411
- return runWranglerDeploymentStatus(root, options);
412
- } catch {
413
- return null;
805
+ const current = runWranglerDeploymentStatus(root, options);
806
+ if (deploymentTrafficEquals(current.versions, expected.desiredTraffic)) reconciliation = expected.desiredDescription;
807
+ else if (deploymentStateEquals(current, expected.priorState)) {
808
+ reconciliation = expected.priorDescription;
809
+ mayNeedCleanup = false;
810
+ } else reconciliation = "Worker deployment state differs from both the requested and prior state, so the deployment outcome is unknown or another deployment changed it.";
811
+ } catch (reconciliationError) {
812
+ reconciliation = `Worker deployment status could not be read after the command failed, so the deployment outcome is unknown (${reconciliationError instanceof Error ? reconciliationError.message : String(reconciliationError)}).`;
813
+ }
814
+ const detail = `${message} ${reconciliation}`;
815
+ return mayNeedCleanup ? new StagedWarmupError(detail, { cause: error }) : new Error(detail, { cause: error });
816
+ }
817
+ function assertDeploymentStateUnchanged(root, options, expected, message) {
818
+ if (!deploymentStateEquals(runWranglerDeploymentStatus(root, options), expected)) throw new Error(message);
819
+ }
820
+ async function deployWithCacheabilityProbe(root, options) {
821
+ if (!options.discoverWarmPlan) throw new Error("Two-stage CDN warming requires staged Worker route discovery before cacheability probing.");
822
+ const prerenderSecret = readPrerenderSecret(root);
823
+ const probeUpload = runWranglerVersionUpload(root, options);
824
+ const initialDeployment = runWranglerDeploymentStatus(root, options);
825
+ const probeTraffic = getZeroPercentStagingTraffic(initialDeployment, probeUpload.versionId);
826
+ if (!probeTraffic) throw new Error("Two-stage CDN warming cannot stage the probe Worker at 0% because the current deployment is not exactly one version serving 100% traffic. No traffic or triggers were changed.");
827
+ let stagedProbe;
828
+ try {
829
+ stagedProbe = runWranglerVersionDeploy(root, probeTraffic, options, "stage");
830
+ } catch (error) {
831
+ throw reconcileVersionDeployFailure(root, options, error, {
832
+ desiredTraffic: probeTraffic,
833
+ desiredDescription: "The probe version is staged at 0% with the previous version still serving 100% traffic; production Worker triggers/routes were not changed.",
834
+ priorState: initialDeployment,
835
+ priorDescription: "Worker deployment traffic was not changed and production Worker triggers/routes were not changed."
836
+ });
414
837
  }
838
+ let stagedProbeDeployment;
839
+ try {
840
+ stagedProbeDeployment = runWranglerDeploymentStatus(root, options);
841
+ if (!deploymentTrafficEquals(stagedProbeDeployment.versions, probeTraffic)) throw new Error("Two-stage CDN warming stopped because Worker deployment traffic changed immediately after the probe version was staged.");
842
+ } catch (error) {
843
+ throw new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} The probe staging command completed, but its current deployment state could not be confirmed; production Worker triggers/routes were not changed. Inspect \`wrangler deployments status\` and remove the staged probe version if necessary.`, { cause: error });
844
+ }
845
+ let prepared;
846
+ try {
847
+ const targetUrl = resolveCdnWarmupTargetUrl(root, stagedProbe.deployedUrl ?? probeUpload.previewUrl, options);
848
+ const wranglerConfig = parseWranglerConfig(root, options.config);
849
+ const headers = buildVersionOverrideHeaders(options.name ?? probeUpload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, options), probeUpload.versionId);
850
+ if (!targetUrl || !headers) throw new Error("Two-stage CDN warming needs a production URL and Worker name for version-overridden cacheability probes.");
851
+ console.log(" CDN warmup: discovering paths from the staged probe Worker...");
852
+ const discovered = await options.discoverWarmPlan({
853
+ headers,
854
+ targetUrl
855
+ });
856
+ if (!discovered.buildId) throw new Error("Two-stage CDN warming requires the staged discovery result to match dist/server/BUILD_ID.");
857
+ if (!discovered.buildIdentity) throw new Error("Two-stage CDN warming requires a CDN adapter that exposes the application build identity.");
858
+ const plan = {
859
+ ...discovered,
860
+ appPaths: discovered.appPaths ? [...discovered.appPaths] : void 0,
861
+ fallbackRoutePatterns: discovered.fallbackRoutePatterns ? [...discovered.fallbackRoutePatterns] : void 0,
862
+ loadingShellPaths: [...discovered.loadingShellPaths],
863
+ pagesDataPaths: [...discovered.pagesDataPaths ?? []],
864
+ pagesPaths: discovered.pagesPaths ? [...discovered.pagesPaths] : void 0,
865
+ paths: [...discovered.paths],
866
+ routeHandlerPaths: [...discovered.routeHandlerPaths ?? []],
867
+ routePatterns: discovered.routePatterns ? { ...discovered.routePatterns } : void 0,
868
+ rscPaths: [...discovered.rscPaths]
869
+ };
870
+ if (!plan.appPaths && !plan.pagesPaths) throw new Error("Two-stage CDN warming requires staged discovery to report App or Pages route ownership.");
871
+ const ownedHtmlPaths = /* @__PURE__ */ new Set([...plan.appPaths ?? [], ...plan.pagesPaths ?? []]);
872
+ plan.paths = plan.paths.filter((pathname) => ownedHtmlPaths.has(pathname));
873
+ const targets = await createCdnWarmTargets({
874
+ deploymentId: plan.deploymentId,
875
+ headers,
876
+ loadingShellPaths: plan.loadingShellPaths,
877
+ pagesDataPaths: plan.pagesDataPaths,
878
+ paths: plan.paths,
879
+ routeHandlerPaths: plan.routeHandlerPaths,
880
+ routePatterns: plan.routePatterns,
881
+ rscPaths: plan.rscPaths
882
+ });
883
+ const routePatternCount = new Set(targets.flatMap((target) => target.route ? [`${target.route.kind}\0${target.route.pattern}`] : [])).size;
884
+ const concreteRoutePathCount = new Set(targets.flatMap((target) => target.route ? [`${target.route.kind}\0${target.route.pattern}\0${target.route.cacheabilityProbe?.concretePathname ?? cacheabilityRoutePathname(target.pathname, target.kind)}`] : [])).size;
885
+ if (targets.length > 0) {
886
+ console.log(" CDN warmup: waiting for the staged probe Worker to become stable...");
887
+ const readiness = await waitForCdnWarmTargetReadiness({
888
+ targetUrl,
889
+ headers,
890
+ plan,
891
+ prerenderSecret,
892
+ deploymentId: plan.deploymentId,
893
+ expectedBuildId: plan.buildIdentity,
894
+ expectedRscBuildId: plan.rscBuildId,
895
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
896
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
897
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
898
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
899
+ timeoutMs: options.warmCdnTimeout
900
+ });
901
+ if (!readiness.ready) throw new Error(`Two-stage CDN warming could not verify staged probe Worker readiness: ${readiness.error}.`);
902
+ console.log(` CDN warmup: probing ${concreteRoutePathCount} concrete route path${concreteRoutePathCount === 1 ? "" : "s"} once across ${routePatternCount} pattern${routePatternCount === 1 ? "" : "s"}; filtering ${targets.length} candidate warm request identit${targets.length === 1 ? "y" : "ies"}...`);
903
+ } else {
904
+ const fallbackPatternCount = plan.fallbackRoutePatterns?.length ?? 0;
905
+ console.log(fallbackPatternCount > 0 ? ` CDN warmup: embedding ${fallbackPatternCount} on-demand static route pattern${fallbackPatternCount === 1 ? "" : "s"} without a speculative render.` : " CDN warmup: no page request identities were discovered; embedding an empty fail-closed cacheability manifest.");
906
+ }
907
+ const probeProgress = new CdnOperationProgress();
908
+ let probe;
909
+ try {
910
+ probe = await probeStagedWorkerCacheability({
911
+ buildId: discovered.buildId,
912
+ concurrency: options.warmCdnConcurrency,
913
+ expectedResponseBuildId: plan.buildIdentity,
914
+ fallbackRoutePatterns: plan.fallbackRoutePatterns,
915
+ phaseTimeoutMs: options.warmCdnProbeTimeout ?? 12e4,
916
+ retries: options.warmCdnProbeRetries ?? options.warmCdnRetries ?? 2,
917
+ retryDelayMs: DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS,
918
+ root,
919
+ targets,
920
+ targetUrl,
921
+ timeoutMs: options.warmCdnTimeout,
922
+ onProgress(progress) {
923
+ probeProgress.update(progress.completed, progress.total, `${progress.static} static, ${progress.dynamic} dynamic, ${progress.skipped} skipped`, "Probing route cacheability");
924
+ }
925
+ });
926
+ } finally {
927
+ probeProgress.finish();
928
+ }
929
+ console.log(` CDN warmup: classified ${probe.classified} route pattern${probe.classified === 1 ? "" : "s"} with ${probe.probed} render probe${probe.probed === 1 ? "" : "s"}; ${probe.dynamic} observed dynamic and ${probe.skipped} excluded by pattern-wide proof.`);
930
+ if (probe.failures.length > 0) throw new Error(`Two-stage CDN warming failed to classify ${probe.failures.length}/${concreteRoutePathCount} concrete route paths after ${probe.probed} render probe(s). First failure: ${probe.failures[0]}`);
931
+ const finalPlan = {
932
+ ...plan,
933
+ loadingShellPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-loading-shell").map((target) => target.sourcePathname),
934
+ pagesDataPaths: probe.cacheableTargets.filter((target) => target.kind === "pages-data").map((target) => target.sourcePathname),
935
+ paths: probe.cacheableTargets.filter((target) => target.kind === "html").map((target) => target.sourcePathname),
936
+ routeHandlerPaths: probe.cacheableTargets.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname),
937
+ rscPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname)
938
+ };
939
+ const selectedFinalPlan = options.selectWarmPlan?.(finalPlan) ?? finalPlan;
940
+ assertDeploymentStateUnchanged(root, options, stagedProbeDeployment, "Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed while cacheability was being probed. No final version was promoted.");
941
+ const finalConfig = writeCacheabilityManifestArtifact(root, options.config, probe.manifest);
942
+ const finalUpload = runWranglerVersionUpload(root, {
943
+ config: finalConfig,
944
+ env: options.env,
945
+ name: options.name,
946
+ preview: options.preview,
947
+ verbose: options.verbose
948
+ });
949
+ prepared = {
950
+ optionalWarmTargetKeys: new Set(probe.speculativeTargets.map(cdnWarmTargetKey)),
951
+ prerenderSecret,
952
+ plan: selectedFinalPlan,
953
+ upload: finalUpload
954
+ };
955
+ } catch (error) {
956
+ throw withStagedProbeVersionCleanupNote(error);
957
+ }
958
+ return deployUploadedVersionWithCdnWarmup(root, prepared.plan.paths, {
959
+ ...options,
960
+ deploymentId: prepared.plan.deploymentId,
961
+ expectedBuildId: prepared.plan.buildIdentity,
962
+ expectedRscBuildId: prepared.plan.rscBuildId,
963
+ expectedDeploymentState: stagedProbeDeployment,
964
+ loadingShellPaths: prepared.plan.loadingShellPaths,
965
+ optionalWarmTargetKeys: prepared.optionalWarmTargetKeys,
966
+ pagesDataPaths: prepared.plan.pagesDataPaths,
967
+ prerenderSecret: prepared.prerenderSecret,
968
+ routeHandlerPaths: prepared.plan.routeHandlerPaths,
969
+ routePatterns: prepared.plan.routePatterns,
970
+ rscPaths: prepared.plan.rscPaths,
971
+ uploadedVersion: prepared.upload
972
+ });
973
+ }
974
+ function resolveCdnWarmupTargetUrl(_root, deployedUrl, options) {
975
+ return options?.warmCdnTarget ?? deployedUrl;
415
976
  }
416
977
  function getZeroPercentStagingTraffic(deployment, versionId) {
417
- const current = deployment?.versions ?? [];
418
- if (current.length !== 1 || current[0].percentage !== 100) return null;
419
- if (current[0].versionId === versionId) return null;
420
- return [current[0], {
978
+ const serving = (deployment?.versions ?? []).filter((version) => version.percentage > 0);
979
+ if (serving.length !== 1 || serving[0].percentage !== 100) return null;
980
+ if (serving[0].versionId === versionId) return null;
981
+ return [serving[0], {
421
982
  versionId,
422
983
  percentage: 0
423
984
  }];
@@ -437,20 +998,31 @@ function quoteStructuredHeaderString(value) {
437
998
  }
438
999
  function buildVersionOverrideHeaders(workerName, versionId) {
439
1000
  if (!workerName) return void 0;
440
- return { "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}` };
1001
+ return {
1002
+ "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}`,
1003
+ [VINEXT_EXPECTED_WORKER_VERSION_HEADER]: versionId
1004
+ };
441
1005
  }
442
1006
  function withStagedVersionCleanupNote(error) {
443
- const message = error instanceof Error ? error.message : String(error);
444
- return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
1007
+ return new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} ${getStagedVersionCleanupNote()}`, { cause: error });
1008
+ }
1009
+ function withStagedProbeVersionCleanupNote(error) {
1010
+ return new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} The probe version may remain staged at 0% with the previous version still serving 100% traffic; production Worker triggers/routes were not changed.`, { cause: error });
445
1011
  }
446
1012
  function getStagedVersionCleanupNote() {
447
- return "The uploaded version may remain staged at 0% with the previous version still serving 100% traffic; rerun deploy to promote it or use `wrangler versions deploy` to choose the desired version split.";
1013
+ return "The uploaded version may remain staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes may also have changed because trigger deployment runs before warming. Rerun deploy to promote it or use `wrangler versions deploy` to choose the desired version split.";
448
1014
  }
449
1015
  function withPromotedVersionTriggerNote(error) {
450
1016
  const message = error instanceof Error ? error.message : String(error);
451
1017
  return new Error(`${message} The uploaded version may already be promoted to 100%, but Worker triggers/routes may not be updated; rerun deploy or \`wrangler triggers deploy\` after fixing the trigger error.`, { cause: error });
452
1018
  }
1019
+ function withPromotedVersionWarmupNote(error) {
1020
+ const message = error instanceof Error ? error.message : String(error);
1021
+ return new Error(`${message} The uploaded version is already promoted to 100% and its Worker triggers/routes were updated; rerun deploy to retry cache warming or roll back with \`wrangler versions deploy\`.`, { cause: error });
1022
+ }
453
1023
  async function deploy(options) {
1024
+ if (options.warmCdnTarget !== void 0 && !options.warmCdnCache) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
1025
+ const warmCdnTarget = options.warmCdnTarget === void 0 ? void 0 : validateCdnWarmTarget(options.warmCdnTarget);
454
1026
  const deployEnv = validateWranglerEnvName(options.env || (options.preview ? "preview" : "production"));
455
1027
  const root = path.resolve(options.root);
456
1028
  loadDotenv({
@@ -483,31 +1055,93 @@ async function deploy(options) {
483
1055
  console.log("\n Dry run complete. No build or deploy performed.\n");
484
1056
  return;
485
1057
  }
486
- const nextConfig = await resolveNextConfig(await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
487
1058
  const buildEnv = deployEnv === "production" && !options.env ? void 0 : deployEnv;
488
- const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? await withCloudflareEnv(buildEnv, async () => {
489
- return loadVinextPrerenderConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
490
- }) : null;
1059
+ const viteConfigMetadata = await withCloudflareEnv(buildEnv, () => loadDeployViteConfigMetadata(info.root));
1060
+ const cdnAdapterConfig = resolveCdnAdapterConfig(viteConfigMetadata.cacheConfig);
1061
+ const nextConfig = await withCloudflareEnv(buildEnv, async () => {
1062
+ const inlineNextConfig = viteConfigMetadata.nextConfig;
1063
+ const rawNextConfig = inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD);
1064
+ return resolveNextConfig(rawNextConfig, info.root);
1065
+ });
1066
+ const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? viteConfigMetadata.prerenderConfig : null;
491
1067
  const prerenderDecision = resolveVinextPrerenderDecision({
492
1068
  prerenderAllFlag: options.prerenderAll,
493
1069
  vinextPrerenderConfig,
494
1070
  nextOutput: nextConfig.output
495
1071
  });
496
- const shouldEmitPrerenderPathManifest = options.warmCdnCache || !options.skipBuild && prerenderDecision;
1072
+ const hasStrictResponseVary = hasVerbatimResponseVary(viteConfigMetadata.cacheConfig);
1073
+ const warmupStatusSource = cacheWarmupStatusSource(viteConfigMetadata.cacheConfig);
1074
+ const hasStagedRequestRouting = hasUncachedRequestRouting(viteConfigMetadata.cacheConfig) || warmupStatusSource === "data-cache";
1075
+ const hasBuildIdentityHeader = hasBuildIdentityResponseHeader(viteConfigMetadata.cacheConfig);
1076
+ const hasCanonicalRscWarmup = supportsCanonicalRscWarmup(viteConfigMetadata.cacheConfig);
1077
+ const needsCacheabilityProbeManifest = projectRequiresRouteCacheabilityProbeManifest(info, viteConfigMetadata.cacheConfig);
1078
+ const shouldEmitPrerenderPathManifest = !options.skipBuild && prerenderDecision;
497
1079
  if (!options.skipBuild) await runBuild(info, buildEnv);
498
1080
  else console.log("\n Skipping build (--skip-build)");
499
- if (shouldEmitPrerenderPathManifest) {
500
- const routeRootConfig = await withCloudflareEnv(buildEnv, async () => {
501
- return loadVinextRouteRootConfigFromViteConfig(await loadProjectViteApi(info.root), info.root);
502
- });
503
- await emitPrerenderPathManifest({
504
- root: info.root,
505
- nextConfigOverride: nextConfig,
506
- routeRootConfig
507
- });
1081
+ const canWarmTpr = options.experimentalTPR && !prerenderDecision && hasBuildIdentityHeader;
1082
+ if (options.experimentalTPR && prerenderDecision) console.log(" TPR: Skipping route selection (all-route prerendering configured)");
1083
+ else if (options.experimentalTPR && !hasBuildIdentityHeader) console.log(workerEntryHasCacheHandler(root) ? " TPR: Skipping pre-warm (legacy imperative cache handlers must migrate to declarative vinext({ cache }) for standard warming)" : " TPR: Skipping pre-warm (configured cache does not expose build identity)");
1084
+ const tpr = canWarmTpr ? await resolveTPRRoutes({
1085
+ root,
1086
+ config: options.config,
1087
+ env: buildEnv,
1088
+ hostname: warmCdnTarget ? new URL(warmCdnTarget).hostname : void 0,
1089
+ window: Math.max(1, options.tprWindow ?? 24)
1090
+ }) : null;
1091
+ if (tpr?.skipped) console.log(` TPR: Skipped (${tpr.skipped})`);
1092
+ const tprRoutes = tpr?.routes ?? [];
1093
+ const wranglerOptions = {
1094
+ env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
1095
+ name: options.name,
1096
+ config: options.config,
1097
+ verbose: options.verbose
1098
+ };
1099
+ let shouldWarmTpr = tprRoutes.length > 0;
1100
+ if (shouldWarmTpr && !options.warmCdnCache) {
1101
+ try {
1102
+ shouldWarmTpr = getZeroPercentStagingTraffic(runWranglerDeploymentStatus(root, wranglerOptions), "tpr-preflight") !== null;
1103
+ } catch {
1104
+ shouldWarmTpr = false;
1105
+ }
1106
+ if (!shouldWarmTpr) console.log(" TPR: Skipping pre-warm (current deployment cannot be staged safely)");
508
1107
  }
1108
+ const shouldWarmCdnCache = options.warmCdnCache || shouldWarmTpr;
1109
+ const shouldSelectTpr = shouldWarmTpr && !options.warmCdnCache;
1110
+ const candidatePathsOnly = shouldSelectTpr && !needsCacheabilityProbeManifest;
1111
+ const shouldPrerenderLocally = prerenderDecision && (!shouldWarmCdnCache || prerenderDecision.reason === "next-export");
1112
+ if (shouldWarmCdnCache && cdnAdapterConfig) {
1113
+ const wrangler = await loadProjectWranglerApi(info.root);
1114
+ const previousCwd = process.cwd();
1115
+ try {
1116
+ process.chdir(info.root);
1117
+ const config = wrangler.unstable_readConfig({
1118
+ config: options.config,
1119
+ env: buildEnv
1120
+ }, {
1121
+ hideWarnings: true,
1122
+ preserveOriginalMain: true,
1123
+ useRedirectIfAvailable: true
1124
+ });
1125
+ assertCdnVersionMetadataConfig({
1126
+ binding: cdnAdapterConfig.versionMetadataBinding,
1127
+ configuredBinding: config.version_metadata?.binding,
1128
+ configPath: config.configPath
1129
+ });
1130
+ } finally {
1131
+ process.chdir(previousCwd);
1132
+ }
1133
+ }
1134
+ if (shouldEmitPrerenderPathManifest) await emitPrerenderPathManifest({
1135
+ root: info.root,
1136
+ nextConfig,
1137
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1138
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1139
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1140
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
1141
+ routeRootConfig: viteConfigMetadata.routeRootConfig
1142
+ });
509
1143
  let ranPrerender = false;
510
- if (prerenderDecision) {
1144
+ if (shouldPrerenderLocally) {
511
1145
  console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
512
1146
  if (nextConfig.enablePrerenderSourceMaps) {
513
1147
  process.setSourceMapsEnabled(true);
@@ -515,52 +1149,84 @@ async function deploy(options) {
515
1149
  }
516
1150
  await runPrerender({
517
1151
  root: info.root,
518
- concurrency: options.prerenderConcurrency
1152
+ concurrency: options.prerenderConcurrency,
1153
+ nextConfig,
1154
+ routeRootConfig: viteConfigMetadata.routeRootConfig
519
1155
  });
520
1156
  ranPrerender = true;
521
1157
  }
522
1158
  if (ranPrerender) try {
523
- await populateKVCacheFromPrerenderedArtifacts(root, buildEnv, deployEnv === "production" && !options.env ? void 0 : deployEnv);
1159
+ await populateKVCacheFromPrerenderedArtifacts(root, deployEnv === "production" && !options.env ? void 0 : deployEnv, viteConfigMetadata.cacheConfig);
524
1160
  } catch (error) {
525
1161
  console.log(` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`);
526
1162
  }
527
- if (options.experimentalTPR) {
528
- console.log();
529
- const tprResult = await runTPR({
530
- root,
531
- config: options.config,
532
- coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
533
- limit: Math.max(1, options.tprLimit ?? 1e3),
534
- window: Math.max(1, options.tprWindow ?? 24)
535
- });
536
- if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
537
- }
538
- const wranglerOptions = {
539
- env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
540
- name: options.name,
541
- config: options.config
542
- };
543
1163
  let url;
544
- if (options.warmCdnCache) {
545
- const warmPaths = readPrerenderWarmPaths(root, {
546
- includeFallbackShells: options.warmCdnIncludeFallbacks,
547
- strict: options.warmCdnStrict
548
- });
549
- if (warmPaths.length > 0) url = await deployWithCdnWarmup(root, warmPaths, {
1164
+ if (shouldWarmCdnCache) try {
1165
+ url = await deployWithCdnWarmup(root, [], {
550
1166
  ...wranglerOptions,
1167
+ allowEmptyWarmPlan: tprRoutes.length > 0 && !options.warmCdnCache,
1168
+ cacheabilityProbe: needsCacheabilityProbeManifest,
1169
+ discoverWarmPlan: async ({ headers, targetUrl }) => {
1170
+ const discovery = await discoverPrerenderPathManifest({
1171
+ root: info.root,
1172
+ candidatePaths: tprRoutes.map(({ path }) => path),
1173
+ candidatePathsOnly,
1174
+ nextConfig,
1175
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1176
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1177
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1178
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1179
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
1180
+ routeRootConfig: viteConfigMetadata.routeRootConfig,
1181
+ pathDiscoveryTarget: {
1182
+ baseUrl: targetUrl,
1183
+ headers,
1184
+ phaseTimeoutMs: options.warmCdnDiscoveryTimeout ?? DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS,
1185
+ retries: options.warmCdnDiscoveryRetries,
1186
+ retryDelayMs: DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS
1187
+ }
1188
+ });
1189
+ if (!discovery) return {
1190
+ loadingShellPaths: [],
1191
+ paths: [],
1192
+ rscPaths: []
1193
+ };
1194
+ return createPrerenderWarmPlan(root, discovery, {
1195
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1196
+ includeFallbackShells: options.warmCdnIncludeFallbacks,
1197
+ strict: options.warmCdnCertify === true || !options.dangerouslyPromoteOnCdnWarmError
1198
+ });
1199
+ },
1200
+ selectWarmPlan: shouldSelectTpr ? (plan) => selectTPRWarmPlan(plan, tprRoutes, Math.max(1, Math.min(100, options.tprCoverage ?? 90)), Math.max(1, options.tprLimit ?? 1e3)) : void 0,
1201
+ statusSource: warmupStatusSource,
551
1202
  warmCdnConcurrency: options.warmCdnConcurrency,
1203
+ warmCdnTarget,
552
1204
  warmCdnTimeout: options.warmCdnTimeout,
553
1205
  warmCdnRetries: options.warmCdnRetries,
554
- warmCdnStrict: options.warmCdnStrict
1206
+ warmCdnDiscoveryTimeout: options.warmCdnDiscoveryTimeout,
1207
+ warmCdnDiscoveryRetries: options.warmCdnDiscoveryRetries,
1208
+ warmCdnProbeTimeout: options.warmCdnProbeTimeout,
1209
+ warmCdnProbeRetries: options.warmCdnProbeRetries,
1210
+ warmCdnCertify: options.warmCdnCertify,
1211
+ warmCdnReadinessTimeout: options.warmCdnReadinessTimeout,
1212
+ warmCdnReadinessRetries: options.warmCdnReadinessRetries,
1213
+ warmCdnReadinessProbes: options.warmCdnReadinessProbes,
1214
+ warmCdnReadinessProbeDelay: options.warmCdnReadinessProbeDelay,
1215
+ dangerouslyPromoteOnCdnWarmError: options.dangerouslyPromoteOnCdnWarmError,
1216
+ warmCdnPromote: options.warmCdnPromote,
1217
+ warmCdnPromotionDelay: options.warmCdnPromotionDelay
555
1218
  });
556
- else {
557
- console.log("\n CDN warmup skipped: no build-discovered paths found.");
558
- url = await runWranglerDeploy(root, wranglerOptions);
559
- }
560
- } else url = await runWranglerDeploy(root, wranglerOptions);
1219
+ } catch (error) {
1220
+ if (options.warmCdnCache || options.warmCdnPromote === false && error instanceof StagedWarmupError) throw error;
1221
+ console.log(` TPR: Skipping pre-warm (${formatUnknownError(error)}). Continuing with deploy.`);
1222
+ }
1223
+ if (url === void 0) url = await runWranglerDeploy(root, {
1224
+ ...wranglerOptions,
1225
+ promote: options.warmCdnPromote
1226
+ });
561
1227
  console.log("\n ─────────────────────────────────────────");
562
- console.log(` Deployed to: ${url}`);
1228
+ console.log(` ${options.warmCdnPromote === false ? "Version URL" : "Deployed to"}: ${url}`);
563
1229
  console.log(" ─────────────────────────────────────────\n");
564
1230
  }
565
1231
  //#endregion
566
- export { buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, parseDeployArgs, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, validateWranglerEnvName, withCloudflareEnv };
1232
+ export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, selectTPRWarmPlan, validateWranglerEnvName, withCloudflareEnv };