@vinext/cloudflare 1.0.0-beta.1 → 1.0.0-beta.11

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 (66) hide show
  1. package/README.md +193 -1
  2. package/dist/cache/cdn-adapter-config.d.ts +31 -0
  3. package/dist/cache/cdn-adapter-config.js +95 -0
  4. package/dist/cache/cdn-adapter.d.ts +48 -15
  5. package/dist/cache/cdn-adapter.js +50 -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/config.d.ts +137 -0
  13. package/dist/cache/config.js +87 -0
  14. package/dist/cache/kv-data-adapter.d.ts +32 -8
  15. package/dist/cache/kv-data-adapter.js +5 -1
  16. package/dist/cache/kv-data-adapter.runtime.d.ts +45 -13
  17. package/dist/cache/kv-data-adapter.runtime.js +132 -52
  18. package/dist/cache/kv-key.d.ts +15 -0
  19. package/dist/cache/kv-key.js +43 -0
  20. package/dist/cache/response-store-adapter.d.ts +73 -0
  21. package/dist/cache/response-store-adapter.js +64 -0
  22. package/dist/cache/response-store-adapter.self-contained.worker.d.ts +18 -0
  23. package/dist/cache/response-store-adapter.self-contained.worker.js +12 -0
  24. package/dist/cache/response-store-adapter.service-binding.worker.d.ts +16 -0
  25. package/dist/cache/response-store-adapter.service-binding.worker.js +11 -0
  26. package/dist/cache/response-store-adapter.worker.d.ts +14 -0
  27. package/dist/cache/response-store-adapter.worker.js +278 -0
  28. package/dist/cache/response-store-cdn.runtime.d.ts +5 -0
  29. package/dist/cache/response-store-cdn.runtime.js +41 -0
  30. package/dist/cache/response-store-data.runtime.d.ts +34 -0
  31. package/dist/cache/response-store-data.runtime.js +300 -0
  32. package/dist/cacheability-artifact.d.ts +10 -0
  33. package/dist/cacheability-artifact.js +215 -0
  34. package/dist/cacheability-manifest-limits.d.ts +4 -0
  35. package/dist/cacheability-manifest-limits.js +7 -0
  36. package/dist/cacheability-probe.d.ts +49 -0
  37. package/dist/cacheability-probe.js +660 -0
  38. package/dist/cdn-warm.d.ts +110 -12
  39. package/dist/cdn-warm.js +880 -82
  40. package/dist/cli.d.ts +1 -1
  41. package/dist/cli.js +15 -1
  42. package/dist/deploy-config.d.ts +14 -11
  43. package/dist/deploy-config.js +24 -5
  44. package/dist/deploy-help.d.ts +3 -4
  45. package/dist/deploy-help.js +65 -22
  46. package/dist/deploy.d.ts +139 -42
  47. package/dist/deploy.js +946 -132
  48. package/dist/images/images-optimizer.d.ts +5 -5
  49. package/dist/images/images-optimizer.runtime.d.ts +1 -5
  50. package/dist/prerender-kv-populate.d.ts +3 -4
  51. package/dist/prerender-kv-populate.js +51 -18
  52. package/dist/tpr.d.ts +34 -70
  53. package/dist/tpr.js +50 -603
  54. package/dist/utils/cache-control-metadata.d.ts +3 -4
  55. package/dist/version-deploy.d.ts +46 -17
  56. package/dist/version-deploy.js +151 -14
  57. package/dist/version-headers.d.ts +2 -0
  58. package/dist/version-headers.js +2 -0
  59. package/dist/worker-deployment-url.d.ts +9 -0
  60. package/dist/worker-deployment-url.js +45 -0
  61. package/dist/workers-dev-url.d.ts +2 -3
  62. package/dist/wrangler-config.d.ts +19 -0
  63. package/dist/wrangler-config.js +294 -0
  64. package/dist/wrangler-version-metadata.d.ts +7 -0
  65. package/dist/wrangler-version-metadata.js +9 -0
  66. package/package.json +21 -11
