diff --git a/.changeset/nine-onions-wonder.md b/.changeset/nine-onions-wonder.md new file mode 100644 index 000000000000..84c0608ea99c --- /dev/null +++ b/.changeset/nine-onions-wonder.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Added `--domain` flag to `wrangler deploy` command for deploying to custom domains. Use `--domain example.com` to deploy directly to a custom domain without manually configuring routes. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index a12c7e1fb408..571ef01dce5b 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -1696,6 +1696,161 @@ Update them to point to this script instead?`, 'Publishing to Custom Domain "api.example.com" was skipped, fix conflict and try again' ); }); + it("should deploy domains passed via --domain flag as custom domains", async () => { + writeWranglerConfig({}); + writeWorkerSource(); + mockSubDomainRequest(); + mockUpdateWorkerSubdomain({ enabled: false }); + mockUploadWorkerRequest({ expectedType: "esm" }); + mockCustomDomainsChangesetRequest({}); + mockPublishCustomDomainsRequest({ + publishFlags: { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }, + domains: [{ hostname: "api.example.com" }], + }); + + await runWrangler("deploy ./index --domain api.example.com"); + expect(std.out).toContain("api.example.com (custom domain)"); + }); + + it("should deploy multiple domains passed via --domain flags", async () => { + writeWranglerConfig({}); + writeWorkerSource(); + mockSubDomainRequest(); + mockUpdateWorkerSubdomain({ enabled: false }); + mockUploadWorkerRequest({ expectedType: "esm" }); + mockCustomDomainsChangesetRequest({}); + mockPublishCustomDomainsRequest({ + publishFlags: { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }, + domains: [ + { hostname: "api.example.com" }, + { hostname: "app.example.com" }, + ], + }); + + await runWrangler( + "deploy ./index --domain api.example.com --domain app.example.com" + ); + expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).toContain("app.example.com (custom domain)"); + }); + + it("should deploy --domain flags alongside routes (from config when no CLI routes)", async () => { + writeWranglerConfig({ + routes: ["example.com/api/*"], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUpdateWorkerSubdomain({ enabled: false }); + mockUploadWorkerRequest({ expectedType: "esm" }); + mockCustomDomainsChangesetRequest({}); + mockPublishCustomDomainsRequest({ + publishFlags: { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }, + domains: [{ hostname: "api.example.com" }], + }); + // Mock the regular route deployment for the configured route + msw.use( + http.put( + "*/accounts/:accountId/workers/scripts/:scriptName/routes", + () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: ["example.com/api/*"], + }, + { status: 200 } + ); + }, + { once: true } + ) + ); + + await runWrangler("deploy ./index --domain api.example.com"); + expect(std.out).toContain("example.com/api/*"); + expect(std.out).toContain("api.example.com (custom domain)"); + }); + + it("should validate domain flags and reject invalid domains with wildcards", async () => { + writeWranglerConfig({}); + writeWorkerSource(); + + await expect(runWrangler("deploy ./index --domain *.example.com")) + .rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: Invalid Routes: + *.example.com: + Wildcard operators (*) are not allowed in Custom Domains] + `); + }); + + it("should validate domain flags and reject invalid domains with paths", async () => { + writeWranglerConfig({}); + writeWorkerSource(); + + await expect( + runWrangler("deploy ./index --domain api.example.com/path") + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [Error: Invalid Routes: + api.example.com/path: + Paths are not allowed in Custom Domains] + `); + }); + + it("should handle both --route and --domain flags together", async () => { + writeWranglerConfig({ + routes: ["config.com/api/*"], + }); + writeWorkerSource(); + mockSubDomainRequest(); + mockUpdateWorkerSubdomain({ enabled: false }); + mockUploadWorkerRequest({ expectedType: "esm" }); + mockCustomDomainsChangesetRequest({}); + mockPublishCustomDomainsRequest({ + publishFlags: { + override_scope: true, + override_existing_origin: false, + override_existing_dns_record: false, + }, + domains: [{ hostname: "api.example.com" }], + }); + // Mock the regular route deployment for the CLI route (should override config) + msw.use( + http.put( + "*/accounts/:accountId/workers/scripts/:scriptName/routes", + () => { + return HttpResponse.json( + { + success: true, + errors: [], + messages: [], + result: ["cli.com/override/*"], + }, + { status: 200 } + ); + }, + { once: true } + ) + ); + + await runWrangler( + "deploy ./index --route cli.com/override/* --domain api.example.com" + ); + expect(std.out).toContain("cli.com/override/*"); + expect(std.out).toContain("api.example.com (custom domain)"); + expect(std.out).not.toContain("config.com/api/*"); + }); }); describe("deploy asset routes", () => { diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 6f594d8ba0d4..f16c399d37e9 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -94,6 +94,7 @@ type Props = { alias: Record | undefined; triggers: string[] | undefined; routes: string[] | undefined; + domains: string[] | undefined; legacyEnv: boolean | undefined; jsxFactory: string | undefined; jsxFragment: string | undefined; @@ -416,9 +417,14 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m ); } + const domainRoutes = (props.domains || []).map((domain) => ({ + pattern: domain, + custom_domain: true, + })); const routes = props.routes ?? config.routes ?? (config.route ? [config.route] : []) ?? []; - validateRoutes(routes, props.assetsOptions); + const allRoutes = [...routes, ...domainRoutes]; + validateRoutes(allRoutes, props.assetsOptions); const jsxFactory = props.jsxFactory || config.jsx_factory; const jsxFragment = props.jsxFragment || config.jsx_fragment; @@ -1050,7 +1056,10 @@ See https://developers.cloudflare.com/workers/platform/compatibility-dates for m } // deploy triggers - const targets = await triggersDeploy(props); + const targets = await triggersDeploy({ + ...props, + routes: allRoutes, + }); logger.log("Current Version ID:", versionId); diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index 678a16fd1b43..f993161fd4a3 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -144,6 +144,13 @@ export const deployCommand = createCommand({ requiresArg: true, array: true, }, + domains: { + describe: "Custom domains to deploy to", + alias: "domain", + type: "string", + requiresArg: true, + array: true, + }, "jsx-factory": { describe: "The function that is called for each JSX element", type: "string", @@ -344,6 +351,7 @@ export const deployCommand = createCommand({ jsxFragment: args.jsxFragment, tsconfig: args.tsconfig, routes: args.routes, + domains: args.domains, assetsOptions, legacyAssetPaths: siteAssetPaths, legacyEnv: isLegacyEnv(config), diff --git a/packages/wrangler/src/triggers/deploy.ts b/packages/wrangler/src/triggers/deploy.ts index edcc88694fa5..a3bf94ce7788 100644 --- a/packages/wrangler/src/triggers/deploy.ts +++ b/packages/wrangler/src/triggers/deploy.ts @@ -27,7 +27,7 @@ type Props = { name: string | undefined; env: string | undefined; triggers: string[] | undefined; - routes: string[] | undefined; + routes: Route[] | undefined; legacyEnv: boolean | undefined; dryRun: boolean | undefined; assetsOptions: AssetsOptions | undefined;