Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/cloudflare/src/deploy-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export function formatDeployHelp(): string {
a custom domain (zone analytics are unavailable on *.workers.dev) and the
CLOUDFLARE_API_TOKEN environment variable with Zone.Analytics read permission.

CDN warmup requests populate the edge cache only in the Cloudflare data centers
reached by the warmup run; they do not globally prefill every edge location.
Workers Cache automatically uses tiered caching. Warmed entries can therefore
be reused outside the data center reached by the warmup request after cache
propagation, but the deploy does not wait for every edge location to fill.

Examples:
npx @vinext/cloudflare deploy Build and deploy to production
Expand Down
24 changes: 6 additions & 18 deletions packages/cloudflare/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,11 +650,8 @@ export async function deployWithCdnWarmup(
} catch (error) {
throw withStagedVersionCleanupNote(error);
}
const targetUrl = resolveCdnWarmupTargetUrl(
root,
staged.deployedUrl ?? triggersDeployedUrl,
options,
);
const targetUrl =
resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? staged.deployedUrl;
const workerName = resolveWorkerNameForVersionOverride(wranglerConfig, options);
const headers = buildVersionOverrideHeaders(workerName, upload.versionId);
if (targetUrl && headers) {
Expand Down Expand Up @@ -741,11 +738,8 @@ export async function deployWithCdnWarmup(
remainingWarmPlan.rscPaths.length +
remainingWarmPlan.loadingShellPaths.length;
if (remainingWarmRequests > 0) {
const targetUrl = resolveCdnWarmupTargetUrl(
root,
deployed.deployedUrl ?? triggersDeployedUrl,
options,
);
const targetUrl =
resolveCdnWarmupTargetUrl(root, triggersDeployedUrl, options) ?? deployed.deployedUrl;
if (targetUrl) {
try {
await warmUploadedVersion(targetUrl, undefined, true, remainingWarmPlan);
Expand Down Expand Up @@ -781,16 +775,10 @@ export function resolveCdnWarmupTargetUrl(
options: Pick<DeployOptions, "preview" | "env" | "config">,
): string | null;
export function resolveCdnWarmupTargetUrl(
root: string,
_root: string,
deployedUrl: string | null,
options?: Pick<DeployOptions, "preview" | "env" | "config">,
_options?: Pick<DeployOptions, "preview" | "env" | "config">,
): string | null {
const config = parseWranglerConfig(root, options?.config);
const env = getWranglerTargetEnv(options ?? {});
const customDomain = env ? config?.env?.[env]?.customDomain : config?.customDomain;
if (customDomain) {
return `https://${customDomain}`;
}
return deployedUrl;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cloudflare/src/version-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
validateWranglerEnvName,
type DeployOptions,
} from "./deploy.js";
import { parseCdnWarmupDeploymentUrl } from "./worker-deployment-url.js";
import { parseWorkersDevUrl } from "./workers-dev-url.js";

export { parseWorkersDevUrl } from "./workers-dev-url.js";
Expand Down Expand Up @@ -324,5 +325,5 @@ export function runWranglerTriggersDeploy(
console.log("\n Applying Worker triggers...");
}
const output = runWranglerCommand(root, args, execute);
return { deployedUrl: parseWorkersDevUrl(output), output };
return { deployedUrl: parseCdnWarmupDeploymentUrl(output), output };
}
37 changes: 37 additions & 0 deletions packages/cloudflare/src/worker-deployment-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@ export function parseWorkerDeploymentUrl(output: string): string | null {
return parseWorkersDevUrl(output) ?? parseCustomDomainUrl(output);
}

/**
* Parse the concrete hostname that Wrangler reports after applying Worker
* triggers. Catch-all routes are valid warmup targets even though they are not
* canonical deployment URLs for general CLI reporting.
*/
export function parseCdnWarmupDeploymentUrl(output: string): string | null {
return parseWorkerDeploymentUrl(output) ?? parseCatchAllWorkerRouteOrigin(output);
}

function parseCatchAllWorkerRouteOrigin(output: string): string | null {
for (const line of output.split(/\r?\n/)) {
const route = line.trim().split(/\s+/, 1)[0];
if (!route || !route.endsWith("/*")) continue;

try {
const url = new URL(route.includes("://") ? route : `https://${route}`);
if (
(url.protocol !== "https:" && url.protocol !== "http:") ||
url.hostname.includes("*") ||
url.username !== "" ||
url.password !== "" ||
url.port !== "" ||
url.pathname !== "/*" ||
url.search !== "" ||
url.hash !== ""
) {
continue;
}
return url.origin;
} catch {
continue;
}
}

return null;
}

function parseCustomDomainUrl(output: string): string | null {
// Wrangler adds a scheme only to workers.dev targets. A successful custom-domain
// deployment is printed as a bare hostname with this explicit route-type marker.
Expand Down
80 changes: 72 additions & 8 deletions tests/cloudflare-cdn-warm-deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down Expand Up @@ -262,6 +262,57 @@ describe("Cloudflare CDN warmup deploy flow", () => {
expect(delayMock).toHaveBeenCalledWith(15_000);
});

it.each([
["singular route", { name: "my-worker", workers_dev: false, route: "sub.example.com/*" }],
[
"routes object with a zone name",
{
name: "my-worker",
workers_dev: false,
routes: [{ pattern: "sub.example.com/*", zone_name: "example.com" }],
},
],
])("warms the concrete Worker host from a %s", async (_label, config) => {
writeFile("wrangler.jsonc", JSON.stringify(config));
execFileSyncMock.mockImplementation((_file: string, args: string[]) => {
if (args.includes("upload")) {
return "Uploaded version 22222222-2222-4222-8222-222222222222\n";
}
if (args.includes("status")) {
return JSON.stringify({
versions: [{ version_id: "11111111-1111-4111-8111-111111111111", percentage: 100 }],
});
}
if (args.includes("deploy") && args.includes("22222222-2222-4222-8222-222222222222@0%")) {
return "Staged version\nhttps://stable.example.workers.dev\n";
}
if (args.includes("deploy") && args.includes("22222222-2222-4222-8222-222222222222@100%")) {
return "Deployed version\nhttps://stable.example.workers.dev\n";
}
if (args.includes("triggers")) {
return "Triggers deployed\n sub.example.com/*\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
const { deployWithCdnWarmup } = await import("../packages/cloudflare/src/deploy.js");

await deployWithCdnWarmup(tmpDir, ["/about"], {
expectedBuildId: "app-build-a",
warmCdnConcurrency: 1,
warmCdnPromotionDelay: 0,
});

expect(fetch).toHaveBeenCalledWith(
new URL("https://sub.example.com/about"),
expect.any(Object),
);
expect(
vi
.mocked(fetch)
.mock.calls.every(([url]) => formatFetchUrl(url).startsWith("https://sub.example.com/")),
).toBe(true);
});

it("does not replay successful staged fills after a non-strict partial failure", async () => {
const events: string[] = [];
writeFile(
Expand Down Expand Up @@ -314,7 +365,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand All @@ -336,7 +387,6 @@ describe("Cloudflare CDN warmup deploy flow", () => {
"fetch:staged:rsc",
"fetch:staged:loading",
"fetch:staged:html",
"fetch:staged:loading",
"promote",
"fetch:promoted:loading",
]);
Expand Down Expand Up @@ -383,7 +433,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down Expand Up @@ -437,7 +487,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
return "Deployed version\nhttps://stable.example.workers.dev\n";
}
if (args.includes("triggers")) {
return "Triggers deployed\n";
return "Triggers deployed\n staging.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down Expand Up @@ -479,6 +529,20 @@ describe("Cloudflare CDN warmup deploy flow", () => {
).toBe("https://my-worker-staging.example.workers.dev");
});

it("does not infer a warmup target from zone-oriented Wrangler config", async () => {
writeFile(
"wrangler.jsonc",
JSON.stringify({
name: "my-worker",
workers_dev: false,
routes: [{ pattern: "sub.example.com/*", zone_name: "example.com" }],
}),
);
const { resolveCdnWarmupTargetUrl } = await import("../packages/cloudflare/src/deploy.js");

expect(resolveCdnWarmupTargetUrl(tmpDir, null)).toBeNull();
});

it("skips unverifiable HTML warmup for adapters without build identity", async () => {
const events: string[] = [];
writeFile(
Expand Down Expand Up @@ -510,7 +574,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down Expand Up @@ -555,7 +619,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down Expand Up @@ -606,7 +670,7 @@ describe("Cloudflare CDN warmup deploy flow", () => {
}
if (args.includes("triggers")) {
events.push("triggers");
return "Triggers deployed\n";
return "Triggers deployed\n app.example.com (custom domain)\n";
}
throw new Error(`Unexpected Wrangler args: ${args.join(" ")}`);
});
Expand Down
20 changes: 19 additions & 1 deletion tests/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ import {
resolveStaticAssetSignal,
} from "../packages/vinext/src/server/worker-utils.js";
import { domainCandidates, parseWranglerConfig, runTPR } from "../packages/cloudflare/src/tpr.js";
import { parseWorkerDeploymentUrl } from "../packages/cloudflare/src/worker-deployment-url.js";
import {
parseCdnWarmupDeploymentUrl,
parseWorkerDeploymentUrl,
} from "../packages/cloudflare/src/worker-deployment-url.js";

// ─── Test Helpers ────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -561,6 +564,21 @@ describe("parseWorkerDeploymentUrl", () => {
expect(parseWorkerDeploymentUrl("Deployed app triggers\n app.example.com/*\n")).toBeNull();
});

it.each([
["app.example.com/*", "https://app.example.com"],
["https://app.example.com/*", "https://app.example.com"],
["http://app.example.com/*", "http://app.example.com"],
])("uses a concrete catch-all Worker route for CDN warmup: %s", (route, expected) => {
expect(parseCdnWarmupDeploymentUrl(`Deployed app triggers\n ${route}\n`)).toBe(expected);
});

it.each(["*.example.com/*", "app.example.com/api/*", "app.example.com/"])(
"rejects a non-canonical Worker route for CDN warmup: %s",
(route) => {
expect(parseCdnWarmupDeploymentUrl(`Deployed app triggers\n ${route}\n`)).toBeNull();
},
);

it("does not report a disabled custom domain", () => {
expect(
parseWorkerDeploymentUrl(
Expand Down
Loading