@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/deploy.js CHANGED
@@ -1,13 +1,19 @@
1
- import { readPrerenderWarmPaths, warmCdnCache } from "./cdn-warm.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";
2
4
  import { parseWranglerConfig, runTPR } from "./tpr.js";
3
- import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
4
- import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
5
+ import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "./version-headers.js";
6
+ import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveCdnAdapterConfig, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
7
+ import { assertCdnVersionMetadataConfig } from "./wrangler-version-metadata.js";
5
8
  import { parseWorkerDeploymentUrl } from "./worker-deployment-url.js";
9
+ import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.js";
6
10
  import { buildPrerenderKVPairs } from "./prerender-kv-populate.js";
7
11
  import { createRequire } from "node:module";
8
- import path from "node:path";
9
12
  import fs from "node:fs";
10
- import { emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
13
+ import path from "node:path";
14
+ import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
15
+ import { setTimeout } from "node:timers/promises";
16
+ import { DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS, DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS, discoverPrerenderPathManifest, emitPrerenderPathManifest } from "vinext/internal/build/prerender-paths";
11
17
  import os from "node:os";
12
18
  import { spawn } from "node:child_process";
13
19
  import { parseArgs } from "node:util";
@@ -15,7 +21,7 @@ import { pathToFileURL } from "node:url";
15
21
  import { runPrerender } from "vinext/internal/build/run-prerender";
16
22
  import { loadDotenv } from "vinext/internal/config/dotenv";
17
23
  import { findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "vinext/internal/config/next-config";
18
- import { findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, resolveVinextPrerenderDecision } from "vinext/internal/config/prerender";
24
+ import { findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasUncachedRequestRouting, hasVerbatimResponseVary, isConfiguredCdnResponsePolicyHeader, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision, supportsCanonicalRscWarmup, usesVinextCacheWarmupStatus } from "vinext/internal/config/prerender";
19
25
  import { detectProject, findInNodeModules, formatMissingCloudflarePluginError, getMissingDeps } from "vinext/internal/utils/project";
20
26
  import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
21
27
  //#region src/deploy.ts
@@ -28,6 +34,7 @@ import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
28
34
  * 2. Runs the Vite build
29
35
  * 3. Deploys to Cloudflare Workers via Wrangler
30
36
  */
37
+ const DEFAULT_CDN_WARM_PROMOTION_DELAY_MS = 15e3;
31
38
  function parsePositiveIntegerArg(raw, flag) {
32
39
  if (raw === "") throw new Error(`${flag} requires a value, but none was provided.`);
33
40
  const parsed = Number(raw);
@@ -40,6 +47,26 @@ function parseNonNegativeIntegerArg(raw, flag) {
40
47
  if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
41
48
  return parsed;
42
49
  }
50
+ const MAX_NODE_TIMER_DELAY_MS = 2147483647;
51
+ function validateTimerDelay(value, flag, raw = String(value)) {
52
+ if (!Number.isInteger(value) || value < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
53
+ if (value > MAX_NODE_TIMER_DELAY_MS) throw new Error(`${flag} must not exceed ${MAX_NODE_TIMER_DELAY_MS} milliseconds, but got "${raw}".`);
54
+ return value;
55
+ }
56
+ function validatePromotionDelay(value, raw = String(value)) {
57
+ return validateTimerDelay(value, "--warm-cdn-promotion-delay", raw);
58
+ }
59
+ function validateCdnWarmTarget(raw) {
60
+ const value = raw.trim();
61
+ let url;
62
+ try {
63
+ url = new URL(value);
64
+ } catch {
65
+ throw new Error(`--warm-cdn-target expects an HTTPS origin, but got "${raw}".`);
66
+ }
67
+ 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}".`);
68
+ return url.origin;
69
+ }
43
70
  function formatUnknownError(error) {
44
71
  if (error instanceof Error && error.message) return error.message;
45
72
  return String(error);
@@ -66,6 +93,10 @@ const deployArgOptions = {
66
93
  type: "boolean",
67
94
  default: false
68
95
  },
96
+ verbose: {
97
+ type: "boolean",
98
+ default: false
99
+ },
69
100
  "prerender-all": {
70
101
  type: "boolean",
71
102
  default: false
@@ -75,13 +106,31 @@ const deployArgOptions = {
75
106
  type: "boolean",
76
107
  default: false
77
108
  },
109
+ "warm-cdn-target": { type: "string" },
78
110
  "warm-cdn-concurrency": { type: "string" },
79
111
  "warm-cdn-timeout": { type: "string" },
80
112
  "warm-cdn-retries": { type: "string" },
81
- "warm-cdn-strict": {
113
+ "warm-cdn-discovery-timeout": { type: "string" },
114
+ "warm-cdn-discovery-retries": { type: "string" },
115
+ "warm-cdn-probe-timeout": { type: "string" },
116
+ "warm-cdn-probe-retries": { type: "string" },
117
+ "warm-cdn-certify": {
82
118
  type: "boolean",
83
119
  default: false
84
120
  },
121
+ "warm-cdn-readiness-timeout": { type: "string" },
122
+ "warm-cdn-readiness-retries": { type: "string" },
123
+ "warm-cdn-readiness-probes": { type: "string" },
124
+ "warm-cdn-readiness-probe-delay": { type: "string" },
125
+ "dangerously-promote-on-cdn-warm-error": {
126
+ type: "boolean",
127
+ default: false
128
+ },
129
+ "warm-cdn-no-promote": {
130
+ type: "boolean",
131
+ default: false
132
+ },
133
+ "warm-cdn-promotion-delay": { type: "string" },
85
134
  "warm-cdn-include-fallbacks": {
86
135
  type: "boolean",
87
136
  default: false
@@ -100,6 +149,8 @@ function parseDeployArgs(args) {
100
149
  options: deployArgOptions,
101
150
  strict: true
102
151
  });
152
+ if (values["warm-cdn-certify"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-certify requires --experimental-warm-cdn-cache.");
153
+ if (values["warm-cdn-target"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
103
154
  function parseIntArg(name, raw) {
104
155
  if (!raw) return void 0;
105
156
  const n = parseInt(raw, 10);
@@ -117,13 +168,26 @@ function parseDeployArgs(args) {
117
168
  config: values.config?.trim() || void 0,
118
169
  skipBuild: values["skip-build"],
119
170
  dryRun: values["dry-run"],
171
+ verbose: values.verbose,
120
172
  prerenderAll: values["prerender-all"],
121
173
  prerenderConcurrency: values["prerender-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["prerender-concurrency"], "--prerender-concurrency"),
122
174
  warmCdnCache: values["experimental-warm-cdn-cache"],
175
+ warmCdnTarget: values["warm-cdn-target"] === void 0 ? void 0 : validateCdnWarmTarget(values["warm-cdn-target"]),
123
176
  warmCdnConcurrency: values["warm-cdn-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-concurrency"], "--warm-cdn-concurrency"),
124
177
  warmCdnTimeout: values["warm-cdn-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-timeout"], "--warm-cdn-timeout"),
125
178
  warmCdnRetries: values["warm-cdn-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-retries"], "--warm-cdn-retries"),
126
- warmCdnStrict: values["warm-cdn-strict"],
179
+ warmCdnDiscoveryTimeout: values["warm-cdn-discovery-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-discovery-timeout"], "--warm-cdn-discovery-timeout"),
180
+ warmCdnDiscoveryRetries: values["warm-cdn-discovery-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-discovery-retries"], "--warm-cdn-discovery-retries"),
181
+ warmCdnProbeTimeout: values["warm-cdn-probe-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-probe-timeout"], "--warm-cdn-probe-timeout"),
182
+ warmCdnProbeRetries: values["warm-cdn-probe-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-probe-retries"], "--warm-cdn-probe-retries"),
183
+ warmCdnCertify: values["warm-cdn-certify"],
184
+ warmCdnReadinessTimeout: values["warm-cdn-readiness-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-timeout"], "--warm-cdn-readiness-timeout"),
185
+ warmCdnReadinessRetries: values["warm-cdn-readiness-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-readiness-retries"], "--warm-cdn-readiness-retries"),
186
+ warmCdnReadinessProbes: values["warm-cdn-readiness-probes"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-probes"], "--warm-cdn-readiness-probes"),
187
+ 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"]),
188
+ dangerouslyPromoteOnCdnWarmError: values["dangerously-promote-on-cdn-warm-error"],
189
+ warmCdnPromote: !values["warm-cdn-no-promote"],
190
+ 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
191
  warmCdnIncludeFallbacks: values["warm-cdn-include-fallbacks"],
128
192
  experimentalTPR: values["experimental-tpr"],
129
193
  tprCoverage: parseIntArg("tpr-coverage", values["tpr-coverage"]),
@@ -164,13 +228,27 @@ async function loadProjectViteApi(root) {
164
228
  }
165
229
  return await (vitePath === "vite" ? import(vitePath) : import(pathToFileURL(vitePath).href));
166
230
  }
231
+ async function loadProjectWranglerApi(root) {
232
+ const req = createRequire(path.join(root, "package.json"));
233
+ let wranglerPath;
234
+ try {
235
+ wranglerPath = req.resolve("wrangler");
236
+ } catch {
237
+ const cloudflarePluginPath = req.resolve("@cloudflare/vite-plugin");
238
+ wranglerPath = createRequire(cloudflarePluginPath).resolve("wrangler");
239
+ }
240
+ return await import(
241
+ /* @vite-ignore */
242
+ pathToFileURL(wranglerPath).href
243
+ );
244
+ }
167
245
  async function loadDeployViteConfigMetadata(root) {
168
246
  const plugins = (await (await loadProjectViteApi(root)).loadConfigFromFile({
169
247
  command: "build",
170
248
  mode: "production"
171
249
  }, void 0, root))?.config.plugins;
172
250
  return {
173
- cacheConfig: viteConfigHasCacheAdapter(root) ? await findVinextCacheConfigInPlugins(plugins) : null,
251
+ cacheConfig: await findVinextCacheConfigInPlugins(plugins),
174
252
  nextConfig: await findVinextNextConfigInPlugins(plugins),
175
253
  prerenderConfig: await findVinextPrerenderConfigInPlugins(plugins),
176
254
  routeRootConfig: await findVinextRouteRootConfigInPlugins(plugins)
@@ -346,88 +424,509 @@ async function runWranglerDeploy(root, options, execute = spawn) {
346
424
  });
347
425
  return parseWorkerDeploymentUrl(output) ?? "(URL not detected in wrangler output)";
348
426
  }
427
+ function hasCdnWarmRequests(plan) {
428
+ return plan.paths.length + (plan.pagesDataPaths?.length ?? 0) + (plan.routeHandlerPaths?.length ?? 0) + plan.rscPaths.length + plan.loadingShellPaths.length > 0;
429
+ }
430
+ function projectRequiresRouteCacheabilityProbeManifest(project, cacheConfig) {
431
+ return (project.isAppRouter || project.isPagesRouter) && requiresRouteCacheabilityProbeManifest(cacheConfig);
432
+ }
433
+ function cdnWarmTargetKey(target) {
434
+ return `${target.kind}\0${target.sourcePathname}`;
435
+ }
436
+ function requiredCdnWarmTargetKeys(plan, optionalTargetKeys) {
437
+ const keys = /* @__PURE__ */ new Set([
438
+ ...plan.paths.map((pathname) => `html\0${pathname}`),
439
+ ...plan.pagesDataPaths.map((pathname) => `pages-data\0${pathname}`),
440
+ ...(plan.routeHandlerPaths ?? []).map((pathname) => `app-route\0${pathname}`),
441
+ ...plan.rscPaths.map((pathname) => `rsc-full\0${pathname}`),
442
+ ...plan.loadingShellPaths.map((pathname) => `rsc-loading-shell\0${pathname}`)
443
+ ]);
444
+ if (optionalTargetKeys) for (const key of optionalTargetKeys) keys.delete(key);
445
+ return keys;
446
+ }
349
447
  async function deployWithCdnWarmup(root, paths, options) {
350
- const upload = runWranglerVersionUpload(root, options);
448
+ if (options.warmCdnTarget !== void 0) options = {
449
+ ...options,
450
+ warmCdnTarget: validateCdnWarmTarget(options.warmCdnTarget)
451
+ };
452
+ if (options.warmCdnDiscoveryTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnDiscoveryTimeout), "--warm-cdn-discovery-timeout");
453
+ if (options.warmCdnDiscoveryRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnDiscoveryRetries), "--warm-cdn-discovery-retries");
454
+ if (options.warmCdnProbeTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnProbeTimeout), "--warm-cdn-probe-timeout");
455
+ if (options.warmCdnProbeRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnProbeRetries), "--warm-cdn-probe-retries");
456
+ if (options.warmCdnReadinessTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessTimeout), "--warm-cdn-readiness-timeout");
457
+ if (options.warmCdnReadinessRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnReadinessRetries), "--warm-cdn-readiness-retries");
458
+ if (options.warmCdnReadinessProbes !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessProbes), "--warm-cdn-readiness-probes");
459
+ if (options.warmCdnReadinessProbeDelay !== void 0) validateTimerDelay(options.warmCdnReadinessProbeDelay, "--warm-cdn-readiness-probe-delay");
460
+ if (options.warmCdnPromotionDelay !== void 0) validatePromotionDelay(options.warmCdnPromotionDelay);
461
+ if (options.cacheabilityProbe) return deployWithCacheabilityProbe(root, options);
462
+ return deployUploadedVersionWithCdnWarmup(root, paths, options);
463
+ }
464
+ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
465
+ const allowUnverifiedPromotion = options.dangerouslyPromoteOnCdnWarmError === true && options.warmCdnCertify !== true;
466
+ let deploymentId = options.deploymentId;
467
+ let expectedBuildId = options.expectedBuildId;
468
+ let expectedRscBuildId = options.expectedRscBuildId;
469
+ const hasPreparedWarmPlan = options.uploadedVersion !== void 0;
470
+ let warmPlanDiscovered = hasPreparedWarmPlan || options.discoverWarmPlan === void 0;
471
+ let remainingWarmPlan = {
472
+ loadingShellPaths: [...options.loadingShellPaths ?? []],
473
+ pagesDataPaths: [...options.pagesDataPaths ?? []],
474
+ paths: [...paths],
475
+ routeHandlerPaths: [...options.routeHandlerPaths ?? []],
476
+ routePatterns: options.routePatterns ? { ...options.routePatterns } : void 0,
477
+ rscPaths: [...options.rscPaths ?? []]
478
+ };
479
+ let discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
480
+ const prepareWarmPlan = (plan) => {
481
+ if (plan.paths.length === 0 && plan.pagesDataPaths.length === 0 && (plan.routeHandlerPaths?.length ?? 0) === 0 || expectedBuildId !== void 0) return plan;
482
+ if (!allowUnverifiedPromotion) {
483
+ const warmupKind = plan.paths.length > 0 ? "CDN HTML warmup" : plan.pagesDataPaths.length > 0 ? "CDN Pages data warmup" : "CDN Route Handler warmup";
484
+ 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.`);
485
+ }
486
+ 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.`);
487
+ return {
488
+ ...plan,
489
+ pagesDataPaths: [],
490
+ paths: [],
491
+ routeHandlerPaths: []
492
+ };
493
+ };
494
+ const discoverWarmPlan = async (targetUrl, headers) => {
495
+ if (!options.discoverWarmPlan) return;
496
+ const plan = await options.discoverWarmPlan({
497
+ headers,
498
+ targetUrl
499
+ });
500
+ deploymentId = plan.deploymentId;
501
+ expectedBuildId = plan.buildIdentity;
502
+ expectedRscBuildId = plan.rscBuildId;
503
+ remainingWarmPlan = {
504
+ loadingShellPaths: [...plan.loadingShellPaths],
505
+ pagesDataPaths: [...plan.pagesDataPaths ?? []],
506
+ paths: [...plan.paths],
507
+ routeHandlerPaths: [...plan.routeHandlerPaths ?? []],
508
+ routePatterns: plan.routePatterns ? { ...plan.routePatterns } : void 0,
509
+ rscPaths: [...plan.rscPaths]
510
+ };
511
+ discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
512
+ warmPlanDiscovered = true;
513
+ };
514
+ if (!options.discoverWarmPlan || hasPreparedWarmPlan) remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
515
+ const upload = options.uploadedVersion ?? runWranglerVersionUpload(root, options);
516
+ const warmUploadedVersion = (targetUrl, headers, propagatingTarget = false, plan = {
517
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
518
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
519
+ paths: remainingWarmPlan.paths,
520
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
521
+ routePatterns: remainingWarmPlan.routePatterns,
522
+ rscPaths: remainingWarmPlan.rscPaths
523
+ }, requireCacheHit = false) => warmCdnCache({
524
+ targetUrl,
525
+ paths: plan.paths,
526
+ headers,
527
+ propagatingTarget,
528
+ deploymentId,
529
+ expectedBuildId,
530
+ expectedRscBuildId,
531
+ loadingShellPaths: plan.loadingShellPaths,
532
+ pagesDataPaths: plan.pagesDataPaths,
533
+ routeHandlerPaths: plan.routeHandlerPaths,
534
+ routePatterns: plan.routePatterns,
535
+ rscPaths: plan.rscPaths,
536
+ concurrency: options.warmCdnConcurrency,
537
+ timeoutMs: options.warmCdnTimeout,
538
+ retries: options.warmCdnRetries,
539
+ retrySkippedTargetKeys: propagatingTarget && hasPreparedWarmPlan ? requiredCdnWarmTargetKeys(plan, options.optionalWarmTargetKeys) : void 0,
540
+ requireCacheHit,
541
+ strict: requireCacheHit || !allowUnverifiedPromotion,
542
+ statusSource: options.statusSource
543
+ });
351
544
  const wranglerConfig = parseWranglerConfig(root, options.config);
