From 8923f9769aaa13229d1cda535f95a9813465d765 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Sat, 6 Jun 2026 16:42:08 +0100 Subject: [PATCH 1/2] Preserve all deployment-affecting CLI flags in the interactive deploy config flow (#14055) --- ...preserve-all-deploy-flags-in-autoconfig.md | 7 + .../src/__tests__/deploy/core.test.ts | 373 +---- .../deploy/deploy-interactive-prompts.test.ts | 1288 +++++++++++++++++ packages/wrangler/src/deploy/autoconfig.ts | 116 +- 4 files changed, 1406 insertions(+), 378 deletions(-) create mode 100644 .changeset/preserve-all-deploy-flags-in-autoconfig.md create mode 100644 packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts diff --git a/.changeset/preserve-all-deploy-flags-in-autoconfig.md b/.changeset/preserve-all-deploy-flags-in-autoconfig.md new file mode 100644 index 0000000000..6d1514b7d7 --- /dev/null +++ b/.changeset/preserve-all-deploy-flags-in-autoconfig.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Preserve all deployment-affecting CLI flags in the interactive deploy config flow + +When running `wrangler deploy` without a config file and going through the interactive setup flow, CLI flags beyond `--compatibility-flags` (such as `--routes`/`--route`, `--domains`/`--domain`, `--triggers`, `--var`, `--define`, `--alias`, `--jsx-factory`, `--jsx-fragment`, `--tsconfig`, `--minify`, `--upload-source-maps`, `--no-bundle`, `--logpush`, `--keep-vars`, `--legacy-env`, and `--dispatch-namespace`) were silently dropped. These flags are now persisted to the generated `wrangler.jsonc` config file (where a config field equivalent exists) and included in the suggested CLI command when the user declines config file generation. diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index 979e6a6574..1b874ecd4b 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -20,7 +20,7 @@ import { fetchSecrets } from "../../utils/fetch-secrets"; import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; import { mockAuthDomain } from "../helpers/mock-auth-domain"; import { mockConsoleMethods } from "../helpers/mock-console"; -import { clearDialogs, mockConfirm, mockPrompt } from "../helpers/mock-dialogs"; +import { clearDialogs, mockConfirm } from "../helpers/mock-dialogs"; import { useMockIsTTY } from "../helpers/mock-istty"; import { mockExchangeRefreshTokenForAccessToken, @@ -1467,375 +1467,4 @@ describe("deploy", () => { } `); }); - - describe("interactive compatibility date prompt", () => { - it("should prompt and use today's date when user confirms", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig( - { compatibility_date: undefined as unknown as string }, - "./wrangler.toml" - ); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - - await runWrangler("deploy ./index.js --name test-name --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - }); - - it("should error when user declines the prompt", async ({ expect }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig( - { compatibility_date: undefined as unknown as string }, - "./wrangler.toml" - ); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: false, - }); - - await expect( - runWrangler("deploy ./index.js --name test-name --dry-run") - ).rejects.toThrow("A compatibility_date is required when publishing"); - }); - - it("should error in non-interactive mode when no compatibility_date is provided", async ({ - expect, - }) => { - setIsTTY(false); - writeWorkerSource(); - writeWranglerConfig( - { compatibility_date: undefined as unknown as string }, - "./wrangler.toml" - ); - - await expect( - runWrangler("deploy ./index.js --name test-name") - ).rejects.toThrow("A compatibility_date is required when publishing"); - }); - - it("should not show config-write prompt when config file already exists", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig( - { compatibility_date: undefined as unknown as string }, - "./wrangler.toml" - ); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - - await runWrangler("deploy ./index.js --name test-name --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - // Should NOT be asked to write a config file since one already exists - expect(std.out).not.toContain( - "Do you want Wrangler to write a wrangler.json config file" - ); - expect(std.out).not.toContain("Proceeding with deployment..."); - }); - - it("should skip the compat date prompt when --latest is passed", async ({ - expect, - }) => { - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig( - { compatibility_date: undefined as unknown as string }, - "./wrangler.toml" - ); - - await runWrangler( - "deploy ./index.js --name test-name --latest --dry-run" - ); - expect(std.out).toContain("--dry-run: exiting now."); - // No compat date prompt should have been shown - expect(std.out).not.toContain("No compatibility date is set"); - }); - - it("should prompt for name, compat date, and offer to write config when no config file exists", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: true, - }); - - await runWrangler("deploy ./index.js --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - // Config file should be written with main but without an assets key - const writtenConfig = JSON.parse( - fs.readFileSync("wrangler.jsonc", "utf-8") - ); - expect(writtenConfig).toEqual({ - name: "test-worker", - compatibility_date: "2024-06-15", - main: "./index.js", - }); - expect(writtenConfig).not.toHaveProperty("assets"); - expect(std.out).toContain( - "Simply run `wrangler deploy` next time. Wrangler will automatically use the configuration saved to wrangler.jsonc." - ); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should show suggested CLI flags when user declines config file write", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: false, - }); - - await runWrangler("deploy ./index.js --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - expect(fs.existsSync("wrangler.jsonc")).toBe(false); - expect(std.out).toContain( - "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15" - ); - // Should not include --assets since no assets were used - expect(std.out).not.toContain("--assets"); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should write config with today's compat date when --latest is used and no config file exists", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - // No compat date prompt — --latest skips it - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: true, - }); - - await runWrangler("deploy ./index.js --latest --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - // Config file should include today's date even though --latest was used - const writtenConfig = JSON.parse( - fs.readFileSync("wrangler.jsonc", "utf-8") - ); - expect(writtenConfig).toEqual({ - name: "test-worker", - compatibility_date: "2024-06-15", - main: "./index.js", - }); - expect(std.out).not.toContain("No compatibility date is set"); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should include compat date in suggested CLI command when --latest is used and config write declined", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - // No compat date prompt — --latest skips it - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: false, - }); - - await runWrangler("deploy ./index.js --latest --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - expect(fs.existsSync("wrangler.jsonc")).toBe(false); - // Suggested command should include the resolved compat date - expect(std.out).toContain( - "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15" - ); - expect(std.out).not.toContain("No compatibility date is set"); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should prompt for name when config file exists but has no name", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig({ name: undefined as unknown as string }); - mockPrompt({ - text: "What do you want to name your project?", - result: "prompted-name", - }); - - await runWrangler("deploy ./index.js --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - // Should NOT be asked to write a config file since one already exists - expect(std.out).not.toContain( - "Do you want Wrangler to write a wrangler.json config file" - ); - expect(std.out).not.toContain("Proceeding with deployment..."); - }); - - it("should not prompt for name when config file provides one", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - writeWranglerConfig({ - name: "config-provided-name", - compatibility_date: undefined as unknown as string, - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - - await runWrangler("deploy ./index.js --dry-run"); - expect(std.out).toContain("--dry-run: exiting now."); - // Should NOT have been asked for a name - expect(std.out).not.toContain("What do you want to name your project?"); - }); - - it("should include compatibility_flags in generated wrangler.jsonc when --compatibility-flags is passed", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: true, - }); - - await runWrangler( - "deploy ./index.js --compatibility-flags=nodejs_compat --dry-run" - ); - expect(std.out).toContain("--dry-run: exiting now."); - const writtenConfig = JSON.parse( - fs.readFileSync("wrangler.jsonc", "utf-8") - ); - expect(writtenConfig).toEqual({ - name: "test-worker", - compatibility_date: "2024-06-15", - main: "./index.js", - compatibility_flags: ["nodejs_compat"], - }); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should include --compatibility-flags in suggested CLI command when user declines config file write", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: false, - }); - - await runWrangler( - "deploy ./index.js --compatibility-flags=nodejs_compat --compatibility-flags=disable_navigator_language --dry-run" - ); - expect(std.out).toContain("--dry-run: exiting now."); - expect(fs.existsSync("wrangler.jsonc")).toBe(false); - // Suggested command should include the compat flags - expect(std.out).toContain( - "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15 --compatibility-flags nodejs_compat disable_navigator_language" - ); - expect(std.out).toContain("Proceeding with deployment..."); - }); - - it("should include multiple --compatibility-flags in suggested CLI command and config file", async ({ - expect, - }) => { - vi.setSystemTime(new Date(2024, 5, 15)); - setIsTTY(true); - writeWorkerSource(); - // No writeWranglerConfig call — no config file exists - mockPrompt({ - text: "What do you want to name your project?", - result: "test-worker", - }); - mockConfirm({ - text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", - result: true, - }); - mockConfirm({ - text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", - result: true, - }); - - await runWrangler( - "deploy ./index.js --compatibility-flags=nodejs_compat --compatibility-flags=url_standard --dry-run" - ); - expect(std.out).toContain("--dry-run: exiting now."); - const writtenConfig = JSON.parse( - fs.readFileSync("wrangler.jsonc", "utf-8") - ); - expect(writtenConfig).toEqual({ - name: "test-worker", - compatibility_date: "2024-06-15", - main: "./index.js", - compatibility_flags: ["nodejs_compat", "url_standard"], - }); - expect(std.out).toContain("Proceeding with deployment..."); - }); - }); }); diff --git a/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts b/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts new file mode 100644 index 0000000000..444cff2723 --- /dev/null +++ b/packages/wrangler/src/__tests__/deploy/deploy-interactive-prompts.test.ts @@ -0,0 +1,1288 @@ +import * as fs from "node:fs"; +import { + runInTempDir, + writeWranglerConfig, +} from "@cloudflare/workers-utils/test-helpers"; +import { http, HttpResponse } from "msw"; +import { afterEach, beforeEach, describe, it, vi } from "vitest"; +import { getInstalledPackageVersion } from "../../autoconfig/frameworks/utils/packages"; +import { clearOutputFilePath } from "../../output"; +import { fetchSecrets } from "../../utils/fetch-secrets"; +import { mockAccountId, mockApiToken } from "../helpers/mock-account-id"; +import { mockConsoleMethods } from "../helpers/mock-console"; +import { clearDialogs, mockConfirm, mockPrompt } from "../helpers/mock-dialogs"; +import { useMockIsTTY } from "../helpers/mock-istty"; +import { mockGetSettings } from "../helpers/mock-worker-settings"; +import { createFetchResult, msw } from "../helpers/msw"; +import { mswListNewDeploymentsLatestFull } from "../helpers/msw/handlers/versions"; +import { runWrangler } from "../helpers/run-wrangler"; +import { writeWorkerSource } from "../helpers/write-worker-source"; +import { + mockDeploymentsListRequest, + mockLastDeploymentRequest, + mockPatchScriptSettings, +} from "./helpers"; + +vi.mock("command-exists"); +vi.mock("../../check/commands", async (importOriginal) => { + return { + ...(await importOriginal()), + analyseBundle() { + return `{}`; + }, + }; +}); + +vi.mock("../../utils/fetch-secrets"); + +vi.mock("../../package-manager", async (importOriginal) => ({ + ...(await importOriginal()), + sniffUserAgent: () => "npm", + getPackageManager() { + return { + type: "npm", + npx: "npx", + }; + }, +})); + +vi.mock("../../autoconfig/run"); +vi.mock("../../autoconfig/frameworks/utils/packages"); +vi.mock("@cloudflare/cli-shared-helpers/command"); + +describe("deploy: interactive deploy config prompts", () => { + mockAccountId(); + mockApiToken(); + runInTempDir(); + const { setIsTTY } = useMockIsTTY(); + const std = mockConsoleMethods(); + + beforeEach(() => { + vi.stubGlobal("setTimeout", (fn: () => void) => { + setImmediate(fn); + }); + setIsTTY(true); + mockLastDeploymentRequest(); + mockDeploymentsListRequest(); + mockPatchScriptSettings(); + mockGetSettings(); + msw.use(...mswListNewDeploymentsLatestFull); + msw.use( + http.get("*/accounts/:accountId/r2/buckets/:bucketName", async () => { + return HttpResponse.json(createFetchResult({})); + }) + ); + vi.mocked(fetchSecrets).mockResolvedValue([]); + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + clearDialogs(); + clearOutputFilePath(); + }); + + it("should prompt and use today's date when user confirms", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig( + { compatibility_date: undefined as unknown as string }, + "./wrangler.toml" + ); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + + await runWrangler("deploy ./index.js --name test-name --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + }); + + it("should error when user declines the compatibility date prompt", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig( + { compatibility_date: undefined as unknown as string }, + "./wrangler.toml" + ); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: false, + }); + + await expect( + runWrangler("deploy ./index.js --name test-name --dry-run") + ).rejects.toThrow("A compatibility_date is required when publishing"); + }); + + it("should error in non-interactive mode when no compatibility_date is provided", async ({ + expect, + }) => { + setIsTTY(false); + writeWorkerSource(); + writeWranglerConfig( + { compatibility_date: undefined as unknown as string }, + "./wrangler.toml" + ); + + await expect( + runWrangler("deploy ./index.js --name test-name") + ).rejects.toThrow("A compatibility_date is required when publishing"); + }); + + it("should not show config-write prompt when config file already exists", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig( + { compatibility_date: undefined as unknown as string }, + "./wrangler.toml" + ); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + + await runWrangler("deploy ./index.js --name test-name --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // Should NOT be asked to write a config file since one already exists + expect(std.out).not.toContain( + "Do you want Wrangler to write a wrangler.json config file" + ); + expect(std.out).not.toContain("Proceeding with deployment..."); + }); + + it("should skip the compat date prompt when --latest is passed", async ({ + expect, + }) => { + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig( + { compatibility_date: undefined as unknown as string }, + "./wrangler.toml" + ); + + await runWrangler("deploy ./index.js --name test-name --latest --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // No compat date prompt should have been shown + expect(std.out).not.toContain("No compatibility date is set"); + }); + + it("should prompt for name, compat date, and offer to write config when no config file exists", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // Config file should be written with main but without an assets key + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + }); + expect(writtenConfig).not.toHaveProperty("assets"); + expect(std.out).toContain( + "Simply run `wrangler deploy` next time. Wrangler will automatically use the configuration saved to wrangler.jsonc." + ); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should show suggested CLI flags when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain( + "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15" + ); + // Should not include --assets since no assets were used + expect(std.out).not.toContain("--assets"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should write config with today's compat date when --latest is used and no config file exists", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + // No compat date prompt — --latest skips it + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --latest --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // Config file should include today's date even though --latest was used + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + }); + expect(std.out).not.toContain("No compatibility date is set"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include compat date in suggested CLI command when --latest is used and config write declined", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + // No compat date prompt — --latest skips it + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --latest --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + // Suggested command should include the resolved compat date + expect(std.out).toContain( + "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15" + ); + expect(std.out).not.toContain("No compatibility date is set"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should prompt for name when config file exists but has no name", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig({ name: undefined as unknown as string }); + mockPrompt({ + text: "What do you want to name your project?", + result: "prompted-name", + }); + + await runWrangler("deploy ./index.js --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // Should NOT be asked to write a config file since one already exists + expect(std.out).not.toContain( + "Do you want Wrangler to write a wrangler.json config file" + ); + expect(std.out).not.toContain("Proceeding with deployment..."); + }); + + it("should not prompt for name when config file provides one", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + writeWranglerConfig({ + name: "config-provided-name", + compatibility_date: undefined as unknown as string, + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + + await runWrangler("deploy ./index.js --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + // Should NOT have been asked for a name + expect(std.out).not.toContain("What do you want to name your project?"); + }); + + it("should include compatibility_flags in generated wrangler.jsonc when --compatibility-flags is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --compatibility-flags=nodejs_compat --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + compatibility_flags: ["nodejs_compat"], + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --compatibility-flags in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --compatibility-flags=nodejs_compat --compatibility-flags=disable_navigator_language --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + // Suggested command should include the compat flags + expect(std.out).toContain( + "wrangler deploy ./index.js --name test-worker --compatibility-date 2024-06-15 --compatibility-flags nodejs_compat disable_navigator_language" + ); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include multiple --compatibility-flags in suggested CLI command and config file", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + // No writeWranglerConfig call — no config file exists + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --compatibility-flags=nodejs_compat --compatibility-flags=url_standard --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + compatibility_flags: ["nodejs_compat", "url_standard"], + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include routes in generated wrangler.jsonc when --routes is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --routes example.com/* --routes other.com/path --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + routes: ["example.com/*", "other.com/path"], + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --routes in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --routes example.com/* --routes other.com/path --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--routes example.com/* other.com/path"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include domains as custom_domain routes in generated wrangler.jsonc when --domains is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --domains api.example.com --domains app.example.com --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + routes: [ + { pattern: "api.example.com", custom_domain: true }, + { pattern: "app.example.com", custom_domain: true }, + ], + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --domains in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --domains api.example.com --domains app.example.com --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--domains api.example.com app.example.com"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should merge --routes and --domains into routes array in generated wrangler.jsonc", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --routes example.com/* --domains api.example.com --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + routes: [ + "example.com/*", + { pattern: "api.example.com", custom_domain: true }, + ], + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include triggers in generated wrangler.jsonc when --triggers is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --triggers '*/5 * * * *' --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + triggers: { crons: ["*/5 * * * *"] }, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --triggers in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --triggers '*/5 * * * *' --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--triggers '*/5 * * * *'"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include vars in generated wrangler.jsonc when --var is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --var MY_VAR:my-value --var OTHER:thing --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + vars: { MY_VAR: "my-value", OTHER: "thing" }, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --var in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --var MY_VAR:my-value --var OTHER:thing --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--var MY_VAR:my-value OTHER:thing"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include define in generated wrangler.jsonc when --define is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --define MY_CONST:true --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + define: { MY_CONST: "true" }, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --define in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --define MY_CONST:true --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--define MY_CONST:true"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include alias in generated wrangler.jsonc when --alias is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --alias some-module:./aliased.js --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + alias: { "some-module": "./aliased.js" }, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include jsx_factory in generated wrangler.jsonc when --jsx-factory is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --jsx-factory h --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + jsx_factory: "h", + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include jsx_fragment in generated wrangler.jsonc when --jsx-fragment is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --jsx-fragment Fragment --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + jsx_fragment: "Fragment", + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include tsconfig in generated wrangler.jsonc when --tsconfig is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + fs.writeFileSync("tsconfig.custom.json", JSON.stringify({})); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --tsconfig ./tsconfig.custom.json --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + tsconfig: "./tsconfig.custom.json", + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include minify in generated wrangler.jsonc when --minify is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --minify --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + minify: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --minify in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --minify --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--minify"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include upload_source_maps in generated wrangler.jsonc when --upload-source-maps is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --upload-source-maps --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + upload_source_maps: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include no_bundle in generated wrangler.jsonc when --no-bundle is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --no-bundle --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + no_bundle: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include logpush in generated wrangler.jsonc when --logpush is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --logpush --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + logpush: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include keep_vars in generated wrangler.jsonc when --keep-vars is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --keep-vars --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + keep_vars: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --keep-vars in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler("deploy ./index.js --keep-vars --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--keep-vars"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include legacy_env in generated wrangler.jsonc when --legacy-env is passed", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler("deploy ./index.js --legacy-env --dry-run"); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + legacy_env: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include --dispatch-namespace in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --dispatch-namespace my-namespace --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--dispatch-namespace my-namespace"); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include multiple flags in generated wrangler.jsonc and suggested CLI command", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: true, + }); + + await runWrangler( + "deploy ./index.js --routes example.com/* --var MY_VAR:hello --minify --logpush --compatibility-flags=nodejs_compat --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + const writtenConfig = JSON.parse( + fs.readFileSync("wrangler.jsonc", "utf-8") + ); + expect(writtenConfig).toEqual({ + name: "test-worker", + compatibility_date: "2024-06-15", + main: "./index.js", + compatibility_flags: ["nodejs_compat"], + routes: ["example.com/*"], + vars: { MY_VAR: "hello" }, + minify: true, + logpush: true, + }); + expect(std.out).toContain("Proceeding with deployment..."); + }); + + it("should include multiple flags in suggested CLI command when user declines config file write", async ({ + expect, + }) => { + vi.setSystemTime(new Date(2024, 5, 15)); + setIsTTY(true); + writeWorkerSource(); + mockPrompt({ + text: "What do you want to name your project?", + result: "test-worker", + }); + mockConfirm({ + text: "No compatibility date is set. Would you like to use today's date (2024-06-15)?", + result: true, + }); + mockConfirm({ + text: "Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\nThis will allow you to simply run `wrangler deploy` on future deployments.", + result: false, + }); + + await runWrangler( + "deploy ./index.js --routes example.com/* --var MY_VAR:hello --minify --logpush --upload-source-maps --dispatch-namespace my-ns --dry-run" + ); + expect(std.out).toContain("--dry-run: exiting now."); + expect(fs.existsSync("wrangler.jsonc")).toBe(false); + expect(std.out).toContain("--routes example.com/*"); + expect(std.out).toContain("--var MY_VAR:hello"); + expect(std.out).toContain("--minify"); + expect(std.out).toContain("--logpush"); + expect(std.out).toContain("--upload-source-maps"); + expect(std.out).toContain("--dispatch-namespace my-ns"); + expect(std.out).toContain("Proceeding with deployment..."); + }); +}); diff --git a/packages/wrangler/src/deploy/autoconfig.ts b/packages/wrangler/src/deploy/autoconfig.ts index a14b10274f..4210082095 100644 --- a/packages/wrangler/src/deploy/autoconfig.ts +++ b/packages/wrangler/src/deploy/autoconfig.ts @@ -18,18 +18,54 @@ import { confirm, prompt } from "../dialogs"; import { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; import { writeOutput } from "../output"; +import { collectKeyValues } from "../utils/collectKeyValues"; import type { ReadConfigCommandArgs } from "../config"; -type AutoConfigArgs = ReadConfigCommandArgs & { - experimentalAutoconfig: boolean | undefined; - assets: string | undefined; - path: string | undefined; - dryRun: boolean | undefined; - latest: boolean | undefined; +/** + * CLI flags that affect the worker's deployment configuration. + * These are persisted to the generated wrangler.jsonc and/or included + * in the suggested CLI command during the interactive deploy flow. + */ +type DeployConfigFlags = { compatibilityDate: string | undefined; compatibilityFlags: string[] | undefined; + // Routing & scheduling + routes: string[] | undefined; + domains: string[] | undefined; + triggers: string[] | undefined; + // Variables & build-time substitutions + var: string[] | undefined; + define: string[] | undefined; + alias: string[] | undefined; + // Build configuration + jsxFactory: string | undefined; + jsxFragment: string | undefined; + tsconfig: string | undefined; + minify: boolean | undefined; + uploadSourceMaps: boolean | undefined; + bundle: boolean | undefined; + // Deployment behavior + logpush: boolean | undefined; + keepVars: boolean | undefined; + legacyEnv: boolean | undefined; + dispatchNamespace: string | undefined; }; +/** + * The full set of CLI args consumed by the interactive deploy autoconfig flow. + * Combines the base config/script/name args from {@link ReadConfigCommandArgs}, + * all deployment-affecting flags from {@link DeployConfigFlags}, and the + * autoconfig-specific control flags (experimentalAutoconfig, assets, dryRun, latest). + */ +type AutoConfigArgs = ReadConfigCommandArgs & + DeployConfigFlags & { + experimentalAutoconfig: boolean | undefined; + path: string | undefined; + assets: string | undefined; + dryRun: boolean | undefined; + latest: boolean | undefined; + }; + /** * Runs autoconfig if applicable, including open-next delegation and interactive * prompts for missing config. Returns `{ aborted: true }` if deploy should not @@ -204,6 +240,52 @@ export async function promptForMissingDeployConfig( if (args.compatibilityFlags?.length) { configContent.compatibility_flags = args.compatibilityFlags; } + if (args.routes?.length || args.domains?.length) { + const routeEntries: unknown[] = [...(args.routes ?? [])]; + for (const domain of args.domains ?? []) { + routeEntries.push({ pattern: domain, custom_domain: true }); + } + configContent.routes = routeEntries; + } + if (args.triggers?.length) { + configContent.triggers = { crons: args.triggers }; + } + if (args.var?.length) { + configContent.vars = collectKeyValues(args.var); + } + if (args.define?.length) { + configContent.define = collectKeyValues(args.define); + } + if (args.alias?.length) { + configContent.alias = collectKeyValues(args.alias); + } + if (args.jsxFactory) { + configContent.jsx_factory = args.jsxFactory; + } + if (args.jsxFragment) { + configContent.jsx_fragment = args.jsxFragment; + } + if (args.tsconfig) { + configContent.tsconfig = args.tsconfig; + } + if (args.minify) { + configContent.minify = true; + } + if (args.uploadSourceMaps) { + configContent.upload_source_maps = true; + } + if (args.bundle === false) { + configContent.no_bundle = true; + } + if (args.logpush) { + configContent.logpush = true; + } + if (args.keepVars) { + configContent.keep_vars = true; + } + if (args.legacyEnv) { + configContent.legacy_env = true; + } const writeConfigFile = await confirm( `Do you want Wrangler to write a wrangler.jsonc config file to store this configuration?\n${chalk.dim( @@ -234,6 +316,28 @@ export async function promptForMissingDeployConfig( ...(args.compatibilityFlags?.length ? [`--compatibility-flags ${args.compatibilityFlags.join(" ")}`] : []), + ...(args.routes?.length ? [`--routes ${args.routes.join(" ")}`] : []), + ...(args.domains?.length + ? [`--domains ${args.domains.join(" ")}`] + : []), + ...(args.triggers?.length + ? [`--triggers ${args.triggers.map((t) => `'${t}'`).join(" ")}`] + : []), + ...(args.var?.length ? [`--var ${args.var.join(" ")}`] : []), + ...(args.define?.length ? [`--define ${args.define.join(" ")}`] : []), + ...(args.alias?.length ? [`--alias ${args.alias.join(" ")}`] : []), + ...(args.jsxFactory ? [`--jsx-factory ${args.jsxFactory}`] : []), + ...(args.jsxFragment ? [`--jsx-fragment ${args.jsxFragment}`] : []), + ...(args.tsconfig ? [`--tsconfig ${args.tsconfig}`] : []), + ...(args.minify ? ["--minify"] : []), + ...(args.uploadSourceMaps ? ["--upload-source-maps"] : []), + ...(args.bundle === false ? ["--no-bundle"] : []), + ...(args.logpush ? ["--logpush"] : []), + ...(args.keepVars ? ["--keep-vars"] : []), + ...(args.legacyEnv ? ["--legacy-env"] : []), + ...(args.dispatchNamespace + ? [`--dispatch-namespace ${args.dispatchNamespace}`] + : []), ] .filter(Boolean) .join(" "); From 48c4ff00483e9346c9fe6dcb981009b081c0a204 Mon Sep 17 00:00:00 2001 From: KT <44502608+ktKongTong@users.noreply.github.com> Date: Sun, 7 Jun 2026 01:08:24 +0800 Subject: [PATCH 2/2] [miniflare]: Fix local `scheduled` handler returning exception when `assets` is enabled (#14177) Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Pete Bacon Darwin --- .changeset/kind-geese-nail.md | 7 ++ .../miniflare/src/plugins/assets/index.ts | 1 + .../src/workers/assets/rpc-proxy.worker.ts | 22 +++++ packages/miniflare/test/index.spec.ts | 83 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 .changeset/kind-geese-nail.md diff --git a/.changeset/kind-geese-nail.md b/.changeset/kind-geese-nail.md new file mode 100644 index 0000000000..7d7918c4a6 --- /dev/null +++ b/.changeset/kind-geese-nail.md @@ -0,0 +1,7 @@ +--- +"miniflare": patch +--- + +Enable local scheduled handler dispatch for Workers + Assets (#9882) + +It is now possible to trigger a scheduled handler on a Worker that has assets. diff --git a/packages/miniflare/src/plugins/assets/index.ts b/packages/miniflare/src/plugins/assets/index.ts index 6a768778a8..0c23bc8a7c 100644 --- a/packages/miniflare/src/plugins/assets/index.ts +++ b/packages/miniflare/src/plugins/assets/index.ts @@ -276,6 +276,7 @@ export const ASSETS_PLUGIN: Plugin = { name: `${RPC_PROXY_SERVICE_NAME}:${id}`, worker: { compatibilityDate: "2024-08-01", + compatibilityFlags: ["service_binding_extra_handlers"], modules: [ { name: "assets-proxy-worker.mjs", diff --git a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts index 671f7836d6..8ed03b6009 100644 --- a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts +++ b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts @@ -26,6 +26,28 @@ export default class RPCProxyWorker extends WorkerEntrypoint { return this.env.ROUTER_WORKER.fetch(request); } + // Forward scheduled events to the User Worker. The proxy itself doesn't run + // any scheduled logic; it just dispatches a real scheduled event to the user + // worker via the Fetcher built-in, then propagates the user worker's noRetry + // decision back onto this controller so the outcome surfaces correctly to + // the caller (e.g. the entry worker's `/cdn-cgi/handler/scheduled` handler). + async scheduled(controller: ScheduledController) { + const result = await this.env.USER_WORKER.scheduled?.({ + cron: controller.cron, + scheduledTime: new Date(controller.scheduledTime), + }); + if (result?.noRetry) { + controller.noRetry(); + } + if (result?.outcome !== "ok") { + // Re-throw so workerd surfaces `outcome: "exception"` to the caller + // rather than swallowing the user worker's failure. + throw new Error( + `User Worker scheduled handler failed with outcome: ${result?.outcome}` + ); + } + } + tail(events: TraceItem[]) { // Temporary workaround: the tail events is not serializable over capnproto yet // But they are effectively JSON, so we are serializing them to JSON and parsing it back to make it transferable. diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index 87c3c1f19a..36fc4761af 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -1736,6 +1736,89 @@ test("Miniflare: manually triggered scheduled events", async ({ expect }) => { expect(await res.text()).toBe("true"); }); +test("Miniflare: manually triggered scheduled events with assets", async ({ + expect, +}) => { + const log = new TestLog(); + const tmp = await useTmp(); + await fs.writeFile( + path.join(tmp, "foo.html"), + "

asset

", + "utf8" + ); + await fs.writeFile(path.join(tmp, "foo.md"), "asset", "utf8"); + const mf = new Miniflare({ + log, + modules: true, + script: ` + let scheduledRun = false; + let cron; + let scheduledTime; + export default { + fetch() { + return Response.json({ scheduledRun, cron, scheduledTime }); + }, + scheduled(controller, env, ctx) { + scheduledRun = true; + cron = controller.cron; + scheduledTime = Number(controller.scheduledTime); + controller.noRetry(); + } + }`, + assets: { + directory: tmp, + routerConfig: { + has_user_worker: true, + }, + }, + unsafeTriggerHandlers: true, + }); + useDispose(mf); + + type ScheduledResult = { + scheduledRun: boolean; + cron?: string; + scheduledTime?: number; + }; + + let res = await mf.dispatchFetch("http://localhost"); + let json = (await res.json()) as ScheduledResult; + expect(json.scheduledRun).toBe(false); + expect(json.cron).toBe(undefined); + expect(json.scheduledTime).toBe(undefined); + + res = await mf.dispatchFetch("http://localhost/foo"); + expect(res.headers.get("content-type")).toBe("text/html; charset=utf-8"); + expect(await res.text()).toBe("

asset

"); + + res = await mf.dispatchFetch("http://localhost/foo.md"); + expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); + expect(await res.text()).toBe("asset"); + + res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); + expect(await res.text()).toBe("ok"); + + res = await mf.dispatchFetch("http://localhost"); + json = (await res.json()) as ScheduledResult; + expect(json.scheduledRun).toBe(true); + expect(json.cron).toBe(""); + expect(json.scheduledTime).toBeDefined(); + + res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/handler/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" + ); + expect(await res.json()).toEqual({ + outcome: "ok", + noRetry: true, + }); + + res = await mf.dispatchFetch("http://localhost"); + json = (await res.json()) as ScheduledResult; + expect(json.scheduledRun).toBe(true); + expect(json.cron).toBe("0 0 0 0 0"); + expect(json.scheduledTime).toBe(1234567890987); +}); + test("Miniflare: manually triggered email handler - valid email", async ({ expect, }) => {