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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/deploy.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { writeCacheabilityManifestArtifact } from "./cacheability-artifact.js";
2
2
  import { DEFAULT_CACHEABILITY_PROBE_RETRY_DELAY_MS, probeStagedWorkerCacheability, readPrerenderSecret } from "./cacheability-probe.js";
3
3
  import { CdnOperationProgress, createCdnWarmTargets, createPrerenderWarmPlan, waitForCdnWarmTargetReadiness, warmCdnCache } from "./cdn-warm.js";
4
- import { parseWranglerConfig, runTPR } from "./tpr.js";
4
+ import { parseWranglerConfig } from "./wrangler-config.js";
5
+ import { resolveTPRRoutes, selectRoutes } from "./tpr.js";
5
6
  import { VINEXT_EXPECTED_WORKER_VERSION_HEADER } from "./version-headers.js";
6
7
  import { formatImageOptimizationHint, formatMissingCacheAdapterError, resolveCdnAdapterConfig, resolveKvDataAdapterConfig, viteConfigHasCacheAdapter, viteConfigHasCloudflarePlugin, viteConfigHasImageAdapter, workerEntryHasCacheHandler } from "./deploy-config.js";
7
8
  import { assertCdnVersionMetadataConfig } from "./wrangler-version-metadata.js";
@@ -14,6 +15,7 @@ import path from "node:path";
14
15
  import { cacheabilityRoutePathname } from "vinext/internal/server/cacheability-manifest";
15
16
  import { setTimeout } from "node:timers/promises";
16
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";
17
19
  import os from "node:os";
18
20
  import { spawn } from "node:child_process";
19
21
  import { parseArgs } from "node:util";
@@ -21,7 +23,7 @@ import { pathToFileURL } from "node:url";
21
23
  import { runPrerender } from "vinext/internal/build/run-prerender";
22
24
  import { loadDotenv } from "vinext/internal/config/dotenv";
23
25
  import { findVinextNextConfigInPlugins, loadNextConfig, resolveNextConfig, resolveNextConfigInput } from "vinext/internal/config/next-config";
24
- import { findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasUncachedRequestRouting, hasVerbatimResponseVary, isConfiguredCdnResponsePolicyHeader, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision, supportsCanonicalRscWarmup, usesVinextCacheWarmupStatus } from "vinext/internal/config/prerender";
26
+ import { cacheWarmupStatusSource, findVinextCacheConfigInPlugins, findVinextPrerenderConfigInPlugins, findVinextRouteRootConfigInPlugins, formatVinextPrerenderLabel, hasBuildIdentityResponseHeader, hasUncachedRequestRouting, hasVerbatimResponseVary, isConfiguredCdnResponsePolicyHeader, requiresRouteCacheabilityProbeManifest, resolveVinextPrerenderDecision, supportsCanonicalRscWarmup } from "vinext/internal/config/prerender";
25
27
  import { detectProject, findInNodeModules, formatMissingCloudflarePluginError, getMissingDeps } from "vinext/internal/utils/project";
26
28
  import { PHASE_PRODUCTION_BUILD } from "vinext/shims/constants";
27
29
  //#region src/deploy.ts
@@ -71,6 +73,7 @@ function formatUnknownError(error) {
71
73
  if (error instanceof Error && error.message) return error.message;
72
74
  return String(error);
73
75
  }
76
+ var StagedWarmupError = class extends Error {};
74
77
  /** Deploy command flag definitions for util.parseArgs. */