352
- const stagingTraffic = getZeroPercentStagingTraffic(readWranglerDeploymentStatus(root, options), upload.versionId);
545
+ const deploymentStatus = runWranglerDeploymentStatus(root, options);
546
+ if (options.expectedDeploymentState && !deploymentStateEquals(deploymentStatus, options.expectedDeploymentState)) throw new Error("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.");
547
+ const stagingTraffic = getZeroPercentStagingTraffic(deploymentStatus, upload.versionId);
548
+ if (hasPreparedWarmPlan && !stagingTraffic) throw new Error("Two-stage CDN warming stopped because Worker deployment traffic changed before the final version could be staged. No final version was promoted.");
353
549
  let staged = null;
354
- let triggersDeployedUrl = null;
355
- let warmedBeforePromotion = false;
356
- let triggersApplied = false;
550
+ let triggersDeployedUrl = options.triggersDeployedUrl ?? null;
551
+ let stagedCacheFilled = false;
552
+ let stagedDeploymentState = null;
553
+ const initialWarmRequests = options.discoverWarmPlan === void 0 || hasPreparedWarmPlan ? remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length : 1;
554
+ let triggersApplied = options.triggersAlreadyApplied === true;
357
555
  function applyTriggers() {
358
556
  if (triggersApplied) return;
359
557
  triggersDeployedUrl = runWranglerTriggersDeploy(root, options).deployedUrl;
360
558
  triggersApplied = true;
361
559
  }
