diff --git a/.changeset/angry-bears-count.md b/.changeset/angry-bears-count.md new file mode 100644 index 0000000000..453ea41912 --- /dev/null +++ b/.changeset/angry-bears-count.md @@ -0,0 +1,8 @@ +--- +"@cloudflare/workers-utils": minor +"wrangler": minor +--- + +Add production_enabled and previews_enabled support for custom domain routes + +Custom domain routes can now include optional production_enabled and previews_enabled boolean fields to control whether a domain serves production and/or preview traffic. When omitted, the API defaults apply (production enabled, previews disabled). diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index b9a0cf70a0..dd2ccb4ba8 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -20,7 +20,12 @@ export type ZoneNameRoute = { zone_name: string; custom_domain?: boolean; }; -export type CustomDomainRoute = { pattern: string; custom_domain: boolean }; +export type CustomDomainRoute = { + pattern: string; + custom_domain: boolean; + enabled?: boolean; + previews_enabled?: boolean; +}; export type Route = | SimpleRoute | ZoneIdRoute diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index e60ca0de7c..bf3241972e 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -964,8 +964,6 @@ function isValidRouteValue(item: unknown): boolean { return false; } - const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern" - const hasZoneId = hasProperty(item, "zone_id") && typeof item.zone_id === "string"; const hasZoneName = @@ -973,13 +971,38 @@ function isValidRouteValue(item: unknown): boolean { const hasCustomDomainFlag = hasProperty(item, "custom_domain") && typeof item.custom_domain === "boolean"; + const hasEnabled = + hasProperty(item, "enabled") && typeof item.enabled === "boolean"; + const hasPreviewsEnabled = + hasProperty(item, "previews_enabled") && + typeof item.previews_enabled === "boolean"; + + const recognizedKeys = [ + hasZoneId, + hasZoneName, + hasCustomDomainFlag, + hasEnabled, + hasPreviewsEnabled, + ].filter(Boolean).length; + const otherKeys = Object.keys(item).length - 1; // minus one to subtract "pattern" - if (otherKeys === 2 && hasCustomDomainFlag && (hasZoneId || hasZoneName)) { - return true; - } else if ( - otherKeys === 1 && - (hasZoneId || hasZoneName || hasCustomDomainFlag) - ) { + // All keys must be recognized + if (recognizedKeys !== otherKeys) { + return false; + } + + // zone_id and zone_name are mutually exclusive + if (hasZoneId && hasZoneName) { + return false; + } + + // enabled and previews_enabled are only valid on custom domain routes + if ((hasEnabled || hasPreviewsEnabled) && !hasCustomDomainFlag) { + return false; + } + + // Must have at least one of: zone_id, zone_name, or custom_domain + if (hasZoneId || hasZoneName || hasCustomDomainFlag) { return true; } } @@ -1030,7 +1053,7 @@ function mutateEmptyStringRouteValue( const isRoute: ValidatorFn = (diagnostics, field, value) => { if (value !== undefined && !isValidRouteValue(value)) { diagnostics.errors.push( - `Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got ${JSON.stringify( + `Expected "${field}" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got ${JSON.stringify( value )}.` ); @@ -1060,7 +1083,7 @@ const isRouteArray: ValidatorFn = (diagnostics, field, value) => { } if (invalidRoutes.length > 0) { diagnostics.errors.push( - `Expected "${field}" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: ${JSON.stringify( + `Expected "${field}" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: ${JSON.stringify( invalidRoutes, null, 2 diff --git a/packages/workers-utils/src/construct-wrangler-config.ts b/packages/workers-utils/src/construct-wrangler-config.ts index 1fc29d267a..fc29cf3709 100644 --- a/packages/workers-utils/src/construct-wrangler-config.ts +++ b/packages/workers-utils/src/construct-wrangler-config.ts @@ -70,6 +70,9 @@ function convertWorkerToWranglerConfig(config: APIWorkerConfig): RawConfig { pattern: c.hostname as string, zone_name: c.zone_name, custom_domain: true, + enabled: (c as typeof c & { enabled: boolean }).enabled, + previews_enabled: (c as typeof c & { previews_enabled: boolean }) + .previews_enabled, })), ]; diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index 33042d2600..ed391edaae 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -1164,9 +1164,9 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasWarnings()).toBe(false); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got 888. + - Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got 888. - Expected "account_id" to be of type string but got 222. - - Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: [ + - Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: [ 666, 777, { @@ -5913,9 +5913,9 @@ describe("normalizeAndValidateConfig()", () => { "Processing wrangler configuration: - "env.ENV1" environment configuration - - Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name }, but got 888. + - Expected "route" to be either a string, or an object with shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but got 888. - Expected "account_id" to be of type string but got 222. - - Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name }, but these weren't valid: [ + - Expected "routes" to be an array of either strings or objects with the shape { pattern, custom_domain, zone_id | zone_name, enabled, previews_enabled }, but these weren't valid: [ 666, 777 ]. diff --git a/packages/wrangler/src/__tests__/deploy/helpers.ts b/packages/wrangler/src/__tests__/deploy/helpers.ts index 40eaaec572..d4e65004ed 100644 --- a/packages/wrangler/src/__tests__/deploy/helpers.ts +++ b/packages/wrangler/src/__tests__/deploy/helpers.ts @@ -215,6 +215,8 @@ export function mockCustomDomainsChangesetRequest({ environment: params.envName, zone_name: "", zone_id: "", + enabled: true, + previews_enabled: false, }; }), removed: [], @@ -247,7 +249,11 @@ export function mockPublishCustomDomainsRequest({ override_existing_dns_record: boolean; }; domains: Array< - { hostname: string } & ({ zone_id?: string } | { zone_name?: string }) + { + hostname: string; + enabled?: boolean; + previews_enabled?: boolean; + } & ({ zone_id?: string } | { zone_name?: string }) >; env?: string | undefined; useServiceEnvironments?: boolean | undefined; diff --git a/packages/wrangler/src/__tests__/deploy/routes.test.ts b/packages/wrangler/src/__tests__/deploy/routes.test.ts index fd95c5b3ee..dea7e76906 100644 --- a/packages/wrangler/src/__tests__/deploy/routes.test.ts +++ b/packages/wrangler/src/__tests__/deploy/routes.test.ts @@ -654,6 +654,44 @@ describe("deploy", () => { expect(std.out).toContain("api.example.com (custom domain)"); }); + it("should pass enabled and previews_enabled to the custom domains API", async ({ + expect, + }) => { + writeWranglerConfig({ + routes: [ + { + pattern: "api.example.com", + custom_domain: true, + enabled: true, + previews_enabled: true, + }, + ], + }); + writeWorkerSource(); + mockUpdateWorkerSubdomain({ enabled: false }); + mockUploadWorkerRequest({ expectedType: "esm" }); + mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); + mockGetZoneWorkerRoutes("api-example-com-id", []); + mockCustomDomainsChangesetRequest({}); + mockPublishCustomDomainsRequest({ + publishFlags: { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }, + domains: [ + { + hostname: "api.example.com", + enabled: true, + previews_enabled: true, + }, + ], + }); + await runWrangler("deploy ./index"); + expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("[enabled, previews: enabled]"); + }); + it("should confirm override if custom domain deploy would override an existing domain", async ({ expect, }) => { @@ -677,6 +715,8 @@ describe("deploy", () => { hostname: "api.example.com", service: "test-name", environment: "", + enabled: true, + previews_enabled: false, }, ], }); @@ -687,6 +727,8 @@ describe("deploy", () => { hostname: "api.example.com", service: "other-script", environment: "", + enabled: true, + previews_enabled: false, }); mockPublishCustomDomainsRequest({ publishFlags: { @@ -729,6 +771,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "test-name", environment: "", + enabled: true, + previews_enabled: false, }, ], }); @@ -773,6 +817,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "test-name", environment: "", + enabled: true, + previews_enabled: false, }, ], dnsRecordConflicts: [ @@ -783,6 +829,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "test-name", environment: "", + enabled: true, + previews_enabled: false, }, ], }); @@ -793,6 +841,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "other-script", environment: "", + enabled: true, + previews_enabled: false, }); mockPublishCustomDomainsRequest({ publishFlags: { @@ -874,6 +924,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "test-name", environment: "", + enabled: true, + previews_enabled: false, }, ], }); @@ -884,6 +936,8 @@ Update them to point to this script instead?`, hostname: "api.example.com", service: "other-script", environment: "", + enabled: true, + previews_enabled: false, }); mockConfirm({ text: `Custom Domains already exist for these domains: diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 03c7b8fc30..ea69235bdc 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -152,6 +152,8 @@ export type CustomDomain = { hostname: string; service: string; environment: string; + enabled: boolean; + previews_enabled: boolean; }; type UpdatedCustomDomain = CustomDomain & { modified: boolean }; type ConflictingCustomDomain = CustomDomain & { @@ -248,6 +250,21 @@ export function renderRoute(route: Route): string { } else if ("zone_name" in route) { result += ` (zone name: ${route.zone_name})`; } + + if (isCustomDomain) { + const flags: string[] = []; + if ("enabled" in route && route.enabled !== undefined) { + flags.push(route.enabled ? "enabled" : "disabled"); + } + if ("previews_enabled" in route && route.previews_enabled !== undefined) { + flags.push( + route.previews_enabled ? "previews: enabled" : "previews: disabled" + ); + } + if (flags.length > 0) { + result += ` [${flags.join(", ")}]`; + } + } } return result; } @@ -286,6 +303,11 @@ export async function publishCustomDomains( hostname: domainRoute.pattern, zone_id: "zone_id" in domainRoute ? domainRoute.zone_id : undefined, zone_name: "zone_name" in domainRoute ? domainRoute.zone_name : undefined, + enabled: "enabled" in domainRoute ? domainRoute.enabled : undefined, + previews_enabled: + "previews_enabled" in domainRoute + ? domainRoute.previews_enabled + : undefined, }; }); diff --git a/packages/wrangler/src/utils/download-worker-config.ts b/packages/wrangler/src/utils/download-worker-config.ts index c3562ac292..3902f12b1f 100644 --- a/packages/wrangler/src/utils/download-worker-config.ts +++ b/packages/wrangler/src/utils/download-worker-config.ts @@ -17,6 +17,8 @@ type CustomDomainsRes = { service: string; environment: string; cert_id: string; + enabled: boolean; + previews_enabled: boolean; }[]; type WorkerSubdomainRes = {