75
78
  const deployArgOptions = {
76
79
  help: {
@@ -126,6 +129,10 @@ const deployArgOptions = {
126
129
  type: "boolean",
127
130
  default: false
128
131
  },
132
+ "no-promote": {
133
+ type: "boolean",
134
+ default: false
135
+ },
129
136
  "warm-cdn-no-promote": {
130
137
  type: "boolean",
131
138
  default: false
@@ -135,6 +142,13 @@ const deployArgOptions = {
135
142
  type: "boolean",
136
143
  default: false
137
144
  },
145
+ "experimental-traffic-aware-warm-cache": {
146
+ type: "boolean",
147
+ default: false
148
+ },
149
+ "traffic-aware-coverage": { type: "string" },
150
+ "traffic-aware-limit": { type: "string" },
151
+ "traffic-aware-window": { type: "string" },
138
152
  "experimental-tpr": {
139
153
  type: "boolean",
140
154
  default: false
@@ -186,13 +200,13 @@ function parseDeployArgs(args) {
186
200
  warmCdnReadinessProbes: values["warm-cdn-readiness-probes"] === void 0 ? void 0 : parsePositiveIntegerArg(values["warm-cdn-readiness-probes"], "--warm-cdn-readiness-probes"),
187
201
  warmCdnReadinessProbeDelay: values["warm-cdn-readiness-probe-delay"] === void 0 ? void 0 : validateTimerDelay(parseNonNegativeIntegerArg(values["warm-cdn-readiness-probe-delay"], "--warm-cdn-readiness-probe-delay"), "--warm-cdn-readiness-probe-delay", values["warm-cdn-readiness-probe-delay"]),
188
202
  dangerouslyPromoteOnCdnWarmError: values["dangerously-promote-on-cdn-warm-error"],
189
- warmCdnPromote: !values["warm-cdn-no-promote"],
203
+ warmCdnPromote: !values["no-promote"] && !values["warm-cdn-no-promote"],
190
204
  warmCdnPromotionDelay: values["warm-cdn-promotion-delay"] === void 0 ? void 0 : validatePromotionDelay(parseNonNegativeIntegerArg(values["warm-cdn-promotion-delay"], "--warm-cdn-promotion-delay"), values["warm-cdn-promotion-delay"]),
191
205
  warmCdnIncludeFallbacks: values["warm-cdn-include-fallbacks"],
192
- experimentalTPR: values["experimental-tpr"],
193
- tprCoverage: parseIntArg("tpr-coverage", values["tpr-coverage"]),
194
- tprLimit: parseIntArg("tpr-limit", values["tpr-limit"]),
195
- tprWindow: parseIntArg("tpr-window", values["tpr-window"])
206
+ experimentalTPR: values["experimental-traffic-aware-warm-cache"] || values["experimental-tpr"],
207
+ tprCoverage: parseIntArg("traffic-aware-coverage", values["traffic-aware-coverage"] ?? values["tpr-coverage"]),
208
+ tprLimit: parseIntArg("traffic-aware-limit", values["traffic-aware-limit"] ?? values["tpr-limit"]),
209
+ tprWindow: parseIntArg("traffic-aware-window", values["traffic-aware-window"] ?? values["tpr-window"])
196
210
  };
197
211
  }
198
212
  /**
@@ -387,6 +401,7 @@ async function runWranglerKVBulkPut(root, options, execute = spawn, nodeExecutab
387
401
  }
388
402
  }
389
403
  async function runWranglerDeploy(root, options, execute = spawn) {
404
+ if (options.promote === false) return runWranglerVersionUpload(root, options).previewUrl ?? "(Preview URL not detected in wrangler output)";
390
405
  const spawnOptions = {
391
406
  cwd: root,
392
407
  stdio: [
@@ -427,6 +442,33 @@ async function runWranglerDeploy(root, options, execute = spawn) {
427
442
  function hasCdnWarmRequests(plan) {
428
443
  return plan.paths.length + (plan.pagesDataPaths?.length ?? 0) + (plan.routeHandlerPaths?.length ?? 0) + plan.rscPaths.length + plan.loadingShellPaths.length > 0;
429
444
  }
445
+ function selectTPRWarmPlan(plan, traffic, coverage, limit) {
446
+ const canonical = (pathname) => normalizePathTrailingSlash(pathname, false);
447
+ const resolved = new Set(Object.entries(plan.routePatterns ?? {}).map(([pathname, route]) => canonical(route.cacheabilityProbe?.concretePathname ?? pathname)));
448
+ const requestsByPath = /* @__PURE__ */ new Map();
449
+ for (const { path, requests } of traffic) {
450
+ const pathname = canonical(path);
451
+ if (!resolved.has(pathname)) continue;
452
+ requestsByPath.set(pathname, (requestsByPath.get(pathname) ?? 0) + requests);
453
+ }
454
+ const selected = new Set(selectRoutes(Array.from(requestsByPath, ([path, requests]) => ({
455
+ path,
456
+ requests
457
+ })).sort((a, b) => b.requests - a.requests), coverage, limit).routes.map(({ path }) => path));
458
+ const includes = (pathname) => selected.has(canonical(plan.routePatterns?.[pathname]?.cacheabilityProbe?.concretePathname ?? pathname));
459
+ const filter = (pathnames) => pathnames?.filter(includes);
460
+ return {
461
+ ...plan,
462
+ appPaths: filter(plan.appPaths),
463
+ loadingShellPaths: filter(plan.loadingShellPaths) ?? [],
464
+ pagesDataPaths: filter(plan.pagesDataPaths),
465
+ pagesPaths: filter(plan.pagesPaths),
466
+ paths: filter(plan.paths) ?? [],
467
+ routeHandlerPaths: filter(plan.routeHandlerPaths),
468
+ routePatterns: plan.routePatterns ? Object.fromEntries(Object.entries(plan.routePatterns).filter(([pathname]) => includes(pathname))) : void 0,
469
+ rscPaths: filter(plan.rscPaths) ?? []
470
+ };
471
+ }
430
472
  function projectRequiresRouteCacheabilityProbeManifest(project, cacheConfig) {
431
473
  return (project.isAppRouter || project.isPagesRouter) && requiresRouteCacheabilityProbeManifest(cacheConfig);
432
474
  }
@@ -493,10 +535,11 @@ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
493
535
  };
494
536
  const discoverWarmPlan = async (targetUrl, headers) => {
495
537
  if (!options.discoverWarmPlan) return;
496
- const plan = await options.discoverWarmPlan({
538
+ const discovered = await options.discoverWarmPlan({
497
539
  headers,
498
540
  targetUrl
499
541
  });
542
+ const plan = options.selectWarmPlan?.(discovered) ?? discovered;
500
543
  deploymentId = plan.deploymentId;
501
544
  expectedBuildId = plan.buildIdentity;
502
545
  expectedRscBuildId = plan.rscBuildId;
@@ -542,10 +585,16 @@ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
542
585
  statusSource: options.statusSource
543
586
  });
544
587
  const wranglerConfig = parseWranglerConfig(root, options.config);
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.");
588
+ let deploymentStatus;
589
+ try {
590
+ deploymentStatus = runWranglerDeploymentStatus(root, options);
591
+ } catch (error) {
592
+ if (!hasPreparedWarmPlan) throw error;
593
+ throw new StagedWarmupError(formatUnknownError(error), { cause: error });
594
+ }
595
+ if (options.expectedDeploymentState && !deploymentStateEquals(deploymentStatus, options.expectedDeploymentState)) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed before the final version could be staged. No final version was promoted.");
547
596
  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.");
597
+ if (hasPreparedWarmPlan && !stagingTraffic) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic changed before the final version could be staged. No final version was promoted.");
549
598
  let staged = null;
550
599
  let triggersDeployedUrl = options.triggersDeployedUrl ?? null;
551
600
  let stagedCacheFilled = false;
@@ -637,7 +686,7 @@ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
637
686
  routeHandlerPaths: warmResult.retryPlan.routeHandlerPaths,
638
687
  rscPaths: warmResult.retryPlan.rscPaths
639
688
  };
640
- if (hasPreparedWarmPlan && options.warmCdnCertify && warmResult.warmed > 0) {
689
+ if (options.warmCdnCertify && warmResult.warmed > 0) {
641
690
  console.log(` CDN warmup: certifying ${warmResult.warmed} staged cache entr${warmResult.warmed === 1 ? "y" : "ies"} before promotion...`);
642
691
  const certification = await warmUploadedVersion(targetUrl, headers, true, warmResult.warmedPlan, true);
643
692
  if (certification.warmed !== warmResult.warmed) throw new Error(`CDN warmup certified ${certification.warmed}/${warmResult.warmed} staged cache entries as reusable.`);
@@ -650,7 +699,7 @@ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
650
699
  }
651
700
  else if (initialWarmRequests > 0) {
652
701
  const message = "CDN warmup failed: pre-traffic warmup needs a production URL and Worker name for version overrides. Configure a route/custom domain and Worker name, or deploy without --experimental-warm-cdn-cache.";
653
- if (!allowUnverifiedPromotion) throw new Error(`${message} ${getStagedVersionCleanupNote()}`);
702
+ if (!allowUnverifiedPromotion) throw new StagedWarmupError(`${message} ${getStagedVersionCleanupNote()}`);
654
703
  console.warn(` ${message} Promoting because the dangerous override is enabled.`);
655
704
  }
656
705
  } else {
@@ -665,19 +714,18 @@ async function deployUploadedVersionWithCdnWarmup(root, paths, options) {
665
714
  if (options.warmCdnPromote === false) {
666
715
  if (!staged) throw new Error("CDN warmup cannot skip promotion because the uploaded Worker version could not be staged at 0% traffic. The current deployment must have exactly one version serving 100% traffic.");
667
716
  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."));
717
+ if (!options.allowEmptyWarmPlan && !hasPreparedWarmPlan && options.discoverWarmPlan && discoveredWarmRequests === 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error("CDN warmup cannot skip promotion because no build-discovered requests were found to warm."));
669
718
  if (remainingWarmRequests > 0) throw withStagedVersionCleanupNote(/* @__PURE__ */ new Error(`CDN warmup cannot skip promotion because ${remainingWarmRequests} request(s) remain unwarmed.`));
670
719
  if (hasPreparedWarmPlan && stagedDeploymentState) {
671
720
  let currentDeployment;
672
721
  try {
673
722
  currentDeployment = runWranglerDeploymentStatus(root, options);
674
723
  } catch (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 });
724
+ throw new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} CDN warmup cannot confirm that the uploaded Worker remains staged at 0% traffic; production Worker triggers/routes remain applied. Inspect \`wrangler deployments status\` before continuing.`, { cause: error });
677
725
  }
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.");
726
+ if (!deploymentStateEquals(currentDeployment, stagedDeploymentState)) throw new StagedWarmupError("Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed before the no-promote handoff. The uploaded Worker is not confirmed staged at 0% traffic; production Worker triggers/routes remain applied.");
679
727
  }