362
560
  if (stagingTraffic) {
363
- staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
364
561
  try {
562
+ staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
563
+ } catch (error) {
564
+ throw reconcileVersionDeployFailure(root, options, error, {
565
+ desiredTraffic: stagingTraffic,
566
+ desiredDescription: "The uploaded version is staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes were not changed.",
567
+ priorState: deploymentStatus,
568
+ priorDescription: "Worker deployment traffic was not changed and Worker triggers/routes were not changed."
569
+ });
570
+ }
571
+ try {
572
+ if (hasPreparedWarmPlan) {
573
+ stagedDeploymentState = runWranglerDeploymentStatus(root, options);
574
+ 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.");
575
+ }
365
576
  applyTriggers();
366
577
  } catch (error) {
367
578
  throw withStagedVersionCleanupNote(error);
368
579
  }
369
- const targetUrl = resolveCdnWarmupTargetUrl(root, staged.deployedUrl ?? triggersDeployedUrl, options);
370
- const headers = buildVersionOverrideHeaders(resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
371
- if (targetUrl && headers) {
372
- try {
373
- await warmCdnCache({
580
+ const targetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? staged.deployedUrl;
581
+ const headers = buildVersionOverrideHeaders(options.name ?? upload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, options), upload.versionId);
582
+ if (targetUrl && headers) try {
583
+ if (!warmPlanDiscovered) {
584
+ console.log(" CDN warmup: discovering paths from the staged Worker version...");
585
+ await discoverWarmPlan(targetUrl, headers);
586
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
587
+ 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).`);
588
+ }
589
+ const stagedWarmPlan = {
590
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
591
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
592
+ paths: remainingWarmPlan.paths,
593
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
594
+ routePatterns: remainingWarmPlan.routePatterns,
595
+ rscPaths: remainingWarmPlan.rscPaths
596
+ };
597
+ const stagedWarmRequests = stagedWarmPlan.paths.length + stagedWarmPlan.pagesDataPaths.length + (stagedWarmPlan.routeHandlerPaths?.length ?? 0) + stagedWarmPlan.rscPaths.length + stagedWarmPlan.loadingShellPaths.length;
598
+ if (stagedWarmRequests > 0) {
599
+ console.log(" CDN warmup: waiting for the staged Worker version to become stable...");
600
+ const readiness = await waitForCdnWarmTargetReadiness({
374
601
  targetUrl,
375
- paths,
376
602
  headers,
377
- concurrency: options.warmCdnConcurrency,
378
- timeoutMs: options.warmCdnTimeout,
379
- retries: options.warmCdnRetries,
380
- strict: options.warmCdnStrict
603
+ plan: stagedWarmPlan,
604
+ prerenderSecret: options.prerenderSecret,
605
+ deploymentId,
606
+ expectedBuildId,
607
+ expectedRscBuildId,
608
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
609
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
610
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
611
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
612
+ timeoutMs: options.warmCdnTimeout
381
613
  });
614
+ if (!readiness.ready) {
615
+ const message = `CDN warmup could not verify staged Worker readiness: ${readiness.error}.`;
616
+ const noPromoteNote = options.warmCdnPromote === false ? " CDN warmup cannot continue because promotion is disabled and the staged version was not warmed." : "";
617
+ if (!allowUnverifiedPromotion || options.warmCdnPromote === false) throw new Error(`${message}${noPromoteNote}`);
618
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
619
+ } else {
620
+ console.log(" CDN warmup: staged Worker version is stable.");
621
+ const warmResult = await warmUploadedVersion(targetUrl, headers, true, stagedWarmPlan);
622
+ const optionalSkipped = options.optionalWarmTargetKeys ? warmResult.skippedTargets.filter((target) => options.optionalWarmTargetKeys.has(cdnWarmTargetKey(target))).length : 0;
623
+ if (hasPreparedWarmPlan && options.warmCdnCertify) {
624
+ 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.`);
625
+ }
626
+ const requiredSkipped = warmResult.skipped - optionalSkipped;
627
+ if (hasPreparedWarmPlan && requiredSkipped > 0) {
628
+ const message = `Two-stage CDN warming could not fill ${requiredSkipped}/${warmResult.total} planned cache entries because Cloudflare refused cache admission.`;
629
+ if (!allowUnverifiedPromotion) throw new Error(message);
630
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
631
+ }
632
+ 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.`);
633
+ remainingWarmPlan = {
634
+ loadingShellPaths: warmResult.retryPlan.loadingShellPaths,
635
+ pagesDataPaths: warmResult.retryPlan.pagesDataPaths,
636
+ paths: warmResult.retryPlan.paths,
637
+ routeHandlerPaths: warmResult.retryPlan.routeHandlerPaths,
638
+ rscPaths: warmResult.retryPlan.rscPaths
639
+ };
640
+ if (hasPreparedWarmPlan && options.warmCdnCertify && warmResult.warmed > 0) {
641
+ console.log(` CDN warmup: certifying ${warmResult.warmed} staged cache entr${warmResult.warmed === 1 ? "y" : "ies"} before promotion...`);
642
+ const certification = await warmUploadedVersion(targetUrl, headers, true, warmResult.warmedPlan, true);
643
+ if (certification.warmed !== warmResult.warmed) throw new Error(`CDN warmup certified ${certification.warmed}/${warmResult.warmed} staged cache entries as reusable.`);
644
+ stagedCacheFilled = true;
645
+ } else stagedCacheFilled = warmResult.warmed > 0;
646
+ }
647
+ }
648
+ } catch (error) {
649
+ throw withStagedVersionCleanupNote(error);
650
+ }
651
+ else if (initialWarmRequests > 0) {
652
+ 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.";
653
+ if (!allowUnverifiedPromotion) throw new Error(`${message} ${getStagedVersionCleanupNote()}`);
654
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
655
+ }
656
+ } else {
657
+ if (initialWarmRequests > 0) {
658
+ const message = "CDN warmup cannot stage the uploaded Worker at 0% because the current deployment is not exactly one version serving 100% traffic.";
659
+ if (!allowUnverifiedPromotion) throw new Error(`${message} No traffic or triggers were changed.`);
660
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
661
+ }
662
+ console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not one version serving 100% traffic.");
663
+ }
664
+ const countRemainingWarmRequests = () => remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
665
+ if (options.warmCdnPromote === false) {
666
+ 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.");
667
+ const remainingWarmRequests = countRemainingWarmRequests();
668
+ if (!hasPreparedWarmPlan && options.discoverWarmPlan && discoveredWarmRequests === 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error("CDN warmup cannot skip promotion because no build-discovered requests were found to warm."));
669
+ if (remainingWarmRequests > 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error(`CDN warmup cannot skip promotion because ${remainingWarmRequests} request(s) remain unwarmed.`));
670
+ if (hasPreparedWarmPlan && stagedDeploymentState) {
671
+ let currentDeployment;
672
+ try {
673
+ currentDeployment = runWranglerDeploymentStatus(root, options);
382
674
  } catch (error) {
383
- throw withStagedVersionCleanupNote(error);
675
+ const message = error instanceof Error ? error.message : String(error);
676
+ throw new Error(`${message} 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 });
384
677
  }
385
- warmedBeforePromotion = true;
386
- } 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());
387
- } else console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not a single 100% version.");
388
- const deployed = runWranglerVersionDeploy(root, [{
678
+ if (!deploymentStateEquals(currentDeployment, stagedDeploymentState)) throw new Error("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.");
679
+ }
680
+ console.log(" CDN warmup: promotion disabled; uploaded Worker version remains staged at 0% traffic and production Worker triggers/routes remain applied.");
681
+ return staged.deployedUrl ?? triggersDeployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
682
+ }
683
+ let deployed;
684
+ let prePromotionState = null;
685
+ const promotionTraffic = [{
389
686
  versionId: upload.versionId,
390
687
  percentage: 100
391
- }], options, warmedBeforePromotion ? "promote-warmed" : "promote-uploaded");
392
- if (!warmedBeforePromotion) {
393
- try {
394
- applyTriggers();
395
- } catch (error) {
396
- throw withPromotedVersionTriggerNote(error);
688
+ }];
689
+ let promotionAttempted = false;
690
+ try {
691
+ if (stagedCacheFilled && options.statusSource !== "vinext") {
692
+ const promotionDelay = options.warmCdnPromotionDelay ?? 15e3;
693
+ if (promotionDelay > 0) {
694
+ console.log(` CDN warmup: waiting ${promotionDelay / 1e3} seconds for cache propagation before promotion...`);
695
+ await setTimeout(promotionDelay);
696
+ }
397
697
  }
398
- const targetUrl = resolveCdnWarmupTargetUrl(root, deployed.deployedUrl ?? triggersDeployedUrl, options);
399
- if (targetUrl) await warmCdnCache({
400
- targetUrl,
401
- paths,
402
- concurrency: options.warmCdnConcurrency,
403
- timeoutMs: options.warmCdnTimeout,
404
- retries: options.warmCdnRetries,
405
- strict: options.warmCdnStrict
698
+ if (hasPreparedWarmPlan && stagingTraffic) {
699
+ prePromotionState = runWranglerDeploymentStatus(root, options);
700
+ 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.");
701
+ }
702
+ promotionAttempted = true;
703
+ deployed = runWranglerVersionDeploy(root, promotionTraffic, options, stagedCacheFilled ? "promote-warmed" : "promote-uploaded");
704
+ } catch (error) {
705
+ if (promotionAttempted) throw reconcileVersionDeployFailure(root, options, error, {
706
+ desiredTraffic: promotionTraffic,
707
+ desiredDescription: "The uploaded version is already promoted to 100%; Worker triggers/routes may already have changed.",
708
+ priorState: prePromotionState ?? (stagingTraffic ? {
709
+ deploymentId: null,
710
+ output: "",
711
+ versions: stagingTraffic
712
+ } : deploymentStatus),
713
+ priorDescription: "The uploaded version remains staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes may already have changed."
406
714
  });
407
- 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.");
408
- else console.warn(" CDN warmup skipped: no production URL could be inferred from wrangler config or output.");
715
+ throw staged ? withStagedVersionCleanupNote(error) : error;
716
+ }
717
+ try {
718
+ applyTriggers();
719
+ } catch (error) {
720
+ throw withPromotedVersionTriggerNote(error);
721
+ }
722
+ let postPromotionTargetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? deployed.deployedUrl;
723
+ if (!warmPlanDiscovered && postPromotionTargetUrl) try {
724
+ console.log(" CDN warmup: discovering paths from the promoted Worker version...");
725
+ await discoverWarmPlan(postPromotionTargetUrl);
726
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
727
+ } catch (error) {
728
+ throw withPromotedVersionWarmupNote(error);
729
+ }
730
+ if (countRemainingWarmRequests() > 0) {
731
+ const targetUrl = postPromotionTargetUrl;
732
+ if (targetUrl) try {
733
+ await warmUploadedVersion(targetUrl, void 0, true, remainingWarmPlan);
734
+ } catch (error) {
735
+ throw withPromotedVersionWarmupNote(error);
736
+ }
737
+ 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."));
738
+ else console.warn(" CDN warmup skipped: no production URL could be inferred after promotion; the dangerous override was enabled.");
409
739
  }
410
740
  return deployed.deployedUrl ?? triggersDeployedUrl ?? staged?.deployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
411
741
  }
412
- function resolveCdnWarmupTargetUrl(root, deployedUrl, options) {
413
- const config = parseWranglerConfig(root, options?.config);
414
- const env = getWranglerTargetEnv(options ?? {});
415
- const customDomain = (env ? config?.env?.[env]?.customDomain : void 0) ?? config?.customDomain;
416
- if (customDomain) return `https://${customDomain}`;
417
- return deployedUrl;
742
+ function deploymentTrafficEquals(actual, expected) {
743
+ const normalize = (traffic) => [...traffic].sort((a, b) => a.versionId.localeCompare(b.versionId));
744
+ const normalizedActual = normalize(actual);
745
+ const normalizedExpected = normalize(expected);
746
+ return normalizedActual.length === normalizedExpected.length && normalizedActual.every((version, index) => version.versionId === normalizedExpected[index]?.versionId && version.percentage === normalizedExpected[index]?.percentage);
747
+ }
748
+ function deploymentStateEquals(actual, expected) {
749
+ if (expected.deploymentId !== null && actual.deploymentId !== expected.deploymentId) return false;
750
+ return deploymentTrafficEquals(actual.versions, expected.versions);
418
751
  }
419
- function readWranglerDeploymentStatus(root, options) {
752
+ function reconcileVersionDeployFailure(root, options, error, expected) {
753
+ const message = error instanceof Error ? error.message : String(error);
754
+ let reconciliation;
420
755
  try {
421
- return runWranglerDeploymentStatus(root, options);
422
- } catch {
423
- return null;
756
+ const current = runWranglerDeploymentStatus(root, options);
757
+ if (deploymentTrafficEquals(current.versions, expected.desiredTraffic)) reconciliation = expected.desiredDescription;
758
+ else if (deploymentStateEquals(current, expected.priorState)) reconciliation = expected.priorDescription;
759
+ else reconciliation = "Worker deployment state differs from both the requested and prior state, so the deployment outcome is unknown or another deployment changed it.";
760
+ } catch (reconciliationError) {
761
+ 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)}).`;
424
762
  }
763
+ return new Error(`${message} ${reconciliation}`, { cause: error });
764
+ }
765
+ function assertDeploymentStateUnchanged(root, options, expected, message) {
766
+ if (!deploymentStateEquals(runWranglerDeploymentStatus(root, options), expected)) throw new Error(message);
767
+ }
768
+ async function deployWithCacheabilityProbe(root, options) {
769
+ if (!options.discoverWarmPlan) throw new Error("Two-stage CDN warming requires staged Worker route discovery before cacheability probing.");
770
+ const prerenderSecret = readPrerenderSecret(root);
771
+ const probeUpload = runWranglerVersionUpload(root, options);
772
+ const initialDeployment = runWranglerDeploymentStatus(root, options);
773
+ const probeTraffic = getZeroPercentStagingTraffic(initialDeployment, probeUpload.versionId);
774
+ 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.");
775
+ let stagedProbe;
776
+ try {
777
+ stagedProbe = runWranglerVersionDeploy(root, probeTraffic, options, "stage");
778
+ } catch (error) {
779
+ throw reconcileVersionDeployFailure(root, options, error, {
780
+ desiredTraffic: probeTraffic,
781
+ desiredDescription: "The probe version is staged at 0% with the previous version still serving 100% traffic; production Worker triggers/routes were not changed.",
782
+ priorState: initialDeployment,
783
+ priorDescription: "Worker deployment traffic was not changed and production Worker triggers/routes were not changed."
784
+ });
785
+ }
786
+ let stagedProbeDeployment;
787
+ try {
788
+ stagedProbeDeployment = runWranglerDeploymentStatus(root, options);
789
+ 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.");
790
+ } catch (error) {
791
+ const message = error instanceof Error ? error.message : String(error);
792
+ throw new Error(`${message} 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 });
793
+ }
794
+ let prepared;
795
+ try {
796
+ const targetUrl = resolveCdnWarmupTargetUrl(root, stagedProbe.deployedUrl ?? probeUpload.previewUrl, options);
797
+ const wranglerConfig = parseWranglerConfig(root, options.config);
798
+ const headers = buildVersionOverrideHeaders(options.name ?? probeUpload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, options), probeUpload.versionId);
799
+ if (!targetUrl || !headers) throw new Error("Two-stage CDN warming needs a production URL and Worker name for version-overridden cacheability probes.");
800
+ console.log(" CDN warmup: discovering paths from the staged probe Worker...");
801
+ const discovered = await options.discoverWarmPlan({
802
+ headers,
803
+ targetUrl
804
+ });
805
+ if (!discovered.buildId) throw new Error("Two-stage CDN warming requires the staged discovery result to match dist/server/BUILD_ID.");
806
+ if (!discovered.buildIdentity) throw new Error("Two-stage CDN warming requires a CDN adapter that exposes the application build identity.");
807
+ const plan = {
808
+ ...discovered,
809
+ appPaths: discovered.appPaths ? [...discovered.appPaths] : void 0,
810
+ fallbackRoutePatterns: discovered.fallbackRoutePatterns ? [...discovered.fallbackRoutePatterns] : void 0,
811
+ loadingShellPaths: [...discovered.loadingShellPaths],
812
+ pagesDataPaths: [...discovered.pagesDataPaths ?? []],
813
+ pagesPaths: discovered.pagesPaths ? [...discovered.pagesPaths] : void 0,
814
+ paths: [...discovered.paths],
815
+ routeHandlerPaths: [...discovered.routeHandlerPaths ?? []],
816
+ routePatterns: discovered.routePatterns ? { ...discovered.routePatterns } : void 0,
817
+ rscPaths: [...discovered.rscPaths]
818
+ };
819
+ if (!plan.appPaths && !plan.pagesPaths) throw new Error("Two-stage CDN warming requires staged discovery to report App or Pages route ownership.");
820
+ const ownedHtmlPaths = /* @__PURE__ */ new Set([...plan.appPaths ?? [], ...plan.pagesPaths ?? []]);
821
+ plan.paths = plan.paths.filter((pathname) => ownedHtmlPaths.has(pathname));
822
+ const targets = await createCdnWarmTargets({
823
+ deploymentId: plan.deploymentId,
824
+ headers,
825
+ loadingShellPaths: plan.loadingShellPaths,
826
+ pagesDataPaths: plan.pagesDataPaths,
827
+ paths: plan.paths,
828
+ routeHandlerPaths: plan.routeHandlerPaths,
829
+ routePatterns: plan.routePatterns,
830
+ rscPaths: plan.rscPaths
831
+ });
832
+ const routePatternCount = new Set(targets.flatMap((target) => target.route ? [`${target.route.kind}\0${target.route.pattern}`] : [])).size;
833
+ 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;
834
+ if (targets.length > 0) {
835
+ console.log(" CDN warmup: waiting for the staged probe Worker to become stable...");
836
+ const readiness = await waitForCdnWarmTargetReadiness({
837
+ targetUrl,
838
+ headers,
839
+ plan,
840
+ prerenderSecret,
841
+ deploymentId: plan.deploymentId,
842
+ expectedBuildId: plan.buildIdentity,
843
+ expectedRscBuildId: plan.rscBuildId,
844
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
845
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
846
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
847
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
848
+ timeoutMs: options.warmCdnTimeout
849
+ });
850
+ if (!readiness.ready) throw new Error(`Two-stage CDN warming could not verify staged probe Worker readiness: ${readiness.error}.`);
851
+ 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"}...`);
852
+ } else {
853
+ const fallbackPatternCount = plan.fallbackRoutePatterns?.length ?? 0;
854
+ 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.");
855
+ }
856
+ const probeProgress = new CdnOperationProgress();
857
+ let probe;
858
+ try {
859
+ probe = await probeStagedWorkerCacheability({
860
+ buildId: discovered.buildId,
861
+ concurrency: options.warmCdnConcurrency,
862
+ expectedResponseBuildId: plan.buildIdentity,
863
+ fallbackRoutePatterns: plan.fallbackRoutePatterns,
864
+ phaseTimeoutMs: options.warmCdnProbeTimeout ?? 12e4,
865
+ retries: options.warmCdnProbeRetries ?? options.warmCdnRetries ?? 2,
866
+ retryDelayMs: DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS,
867
+ root,
868
+ targets,
869
+ targetUrl,
870
+ timeoutMs: options.warmCdnTimeout,
871
+ onProgress(progress) {
872
+ probeProgress.update(progress.completed, progress.total, `${progress.static} static, ${progress.dynamic} dynamic, ${progress.skipped} skipped`, "Probing route cacheability");
873
+ }
874
+ });
875
+ } finally {
876
+ probeProgress.finish();
877
+ }
878
+ 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.`);
879
+ 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]}`);
880
+ const finalPlan = {
881
+ ...plan,
882
+ loadingShellPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-loading-shell").map((target) => target.sourcePathname),
883
+ pagesDataPaths: probe.cacheableTargets.filter((target) => target.kind === "pages-data").map((target) => target.sourcePathname),
884
+ paths: probe.cacheableTargets.filter((target) => target.kind === "html").map((target) => target.sourcePathname),
885
+ routeHandlerPaths: probe.cacheableTargets.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname),
886
+ rscPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname)
887
+ };
888
+ 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.");
889
+ const finalConfig = writeCacheabilityManifestArtifact(root, options.config, probe.manifest);
890
+ const finalUpload = runWranglerVersionUpload(root, {
891
+ config: finalConfig,
892
+ env: options.env,
893
+ name: options.name,
894
+ preview: options.preview,
895
+ verbose: options.verbose
896
+ });
897
+ prepared = {
898
+ optionalWarmTargetKeys: new Set(probe.speculativeTargets.map(cdnWarmTargetKey)),
899
+ prerenderSecret,
900
+ plan: finalPlan,
901
+ upload: finalUpload
902
+ };
903
+ } catch (error) {
904
+ throw withStagedProbeVersionCleanupNote(error);
905
+ }
906
+ return deployUploadedVersionWithCdnWarmup(root, prepared.plan.paths, {
907
+ ...options,
908
+ deploymentId: prepared.plan.deploymentId,
909
+ expectedBuildId: prepared.plan.buildIdentity,
910
+ expectedRscBuildId: prepared.plan.rscBuildId,
911
+ expectedDeploymentState: stagedProbeDeployment,
912
+ loadingShellPaths: prepared.plan.loadingShellPaths,
913
+ optionalWarmTargetKeys: prepared.optionalWarmTargetKeys,
914
+ pagesDataPaths: prepared.plan.pagesDataPaths,
915
+ prerenderSecret: prepared.prerenderSecret,
916
+ routeHandlerPaths: prepared.plan.routeHandlerPaths,
917
+ routePatterns: prepared.plan.routePatterns,
918
+ rscPaths: prepared.plan.rscPaths,
919
+ uploadedVersion: prepared.upload
920
+ });
921
+ }
922
+ function resolveCdnWarmupTargetUrl(_root, deployedUrl, options) {
923
+ return options?.warmCdnTarget ?? deployedUrl;
425
924
  }