package/dist/deploy.js CHANGED
@@ -1,21 +1,31 @@
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";
5
- import { runWranglerDeploymentStatus, runWranglerTriggersDeploy, runWranglerVersionDeploy, runWranglerVersionUpload } from "./version-deploy.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";
10
+ import { readBuildOutputWorkerName, runCfDeploymentStatus, runCfTriggersDeploy, runCfVersionDeploy, runCfVersionUpload, 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";
23
+ import { VINEXT_BUILD_LIFECYCLE_CONFIG } from "vinext/internal/build/lifecycle";
15
24
  import { runPrerender } from "vinext/internal/build/run-prerender";
25
+ import { printBuildReport } from "vinext/internal/build/report";
16
26
  import { loadDotenv } from "vinext/internal/config/dotenv";
17
27
  import { findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "vinext/internal/config/next-config";
18
- import { findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, resolveVinextPrerenderDecision } from "vinext/internal/config/prerender";
28
+ import { cacheWarmupStatusSource, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasUncachedRequestRouting, hasVerbatimResponseVary, isConfiguredCdnResponsePolicyHeader, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision, supportsCanonicalRscWarmup } from "vinext/internal/config/prerender";
19
29
  import { detectProject, findInNodeModules, formatMissingCloudflarePluginError, getMissingDeps } from "vinext/internal/utils/project";
20
30
  import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
21
31
  //#region src/deploy.ts
@@ -28,6 +38,7 @@ import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
28
38
  * 2. Runs the Vite build
29
39
  * 3. Deploys to Cloudflare Workers via Wrangler
30
40
  */
41
+ const DEFAULT_CDN_WARM_PROMOTION_DELAY_MS = 15e3;
31
42
  function parsePositiveIntegerArg(raw, flag) {
32
43
  if (raw === "") throw new Error(`${flag} requires a value, but none was provided.`);
33
44
  const parsed = Number(raw);
@@ -40,10 +51,31 @@ function parseNonNegativeIntegerArg(raw, flag) {
40
51
  if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
41
52
  return parsed;
42
53
  }
54
+ const MAX_NODE_TIMER_DELAY_MS = 2147483647;
55
+ function validateTimerDelay(value, flag, raw = String(value)) {
56
+ if (!Number.isInteger(value) || value < 0) throw new Error(`${flag} expects a non-negative integer, but got "${raw}".`);
57
+ if (value > MAX_NODE_TIMER_DELAY_MS) throw new Error(`${flag} must not exceed ${MAX_NODE_TIMER_DELAY_MS} milliseconds, but got "${raw}".`);
58
+ return value;
59
+ }
60
+ function validatePromotionDelay(value, raw = String(value)) {
61
+ return validateTimerDelay(value, "--warm-cdn-promotion-delay", raw);
62
+ }
63
+ function validateCdnWarmTarget(raw) {
64
+ const value = raw.trim();
65
+ let url;
66
+ try {
67
+ url = new URL(value);
68
+ } catch {
69
+ throw new Error(`--warm-cdn-target expects an HTTPS origin, but got "${raw}".`);
70
+ }
71
+ 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}".`);
72
+ return url.origin;
73
+ }
43
74
  function formatUnknownError(error) {
44
75
  if (error instanceof Error && error.message) return error.message;
45
76
  return String(error);
46
77
  }
78
+ var StagedWarmupError = class extends Error {};
47
79
  /** Deploy command flag definitions for util.parseArgs. */
48
80
  const deployArgOptions = {
49
81
  help: {
@@ -66,6 +98,10 @@ const deployArgOptions = {
66
98
  type: "boolean",
67
99
  default: false
68
100
  },
101
+ verbose: {
102
+ type: "boolean",
103
+ default: false
104
+ },
69
105
  "prerender-all": {
70
106
  type: "boolean",
71
107
  default: false
@@ -75,17 +111,46 @@ const deployArgOptions = {
75
111
  type: "boolean",
76
112
  default: false
77
113
  },
114
+ "warm-cdn-target": { type: "string" },
78
115
  "warm-cdn-concurrency": { type: "string" },
79
116
  "warm-cdn-timeout": { type: "string" },
80
117
  "warm-cdn-retries": { type: "string" },
81
- "warm-cdn-strict": {
118
+ "warm-cdn-discovery-timeout": { type: "string" },
119
+ "warm-cdn-discovery-retries": { type: "string" },
120
+ "warm-cdn-probe-timeout": { type: "string" },
121
+ "warm-cdn-probe-retries": { type: "string" },
122
+ "warm-cdn-certify": {
82
123
  type: "boolean",
83
124
  default: false
84
125
  },
126
+ "warm-cdn-readiness-timeout": { type: "string" },
127
+ "warm-cdn-readiness-retries": { type: "string" },
128
+ "warm-cdn-readiness-probes": { type: "string" },
129
+ "warm-cdn-readiness-probe-delay": { type: "string" },
130
+ "dangerously-promote-on-cdn-warm-error": {
131
+ type: "boolean",
132
+ default: false
133
+ },
134
+ "no-promote": {
135
+ type: "boolean",
136
+ default: false
137
+ },
138
+ "warm-cdn-no-promote": {
139
+ type: "boolean",
140
+ default: false
141
+ },
142
+ "warm-cdn-promotion-delay": { type: "string" },
85
143
  "warm-cdn-include-fallbacks": {
86
144
  type: "boolean",
87
145
  default: false
88
146
  },
147
+ "experimental-traffic-aware-warm-cache": {
148
+ type: "boolean",
149
+ default: false
150
+ },
151
+ "traffic-aware-coverage": { type: "string" },
152
+ "traffic-aware-limit": { type: "string" },
153
+ "traffic-aware-window": { type: "string" },
89
154
  "experimental-tpr": {
90
155
  type: "boolean",
91
156
  default: false
@@ -100,6 +165,8 @@ function parseDeployArgs(args) {
100
165
  options: deployArgOptions,
101
166
  strict: true
102
167
  });
168
+ if (values["warm-cdn-certify"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-certify requires --experimental-warm-cdn-cache.");
169
+ if (values["warm-cdn-target"] && !values["experimental-warm-cdn-cache"]) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
103
170
  function parseIntArg(name, raw) {
104
171
  if (!raw) return void 0;
105
172
  const n = parseInt(raw, 10);
@@ -117,18 +184,31 @@ function parseDeployArgs(args) {
117
184
  config: values.config?.trim() || void 0,
118
185
  skipBuild: values["skip-build"],
119
186
  dryRun: values["dry-run"],
187
+ verbose: values.verbose,
120
188
  prerenderAll: values["prerender-all"],
121
189
  prerenderConcurrency: values["prerender-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["prerender-concurrency"], "--prerender-concurrency"),
122
190
  warmCdnCache: values["experimental-warm-cdn-cache"],
191
+ warmCdnTarget: values["warm-cdn-target"] === void 0 ? void 0 : validateCdnWarmTarget(values["warm-cdn-target"]),
123
192
  warmCdnConcurrency: values["warm-cdn-concurrency"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-concurrency"], "--warm-cdn-concurrency"),
124
193
  warmCdnTimeout: values["warm-cdn-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-timeout"], "--warm-cdn-timeout"),
125
194
  warmCdnRetries: values["warm-cdn-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-retries"], "--warm-cdn-retries"),
126
- warmCdnStrict: values["warm-cdn-strict"],
195
+ warmCdnDiscoveryTimeout: values["warm-cdn-discovery-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-discovery-timeout"], "--warm-cdn-discovery-timeout"),
196
+ warmCdnDiscoveryRetries: values["warm-cdn-discovery-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-discovery-retries"], "--warm-cdn-discovery-retries"),
197
+ warmCdnProbeTimeout: values["warm-cdn-probe-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-probe-timeout"], "--warm-cdn-probe-timeout"),
198
+ warmCdnProbeRetries: values["warm-cdn-probe-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-probe-retries"], "--warm-cdn-probe-retries"),
199
+ warmCdnCertify: values["warm-cdn-certify"],
200
+ warmCdnReadinessTimeout: values["warm-cdn-readiness-timeout"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-timeout"], "--warm-cdn-readiness-timeout"),
201
+ warmCdnReadinessRetries: values["warm-cdn-readiness-retries"] === void 0 ? void 0 : parseNonNegativeIntegerArg(values["warm-cdn-readiness-retries"], "--warm-cdn-readiness-retries"),
202
+ warmCdnReadinessProbes: values["warm-cdn-readiness-probes"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-probes"], "--warm-cdn-readiness-probes"),
203
+ 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"]),
204
+ dangerouslyPromoteOnCdnWarmError: values["dangerously-promote-on-cdn-warm-error"],
205
+ warmCdnPromote: !values["no-promote"] && !values["warm-cdn-no-promote"],
206
+ 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
207
  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"])
208
+ experimentalTPR: values["experimental-traffic-aware-warm-cache"] || values["experimental-tpr"],
209
+ tprCoverage: parseIntArg("traffic-aware-coverage", values["traffic-aware-coverage"] ?? values["tpr-coverage"]),
210
+ tprLimit: parseIntArg("traffic-aware-limit", values["traffic-aware-limit"] ?? values["tpr-limit"]),
211
+ tprWindow: parseIntArg("traffic-aware-window", values["traffic-aware-window"] ?? values["tpr-window"])
132
212
  };
133
213
  }
134
214
  /**
@@ -164,26 +244,52 @@ async function loadProjectViteApi(root) {
164
244
  }
165
245
  return await (vitePath === "vite" ? import(vitePath) : import(pathToFileURL(vitePath).href));
166
246
  }
167
- async function loadDeployViteConfigMetadata(root) {
247
+ async function loadProjectWranglerApi(root) {
248
+ const req = createRequire(path.join(root, "package.json"));
249
+ let wranglerPath;
250
+ try {
251
+ wranglerPath = req.resolve("wrangler");
252
+ } catch {
253
+ const cloudflarePluginPath = req.resolve("@cloudflare/vite-plugin");
254
+ wranglerPath = createRequire(cloudflarePluginPath).resolve("wrangler");
255
+ }
256
+ return await import(
257
+ /* @vite-ignore */
258
+ pathToFileURL(wranglerPath).href
259
+ );
260
+ }
261
+ function resolveViteBuildMode(deploymentTool, env) {
262
+ return deploymentTool === "cf" ? env ?? "production" : "production";
263
+ }
264
+ async function loadDeployViteConfigMetadata(root, mode) {
168
265
  const plugins = (await (await loadProjectViteApi(root)).loadConfigFromFile({
169
266
  command: "build",
170
- mode: "production"
267
+ mode
171
268
  }, void 0, root))?.config.plugins;
172
269
  return {
173
- cacheConfig: viteConfigHasCacheAdapter(root) ? await findVinextCacheConfigInPlugins(plugins) : null,
270
+ cacheConfig: await findVinextCacheConfigInPlugins(plugins),
174
271
  nextConfig: await findVinextNextConfigInPlugins(plugins),
175
272
  prerenderConfig: await findVinextPrerenderConfigInPlugins(plugins),
176
273
  routeRootConfig: await findVinextRouteRootConfigInPlugins(plugins)
177
274
  };
178
275
  }
179
- async function runBuild(info, env) {
276
+ async function runBuild(info, env, mode) {
180
277
  console.log("\n Building for Cloudflare Workers...\n");
181
278
  const { createBuilder } = await loadProjectViteApi(info.root);
279
+ let completed = false;
182
280
  await withCloudflareEnv(env, async () => {
183
- await (await createBuilder({ root: info.root })).buildApp();
281
+ const invocation = { onComplete() {
282
+ completed = true;
283
+ } };
284
+ await (await createBuilder({
285
+ root: info.root,
286
+ mode,
287
+ [VINEXT_BUILD_LIFECYCLE_CONFIG]: invocation
288
+ })).buildApp();
184
289
  });
290
+ if (!completed) throw new Error("[vinext] The Cloudflare build lifecycle did not complete.");
185
291
  }
186
- async function populateKVCacheFromPrerenderedArtifacts(root, wranglerEnv, cacheConfig) {
292
+ async function populateKVCacheFromPrerenderedArtifacts(root, deploymentTool, env, cacheConfig) {
187
293
  const kvConfig = resolveKvDataAdapterConfig(cacheConfig);
188
294
  if (!kvConfig) return;
189
295
  const { routeCount, pairs } = buildPrerenderKVPairs(path.join(root, "dist", "server"), {
@@ -194,9 +300,10 @@ async function populateKVCacheFromPrerenderedArtifacts(root, wranglerEnv, cacheC
194
300
  console.log(" KV cache: Skipping prerender upload (no App Router prerendered cache entries found).");
195
301
  return;
196
302
  }
197
- await runWranglerKVBulkPut(root, {
303
+ await runKVBulkPut(root, {
198
304
  binding: kvConfig.binding,
199
- env: wranglerEnv,
305
+ deploymentTool,
306
+ env,
200
307
  pairs
201
308
  });
202
309
  console.log(` KV cache: Uploaded ${pairs.length} entr${pairs.length === 1 ? "y" : "ies"} for ${routeCount} prerendered route${routeCount === 1 ? "" : "s"}.`);
@@ -217,6 +324,27 @@ function buildWranglerDeployArgs(options) {
217
324
  env
218
325
  };
219
326
  }
327
+ function resolveDeploymentTool(root) {
328
+ return fs.existsSync(path.join(root, "cloudflare.config.ts")) ? "cf" : "wrangler";
329
+ }
330
+ function isCfCliInstalled(root, resolvePackageJson = (projectRoot) => {
331
+ try {
332
+ return createRequire(path.join(projectRoot, "package.json")).resolve("cf/package.json");
333
+ } catch {
334
+ return findInNodeModules(projectRoot, "cf/package.json");
335
+ }
336
+ }) {
337
+ return resolvePackageJson(root) !== null;
338
+ }
339
+ function buildCfDeployArgs(options) {
340
+ const mode = options.env || (options.preview ? "preview" : void 0);
341
+ const args = ["deploy", "--prebuilt"];
342
+ if (mode) args.push("--mode", validateWranglerEnvName(mode));
343
+ return {
344
+ args,
345
+ mode
346
+ };
347
+ }
220
348
  function buildWranglerKVBulkPutArgs(options) {
221
349
  const env = options.env || void 0;
222
350
  const args = [
@@ -255,6 +383,21 @@ function resolveWranglerBin(root, resolvePackageJson = (projectRoot) => {
255
383
  }
256
384
  return path.join(root, "node_modules", "wrangler", "bin", "wrangler.js");
257
385
  }
386
+ function resolveCfBin(root, resolvePackageJson = (projectRoot) => {
387
+ try {
388
+ return createRequire(path.join(projectRoot, "package.json")).resolve("cf/package.json");
389
+ } catch {
390
+ return findInNodeModules(projectRoot, "cf/package.json");
391
+ }
392
+ }) {
393
+ const packageJsonPath = resolvePackageJson(root);
394
+ if (packageJsonPath) {
395
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
396
+ const bin = typeof packageJson.bin === "string" ? packageJson.bin : packageJson.bin?.cf;
397
+ if (bin) return path.resolve(path.dirname(packageJsonPath), bin);
398
+ }
399
+ return path.join(root, "node_modules", "cf", "bin", "cf");
400
+ }
258
401
  function buildNodeCliInvocation(scriptPath, args, nodeExecutable = process.execPath) {
259
402
  return {
260
403
  file: nodeExecutable,
@@ -269,21 +412,35 @@ function buildWranglerInvocation(root, options, nodeExecutable = process.execPat
269
412
  env
270
413
  };
271
414
  }
272
- async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutable = process.execPath) {
415
+ async function runKVBulkPut(root, options, execute = spawn, nodeExecutable = process.execPath) {
416
+ const cfNamespaceId = (() => {
417
+ if (options.deploymentTool !== "cf") return void 0;
418
+ const configPath = path.join(root, ".cloudflare/output/v0/workers/default/worker.config.json");
419
+ const binding = JSON.parse(fs.readFileSync(configPath, "utf8")).env?.[options.binding];
420
+ if (binding?.type !== "kv" || typeof binding.id !== "string" || !binding.id) throw new Error(`[vinext] Generated Cloudflare Build Output does not declare KV binding ${JSON.stringify(options.binding)} with a namespace ID.`);
421
+ return binding.id;
422
+ })();
273
423
  const tempDir = fs.mkdtempSync(path.join(options.tempDir ?? os.tmpdir(), "vinext-kv-bulk-"));
274
424
  try {
275
- const wranglerBin = resolveWranglerBin(root);
425
+ const cliBin = cfNamespaceId ? resolveCfBin(root) : resolveWranglerBin(root);
276
426
  const totalChunks = Math.ceil(options.pairs.length / KV_BULK_PUT_CHUNK_SIZE);
277
427
  for (let i = 0; i < totalChunks; i++) {
278
428
  const filePath = path.join(tempDir, `prerender-kv-${i}.json`);
279
429
  const chunk = options.pairs.slice(i * KV_BULK_PUT_CHUNK_SIZE, (i + 1) * KV_BULK_PUT_CHUNK_SIZE);
280
430
  fs.writeFileSync(filePath, JSON.stringify(chunk), "utf-8");
281
- const { args } = buildWranglerKVBulkPutArgs({
431
+ const invocation = buildNodeCliInvocation(cliBin, cfNamespaceId ? [
432
+ "kv",
433
+ "bulk",
434
+ "update",
435
+ cfNamespaceId,
436
+ "--body",
437
+ `@${filePath}`,
438
+ ...options.env ? ["--mode", validateWranglerEnvName(options.env)] : []
439
+ ] : buildWranglerKVBulkPutArgs({
282
440
  binding: options.binding,
283
441
  env: options.env,
284
442
  filePath
285
- });
286
- const invocation = buildNodeCliInvocation(wranglerBin, args, nodeExecutable);
443
+ }).args, nodeExecutable);
287
444
  const child = execute(invocation.file, invocation.args, {
288
445
  cwd: root,
289
446
  stdio: "inherit",
@@ -297,7 +454,7 @@ async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutab
297
454
  return;
298
455
  }
299
456
  const exitReason = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
300
- reject(/* @__PURE__ */ new Error(`Wrangler KV bulk put failed with ${exitReason}.`));
457
+ reject(/* @__PURE__ */ new Error(`${cfNamespaceId ? "cf" : "Wrangler"} KV bulk upload failed with ${exitReason}.`));
301
458
  });
302
459
  });
303
460
  }
@@ -309,6 +466,7 @@ async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutab
309
466
  }
310
467
  }
311
468
  async function runWranglerDeploy(root, options, execute = spawn) {
469
+ if (options.promote === false) return runWranglerVersionUpload(root, options).previewUrl ?? "(Preview URL not detected in wrangler output)";
312
470
  const spawnOptions = {
313
471
  cwd: root,
314
472
  stdio: [
@@ -344,90 +502,603 @@ async function runWranglerDeploy(root, options, execute = spawn) {
344
502
  reject(/* @__PURE__ */ new Error(`Wrangler deploy failed with ${exitReason}.`));
345
503
  });
346
504
  });
347
- return parseWorkersDevUrl(output) ?? "(URL not detected in wrangler output)";
505
+ return parseWorkerDeploymentUrl(output) ?? "(URL not detected in wrangler output)";
506
+ }
507
+ async function runCfDeploy(root, options, execute = spawn) {
508
+ const { args, mode } = buildCfDeployArgs(options);
509
+ console.log(mode ? `\n Deploying Build Output in mode: ${mode}...` : "\n Deploying Build Output...");
510
+ const child = execute(process.execPath, [resolveCfBin(root), ...args], {
511
+ cwd: root,
512
+ stdio: [
513
+ "inherit",
514
+ "pipe",
515
+ "pipe"
516
+ ],
517
+ shell: false
518
+ });
519
+ let output = "";
520
+ child.stdout?.on("data", (chunk) => {
521
+ const text = chunk.toString();
522
+ output += text;
523
+ process.stdout.write(text);
524
+ });
525
+ child.stderr?.on("data", (chunk) => {
526
+ const text = chunk.toString();
527
+ output += text;
528
+ process.stderr.write(text);
529
+ });
530
+ await new Promise((resolve, reject) => {
531
+ child.once("error", reject);
532
+ child.once("close", (code, signal) => {
533
+ if (code === 0) resolve();
534
+ else reject(/* @__PURE__ */ new Error(`cf deploy failed with ${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`}.`));
535
+ });
536
+ });
537
+ return parseWorkerDeploymentUrl(output) ?? "(URL not detected in cf output)";
538
+ }
539
+ function hasCdnWarmRequests(plan) {
540
+ return plan.paths.length + (plan.pagesDataPaths?.length ?? 0) + (plan.routeHandlerPaths?.length ?? 0) + plan.rscPaths.length + plan.loadingShellPaths.length > 0;
541
+ }
542
+ function selectTPRWarmPlan(plan, traffic, coverage, limit) {
543
+ const canonical = (pathname) => normalizePathTrailingSlash(pathname, false);
544
+ const resolved = new Set(Object.entries(plan.routePatterns ?? {}).map(([pathname, route]) => canonical(route.cacheabilityProbe?.concretePathname ?? pathname)));
545
+ const requestsByPath = /* @__PURE__ */ new Map();
546
+ for (const { path, requests } of traffic) {
547
+ const pathname = canonical(path);
548
+ if (!resolved.has(pathname)) continue;
549
+ requestsByPath.set(pathname, (requestsByPath.get(pathname) ?? 0) + requests);
550
+ }
551
+ const selected = new Set(selectRoutes(Array.from(requestsByPath, ([path, requests]) => ({
552
+ path,
553
+ requests
554
+ })).sort((a, b) => b.requests - a.requests), coverage, limit).routes.map(({ path }) => path));
555
+ const includes = (pathname) => selected.has(canonical(plan.routePatterns?.[pathname]?.cacheabilityProbe?.concretePathname ?? pathname));
556
+ const filter = (pathnames) => pathnames?.filter(includes);
557
+ return {
558
+ ...plan,
559
+ appPaths: filter(plan.appPaths),
560
+ loadingShellPaths: filter(plan.loadingShellPaths) ?? [],
561
+ pagesDataPaths: filter(plan.pagesDataPaths),
562
+ pagesPaths: filter(plan.pagesPaths),
563
+ paths: filter(plan.paths) ?? [],
564
+ routeHandlerPaths: filter(plan.routeHandlerPaths),
565
+ routePatterns: plan.routePatterns ? Object.fromEntries(Object.entries(plan.routePatterns).filter(([pathname]) => includes(pathname))) : void 0,
566
+ rscPaths: filter(plan.rscPaths) ?? []
567
+ };
568
+ }
569
+ function projectRequiresRouteCacheabilityProbeManifest(project, cacheConfig) {
570
+ return (project.isAppRouter || project.isPagesRouter) && requiresRouteCacheabilityProbeManifest(cacheConfig);
571
+ }
572
+ function deploymentWorkerName(options) {
573
+ if (!options.name) throw new Error("Could not detect the Worker name for its deployment.");
574
+ return options.name;
575
+ }
576
+ function runDeploymentStatus(root, options) {
577
+ return options.deploymentTool === "cf" ? runCfDeploymentStatus(root, {
578
+ name: deploymentWorkerName(options),
579
+ env: options.env,
580
+ verbose: options.verbose
581
+ }) : runWranglerDeploymentStatus(root, options);
582
+ }
583
+ function runVersionDeploy(root, traffic, options, phase) {
584
+ return options.deploymentTool === "cf" ? runCfVersionDeploy(root, traffic, {
585
+ name: deploymentWorkerName(options),
586
+ env: options.env,
587
+ verbose: options.verbose
588
+ }, phase) : runWranglerVersionDeploy(root, traffic, options, phase);
589
+ }
590
+ function runTriggersDeploy(root, options) {
591
+ return options.deploymentTool === "cf" ? runCfTriggersDeploy(root, options) : runWranglerTriggersDeploy(root, options);
592
+ }
593
+ function runVersionUpload(root, options) {
594
+ return options.deploymentTool === "cf" ? runCfVersionUpload(root, options) : runWranglerVersionUpload(root, options);
595
+ }
596
+ function cdnWarmTargetKey(target) {
597
+ return `${target.kind}\0${target.sourcePathname}`;
598
+ }
599
+ function requiredCdnWarmTargetKeys(plan, optionalTargetKeys) {
600
+ const keys = /* @__PURE__ */ new Set([
601
+ ...plan.paths.map((pathname) => `html\0${pathname}`),
602
+ ...plan.pagesDataPaths.map((pathname) => `pages-data\0${pathname}`),
603
+ ...(plan.routeHandlerPaths ?? []).map((pathname) => `app-route\0${pathname}`),
604
+ ...plan.rscPaths.map((pathname) => `rsc-full\0${pathname}`),
605
+ ...plan.loadingShellPaths.map((pathname) => `rsc-loading-shell\0${pathname}`)
606
+ ]);
607
+ if (optionalTargetKeys) for (const key of optionalTargetKeys) keys.delete(key);
608
+ return keys;
348
609
  }
349
610
  async function deployWithCdnWarmup(root, paths, options) {
350
- const upload = runWranglerVersionUpload(root, options);
351
- const wranglerConfig = parseWranglerConfig(root, options.config);
352
- const stagingTraffic = getZeroPercentStagingTraffic(readWranglerDeploymentStatus(root, options), upload.versionId);
611
+ if (options.warmCdnTarget !== void 0) options = {
612
+ ...options,
613
+ warmCdnTarget: validateCdnWarmTarget(options.warmCdnTarget)
614
+ };
615
+ if (options.warmCdnDiscoveryTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnDiscoveryTimeout), "--warm-cdn-discovery-timeout");
616
+ if (options.warmCdnDiscoveryRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnDiscoveryRetries), "--warm-cdn-discovery-retries");
617
+ if (options.warmCdnProbeTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnProbeTimeout), "--warm-cdn-probe-timeout");
618
+ if (options.warmCdnProbeRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnProbeRetries), "--warm-cdn-probe-retries");
619
+ if (options.warmCdnReadinessTimeout !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessTimeout), "--warm-cdn-readiness-timeout");
620
+ if (options.warmCdnReadinessRetries !== void 0) parseNonNegativeIntegerArg(String(options.warmCdnReadinessRetries), "--warm-cdn-readiness-retries");
621
+ if (options.warmCdnReadinessProbes !== void 0) parsePositiveIntegerArg(String(options.warmCdnReadinessProbes), "--warm-cdn-readiness-probes");
622
+ if (options.warmCdnReadinessProbeDelay !== void 0) validateTimerDelay(options.warmCdnReadinessProbeDelay, "--warm-cdn-readiness-probe-delay");
623
+ if (options.warmCdnPromotionDelay !== void 0) validatePromotionDelay(options.warmCdnPromotionDelay);
624
+ if (options.cacheabilityProbe) return deployWithCacheabilityProbe(root, options);
625
+ return deployUploadedVersionWithCdnWarmup(root, paths, options);
626
+ }
627
+ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
628
+ const allowUnverifiedPromotion = options.dangerouslyPromoteOnCdnWarmError === true && options.warmCdnCertify !== true;
629
+ let deploymentId = options.deploymentId;
630
+ let expectedBuildId = options.expectedBuildId;
631
+ let expectedRscBuildId = options.expectedRscBuildId;
632
+ const hasPreparedWarmPlan = options.uploadedVersion !== void 0;
633
+ let warmPlanDiscovered = hasPreparedWarmPlan || options.discoverWarmPlan === void 0;
634
+ let remainingWarmPlan = {
635
+ loadingShellPaths: [...options.loadingShellPaths ?? []],
636
+ pagesDataPaths: [...options.pagesDataPaths ?? []],
637
+ paths: [...paths],
638
+ routeHandlerPaths: [...options.routeHandlerPaths ?? []],
639
+ routePatterns: options.routePatterns ? { ...options.routePatterns } : void 0,
640
+ rscPaths: [...options.rscPaths ?? []]
641
+ };
642
+ let discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
643
+ const prepareWarmPlan = (plan) => {
644
+ if (plan.paths.length === 0 && plan.pagesDataPaths.length === 0 && (plan.routeHandlerPaths?.length ?? 0) === 0 || expectedBuildId !== void 0) return plan;
645
+ if (!allowUnverifiedPromotion) {
646
+ const warmupKind = plan.paths.length > 0 ? "CDN HTML warmup" : plan.pagesDataPaths.length > 0 ? "CDN Pages data warmup" : "CDN Route Handler warmup";
647
+ 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.`);
648
+ }
649
+ 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.`);
650
+ return {
651
+ ...plan,
652
+ pagesDataPaths: [],
653
+ paths: [],
654
+ routeHandlerPaths: []
655
+ };
656
+ };
657
+ const discoverWarmPlan = async (targetUrl, headers) => {
658
+ if (!options.discoverWarmPlan) return;
659
+ const discovered = await options.discoverWarmPlan({
660
+ headers,
661
+ targetUrl
662
+ });
663
+ const plan = options.selectWarmPlan?.(discovered) ?? discovered;
664
+ deploymentId = plan.deploymentId;
665
+ expectedBuildId = plan.buildIdentity;
666
+ expectedRscBuildId = plan.rscBuildId;
667
+ remainingWarmPlan = {
668
+ loadingShellPaths: [...plan.loadingShellPaths],
669
+ pagesDataPaths: [...plan.pagesDataPaths ?? []],
670
+ paths: [...plan.paths],
671
+ routeHandlerPaths: [...plan.routeHandlerPaths ?? []],
672
+ routePatterns: plan.routePatterns ? { ...plan.routePatterns } : void 0,
673
+ rscPaths: [...plan.rscPaths]
674
+ };
675
+ discoveredWarmRequests = remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
676
+ warmPlanDiscovered = true;
677
+ };
678
+ if (!options.discoverWarmPlan || hasPreparedWarmPlan) remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
679
+ const upload = options.uploadedVersion ?? runVersionUpload(root, options);
680
+ const wranglerOptions = resolveDeploymentControlPlaneOptions(options, upload);
681
+ const warmUploadedVersion = (targetUrl, headers, propagatingTarget = false, plan = {
682
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
683
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
684
+ paths: remainingWarmPlan.paths,
685
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
686
+ routePatterns: remainingWarmPlan.routePatterns,
687
+ rscPaths: remainingWarmPlan.rscPaths
688
+ }, requireCacheHit = false) => warmCdnCache({
689
+ targetUrl,
690
+ paths: plan.paths,
691
+ headers,
692
+ propagatingTarget,
693
+ deploymentId,
694
+ expectedBuildId,
695
+ expectedRscBuildId,
696
+ loadingShellPaths: plan.loadingShellPaths,
697
+ pagesDataPaths: plan.pagesDataPaths,
698
+ routeHandlerPaths: plan.routeHandlerPaths,
699
+ routePatterns: plan.routePatterns,
700
+ rscPaths: plan.rscPaths,
701
+ concurrency: options.warmCdnConcurrency,
702
+ timeoutMs: options.warmCdnTimeout,
703
+ retries: options.warmCdnRetries,
704
+ retrySkippedTargetKeys: propagatingTarget && hasPreparedWarmPlan ? requiredCdnWarmTargetKeys(plan, options.optionalWarmTargetKeys) : void 0,
705
+ requireCacheHit,
706
+ strict: requireCacheHit || !allowUnverifiedPromotion,
707
+ statusSource: options.statusSource
708
+ });
709
+ const wranglerConfig = options.deploymentTool === "cf" ? null : parseWranglerConfig(root, options.config);
710
+ let deploymentStatus;
711
+ try {
712
+ deploymentStatus = runDeploymentStatus(root, wranglerOptions);
713
+ } catch (error) {
714
+ if (!hasPreparedWarmPlan) throw error;
715
+ throw new StagedWarmupError(formatUnknownError(error), { cause: error });
716
+ }
717
+ 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.");
718
+ const stagingTraffic = getZeroPercentStagingTraffic(deploymentStatus, upload.versionId);
719
+ 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.");
353
720
  let staged = null;
354
- let triggersDeployedUrl = null;
355
- let warmedBeforePromotion = false;
356
- let triggersApplied = false;
721
+ let triggersDeployedUrl = options.triggersDeployedUrl ?? null;
722
+ let stagedCacheFilled = false;
723
+ let stagedDeploymentState = null;
724
+ const initialWarmRequests = options.discoverWarmPlan === void 0 || hasPreparedWarmPlan ? remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length : 1;
725
+ let triggersApplied = options.triggersAlreadyApplied === true;
357
726
  function applyTriggers() {
358
727
  if (triggersApplied) return;
359
- triggersDeployedUrl = runWranglerTriggersDeploy(root, options).deployedUrl;
728
+ triggersDeployedUrl = runTriggersDeploy(root, wranglerOptions).deployedUrl;
360
729
  triggersApplied = true;
361
730
  }
362
731
  if (stagingTraffic) {
363
- staged = runWranglerVersionDeploy(root, stagingTraffic, options, "stage");
364
732
  try {
733
+ staged = runVersionDeploy(root, stagingTraffic, wranglerOptions, "stage");
734
+ } catch (error) {
735
+ throw reconcileVersionDeployFailure(root, wranglerOptions, error, {
736
+ desiredTraffic: stagingTraffic,
737
+ desiredDescription: "The uploaded version is staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes were not changed.",
738
+ priorState: deploymentStatus,
739
+ priorDescription: "Worker deployment traffic was not changed and Worker triggers/routes were not changed."
740
+ });
741
+ }
742
+ try {
743
+ if (hasPreparedWarmPlan) {
744
+ stagedDeploymentState = runDeploymentStatus(root, wranglerOptions);
745
+ 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.");
746
+ }
365
747
  applyTriggers();
366
748
  } catch (error) {
367
749
  throw withStagedVersionCleanupNote(error);
368
750
  }
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({
751
+ const targetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? staged.deployedUrl;
752
+ const headers = buildVersionOverrideHeaders(options.name ?? upload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, wranglerOptions), upload.versionId);
753
+ if (targetUrl && headers) try {
754
+ if (!warmPlanDiscovered) {
755
+ console.log(" CDN warmup: discovering paths from the staged Worker version...");
756
+ await discoverWarmPlan(targetUrl, headers);
757
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
758
+ 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).`);
759
+ }
760
+ const stagedWarmPlan = {
761
+ loadingShellPaths: remainingWarmPlan.loadingShellPaths,
762
+ pagesDataPaths: remainingWarmPlan.pagesDataPaths,
763
+ paths: remainingWarmPlan.paths,
764
+ routeHandlerPaths: remainingWarmPlan.routeHandlerPaths,
765
+ routePatterns: remainingWarmPlan.routePatterns,
766
+ rscPaths: remainingWarmPlan.rscPaths
767
+ };
768
+ const stagedWarmRequests = stagedWarmPlan.paths.length + stagedWarmPlan.pagesDataPaths.length + (stagedWarmPlan.routeHandlerPaths?.length ?? 0) + stagedWarmPlan.rscPaths.length + stagedWarmPlan.loadingShellPaths.length;
769
+ if (stagedWarmRequests > 0) {
770
+ console.log(" CDN warmup: waiting for the staged Worker version to become stable...");
771
+ const readiness = await waitForCdnWarmTargetReadiness({
374
772
  targetUrl,
375
- paths,
376
773
  headers,
377
- concurrency: options.warmCdnConcurrency,
378
- timeoutMs: options.warmCdnTimeout,
379
- retries: options.warmCdnRetries,
380
- strict: options.warmCdnStrict
774
+ plan: stagedWarmPlan,
775
+ prerenderSecret: options.prerenderSecret,
776
+ deploymentId,
777
+ expectedBuildId,
778
+ expectedRscBuildId,
779
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
780
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
781
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
782
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
783
+ timeoutMs: options.warmCdnTimeout
381
784
  });
785
+ if (!readiness.ready) {
786
+ const message = `CDN warmup could not verify staged Worker readiness: ${readiness.error}.`;
787
+ const noPromoteNote = options.warmCdnPromote === false ? " CDN warmup cannot continue because promotion is disabled and the staged version was not warmed." : "";
788
+ if (!allowUnverifiedPromotion || options.warmCdnPromote === false) throw new Error(`${message}${noPromoteNote}`);
789
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
790
+ } else {
791
+ console.log(" CDN warmup: staged Worker version is stable.");
792
+ const warmResult = await warmUploadedVersion(targetUrl, headers, true, stagedWarmPlan);
793
+ const optionalSkipped = options.optionalWarmTargetKeys ? warmResult.skippedTargets.filter((target) => options.optionalWarmTargetKeys.has(cdnWarmTargetKey(target))).length : 0;
794
+ if (hasPreparedWarmPlan && options.warmCdnCertify) {
795
+ 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.`);
796
+ }
797
+ const requiredSkipped = warmResult.skipped - optionalSkipped;
798
+ if (hasPreparedWarmPlan && requiredSkipped > 0) {
799
+ const message = `Two-stage CDN warming could not fill ${requiredSkipped}/${warmResult.total} planned cache entries because Cloudflare refused cache admission.`;
800
+ if (!allowUnverifiedPromotion) throw new Error(message);
801
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
802
+ }
803
+ 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.`);
804
+ remainingWarmPlan = {
805
+ loadingShellPaths: warmResult.retryPlan.loadingShellPaths,
806
+ pagesDataPaths: warmResult.retryPlan.pagesDataPaths,
807
+ paths: warmResult.retryPlan.paths,
808
+ routeHandlerPaths: warmResult.retryPlan.routeHandlerPaths,
809
+ rscPaths: warmResult.retryPlan.rscPaths
810
+ };
811
+ if (options.warmCdnCertify && warmResult.warmed > 0) {
812
+ console.log(` CDN warmup: certifying ${warmResult.warmed} staged cache entr${warmResult.warmed === 1 ? "y" : "ies"} before promotion...`);
813
+ const certification = await warmUploadedVersion(targetUrl, headers, true, warmResult.warmedPlan, true);
814
+ if (certification.warmed !== warmResult.warmed) throw new Error(`CDN warmup certified ${certification.warmed}/${warmResult.warmed} staged cache entries as reusable.`);
815
+ stagedCacheFilled = true;
816
+ } else stagedCacheFilled = warmResult.warmed > 0;
817
+ }
818
+ }
819
+ } catch (error) {
820
+ throw withStagedVersionCleanupNote(error);
821
+ }
822
+ else if (initialWarmRequests > 0) {
823
+ 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.";
824
+ if (!allowUnverifiedPromotion) throw new StagedWarmupError(`${message} ${getStagedVersionCleanupNote()}`);
825
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
826
+ }
827
+ } else {
828
+ if (initialWarmRequests > 0) {
829
+ const message = "CDN warmup cannot stage the uploaded Worker at 0% because the current deployment is not exactly one version serving 100% traffic.";
830
+ if (!allowUnverifiedPromotion) throw new Error(`${message} No traffic or triggers were changed.`);
831
+ console.warn(` ${message} Promoting because the dangerous override is enabled.`);
832
+ }
833
+ console.warn(" CDN warmup: pre-traffic version override skipped because the current deployment is not one version serving 100% traffic.");
834
+ }
835
+ const countRemainingWarmRequests = () => remainingWarmPlan.paths.length + remainingWarmPlan.pagesDataPaths.length + (remainingWarmPlan.routeHandlerPaths?.length ?? 0) + remainingWarmPlan.rscPaths.length + remainingWarmPlan.loadingShellPaths.length;
836
+ if (options.warmCdnPromote === false) {
837
+ 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.");
838
+ const remainingWarmRequests = countRemainingWarmRequests();
839
+ 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."));
840
+ if (remainingWarmRequests > 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error(`CDN warmup cannot skip promotion because ${remainingWarmRequests} request(s) remain unwarmed.`));
841
+ if (hasPreparedWarmPlan && stagedDeploymentState) {
842
+ let currentDeployment;
843
+ try {
844
+ currentDeployment = runDeploymentStatus(root, wranglerOptions);
382
845
  } catch (error) {
383
- throw withStagedVersionCleanupNote(error);
846
+ 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 the active Worker deployment before continuing.`, { cause: error });
384
847
  }
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, [{
848
+ 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.");
849
+ }
850
+ console.log(` CDN warmup: promotion disabled; uploaded Worker version ${upload.versionId} remains staged at 0% traffic and production Worker triggers/routes remain applied.`);
851
+ return staged.deployedUrl ?? triggersDeployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
852
+ }
853
+ let deployed;
854
+ let prePromotionState = null;
855
+ const promotionTraffic = [{
389
856
  versionId: upload.versionId,
390
857
  percentage: 100
391
- }], options, warmedBeforePromotion ? "promote-warmed" : "promote-uploaded");
392
- if (!warmedBeforePromotion) {
393
- try {
394
- applyTriggers();
395
- } catch (error) {
396
- throw withPromotedVersionTriggerNote(error);
858
+ }];
859
+ let promotionAttempted = false;
860
+ try {
861
+ if (stagedCacheFilled && options.statusSource !== "vinext") {
862
+ const promotionDelay = options.warmCdnPromotionDelay ?? 15e3;
863
+ if (promotionDelay > 0) {
864
+ console.log(` CDN warmup: waiting ${promotionDelay / 1e3} seconds for cache propagation before promotion...`);
865
+ await setTimeout(promotionDelay);
866
+ }
397
867
  }
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
868
+ if (hasPreparedWarmPlan && stagingTraffic) {
869
+ prePromotionState = runDeploymentStatus(root, wranglerOptions);
870
+ 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.");
871
+ }
872
+ promotionAttempted = true;
873
+ deployed = runVersionDeploy(root, promotionTraffic, wranglerOptions, stagedCacheFilled ? "promote-warmed" : "promote-uploaded");
874
+ } catch (error) {
875
+ if (promotionAttempted) throw reconcileVersionDeployFailure(root, wranglerOptions, error, {
876
+ desiredTraffic: promotionTraffic,
877
+ desiredDescription: "The uploaded version is already promoted to 100%; Worker triggers/routes may already have changed.",
878
+ priorState: prePromotionState ?? (stagingTraffic ? {
879
+ deploymentId: null,
880
+ output: "",
881
+ versions: stagingTraffic
882
+ } : deploymentStatus),
883
+ priorDescription: "The uploaded version remains staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes may already have changed."
406
884
  });
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.");
885
+ throw staged ? withStagedVersionCleanupNote(error) : error;
886
+ }
887
+ try {
888
+ applyTriggers();
889
+ } catch (error) {
890
+ throw withPromotedVersionTriggerNote(error);
891
+ }
892
+ let postPromotionTargetUrl = resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? deployed.deployedUrl;
893
+ if (!warmPlanDiscovered && postPromotionTargetUrl) try {
894
+ console.log(" CDN warmup: discovering paths from the promoted Worker version...");
895
+ await discoverWarmPlan(postPromotionTargetUrl);
896
+ remainingWarmPlan = prepareWarmPlan(remainingWarmPlan);
897
+ } catch (error) {
898
+ throw withPromotedVersionWarmupNote(error);
899
+ }
900
+ if (countRemainingWarmRequests() > 0) {
901
+ const targetUrl = postPromotionTargetUrl;
902
+ if (targetUrl) try {
903
+ await warmUploadedVersion(targetUrl, void 0, true, remainingWarmPlan);
904
+ } catch (error) {
905
+ throw withPromotedVersionWarmupNote(error);
906
+ }
907
+ 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."));
908
+ else console.warn(" CDN warmup skipped: no production URL could be inferred after promotion; the dangerous override was enabled.");
409
909
  }
410
910
  return deployed.deployedUrl ?? triggersDeployedUrl ?? staged?.deployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
411
911
  }
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;
912
+ function deploymentTrafficEquals(actual, expected) {
913
+ const normalize = (traffic) => [...traffic].sort((a, b) => a.versionId.localeCompare(b.versionId));
914
+ const normalizedActual = normalize(actual);
915
+ const normalizedExpected = normalize(expected);
916
+ return normalizedActual.length === normalizedExpected.length && normalizedActual.every((version, index) => version.versionId === normalizedExpected[index]?.versionId && version.percentage === normalizedExpected[index]?.percentage);
418
917
  }
419
- function readWranglerDeploymentStatus(root, options) {
918
+ function deploymentStateEquals(actual, expected) {
919
+ if (expected.deploymentId !== null && actual.deploymentId !== expected.deploymentId) return false;
920
+ return deploymentTrafficEquals(actual.versions, expected.versions);
921
+ }
922
+ function reconcileVersionDeployFailure(root, options, error, expected) {
923
+ const message = error instanceof Error ? error.message : String(error);
924
+ let reconciliation;
925
+ let mayNeedCleanup = true;
420
926
  try {
421
- return runWranglerDeploymentStatus(root, options);
422
- } catch {
423
- return null;
927
+ const current = runDeploymentStatus(root, options);
928
+ if (deploymentTrafficEquals(current.versions, expected.desiredTraffic)) reconciliation = expected.desiredDescription;
929
+ else if (deploymentStateEquals(current, expected.priorState)) {
930
+ reconciliation = expected.priorDescription;
931
+ mayNeedCleanup = false;
932
+ } else reconciliation = "Worker deployment state differs from both the requested and prior state, so the deployment outcome is unknown or another deployment changed it.";
933
+ } catch (reconciliationError) {
934
+ 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
935
  }
936
+ const detail = `${message} ${reconciliation}`;
937
+ return mayNeedCleanup ? new StagedWarmupError(detail, { cause: error }) : new Error(detail, { cause: error });
938
+ }
939
+ function assertDeploymentStateUnchanged(root, options, expected, message) {
940
+ if (!deploymentStateEquals(runDeploymentStatus(root, options), expected)) throw new Error(message);
941
+ }
942
+ async function deployWithCacheabilityProbe(root, options) {
943
+ if (!options.discoverWarmPlan) throw new Error("Two-stage CDN warming requires staged Worker route discovery before cacheability probing.");
944
+ const prerenderSecret = readPrerenderSecret(root);
945
+ const probeUpload = runVersionUpload(root, options);
946
+ const wranglerOptions = resolveDeploymentControlPlaneOptions(options, probeUpload);
947
+ const initialDeployment = runDeploymentStatus(root, wranglerOptions);
948
+ const probeTraffic = getZeroPercentStagingTraffic(initialDeployment, probeUpload.versionId);
949
+ 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.");
950
+ let stagedProbe;
951
+ try {
952
+ stagedProbe = runVersionDeploy(root, probeTraffic, wranglerOptions, "stage");
953
+ } catch (error) {
954
+ throw reconcileVersionDeployFailure(root, wranglerOptions, error, {
955
+ desiredTraffic: probeTraffic,
956
+ desiredDescription: "The probe version is staged at 0% with the previous version still serving 100% traffic; production Worker triggers/routes were not changed.",
957
+ priorState: initialDeployment,
958
+ priorDescription: "Worker deployment traffic was not changed and production Worker triggers/routes were not changed."
959
+ });
960
+ }
961
+ let stagedProbeDeployment;
962
+ try {
963
+ stagedProbeDeployment = runDeploymentStatus(root, wranglerOptions);
964
+ 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.");
965
+ } catch (error) {
966
+ 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 the active Worker deployment and remove the staged probe version if necessary.`, { cause: error });
967
+ }
968
+ let prepared;
969
+ try {
970
+ const targetUrl = resolveCdnWarmupTargetUrl(root, stagedProbe.deployedUrl ?? probeUpload.previewUrl, options);
971
+ const wranglerConfig = options.deploymentTool === "cf" ? null : parseWranglerConfig(root, wranglerOptions.config);
972
+ const headers = buildVersionOverrideHeaders(options.name ?? probeUpload.workerName ?? resolveWorkerNameForVersionOverride(wranglerConfig, wranglerOptions), probeUpload.versionId);
973
+ if (!targetUrl || !headers) throw new Error("Two-stage CDN warming needs a production URL and Worker name for version-overridden cacheability probes.");
974
+ console.log(" CDN warmup: discovering paths from the staged probe Worker...");
975
+ const discovered = await options.discoverWarmPlan({
976
+ headers,
977
+ targetUrl
978
+ });
979
+ if (!discovered.buildId) throw new Error("Two-stage CDN warming requires the staged discovery result to match dist/server/BUILD_ID.");
980
+ if (!discovered.buildIdentity) throw new Error("Two-stage CDN warming requires a CDN adapter that exposes the application build identity.");
981
+ const plan = {
982
+ ...discovered,
983
+ appPaths: discovered.appPaths ? [...discovered.appPaths] : void 0,
984
+ fallbackRoutePatterns: discovered.fallbackRoutePatterns ? [...discovered.fallbackRoutePatterns] : void 0,
985
+ loadingShellPaths: [...discovered.loadingShellPaths],
986
+ pagesDataPaths: [...discovered.pagesDataPaths ?? []],
987
+ pagesPaths: discovered.pagesPaths ? [...discovered.pagesPaths] : void 0,
988
+ paths: [...discovered.paths],
989
+ routeHandlerPaths: [...discovered.routeHandlerPaths ?? []],
990
+ routePatterns: discovered.routePatterns ? { ...discovered.routePatterns } : void 0,
991
+ rscPaths: [...discovered.rscPaths]
992
+ };
993
+ if (!plan.appPaths && !plan.pagesPaths) throw new Error("Two-stage CDN warming requires staged discovery to report App or Pages route ownership.");
994
+ const ownedHtmlPaths = /* @__PURE__ */ new Set([...plan.appPaths ?? [], ...plan.pagesPaths ?? []]);
995
+ plan.paths = plan.paths.filter((pathname) => ownedHtmlPaths.has(pathname));
996
+ const targets = await createCdnWarmTargets({
997
+ deploymentId: plan.deploymentId,
998
+ headers,
999
+ loadingShellPaths: plan.loadingShellPaths,
1000
+ pagesDataPaths: plan.pagesDataPaths,
1001
+ paths: plan.paths,
1002
+ routeHandlerPaths: plan.routeHandlerPaths,
1003
+ routePatterns: plan.routePatterns,
1004
+ rscPaths: plan.rscPaths
1005
+ });
1006
+ const routePatternCount = new Set(targets.flatMap((target) => target.route ? [`${target.route.kind}\0${target.route.pattern}`] : [])).size;
1007
+ 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;
1008
+ if (targets.length > 0) {
1009
+ console.log(" CDN warmup: waiting for the staged probe Worker to become stable...");
1010
+ const readiness = await waitForCdnWarmTargetReadiness({
1011
+ targetUrl,
1012
+ headers,
1013
+ plan,
1014
+ prerenderSecret,
1015
+ deploymentId: plan.deploymentId,
1016
+ expectedBuildId: plan.buildIdentity,
1017
+ expectedRscBuildId: plan.rscBuildId,
1018
+ phaseTimeoutMs: options.warmCdnReadinessTimeout,
1019
+ probeIntervalMs: options.warmCdnReadinessProbeDelay,
1020
+ requiredConsecutiveSuccesses: options.warmCdnReadinessProbes,
1021
+ retries: options.warmCdnReadinessRetries ?? options.warmCdnRetries,
1022
+ timeoutMs: options.warmCdnTimeout
1023
+ });
1024
+ if (!readiness.ready) throw new Error(`Two-stage CDN warming could not verify staged probe Worker readiness: ${readiness.error}.`);
1025
+ 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"}...`);
1026
+ } else {
1027
+ const fallbackPatternCount = plan.fallbackRoutePatterns?.length ?? 0;
1028
+ 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.");
1029
+ }
1030
+ const probeProgress = new CdnOperationProgress();
1031
+ let probe;
1032
+ try {
1033
+ probe = await probeStagedWorkerCacheability({
1034
+ buildId: discovered.buildId,
1035
+ concurrency: options.warmCdnConcurrency,
1036
+ expectedResponseBuildId: plan.buildIdentity,
1037
+ fallbackRoutePatterns: plan.fallbackRoutePatterns,
1038
+ phaseTimeoutMs: options.warmCdnProbeTimeout ?? 12e4,
1039
+ retries: options.warmCdnProbeRetries ?? options.warmCdnRetries ?? 2,
1040
+ retryDelayMs: DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS,
1041
+ root,
1042
+ targets,
1043
+ targetUrl,
1044
+ timeoutMs: options.warmCdnTimeout,
1045
+ onProgress(progress) {
1046
+ probeProgress.update(progress.completed, progress.total, `${progress.static} static, ${progress.dynamic} dynamic, ${progress.skipped} skipped`, "Probing route cacheability");
1047
+ }
1048
+ });
1049
+ } finally {
1050
+ probeProgress.finish();
1051
+ }
1052
+ 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.`);
1053
+ 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]}`);
1054
+ const finalPlan = {
1055
+ ...plan,
1056
+ loadingShellPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-loading-shell").map((target) => target.sourcePathname),
1057
+ pagesDataPaths: probe.cacheableTargets.filter((target) => target.kind === "pages-data").map((target) => target.sourcePathname),
1058
+ paths: probe.cacheableTargets.filter((target) => target.kind === "html").map((target) => target.sourcePathname),
1059
+ routeHandlerPaths: probe.cacheableTargets.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname),
1060
+ rscPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname)
1061
+ };
1062
+ const selectedFinalPlan = options.selectWarmPlan?.(finalPlan) ?? finalPlan;
1063
+ assertDeploymentStateUnchanged(root, wranglerOptions, stagedProbeDeployment, "Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed while cacheability was being probed. No final version was promoted.");
1064
+ const finalConfig = writeCacheabilityManifestArtifact(root, options.config, probe.manifest, options.deploymentTool);
1065
+ const finalUpload = runVersionUpload(root, {
1066
+ ...options,
1067
+ config: options.deploymentTool === "cf" ? options.config : finalConfig
1068
+ });
1069
+ prepared = {
1070
+ optionalWarmTargetKeys: new Set(probe.speculativeTargets.map(cdnWarmTargetKey)),
1071
+ prerenderSecret,
1072
+ plan: selectedFinalPlan,
1073
+ upload: finalUpload
1074
+ };
1075
+ } catch (error) {
1076
+ throw withStagedProbeVersionCleanupNote(error);
1077
+ }
1078
+ return deployUploadedVersionWithCdnWarmup(root, prepared.plan.paths, {
1079
+ ...options,
1080
+ deploymentId: prepared.plan.deploymentId,
1081
+ expectedBuildId: prepared.plan.buildIdentity,
1082
+ expectedRscBuildId: prepared.plan.rscBuildId,
1083
+ expectedDeploymentState: stagedProbeDeployment,
1084
+ loadingShellPaths: prepared.plan.loadingShellPaths,
1085
+ optionalWarmTargetKeys: prepared.optionalWarmTargetKeys,
1086
+ pagesDataPaths: prepared.plan.pagesDataPaths,
1087
+ prerenderSecret: prepared.prerenderSecret,
1088
+ routeHandlerPaths: prepared.plan.routeHandlerPaths,
1089
+ routePatterns: prepared.plan.routePatterns,
1090
+ rscPaths: prepared.plan.rscPaths,
1091
+ uploadedVersion: prepared.upload
1092
+ });
1093
+ }
1094
+ function resolveCdnWarmupTargetUrl(_root, deployedUrl, options) {
1095
+ return options?.warmCdnTarget ?? deployedUrl;
425
1096
  }
426
1097
  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], {
1098
+ const serving = (deployment?.versions ?? []).filter((version) => version.percentage > 0);
1099
+ if (serving.length !== 1 || serving[0].percentage !== 100) return null;
1100
+ if (serving[0].versionId === versionId) return null;
1101
+ return [serving[0], {
431
1102
  versionId,
432
1103
  percentage: 0
433
1104
  }];
@@ -435,6 +1106,15 @@ function getZeroPercentStagingTraffic(deployment, versionId) {
435
1106
  function getWranglerTargetEnv(options) {
436
1107
  return options.env || (options.preview ? "preview" : void 0);
437
1108
  }
1109
+ function resolveDeploymentControlPlaneOptions(options, upload) {
1110
+ if (options.deploymentTool !== "cf") return options;
1111
+ const name = upload.workerName ?? options.name;
1112
+ if (!name) throw new Error("Could not detect the uploaded Worker name needed for staged CDN warming.");
1113
+ return {
1114
+ ...options,
1115
+ name
1116
+ };
1117
+ }
438
1118
  function resolveWorkerNameForVersionOverride(config, options) {
439
1119
  if (options.name) return options.name;
440
1120
  const env = getWranglerTargetEnv(options);
@@ -447,25 +1127,41 @@ function quoteStructuredHeaderString(value) {
447
1127
  }
448
1128
  function buildVersionOverrideHeaders(workerName, versionId) {
449
1129
  if (!workerName) return void 0;
450
- return { "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}` };
1130
+ return {
1131
+ "Cloudflare-Workers-Version-Overrides": `${workerName}=${quoteStructuredHeaderString(versionId)}`,
1132
+ [VINEXT_EXPECTED_WORKER_VERSION_HEADER]: versionId
1133
+ };
451
1134
  }
452
1135
  function withStagedVersionCleanupNote(error) {
453
- const message = error instanceof Error ? error.message : String(error);
454
- return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
1136
+ return new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} ${getStagedVersionCleanupNote()}`, { cause: error });
1137
+ }
1138
+ function withStagedProbeVersionCleanupNote(error) {
1139
+ 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 });
455
1140
  }
456
1141
  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.";
1142
+ 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 create a deployment with the desired version split.";
458
1143
  }
459
1144
  function withPromotedVersionTriggerNote(error) {
460
1145
  const message = error instanceof Error ? error.message : String(error);
461
- 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 });
1146
+ return new Error(`${message} The uploaded version may already be promoted to 100%, but Worker triggers/routes may not be updated; rerun deploy or apply the Worker triggers after fixing the trigger error.`, { cause: error });
1147
+ }
1148
+ function withPromotedVersionWarmupNote(error) {
1149
+ const message = error instanceof Error ? error.message : String(error);
1150
+ 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 create a deployment for the previous version.`, { cause: error });
462
1151
  }
463
1152
  async function deploy(options) {
464
- const deployEnv = validateWranglerEnvName(options.env || (options.preview ? "preview" : "production"));
1153
+ if (options.warmCdnTarget !== void 0 && !options.warmCdnCache) throw new Error("--warm-cdn-target requires --experimental-warm-cdn-cache.");
1154
+ const warmCdnTarget = options.warmCdnTarget === void 0 ? void 0 : validateCdnWarmTarget(options.warmCdnTarget);
1155
+ const deployEnv = options.env || (options.preview ? "preview" : void 0);
1156
+ if (deployEnv) validateWranglerEnvName(deployEnv);
465
1157
  const root = path.resolve(options.root);
1158
+ const deploymentTool = resolveDeploymentTool(root);
1159
+ if (deploymentTool === "cf" && options.name) throw new Error("--name is not supported for cloudflare.config.ts projects. Set `name` in cloudflare.config.ts instead.");
1160
+ const viteMode = resolveViteBuildMode(deploymentTool, deployEnv);
1161
+ const wranglerFallbackEnv = deploymentTool === "cf" ? void 0 : deployEnv;
466
1162
  loadDotenv({
467
1163
  root,
468
- mode: "production"
1164
+ mode: viteMode
469
1165
  });
470
1166
  console.log("\n vinext-cloudflare deploy\n");
471
1167
  const info = detectProject(root);
@@ -483,21 +1179,23 @@ async function deploy(options) {
483
1179
  if (missingScaffolding.length > 0) throw new Error(`Missing Cloudflare deployment setup: ${missingScaffolding.join(", ")}. Run \`vinext init --platform=cloudflare\` first.`);
484
1180
  const missingDeps = getMissingDeps(info);
485
1181
  if (missingDeps.length > 0) throw new Error(`Missing deployment dependencies: ${missingDeps.map((dependency) => dependency.name).join(", ")}. Run \`vinext init --platform=cloudflare\` first.`);
1182
+ if (deploymentTool === "cf" && !isCfCliInstalled(root)) throw new Error("Missing deployment dependencies: cf. Run `vinext init --platform=cloudflare` first.");
486
1183
  if (info.hasViteConfig && !viteConfigHasCloudflarePlugin(root)) throw new Error(formatMissingCloudflarePluginError({ isAppRouter: info.isAppRouter }));
487
- if (info.hasISR && !viteConfigHasCacheAdapter(root) && !workerEntryHasCacheHandler(root)) throw new Error(formatMissingCacheAdapterError({}));
1184
+ if (info.hasISR && !viteConfigHasCacheAdapter(root) && !workerEntryHasCacheHandler(root)) throw new Error(formatMissingCacheAdapterError({ typedConfig: deploymentTool === "cf" }));
488
1185
  if (!viteConfigHasImageAdapter(root)) {
489
1186
  console.log();
490
- console.log(formatImageOptimizationHint());
1187
+ console.log(formatImageOptimizationHint(deploymentTool === "cf"));
491
1188
  }
492
1189
  if (options.dryRun) {
493
1190
  console.log("\n Dry run complete. No build or deploy performed.\n");
494
1191
  return;
495
1192
  }
496
- const buildEnv = deployEnv === "production" && !options.env ? void 0 : deployEnv;
497
- const viteConfigMetadata = await withCloudflareEnv(buildEnv, () => loadDeployViteConfigMetadata(info.root));
498
- const nextConfig = await withCloudflareEnv(buildEnv, async () => {
1193
+ const viteConfigMetadata = await withCloudflareEnv(deployEnv, () => loadDeployViteConfigMetadata(info.root, viteMode));
1194
+ const cdnAdapterConfig = resolveCdnAdapterConfig(viteConfigMetadata.cacheConfig);
1195
+ const nextConfig = await withCloudflareEnv(deployEnv, async () => {
499
1196
  const inlineNextConfig = viteConfigMetadata.nextConfig;
500
- return resolveNextConfig(inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD), info.root);
1197
+ const rawNextConfig = inlineNextConfig ? await resolveNextConfigInput(inlineNextConfig, PHASE_PRODUCTION_BUILD) : await loadNextConfig(info.root, PHASE_PRODUCTION_BUILD);
1198
+ return resolveNextConfig(rawNextConfig, info.root);
501
1199
  });
502
1200
  const vinextPrerenderConfig = !options.prerenderAll && nextConfig.output !== "export" ? viteConfigMetadata.prerenderConfig : null;
503
1201
  const prerenderDecision = resolveVinextPrerenderDecision({
@@ -505,70 +1203,186 @@ async function deploy(options) {
505
1203
  vinextPrerenderConfig,
506
1204
  nextOutput: nextConfig.output
507
1205
  });
508
- const shouldEmitPrerenderPathManifest = options.warmCdnCache || !options.skipBuild && prerenderDecision;
509
- if (!options.skipBuild) await runBuild(info, buildEnv);
1206
+ const hasStrictResponseVary = hasVerbatimResponseVary(viteConfigMetadata.cacheConfig);
1207
+ const warmupStatusSource = cacheWarmupStatusSource(viteConfigMetadata.cacheConfig);
1208
+ const hasStagedRequestRouting = hasUncachedRequestRouting(viteConfigMetadata.cacheConfig) || warmupStatusSource === "data-cache";
1209
+ const hasBuildIdentityHeader = hasBuildIdentityResponseHeader(viteConfigMetadata.cacheConfig);
1210
+ const hasCanonicalRscWarmup = supportsCanonicalRscWarmup(viteConfigMetadata.cacheConfig);
1211
+ const needsCacheabilityProbeManifest = projectRequiresRouteCacheabilityProbeManifest(info, viteConfigMetadata.cacheConfig);
1212
+ const shouldEmitPrerenderPathManifest = !options.skipBuild && prerenderDecision;
1213
+ if (!options.skipBuild) await runBuild(info, deployEnv, viteMode);
510
1214
  else console.log("\n Skipping build (--skip-build)");
1215
+ const canWarmTpr = options.experimentalTPR && !prerenderDecision && hasBuildIdentityHeader;
1216
+ if (options.experimentalTPR && prerenderDecision) console.log(" TPR: Skipping route selection (all-route prerendering configured)");
1217
+ 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)");
1218
+ const tpr = canWarmTpr ? await resolveTPRRoutes({
1219
+ root,
1220
+ config: options.config,
1221
+ env: wranglerFallbackEnv,
1222
+ hostname: warmCdnTarget ? new URL(warmCdnTarget).hostname : void 0,
1223
+ typedConfig: deploymentTool === "cf",
1224
+ window: Math.max(1, options.tprWindow ?? 24)
1225
+ }) : null;
1226
+ if (tpr?.skipped) console.log(` TPR: Skipped (${tpr.skipped})`);
1227
+ const tprRoutes = tpr?.routes ?? [];
1228
+ const wranglerOptions = {
1229
+ env: deploymentTool === "cf" ? viteMode : deployEnv,
1230
+ name: deploymentTool === "cf" ? readBuildOutputWorkerName(root) : options.name,
1231
+ config: options.config,
1232
+ verbose: options.verbose,
1233
+ deploymentTool
1234
+ };
1235
+ let shouldWarmTpr = tprRoutes.length > 0;
1236
+ if (shouldWarmTpr && !options.warmCdnCache) {
1237
+ try {
1238
+ shouldWarmTpr = getZeroPercentStagingTraffic(runDeploymentStatus(root, wranglerOptions), "tpr-preflight") !== null;
1239
+ } catch {
1240
+ shouldWarmTpr = false;
1241
+ }
1242
+ if (!shouldWarmTpr) console.log(" TPR: Skipping pre-warm (current deployment cannot be staged safely)");
1243
+ }
1244
+ const shouldWarmCdnCache = options.warmCdnCache || shouldWarmTpr;
1245
+ const shouldSelectTpr = shouldWarmTpr && !options.warmCdnCache;
1246
+ const candidatePathsOnly = shouldSelectTpr && !needsCacheabilityProbeManifest;
1247
+ const shouldPrerenderLocally = prerenderDecision && (!shouldWarmCdnCache || prerenderDecision.reason === "next-export");
1248
+ if (shouldWarmCdnCache && cdnAdapterConfig) {
1249
+ if (deploymentTool === "cf") {
1250
+ const configPath = path.join(root, ".cloudflare/output/v0/workers/default/worker.config.json");
1251
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
1252
+ const binding = cdnAdapterConfig.versionMetadataBinding;
1253
+ if (config.env?.[binding]?.type !== "version-metadata") throw new Error(`[vinext] Cloudflare CDN warmup requires version metadata binding ${JSON.stringify(binding)} in the generated Build Output Worker config ${configPath}.`);
1254
+ } else {
1255
+ const wrangler = await loadProjectWranglerApi(info.root);
1256
+ const previousCwd = process.cwd();
1257
+ try {
1258
+ process.chdir(info.root);
1259
+ const config = wrangler.unstable_readConfig({
1260
+ config: options.config,
1261
+ env: wranglerFallbackEnv
1262
+ }, {
1263
+ hideWarnings: true,
1264
+ preserveOriginalMain: true,
1265
+ useRedirectIfAvailable: true
1266
+ });
1267
+ assertCdnVersionMetadataConfig({
1268
+ binding: cdnAdapterConfig.versionMetadataBinding,
1269
+ configuredBinding: config.version_metadata?.binding,
1270
+ configPath: config.configPath
1271
+ });
1272
+ } finally {
1273
+ process.chdir(previousCwd);
1274
+ }
1275
+ }
1276
+ }
511
1277
  if (shouldEmitPrerenderPathManifest) await emitPrerenderPathManifest({
512
1278
  root: info.root,
513
1279
  nextConfig,
1280
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1281
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1282
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1283
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
514
1284
  routeRootConfig: viteConfigMetadata.routeRootConfig
515
1285
  });
516
1286
  let ranPrerender = false;
517
- if (prerenderDecision) {
1287
+ let prerenderResult = void 0;
1288
+ if (shouldPrerenderLocally && !ranPrerender) {
518
1289
  console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
519
1290
  if (nextConfig.enablePrerenderSourceMaps) {
520
1291
  process.setSourceMapsEnabled(true);
521
1292
  Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 50);
522
1293
  }
523
- await runPrerender({
1294
+ prerenderResult = await runPrerender({
524
1295
  root: info.root,
525
- concurrency: options.prerenderConcurrency,
526
- nextConfig
1296
+ concurrency: options.prerenderConcurrency ?? viteConfigMetadata.prerenderConfig?.concurrency,
1297
+ nextConfig,
1298
+ routeRootConfig: viteConfigMetadata.routeRootConfig
527
1299
  });
528
1300
  ranPrerender = true;
529
1301
  }
1302
+ if (!options.skipBuild) {
1303
+ await printBuildReport({
1304
+ root: info.root,
1305
+ pageExtensions: nextConfig.pageExtensions,
1306
+ prerenderResult: prerenderResult ?? void 0
1307
+ });
1308
+ console.log("\n Build complete.\n");
1309
+ }
530
1310
  if (ranPrerender) try {
531
- await populateKVCacheFromPrerenderedArtifacts(root, deployEnv === "production" && !options.env ? void 0 : deployEnv, viteConfigMetadata.cacheConfig);
1311
+ await populateKVCacheFromPrerenderedArtifacts(root, deploymentTool, deploymentTool === "cf" ? viteMode : wranglerFallbackEnv, viteConfigMetadata.cacheConfig);
532
1312
  } catch (error) {
533
1313
  console.log(` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`);
534
1314
  }
535
- if (options.experimentalTPR) {
536
- console.log();
537
- const tprResult = await runTPR({
538
- root,
539
- config: options.config,
540
- coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
541
- limit: Math.max(1, options.tprLimit ?? 1e3),
542
- window: Math.max(1, options.tprWindow ?? 24)
543
- });
544
- if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
545
- }
546
- const wranglerOptions = {
547
- env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
548
- name: options.name,
549
- config: options.config
550
- };
551
1315
  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, {
1316
+ if (shouldWarmCdnCache) try {
1317
+ url = await deployWithCdnWarmup(root, [], {
558
1318
  ...wranglerOptions,
1319
+ allowEmptyWarmPlan: tprRoutes.length > 0 && !options.warmCdnCache,
1320
+ cacheabilityProbe: needsCacheabilityProbeManifest,
1321
+ discoverWarmPlan: async ({ headers, targetUrl }) => {
1322
+ const discovery = await discoverPrerenderPathManifest({
1323
+ root: info.root,
1324
+ candidatePaths: tprRoutes.map(({ path }) => path),
1325
+ candidatePathsOnly,
1326
+ nextConfig,
1327
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1328
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1329
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1330
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1331
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
1332
+ routeRootConfig: viteConfigMetadata.routeRootConfig,
1333
+ pathDiscoveryTarget: {
1334
+ baseUrl: targetUrl,
1335
+ headers,
1336
+ phaseTimeoutMs: options.warmCdnDiscoveryTimeout ?? DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS,
1337
+ retries: options.warmCdnDiscoveryRetries,
1338
+ retryDelayMs: DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS
1339
+ }
1340
+ });
1341
+ if (!discovery) return {
1342
+ loadingShellPaths: [],
1343
+ paths: [],
1344
+ rscPaths: []
1345
+ };
1346
+ return createPrerenderWarmPlan(root, discovery, {
1347
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1348
+ includeFallbackShells: options.warmCdnIncludeFallbacks,
1349
+ strict: options.warmCdnCertify === true || !options.dangerouslyPromoteOnCdnWarmError
1350
+ });
1351
+ },
1352
+ selectWarmPlan: shouldSelectTpr ? (plan) => selectTPRWarmPlan(plan, tprRoutes, Math.max(1, Math.min(100, options.tprCoverage ?? 90)), Math.max(1, options.tprLimit ?? 1e3)) : void 0,
1353
+ statusSource: warmupStatusSource,
559
1354
  warmCdnConcurrency: options.warmCdnConcurrency,
1355
+ warmCdnTarget,
560
1356
  warmCdnTimeout: options.warmCdnTimeout,
561
1357
  warmCdnRetries: options.warmCdnRetries,
562
- warmCdnStrict: options.warmCdnStrict
1358
+ warmCdnDiscoveryTimeout: options.warmCdnDiscoveryTimeout,
1359
+ warmCdnDiscoveryRetries: options.warmCdnDiscoveryRetries,
1360
+ warmCdnProbeTimeout: options.warmCdnProbeTimeout,
1361
+ warmCdnProbeRetries: options.warmCdnProbeRetries,
1362
+ warmCdnCertify: options.warmCdnCertify,
1363
+ warmCdnReadinessTimeout: options.warmCdnReadinessTimeout,
1364
+ warmCdnReadinessRetries: options.warmCdnReadinessRetries,
1365
+ warmCdnReadinessProbes: options.warmCdnReadinessProbes,
1366
+ warmCdnReadinessProbeDelay: options.warmCdnReadinessProbeDelay,
1367
+ dangerouslyPromoteOnCdnWarmError: options.dangerouslyPromoteOnCdnWarmError,
1368
+ warmCdnPromote: options.warmCdnPromote,
1369
+ warmCdnPromotionDelay: options.warmCdnPromotionDelay
563
1370
  });
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);
1371
+ } catch (error) {
1372
+ if (options.warmCdnCache || options.warmCdnPromote === false && error instanceof StagedWarmupError) throw error;
1373
+ console.log(` TPR: Skipping pre-warm (${formatUnknownError(error)}). Continuing with deploy.`);
1374
+ }
1375
+ if (url === void 0) {
1376
+ if (deploymentTool === "cf" && options.warmCdnPromote === false) url = runCfVersionUpload(root, wranglerOptions).previewUrl ?? "(Preview URL not detected in cf output)";
1377
+ else if (deploymentTool === "cf") url = await runCfDeploy(root, wranglerOptions);
1378
+ else url = await runWranglerDeploy(root, {
1379
+ ...wranglerOptions,
1380
+ promote: options.warmCdnPromote
1381
+ });
1382
+ }
569
1383
  console.log("\n ─────────────────────────────────────────");
570
- console.log(` Deployed to: ${url}`);
1384
+ console.log(` ${options.warmCdnPromote === false ? "Version URL" : "Deployed to"}: ${url}`);
571
1385
  console.log(" ─────────────────────────────────────────\n");
572
1386
  }
573
1387
  //#endregion
574
- export { buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, parseDeployArgs, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, validateWranglerEnvName, withCloudflareEnv };
1388
+ export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildCfDeployArgs, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, isCfCliInstalled, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveCfBin, resolveDeploymentControlPlaneOptions, resolveDeploymentTool, resolveViteBuildMode, resolveWorkerNameForVersionOverride, resolveWranglerBin, runCfDeploy, runKVBulkPut, runWranglerDeploy, selectTPRWarmPlan, validateWranglerEnvName, withCloudflareEnv };