680
- console.log(" CDN warmup: promotion disabled; uploaded Worker version remains staged at 0% traffic and production Worker triggers/routes remain applied.");
728
+ console.log(` CDN warmup: promotion disabled; uploaded Worker version ${upload.versionId} remains staged at 0% traffic and production Worker triggers/routes remain applied.`);
681
729
  return staged.deployedUrl ?? triggersDeployedUrl ?? upload.previewUrl ?? "(URL not detected in wrangler output)";
682
730
  }
683
731
  let deployed;
@@ -752,15 +800,19 @@ function deploymentStateEquals(actual, expected) {
752
800
  function reconcileVersionDeployFailure(root, options, error, expected) {
753
801
  const message = error instanceof Error ? error.message : String(error);
754
802
  let reconciliation;
803
+ let mayNeedCleanup = true;
755
804
  try {
756
805
  const current = runWranglerDeploymentStatus(root, options);
757
806
  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.";
807
+ else if (deploymentStateEquals(current, expected.priorState)) {
808
+ reconciliation = expected.priorDescription;
809
+ mayNeedCleanup = false;
810
+ } else reconciliation = "Worker deployment state differs from both the requested and prior state, so the deployment outcome is unknown or another deployment changed it.";
760
811
  } catch (reconciliationError) {
761
812
  reconciliation = `Worker deployment status could not be read after the command failed, so the deployment outcome is unknown (${reconciliationError instanceof Error ? reconciliationError.message : String(reconciliationError)}).`;
762
813
  }
763
- return new Error(`${message} ${reconciliation}`, { cause: error });
814
+ const detail = `${message} ${reconciliation}`;
815
+ return mayNeedCleanup ? new StagedWarmupError(detail, { cause: error }) : new Error(detail, { cause: error });
764
816
  }
765
817
  function assertDeploymentStateUnchanged(root, options, expected, message) {
766
818
  if (!deploymentStateEquals(runWranglerDeploymentStatus(root, options), expected)) throw new Error(message);
@@ -788,8 +840,7 @@ async function deployWithCacheabilityProbe(root, options) {
788
840
  stagedProbeDeployment = runWranglerDeploymentStatus(root, options);
789
841
  if (!deploymentTrafficEquals(stagedProbeDeployment.versions, probeTraffic)) throw new Error("Two-stage CDN warming stopped because Worker deployment traffic changed immediately after the probe version was staged.");
790
842
  } 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 });