426
925
  function getZeroPercentStagingTraffic(deployment, versionId) {
427
- const current = deployment?.versions ?? [];
428
- if (current.length !== 1 || current[0].percentage !== 100) return null;
429
- if (current[0].versionId === versionId) return null;
430
- return [current[0], {
926
+ const serving = (deployment?.versions ?? []).filter((version) => version.percentage > 0);
927
+ if (serving.length !== 1 || serving[0].percentage !== 100) return null;
928
+ if (serving[0].versionId === versionId) return null;
929
+ return [serving[0], {
431
930
  versionId,
432
931
  percentage: 0
433
932
  }];
@@ -447,20 +946,33 @@ function quoteStructuredHeaderString(value) {
447
946
  }
448
947
  function buildVersionOverrideHeaders(workerName, versionId) {
449
948
  if (!workerName) return void 0;
450
- return { "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}` };
949
+ return {
950
+ "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}`,
951
+ [VINEXT_EXPECTED_WORKER_VERSION_HEADER]: versionId
952
+ };
451
953
  }
452
954
  function withStagedVersionCleanupNote(error) {
453
955
  const message = error instanceof Error ? error.message : String(error);
454
956
  return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
455
957
  }
958
+ function withStagedProbeVersionCleanupNote(error) {
959
+ const message = error instanceof Error ? error.message : String(error);
960
+ return new Error(`${message} 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 });
961
+ }
456
962
  function getStagedVersionCleanupNote() {
457
- 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.";
963
+ 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.";
458
964
  }
459
965
  function withPromotedVersionTriggerNote(error) {
460
966
  const message = error instanceof Error ? error.message : String(error);
461
967
  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 });
462
968
  }
969
+ function withPromotedVersionWarmupNote(error) {
970
+ const message = error instanceof Error ? error.message : String(error);
971
+ 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 });
972
+ }
463
973
  async function deploy(options) {
974
+ if (options.warmCdnTarget !== void 0 && !options.warmCdnCache) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
975
+ const warmCdnTarget = options.warmCdnTarget === void 0 ? void 0 : validateCdnWarmTarget(options.warmCdnTarget);
464
976
  const deployEnv = validateWranglerEnvName(options.env || (options.preview ? "preview" : "production"));
465
977
  const root = path.resolve(options.root);
466
978
  loadDotenv({
@@ -495,9 +1007,11 @@ async function deploy(options) {
495
1007
  }
496
1008
  const buildEnv = deployEnv === "production" && !options.env ? void 0 : deployEnv;
497
1009
  const viteConfigMetadata = await withCloudflareEnv(buildEnv, () => loadDeployViteConfigMetadata(info.root));
1010
+ const cdnAdapterConfig = resolveCdnAdapterConfig(viteConfigMetadata.cacheConfig);
498
1011
  const nextConfig = await withCloudflareEnv(buildEnv, async () => {
499
1012
  const inlineNextConfig = viteConfigMetadata.nextConfig;
500
- return resolveNextConfig(inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
1013
+ const rawNextConfig = inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD);
1014
+ return resolveNextConfig(rawNextConfig, info.root);
501
1015
  });
502
1016
  const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? viteConfigMetadata.prerenderConfig : null;
503
1017
  const prerenderDecision = resolveVinextPrerenderDecision({
@@ -505,12 +1019,44 @@ async function deploy(options) {
505
1019
  vinextPrerenderConfig,
506
1020
  nextOutput: nextConfig.output
507
1021
  });
508
- const shouldEmitPrerenderPathManifest = options.warmCdnCache || !options.skipBuild && prerenderDecision;
1022
+ const hasStrictResponseVary = hasVerbatimResponseVary(viteConfigMetadata.cacheConfig);
1023
+ const hasStagedRequestRouting = hasUncachedRequestRouting(viteConfigMetadata.cacheConfig);
1024
+ const hasBuildIdentityHeader = hasBuildIdentityResponseHeader(viteConfigMetadata.cacheConfig);
1025
+ const hasCanonicalRscWarmup = supportsCanonicalRscWarmup(viteConfigMetadata.cacheConfig);
1026
+ const hasVinextCacheWarmupStatus = usesVinextCacheWarmupStatus(viteConfigMetadata.cacheConfig);
1027
+ const needsCacheabilityProbeManifest = projectRequiresRouteCacheabilityProbeManifest(info, viteConfigMetadata.cacheConfig);
1028
+ const shouldEmitPrerenderPathManifest = !options.skipBuild && prerenderDecision;
509
1029
  if (!options.skipBuild) await runBuild(info, buildEnv);
510
1030
  else console.log("\n Skipping build (--skip-build)");
1031
+ if (options.warmCdnCache && cdnAdapterConfig) {
1032
+ const wrangler = await loadProjectWranglerApi(info.root);
1033
+ const previousCwd = process.cwd();
1034
+ try {
1035
+ process.chdir(info.root);
1036
+ const config = wrangler.unstable_readConfig({
1037
+ config: options.config,
1038
+ env: buildEnv
1039
+ }, {
1040
+ hideWarnings: true,
1041
+ preserveOriginalMain: true,
1042
+ useRedirectIfAvailable: true
1043
+ });
1044
+ assertCdnVersionMetadataConfig({
1045
+ binding: cdnAdapterConfig.versionMetadataBinding,
1046
+ configuredBinding: config.version_metadata?.binding,
1047
+ configPath: config.configPath
1048
+ });
1049
+ } finally {
1050
+ process.chdir(previousCwd);
1051
+ }
1052
+ }
511
1053
  if (shouldEmitPrerenderPathManifest) await emitPrerenderPathManifest({
512
1054
  root: info.root,
513
1055
  nextConfig,
1056
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1057
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1058
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1059
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
514
1060
  routeRootConfig: viteConfigMetadata.routeRootConfig
515
1061
  });
516
1062
  let ranPrerender = false;
@@ -523,7 +1069,8 @@ async function deploy(options) {
523
1069
  await runPrerender({
524
1070
  root: info.root,
525
1071
  concurrency: options.prerenderConcurrency,
526
- nextConfig
1072
+ nextConfig,
1073
+ routeRootConfig: viteConfigMetadata.routeRootConfig
527
1074
  });
528
1075
  ranPrerender = true;
529
1076
  }
@@ -546,29 +1093,64 @@ async function deploy(options) {
546
1093
  const wranglerOptions = {
547
1094
  env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
548
1095
  name: options.name,
549
- config: options.config
1096
+ config: options.config,
1097
+ verbose: options.verbose
550
1098
  };
551
1099
  let url;
552
- if (options.warmCdnCache) {
553
- const warmPaths = readPrerenderWarmPaths(root, {
554
- includeFallbackShells: options.warmCdnIncludeFallbacks,
555
- strict: options.warmCdnStrict
556
- });
557
- if (warmPaths.length > 0) url = await deployWithCdnWarmup(root, warmPaths, {
558
- ...wranglerOptions,
559
- warmCdnConcurrency: options.warmCdnConcurrency,
560
- warmCdnTimeout: options.warmCdnTimeout,
561
- warmCdnRetries: options.warmCdnRetries,
562
- warmCdnStrict: options.warmCdnStrict
563
- });
564
- else {
565
- console.log("\n CDN warmup skipped: no build-discovered paths found.");
566
- url = await runWranglerDeploy(root, wranglerOptions);
567
- }
568
- } else url = await runWranglerDeploy(root, wranglerOptions);
1100
+ if (options.warmCdnCache) url = await deployWithCdnWarmup(root, [], {
1101
+ ...wranglerOptions,
1102
+ cacheabilityProbe: needsCacheabilityProbeManifest,
1103
+ discoverWarmPlan: async ({ headers, targetUrl }) => {
1104
+ const discovery = await discoverPrerenderPathManifest({
1105
+ root: info.root,
1106
+ nextConfig,
1107
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1108
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1109
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1110
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1111
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
1112
+ routeRootConfig: viteConfigMetadata.routeRootConfig,
1113
+ pathDiscoveryTarget: {
1114
+ baseUrl: targetUrl,
1115
+ headers,
1116
+ phaseTimeoutMs: options.warmCdnDiscoveryTimeout ?? DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS,
1117
+ retries: options.warmCdnDiscoveryRetries,
1118
+ retryDelayMs: DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS
1119
+ }
1120
+ });
1121
+ if (!discovery) return {
1122
+ loadingShellPaths: [],
1123
+ paths: [],
1124
+ rscPaths: []
1125
+ };
1126
+ return createPrerenderWarmPlan(root, discovery, {
1127
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1128
+ includeFallbackShells: options.warmCdnIncludeFallbacks,
1129
+ strict: options.warmCdnCertify === true || !options.dangerouslyPromoteOnCdnWarmError
1130
+ });
1131
+ },
1132
+ statusSource: hasVinextCacheWarmupStatus ? "vinext" : "cloudflare",
1133
+ warmCdnConcurrency: options.warmCdnConcurrency,
1134
+ warmCdnTarget,
1135
+ warmCdnTimeout: options.warmCdnTimeout,
1136
+ warmCdnRetries: options.warmCdnRetries,
1137
+ warmCdnDiscoveryTimeout: options.warmCdnDiscoveryTimeout,
1138
+ warmCdnDiscoveryRetries: options.warmCdnDiscoveryRetries,
1139
+ warmCdnProbeTimeout: options.warmCdnProbeTimeout,
1140
+ warmCdnProbeRetries: options.warmCdnProbeRetries,
1141
+ warmCdnCertify: options.warmCdnCertify,
1142
+ warmCdnReadinessTimeout: options.warmCdnReadinessTimeout,
1143
+ warmCdnReadinessRetries: options.warmCdnReadinessRetries,
1144
+ warmCdnReadinessProbes: options.warmCdnReadinessProbes,
1145
+ warmCdnReadinessProbeDelay: options.warmCdnReadinessProbeDelay,
1146
+ dangerouslyPromoteOnCdnWarmError: options.dangerouslyPromoteOnCdnWarmError,
1147
+ warmCdnPromote: options.warmCdnPromote,
1148
+ warmCdnPromotionDelay: options.warmCdnPromotionDelay
1149
+ });
1150
+ else url = await runWranglerDeploy(root, wranglerOptions);
569
1151
  console.log("\n ─────────────────────────────────────────");
570
1152
  console.log(` Deployed to: ${url}`);
571
1153
  console.log(" ─────────────────────────────────────────\n");
572
1154
  }
573
1155
  //#endregion
574
- export { buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, parseDeployArgs, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, validateWranglerEnvName, withCloudflareEnv };
1156
+ export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, validateWranglerEnvName, withCloudflareEnv };