843
+ throw new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} The probe staging command completed, but its current deployment state could not be confirmed; production Worker triggers/routes were not changed. Inspect \`wrangler deployments status\` and remove the staged probe version if necessary.`, { cause: error });
793
844
  }
794
845
  let prepared;
795
846
  try {
@@ -885,6 +936,7 @@ async function deployWithCacheabilityProbe(root, options) {
885
936
  routeHandlerPaths: probe.cacheableTargets.filter((target) => target.kind === "app-route").map((target) => target.sourcePathname),
886
937
  rscPaths: probe.cacheableTargets.filter((target) => target.kind === "rsc-full").map((target) => target.sourcePathname)
887
938
  };
939
+ const selectedFinalPlan = options.selectWarmPlan?.(finalPlan) ?? finalPlan;
888
940
  assertDeploymentStateUnchanged(root, options, stagedProbeDeployment, "Two-stage CDN warming stopped because Worker deployment traffic or deployment identity changed while cacheability was being probed. No final version was promoted.");
889
941
  const finalConfig = writeCacheabilityManifestArtifact(root, options.config, probe.manifest);
890
942
  const finalUpload = runWranglerVersionUpload(root, {
@@ -897,7 +949,7 @@ async function deployWithCacheabilityProbe(root, options) {
897
949
  prepared = {
898
950
  optionalWarmTargetKeys: new Set(probe.speculativeTargets.map(cdnWarmTargetKey)),
899
951
  prerenderSecret,
900
- plan: finalPlan,
952
+ plan: selectedFinalPlan,
901
953
  upload: finalUpload
902
954
  };
903
955
  } catch (error) {
@@ -952,12 +1004,10 @@ function buildVersionOverrideHeaders(workerName, versionId) {
952
1004
  };
953
1005
  }
954
1006
  function withStagedVersionCleanupNote(error) {
955
- const message = error instanceof Error ? error.message : String(error);
956
- return new Error(`${message} ${getStagedVersionCleanupNote()}`, { cause: error });
1007
+ return new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} ${getStagedVersionCleanupNote()}`, { cause: error });
957
1008
  }
958
1009
  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 });
1010
+ return new StagedWarmupError(`${error instanceof Error ? error.message : String(error)} The probe version may remain staged at 0% with the previous version still serving 100% traffic; production Worker triggers/routes were not changed.`, { cause: error });
961
1011
  }
962
1012
  function getStagedVersionCleanupNote() {
963
1013
  return "The uploaded version may remain staged at 0% with the previous version still serving 100% traffic; Worker triggers/routes may also have changed because trigger deployment runs before warming. Rerun deploy to promote it or use `wrangler versions deploy` to choose the desired version split.";
@@ -1020,15 +1070,46 @@ async function deploy(options) {
1020
1070
  nextOutput: nextConfig.output
1021
1071
  });
1022
1072
  const hasStrictResponseVary = hasVerbatimResponseVary(viteConfigMetadata.cacheConfig);
1023
- const hasStagedRequestRouting = hasUncachedRequestRouting(viteConfigMetadata.cacheConfig);
1073
+ const warmupStatusSource = cacheWarmupStatusSource(viteConfigMetadata.cacheConfig);
1074
+ const hasStagedRequestRouting = hasUncachedRequestRouting(viteConfigMetadata.cacheConfig) || warmupStatusSource === "data-cache";
1024
1075
  const hasBuildIdentityHeader = hasBuildIdentityResponseHeader(viteConfigMetadata.cacheConfig);
1025
1076
  const hasCanonicalRscWarmup = supportsCanonicalRscWarmup(viteConfigMetadata.cacheConfig);
1026
- const hasVinextCacheWarmupStatus = usesVinextCacheWarmupStatus(viteConfigMetadata.cacheConfig);
1027
1077
  const needsCacheabilityProbeManifest = projectRequiresRouteCacheabilityProbeManifest(info, viteConfigMetadata.cacheConfig);
1028
1078
  const shouldEmitPrerenderPathManifest = !options.skipBuild && prerenderDecision;
1029
1079
  if (!options.skipBuild) await runBuild(info, buildEnv);
1030
1080
  else console.log("\n Skipping build (--skip-build)");
1031
- if (options.warmCdnCache && cdnAdapterConfig) {
1081
+ const canWarmTpr = options.experimentalTPR && !prerenderDecision && hasBuildIdentityHeader;
1082
+ if (options.experimentalTPR && prerenderDecision) console.log(" TPR: Skipping route selection (all-route prerendering configured)");
1083
+ else if (options.experimentalTPR && !hasBuildIdentityHeader) console.log(workerEntryHasCacheHandler(root) ? " TPR: Skipping pre-warm (legacy imperative cache handlers must migrate to declarative vinext({ cache }) for standard warming)" : " TPR: Skipping pre-warm (configured cache does not expose build identity)");
1084
+ const tpr = canWarmTpr ? await resolveTPRRoutes({
1085
+ root,
1086
+ config: options.config,
1087
+ env: buildEnv,
1088
+ hostname: warmCdnTarget ? new URL(warmCdnTarget).hostname : void 0,
1089
+ window: Math.max(1, options.tprWindow ?? 24)
1090
+ }) : null;
1091
+ if (tpr?.skipped) console.log(` TPR: Skipped (${tpr.skipped})`);
1092
+ const tprRoutes = tpr?.routes ?? [];
1093
+ const wranglerOptions = {
1094
+ env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
1095
+ name: options.name,
1096
+ config: options.config,
1097
+ verbose: options.verbose
1098
+ };
1099
+ let shouldWarmTpr = tprRoutes.length > 0;
1100
+ if (shouldWarmTpr && !options.warmCdnCache) {
1101
+ try {
1102
+ shouldWarmTpr = getZeroPercentStagingTraffic(runWranglerDeploymentStatus(root, wranglerOptions), "tpr-preflight") !== null;
1103
+ } catch {
1104
+ shouldWarmTpr = false;
1105
+ }
1106
+ if (!shouldWarmTpr) console.log(" TPR: Skipping pre-warm (current deployment cannot be staged safely)");
1107
+ }
1108
+ const shouldWarmCdnCache = options.warmCdnCache || shouldWarmTpr;
1109
+ const shouldSelectTpr = shouldWarmTpr && !options.warmCdnCache;
1110
+ const candidatePathsOnly = shouldSelectTpr && !needsCacheabilityProbeManifest;
1111
+ const shouldPrerenderLocally = prerenderDecision && (!shouldWarmCdnCache || prerenderDecision.reason === "next-export");
1112
+ if (shouldWarmCdnCache && cdnAdapterConfig) {
1032
1113
  const wrangler = await loadProjectWranglerApi(info.root);
1033
1114
  const previousCwd = process.cwd();
1034
1115
  try {
@@ -1060,7 +1141,7 @@ async function deploy(options) {
1060
1141
  routeRootConfig: viteConfigMetadata.routeRootConfig
1061
1142
  });
1062
1143
  let ranPrerender = false;
1063
- if (prerenderDecision) {
1144
+ if (shouldPrerenderLocally) {
1064
1145
  console.log(`\n ${formatVinextPrerenderLabel(prerenderDecision)}`);
1065
1146
  if (nextConfig.enablePrerenderSourceMaps) {
1066
1147
  process.setSourceMapsEnabled(true);
@@ -1079,78 +1160,73 @@ async function deploy(options) {
1079
1160
  } catch (error) {
1080
1161
  console.log(` KV cache: Skipping prerender upload (${formatUnknownError(error)}). Continuing with deploy.`);
1081
1162
  }
1082
- if (options.experimentalTPR) {
1083
- console.log();
1084
- const tprResult = await runTPR({
1085
- root,
1086
- config: options.config,
1087
- coverage: Math.max(1, Math.min(100, options.tprCoverage ?? 90)),
1088
- limit: Math.max(1, options.tprLimit ?? 1e3),
1089
- window: Math.max(1, options.tprWindow ?? 24)
1163
+ let url;
1164
+ if (shouldWarmCdnCache) try {
1165
+ url = await deployWithCdnWarmup(root, [], {
1166
+ ...wranglerOptions,
1167
+ allowEmptyWarmPlan: tprRoutes.length > 0 && !options.warmCdnCache,
1168
+ cacheabilityProbe: needsCacheabilityProbeManifest,
1169
+ discoverWarmPlan: async ({ headers, targetUrl }) => {
1170
+ const discovery = await discoverPrerenderPathManifest({
1171
+ root: info.root,
1172
+ candidatePaths: tprRoutes.map(({ path }) => path),
1173
+ candidatePathsOnly,
1174
+ nextConfig,
1175
+ buildIdentity: hasBuildIdentityHeader ? "response-header" : void 0,
1176
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1177
+ requestRouting: hasStagedRequestRouting ? "uncached-stage" : void 0,
1178
+ responseVary: hasStrictResponseVary ? "verbatim" : void 0,
1179
+ isResponsePolicyHeader: (name) => isConfiguredCdnResponsePolicyHeader(viteConfigMetadata.cacheConfig, name),
1180
+ routeRootConfig: viteConfigMetadata.routeRootConfig,
1181
+ pathDiscoveryTarget: {
1182
+ baseUrl: targetUrl,
1183
+ headers,
1184
+ phaseTimeoutMs: options.warmCdnDiscoveryTimeout ?? DEFAULT_REMOTE_PATH_DISCOVERY_PHASE_TIMEOUT_MS,
1185
+ retries: options.warmCdnDiscoveryRetries,
1186
+ retryDelayMs: DEFAULT_REMOTE_PATH_DISCOVERY_RETRY_DELAY_MS
1187
+ }
1188
+ });
1189
+ if (!discovery) return {
1190
+ loadingShellPaths: [],
1191
+ paths: [],
1192
+ rscPaths: []
1193
+ };
1194
+ return createPrerenderWarmPlan(root, discovery, {
1195
+ includeCanonicalRsc: hasCanonicalRscWarmup,
1196
+ includeFallbackShells: options.warmCdnIncludeFallbacks,
1197
+ strict: options.warmCdnCertify === true || !options.dangerouslyPromoteOnCdnWarmError
1198
+ });
1199
+ },
1200
+ selectWarmPlan: shouldSelectTpr ? (plan) => selectTPRWarmPlan(plan, tprRoutes, Math.max(1, Math.min(100, options.tprCoverage ?? 90)), Math.max(1, options.tprLimit ?? 1e3)) : void 0,
1201
+ statusSource: warmupStatusSource,
1202
+ warmCdnConcurrency: options.warmCdnConcurrency,
1203
+ warmCdnTarget,
1204
+ warmCdnTimeout: options.warmCdnTimeout,
1205
+ warmCdnRetries: options.warmCdnRetries,
1206
+ warmCdnDiscoveryTimeout: options.warmCdnDiscoveryTimeout,
1207
+ warmCdnDiscoveryRetries: options.warmCdnDiscoveryRetries,
1208
+ warmCdnProbeTimeout: options.warmCdnProbeTimeout,
1209
+ warmCdnProbeRetries: options.warmCdnProbeRetries,
1210
+ warmCdnCertify: options.warmCdnCertify,
1211
+ warmCdnReadinessTimeout: options.warmCdnReadinessTimeout,
1212
+ warmCdnReadinessRetries: options.warmCdnReadinessRetries,
1213
+ warmCdnReadinessProbes: options.warmCdnReadinessProbes,
1214
+ warmCdnReadinessProbeDelay: options.warmCdnReadinessProbeDelay,
1215
+ dangerouslyPromoteOnCdnWarmError: options.dangerouslyPromoteOnCdnWarmError,
1216
+ warmCdnPromote: options.warmCdnPromote,
1217
+ warmCdnPromotionDelay: options.warmCdnPromotionDelay
1090
1218
  });
1091
- if (tprResult.skipped) console.log(` TPR: Skipped (${tprResult.skipped})`);
1219
+ } catch (error) {
1220
+ if (options.warmCdnCache || options.warmCdnPromote === false && error instanceof StagedWarmupError) throw error;
1221
+ console.log(` TPR: Skipping pre-warm (${formatUnknownError(error)}). Continuing with deploy.`);
1092
1222
  }
1093
- const wranglerOptions = {
1094
- env: deployEnv === "production" && !options.env ? void 0 : deployEnv,
1095
- name: options.name,
1096
- config: options.config,
1097
- verbose: options.verbose
1098
- };
1099
- let url;
1100
- if (options.warmCdnCache) url = await deployWithCdnWarmup(root, [], {
1223
+ if (url === void 0) url = await runWranglerDeploy(root, {
1101
1224
  ...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
1225
+ promote: options.warmCdnPromote
1149
1226
  });
1150
- else url = await runWranglerDeploy(root, wranglerOptions);
1151
1227
  console.log("\n ─────────────────────────────────────────");
1152
- console.log(` Deployed to: ${url}`);
1228
+ console.log(` ${options.warmCdnPromote === false ? "Version URL" : "Deployed to"}: ${url}`);
1153
1229
  console.log(" ─────────────────────────────────────────\n");
1154
1230
  }
1155
1231
  //#endregion
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 };
1232
+ export { DEFAULT_CDN_WARM_PROMOTION_DELAY_MS, buildNodeCliInvocation, buildVersionOverrideHeaders, buildWranglerDeployArgs, buildWranglerInvocation, buildWranglerKVBulkPutArgs, deploy, deployWithCdnWarmup, getZeroPercentStagingTraffic, hasCdnWarmRequests, parseDeployArgs, projectRequiresRouteCacheabilityProbeManifest, resolveCdnWarmupTargetUrl, resolveWorkerNameForVersionOverride, resolveWranglerBin, runWranglerDeploy, runWranglerKVBulkPut, selectTPRWarmPlan, validateWranglerEnvName, withCloudflareEnv };
package/dist/tpr.d.ts CHANGED
@@ -1,71 +1,32 @@
1
+ import { parseWranglerConfig } from "./wrangler-config.js";
1
2
  //#region src/tpr.d.ts
2
- /**
3
- * TPR: Traffic-aware Pre-Rendering
4
- *
5
- * Uses Cloudflare zone analytics to determine which pages actually get
6
- * traffic, and pre-renders only those during deploy. The pre-rendered
7
- * HTML is uploaded to KV in the same format ISR uses at runtime — no
8
- * runtime changes needed.
9
- *
10
- * Flow:
11
- * 1. Parse wrangler config to find custom domain and KV namespace
12
- * 2. Resolve the Cloudflare zone for the custom domain
13
- * 3. Query zone analytics (GraphQL) for top pages by request count
14
- * 4. Walk ranked list until coverage threshold is met
15
- * 5. Start the built production server locally
16
- * 6. Fetch each hot route to produce HTML
17
- * 7. Upload pre-rendered HTML to KV (same KVCacheEntry format ISR reads)
18
- *
19
- * TPR is an experimental feature enabled via --experimental-tpr. It
20
- * gracefully skips when no custom domain, no API token, no traffic data,
21
- * or no KV namespace is configured.
22
- */
23
3
  export type TPROptions = {
24
4
  /** Project root directory. */
25
5
  root: string;
26
6
  /** Wrangler config path, relative to root unless absolute. */
27
7
  config?: string;
28
- /** Traffic coverage percentage (0–100). Default: 90. */
29
- coverage: number;
30
- /** Hard cap on number of pages to pre-render. Default: 1000. */
31
- limit: number;
8
+ /** Wrangler environment whose custom domain should be analyzed. */
9
+ env?: string;
10
+ /** Explicit domain used to resolve the analytics zone, overriding Wrangler routes. */
11
+ hostname?: string;
32
12
  /** Analytics lookback window in hours. Default: 24. */
33
13
  window: number;
34
14
  };
35
- export type TPRResult = {
36
- /** Total unique page paths found in analytics. */
37
- totalPaths: number;
38
- /** Number of pages successfully pre-rendered and uploaded. */
39
- prerenderedCount: number;
40
- /** Actual traffic coverage achieved (percentage). */
41
- coverageAchieved: number;
42
- /** Wall-clock duration of the TPR step in milliseconds. */
43
- durationMs: number;
15
+ export type TPRRouteResult = {
16
+ routes: TrafficEntry[];
44
17
  /** If TPR was skipped, the reason. */
45
18
  skipped?: string;
46
19
  };
47
- type PrerenderResult = {
48
- html: string;
49
- status: number;
50
- headers: Record<string, string>;
20
+ export type TrafficEntry = {
21
+ path: string;
22
+ requests: number;
51
23
  };
52
- type WranglerConfig = {
53
- accountId?: string;
54
- kvNamespaceId?: string;
55
- customDomain?: string;
56
- name?: string;
57
- legacyEnv?: boolean;
58
- env?: Record<string, WranglerEnvironmentConfig>;
24
+ export type SelectedRoutes = {
25
+ routes: TrafficEntry[];
26
+ totalRequests: number;
27
+ coveredRequests: number;
28
+ coveragePercent: number;
59
29
  };
60
- export type WranglerEnvironmentConfig = {
61
- customDomain?: string;
62
- name?: string;
63
- };
64
- /**
65
- * Parse wrangler config (JSONC or TOML) to extract the fields TPR needs:
66
- * account_id, VINEXT_KV_CACHE KV namespace ID, and custom domain.
67
- */
68
- export declare function parseWranglerConfig(root: string, configPath?: string): WranglerConfig | null;
69
30
  /**
70
31
  * Generate zone lookup candidates from shortest (2-part) to longest.
71
32
  * Tries the most common case first (e.g., "example.com") and progressively
@@ -76,25 +37,17 @@ export declare function parseWranglerConfig(root: string, configPath?: string):
76
37
  * "example.com" → ["example.com"]
77
38
  */
78
39
  export declare function domainCandidates(domain: string): string[];
79
- export declare function resolveVinextProdServerPath(root: string): string;
40
+ /** Filter out non-page requests (static assets, API routes, internal routes). */
41
+ export declare function filterTrafficPaths(entries: TrafficEntry[]): TrafficEntry[];
80
42
  /**
81
- * Build KV bulk API pairs from pre-rendered entries.
82
- *
83
- * Key format matches the runtime KVCacheHandler exactly:
84
- * createKvKeySpace().entryKey(isrCacheKey("app", pathname, buildId) + ":html")
85
- * → "cache:app:<buildId>:<pathname>:html"
43
+ * Walk the ranked traffic list, accumulating request counts until the
44
+ * coverage target is met or the hard cap is reached.
86
45
  */
87
- export declare function buildTprKVPairs(entries: Map<string, PrerenderResult>, buildId: string | undefined, defaultRevalidateSeconds: number): Array<{
88
- key: string;
89
- value: string;
90
- expiration_ttl: number;
91
- }>;
46
+ export declare function selectRoutes(traffic: TrafficEntry[], coverageTarget: number, limit: number): SelectedRoutes;
92
47
  /**
93
- * Run the TPR pipeline: query traffic, select routes, pre-render, upload.
94
- *
95
- * Designed to be called between the build step and wrangler deploy in the
96
- * `vinext-cloudflare deploy` pipeline. Gracefully skips (never errors) when
97
- * the prerequisites aren't met.
48
+ * Resolve ranked traffic paths. The standard CDN pre-warming flow owns route
49
+ * matching, coverage selection, rendering, and cache admission.
98
50
  */
99
- export declare function runTPR(options: TPROptions): Promise<TPRResult>;
100
- //#endregion
51
+ export declare function resolveTPRRoutes(options: TPROptions): Promise<TPRRouteResult>;
52
+ //#endregion
53
+ export { parseWranglerConfig };