From bba09689ca258b6da36b21b7300845ce031eaca6 Mon Sep 17 00:00:00 2001 From: AmirHossein Sakhravi Date: Wed, 21 Jan 2026 23:53:53 +0330 Subject: [PATCH 1/2] feat(wrangler/cli): add `@bomb.sh/tab` completions (#11113) * feat(wrangler/cli): add tab completions * chore: update tab to version 0.0.9 * refactor(wrangler): use experimental_getWranglerCommands function for complete.ts * Update changelog for wrangler tab completions * Format * chore: update bomb.sh/tab to version 0.0.11 * add tests * address Ben's review * update changeset .md file * Removed self-explanitory doc comments * Switched to top-level `expect` import for completion tests * Minor linting fixes * Added Fish shell to changeset * Refactored to use dedicated completion command namespace handler * Minor help menu formatting fixes * Minor status label fixes * Overhauled `complete` command logic * Fixed test help menu snapshots * Minor `completionArgs` filtering tweak --------- Co-authored-by: Matt Kane Co-authored-by: Matt Kane Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com> Co-authored-by: Ben Dixon --- .changeset/rude-cows-cheat.md | 24 +++ packages/wrangler/package.json | 1 + .../wrangler/src/__tests__/complete.test.ts | 196 ++++++++++++++++++ packages/wrangler/src/__tests__/docs.test.ts | 1 - packages/wrangler/src/__tests__/index.test.ts | 2 + packages/wrangler/src/complete.ts | 164 +++++++++++++++ packages/wrangler/src/docs/index.ts | 2 +- packages/wrangler/src/index.ts | 10 + pnpm-lock.yaml | 23 ++ 9 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 .changeset/rude-cows-cheat.md create mode 100644 packages/wrangler/src/__tests__/complete.test.ts create mode 100644 packages/wrangler/src/complete.ts diff --git a/.changeset/rude-cows-cheat.md b/.changeset/rude-cows-cheat.md new file mode 100644 index 000000000000..9db2766b77b6 --- /dev/null +++ b/.changeset/rude-cows-cheat.md @@ -0,0 +1,24 @@ +--- +"wrangler": minor +--- + +Add `wrangler complete` command for shell completion scripts (bash, zsh, powershell) + +Usage: + +```bash +# Bash +wrangler complete bash >> ~/.bashrc + +# Zsh +wrangler complete zsh >> ~/.zshrc + +# Fish +wrangler complete fish >> ~/.config/fish/completions/wrangler.fish + +# PowerShell +wrangler complete powershell > $PROFILE +``` + +- Uses `@bomb.sh/tab` library for cross-shell compatibility +- Completions are dynamically generated from `experimental_getWranglerCommands()` API diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index fe7ed2e84865..ab04d53d92df 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -77,6 +77,7 @@ }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", + "@bomb.sh/tab": "^0.0.11", "@cloudflare/cli": "workspace:*", "@cloudflare/containers-shared": "workspace:*", "@cloudflare/eslint-config-shared": "workspace:*", diff --git a/packages/wrangler/src/__tests__/complete.test.ts b/packages/wrangler/src/__tests__/complete.test.ts new file mode 100644 index 000000000000..f579af724ac7 --- /dev/null +++ b/packages/wrangler/src/__tests__/complete.test.ts @@ -0,0 +1,196 @@ +import { execSync } from "node:child_process"; +import { describe, expect, test } from "vitest"; +import { mockConsoleMethods } from "./helpers/mock-console"; +import { runInTempDir } from "./helpers/run-in-tmp"; +import { runWrangler } from "./helpers/run-wrangler"; + +function shellAvailable(shell: string): boolean { + try { + execSync(`which ${shell}`, { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +describe("wrangler", () => { + describe("complete", () => { + const std = mockConsoleMethods(); + runInTempDir(); + + describe("complete --", () => { + test("should return top-level commands", async () => { + await runWrangler('complete -- ""'); + + expect(std.out).toContain("deploy\t"); + expect(std.out).toContain("dev\t"); + expect(std.out).toContain("kv\t"); + }); + + test("should return subcommands for namespace", async () => { + await runWrangler('complete -- kv ""'); + + expect(std.out).toContain("namespace\t"); + expect(std.out).toContain("key\t"); + }); + + test("should return flags for a command", async () => { + await runWrangler("complete -- dev --"); + + expect(std.out).toContain("--port\t"); + expect(std.out).toContain("--ip\t"); + }); + + test("should not include internal commands", async () => { + await runWrangler('complete -- ""'); + + expect(std.out).toContain("deploy\t"); + expect(std.out).toContain("dev\t"); + // Internal commands like "_dev" should not be exposed + expect(std.out).not.toMatch(/^_dev\t/m); + }); + + test("should handle deeply nested commands", async () => { + await runWrangler('complete -- queues consumer http ""'); + + expect(std.out).toContain("add\t"); + expect(std.out).toContain("remove\t"); + }); + + test("should output tab-separated format", async () => { + await runWrangler('complete -- ""'); + + // Most lines should be "value\tdescription" format + const lines = std.out.trim().split("\n"); + let tabSeparatedCount = 0; + for (const line of lines) { + if (line.trim() && !line.startsWith(":")) { + if (line.includes("\t")) { + tabSeparatedCount++; + } + } + } + // Most commands should have descriptions + expect(tabSeparatedCount).toBeGreaterThan(10); + }); + + test("should return options with choices", async () => { + await runWrangler('complete -- dev --log-level=""'); + + expect(std.out).toContain("debug\t"); + expect(std.out).toContain("info\t"); + expect(std.out).toContain("warn\t"); + expect(std.out).toContain("error\t"); + }); + }); + + const shells = ["bash", "zsh", "fish"] as const; + + describe.each(shells)("%s", (shell) => { + test("should output valid shell script", async () => { + await runWrangler(`complete ${shell}`); + + expect(std.out.length).toBeGreaterThan(100); + }); + + test("should reference wrangler complete", async () => { + await runWrangler(`complete ${shell}`); + + expect(std.out).toContain("wrangler complete --"); + }); + }); + + describe("bash", () => { + test.skipIf(!shellAvailable("bash"))( + "should generate valid bash syntax", + async () => { + await runWrangler("complete bash"); + + // bash -n checks syntax without executing + expect(() => { + execSync("bash -n", { input: std.out }); + }).not.toThrow(); + } + ); + + test("should define __wrangler_complete function", async () => { + await runWrangler("complete bash"); + + expect(std.out).toContain("__wrangler_complete()"); + }); + + test("should register completion with complete builtin", async () => { + await runWrangler("complete bash"); + + expect(std.out).toContain("complete -F __wrangler_complete wrangler"); + }); + }); + + describe("zsh", () => { + test.skipIf(!shellAvailable("zsh"))( + "should generate valid zsh syntax", + async () => { + await runWrangler("complete zsh"); + + // zsh -n checks syntax without executing + expect(() => { + execSync("zsh -n", { input: std.out }); + }).not.toThrow(); + } + ); + + test("should start with #compdef directive", async () => { + await runWrangler("complete zsh"); + + expect(std.out).toContain("#compdef wrangler"); + }); + + test("should define _wrangler function", async () => { + await runWrangler("complete zsh"); + + expect(std.out).toContain("_wrangler()"); + }); + + test("should register with compdef", async () => { + await runWrangler("complete zsh"); + + expect(std.out).toContain("compdef _wrangler wrangler"); + }); + }); + + describe("fish", () => { + test.skipIf(!shellAvailable("fish"))( + "should generate valid fish syntax", + async () => { + await runWrangler("complete fish"); + + // fish -n checks syntax without executing + expect(() => { + execSync("fish -n", { input: std.out }); + }).not.toThrow(); + } + ); + + test("should define __wrangler_perform_completion function", async () => { + await runWrangler("complete fish"); + + expect(std.out).toContain("function __wrangler_perform_completion"); + }); + + test("should register completion with complete builtin", async () => { + await runWrangler("complete fish"); + + expect(std.out).toContain("complete -c wrangler"); + }); + + test("should use commandline for token extraction", async () => { + await runWrangler("complete fish"); + + // commandline -opc gets completed tokens + expect(std.out).toContain("commandline -opc"); + // commandline -ct gets current token being typed + expect(std.out).toContain("commandline -ct"); + }); + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/docs.test.ts b/packages/wrangler/src/__tests__/docs.test.ts index 6f1ed5d611b1..f2c9cc2ce914 100644 --- a/packages/wrangler/src/__tests__/docs.test.ts +++ b/packages/wrangler/src/__tests__/docs.test.ts @@ -42,7 +42,6 @@ describe("wrangler docs", () => { 📚 Open Wrangler's command documentation in your browser - POSITIONALS search Enter search terms (e.g. the wrangler command) you want to know more about [array] [default: []] diff --git a/packages/wrangler/src/__tests__/index.test.ts b/packages/wrangler/src/__tests__/index.test.ts index dfc59ffca7c4..08851b3b1a47 100644 --- a/packages/wrangler/src/__tests__/index.test.ts +++ b/packages/wrangler/src/__tests__/index.test.ts @@ -37,6 +37,7 @@ describe("wrangler", () => { COMMANDS wrangler docs [search..] 📚 Open Wrangler's command documentation in your browser + wrangler complete [shell] ⌨️ Generate and handle shell completions ACCOUNT wrangler auth 🔐 Manage authentication @@ -108,6 +109,7 @@ describe("wrangler", () => { COMMANDS wrangler docs [search..] 📚 Open Wrangler's command documentation in your browser + wrangler complete [shell] ⌨️ Generate and handle shell completions ACCOUNT wrangler auth 🔐 Manage authentication diff --git a/packages/wrangler/src/complete.ts b/packages/wrangler/src/complete.ts new file mode 100644 index 000000000000..1a9be2d2da84 --- /dev/null +++ b/packages/wrangler/src/complete.ts @@ -0,0 +1,164 @@ +import t from "@bomb.sh/tab"; +import { CommandLineArgsError } from "@cloudflare/workers-utils"; +import { createCommand } from "./core/create-command"; +import { experimental_getWranglerCommands } from "./experimental-commands-api"; +import type { DefinitionTreeNode } from "./core/types"; + +function setupCompletions(): void { + const { registry, globalFlags } = experimental_getWranglerCommands(); + + // Global flags that work on every command + for (const [flagName, flagDef] of Object.entries(globalFlags)) { + if ("hidden" in flagDef && flagDef.hidden) { + continue; + } + + const description = flagDef.describe || ""; + t.option(flagName, description); + + if ("alias" in flagDef && flagDef.alias) { + const aliases = Array.isArray(flagDef.alias) + ? flagDef.alias + : [flagDef.alias]; + for (const alias of aliases) { + t.option(alias, `Alias for --${flagName}`); + } + } + } + + // Recursively add commands from the registry tree + function addCommandsFromTree( + node: DefinitionTreeNode, + parentPath: string[] = [] + ): void { + for (const [name, childNode] of node.subtree.entries()) { + const commandPath = [...parentPath, name]; + const commandName = commandPath.join(" "); + + if (childNode.definition) { + const def = childNode.definition; + let description = ""; + + if (def.metadata?.description) { + description = def.metadata.description; + } + + if ( + def.metadata?.status && + def.metadata.status !== "stable" && + !def.metadata.hidden + ) { + const statusLabels = { + experimental: "[experimental]", + alpha: "[alpha]", + "private beta": "[private beta]", + "open beta": "[open beta]", + }; + const statusLabel = + statusLabels[def.metadata.status as keyof typeof statusLabels]; + if (statusLabel) { + description = `${description} ${statusLabel}`; + } + } + + if (!def.metadata?.hidden) { + const cmd = t.command(commandName, description); + + if (def.type === "command" && "args" in def) { + const args = def.args || {}; + for (const [argName, argDef] of Object.entries(args)) { + if (argDef.hidden) { + continue; + } + + const argDescription = argDef.describe || ""; + + if (argDef.choices && Array.isArray(argDef.choices)) { + cmd.option(argName, argDescription, (complete) => { + for (const choice of argDef.choices as string[]) { + complete(choice, choice); + } + }); + } else { + cmd.option(argName, argDescription); + } + + if (argDef.alias) { + const aliases = Array.isArray(argDef.alias) + ? argDef.alias + : [argDef.alias]; + for (const alias of aliases) { + cmd.option(alias, `Alias for --${argName}`); + } + } + } + } + } + } + + if (childNode.subtree.size > 0) { + addCommandsFromTree(childNode, commandPath); + } + } + } + + addCommandsFromTree(registry); +} + +export const completionsCommand = createCommand({ + metadata: { + description: "⌨️ Generate and handle shell completions", + owner: "Workers: Authoring and Testing", + status: "stable", + examples: [ + { + description: "Generate bash completion script", + command: "wrangler complete bash", + }, + { + description: "Generate fish completion script", + command: "wrangler complete fish", + }, + { + description: "Generate powershell completion script", + command: "wrangler complete powershell", + }, + { + description: "Generate zsh completion script", + command: "wrangler complete zsh", + }, + ], + }, + behaviour: { + printBanner: false, + provideConfig: false, + }, + positionalArgs: ["shell"], + args: { + shell: { + choices: ["bash", "fish", "powershell", "zsh"], + describe: "Shell type to generate completions for", + type: "string", + }, + }, + handler(args) { + // When shells request completions, they call: wrangler complete -- + // Yargs puts everything after -- into the _ array + const rawArgs = (args as unknown as { _: string[] })._ ?? []; + + const completionArgs = rawArgs.slice(rawArgs[0] === "complete" ? 1 : 0); + + if (completionArgs.length > 0) { + setupCompletions(); + t.parse(completionArgs); + return; + } + + if (!args.shell) { + throw new CommandLineArgsError("Missing required argument: shell"); + } + + setupCompletions(); + t.setup("wrangler", "wrangler", args.shell); + }, +}); diff --git a/packages/wrangler/src/docs/index.ts b/packages/wrangler/src/docs/index.ts index 99334d9d80a9..ba3ebf64aea6 100644 --- a/packages/wrangler/src/docs/index.ts +++ b/packages/wrangler/src/docs/index.ts @@ -6,7 +6,7 @@ import { runSearch } from "./helpers"; export const docs = createCommand({ metadata: { - description: "📚 Open Wrangler's command documentation in your browser\n", + description: "📚 Open Wrangler's command documentation in your browser", owner: "Workers: Authoring and Testing", status: "stable", }, diff --git a/packages/wrangler/src/index.ts b/packages/wrangler/src/index.ts index 78f36508bb37..12684a58e541 100644 --- a/packages/wrangler/src/index.ts +++ b/packages/wrangler/src/index.ts @@ -26,6 +26,7 @@ import { } from "./cert/cert"; import { checkNamespace, checkStartupCommand } from "./check/commands"; import { cloudchamber } from "./cloudchamber"; +import { completionsCommand } from "./complete"; import { getDefaultEnvFiles, loadDotEnv } from "./config/dot-env"; import { containers } from "./containers"; import { demandSingleValue } from "./core"; @@ -640,6 +641,15 @@ export function createCLIParser(argv: string[]) { ]); registry.registerNamespace("docs"); + // completions + registry.define([ + { + command: "wrangler complete", + definition: completionsCommand, + }, + ]); + registry.registerNamespace("complete"); + /******************** CMD GROUP ***********************/ registry.define([ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13e2394b7d7b..06f91e66f60f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3859,6 +3859,9 @@ importers: '@aws-sdk/client-s3': specifier: ^3.721.0 version: 3.721.0 + '@bomb.sh/tab': + specifier: ^0.0.11 + version: 0.0.11(cac@6.7.14)(citty@0.1.6) '@cloudflare/cli': specifier: workspace:* version: link:../cli @@ -4572,6 +4575,21 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bomb.sh/tab@0.0.11': + resolution: {integrity: sha512-RSqyreeicYBALcMaNxIUJTBknftXsyW45VRq5gKDNwKroh0Re5SDoWwXZaphb+OTEzVdpm/BA8Uq6y0P+AtVYw==} + hasBin: true + peerDependencies: + cac: ^6.7.14 + citty: ^0.1.6 + commander: ^13.1.0 + peerDependenciesMeta: + cac: + optional: true + citty: + optional: true + commander: + optional: true + '@bugsnag/browser@8.6.0': resolution: {integrity: sha512-7UGqTGnQqXUQ09gOlWbDTFUSbeLIIrP+hML3kTOq8Zdc8nP/iuOEflXGLV2TxWBWW8xIUPc928caFPr9EcaDuw==} @@ -14873,6 +14891,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bomb.sh/tab@0.0.11(cac@6.7.14)(citty@0.1.6)': + optionalDependencies: + cac: 6.7.14 + citty: 0.1.6 + '@bugsnag/browser@8.6.0': dependencies: '@bugsnag/core': 8.6.0 From ae2459c6ef0dc2d5419bc692dea4a936c1859c21 Mon Sep 17 00:00:00 2001 From: Dex Devlon <51504045+bxff@users.noreply.github.com> Date: Thu, 22 Jan 2026 02:03:54 +0530 Subject: [PATCH 2/2] fix(vite-plugin-cloudflare): prevent interactive shortcuts in Turbo environment (#11875) * fix(vite-plugin-cloudflare): prevent interactive shortcuts in Turbo environment * fix(vite-plugin-cloudflare): prevent interactive shortcuts in non-TTY environments * test(vite-plugin-cloudflare): fix interactive shortcuts tests * fix test * Improve changeset description --------- Co-authored-by: Edmund Hung --- .changeset/witty-taxes-itch.md | 7 + .../bindings/__tests__/shortcuts.spec.ts | 250 ++++++++++-------- .../src/plugins/shortcuts.ts | 5 + 3 files changed, 148 insertions(+), 114 deletions(-) create mode 100644 .changeset/witty-taxes-itch.md diff --git a/.changeset/witty-taxes-itch.md b/.changeset/witty-taxes-itch.md new file mode 100644 index 000000000000..eefad81df411 --- /dev/null +++ b/.changeset/witty-taxes-itch.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Skip shortcut registration in non-TTY environments + +Previously, registering keyboard shortcuts in non-TTY environments (e.g., Turborepo) caused Miniflare `ERR_DISPOSED` errors during prerendering. Shortcuts are now only registered when running in an interactive terminal. diff --git a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts index 7bd882f4332a..e7961fbcf395 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts @@ -29,6 +29,26 @@ describe.skipIf(!satisfiesViteVersion("7.2.7"))("shortcuts", () => { }); test("display binding shortcut hint", () => { + // Set up the shortcut wrapper (after stubs are in place from beforeAll) + const mockContext = new PluginContext({ + hasShownWorkerConfigWarnings: false, + isRestartingDevServer: false, + }); + mockContext.setResolvedPluginConfig( + resolvePluginConfig( + { + configPath: path.resolve(__dirname, "../wrangler.jsonc"), + }, + {}, + { + command: "serve", + mode: "development", + } + ) + ); + addBindingsShortcut(viteServer, mockContext); + + resetServerLogs(); viteServer.bindCLIShortcuts(); expect(normalize(serverLogs.info)).not.toMatch( @@ -43,125 +63,127 @@ describe.skipIf(!satisfiesViteVersion("7.2.7"))("shortcuts", () => { "press b + enter to list configured Cloudflare bindings" ); }); -}); + test("prints bindings with a single Worker", () => { + // Create a test server with a spy on bindCLIShortcuts + const mockBindCLIShortcuts = vi.spyOn(viteServer, "bindCLIShortcuts"); + // Create mock plugin context + const mockContext = new PluginContext({ + hasShownWorkerConfigWarnings: false, + isRestartingDevServer: false, + }); -test("prints bindings with a single Worker", () => { - // Create a test server with a spy on bindCLIShortcuts - const mockBindCLIShortcuts = vi.spyOn(viteServer, "bindCLIShortcuts"); - // Create mock plugin context - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - isRestartingDevServer: false, - }); + mockContext.setResolvedPluginConfig( + resolvePluginConfig( + { + configPath: path.resolve(__dirname, "../wrangler.jsonc"), + }, + {}, + { + command: "serve", + mode: "development", + } + ) + ); + + addBindingsShortcut(viteServer, mockContext); + // Confirm that addBindingsShortcut wrapped the original method + expect(viteServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); + expect(mockBindCLIShortcuts).toHaveBeenCalledExactlyOnceWith({ + customShortcuts: [ + { + key: "b", + description: "list configured Cloudflare bindings", + action: expect.any(Function), + }, + ], + }); - mockContext.setResolvedPluginConfig( - resolvePluginConfig( - { - configPath: path.resolve(__dirname, "../wrangler.jsonc"), - }, - {}, - { - command: "serve", - mode: "development", - } - ) - ); - - addBindingsShortcut(viteServer, mockContext); - // Confirm that addBindingsShortcut wrapped the original method - expect(viteServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); - expect(mockBindCLIShortcuts).toHaveBeenCalledExactlyOnceWith({ - customShortcuts: [ - { - key: "b", - description: "list configured Cloudflare bindings", - action: expect.any(Function), - }, - ], + const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; + const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); + + resetServerLogs(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-floating-promises + printBindingShortcut?.action?.(viteServer as any); + + expect(normalize(serverLogs.info)).toMatchInlineSnapshot(` + " + Your Worker has access to the following bindings: + Binding Resource + env.KV (test-kv-id) KV Namespace + env.HYPERDRIVE (test-hyperdrive-id) Hyperdrive Config + env.HELLO_WORLD (Timer disabled) Hello World + env.WAE (test) Analytics Engine Dataset + env.IMAGES Images + env.RATE_LIMITER (ratelimit) Unsafe Metadata + " + `); }); - const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; - const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); - - resetServerLogs(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-floating-promises - printBindingShortcut?.action?.(viteServer as any); - - expect(normalize(serverLogs.info)).toMatchInlineSnapshot(` - " - Your Worker has access to the following bindings: - Binding Resource - env.KV (test-kv-id) KV Namespace - env.HYPERDRIVE (test-hyperdrive-id) Hyperdrive Config - env.HELLO_WORLD (Timer disabled) Hello World - env.WAE (test) Analytics Engine Dataset - env.IMAGES Images - env.RATE_LIMITER (ratelimit) Unsafe Metadata - " - `); -}); + test("prints bindings with multi Workers", () => { + // Create a test server with a spy on bindCLIShortcuts + const mockBindCLIShortcuts = vi.spyOn(viteServer, "bindCLIShortcuts"); + // Create mock plugin context + const mockContext = new PluginContext({ + hasShownWorkerConfigWarnings: false, + isRestartingDevServer: false, + }); -test("prints bindings with multi Workers", () => { - // Create a test server with a spy on bindCLIShortcuts - const mockBindCLIShortcuts = vi.spyOn(viteServer, "bindCLIShortcuts"); - // Create mock plugin context - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - isRestartingDevServer: false, - }); + mockContext.setResolvedPluginConfig( + resolvePluginConfig( + { + configPath: path.resolve(__dirname, "../wrangler.jsonc"), + auxiliaryWorkers: [ + { + configPath: path.resolve( + __dirname, + "../wrangler.auxiliary.jsonc" + ), + }, + ], + }, + {}, + { + command: "serve", + mode: "development", + } + ) + ); - mockContext.setResolvedPluginConfig( - resolvePluginConfig( - { - configPath: path.resolve(__dirname, "../wrangler.jsonc"), - auxiliaryWorkers: [ - { - configPath: path.resolve(__dirname, "../wrangler.auxiliary.jsonc"), - }, - ], - }, - {}, - { - command: "serve", - mode: "development", - } - ) - ); - - addBindingsShortcut(viteServer, mockContext); - // Confirm that addBindingsShortcut wrapped the original method - expect(viteServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); - expect(mockBindCLIShortcuts).toHaveBeenCalledExactlyOnceWith({ - customShortcuts: [ - { - key: "b", - description: "list configured Cloudflare bindings", - action: expect.any(Function), - }, - ], - }); + addBindingsShortcut(viteServer, mockContext); + // Confirm that addBindingsShortcut wrapped the original method + expect(viteServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); + expect(mockBindCLIShortcuts).toHaveBeenCalledExactlyOnceWith({ + customShortcuts: [ + { + key: "b", + description: "list configured Cloudflare bindings", + action: expect.any(Function), + }, + ], + }); - const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; - const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); - - resetServerLogs(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-floating-promises - printBindingShortcut?.action?.(viteServer as any); - - expect(normalize(serverLogs.info)).toMatchInlineSnapshot(` - " - primary-worker has access to the following bindings: - Binding Resource - env.KV (test-kv-id) KV Namespace - env.HYPERDRIVE (test-hyperdrive-id) Hyperdrive Config - env.HELLO_WORLD (Timer disabled) Hello World - env.WAE (test) Analytics Engine Dataset - env.IMAGES Images - env.RATE_LIMITER (ratelimit) Unsafe Metadata - - auxiliary-worker has access to the following bindings: - Binding Resource - env.SERVICE (primary-worker) Worker - " - `); + const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; + const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); + + resetServerLogs(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-floating-promises + printBindingShortcut?.action?.(viteServer as any); + + expect(normalize(serverLogs.info)).toMatchInlineSnapshot(` + " + primary-worker has access to the following bindings: + Binding Resource + env.KV (test-kv-id) KV Namespace + env.HYPERDRIVE (test-hyperdrive-id) Hyperdrive Config + env.HELLO_WORLD (Timer disabled) Hello World + env.WAE (test) Analytics Engine Dataset + env.IMAGES Images + env.RATE_LIMITER (ratelimit) Unsafe Metadata + + auxiliary-worker has access to the following bindings: + Binding Resource + env.SERVICE (primary-worker) Worker + " + `); + }); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts index 1c2c762347f5..3a6bd864baa4 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts @@ -41,6 +41,11 @@ export function addBindingsShortcut( return; } + // Interactive shortcuts should only be registered in a TTY environment + if (!process.stdin.isTTY) { + return; + } + const registryPath = getDefaultDevRegistryPath(); const printBindingsShortcut = { key: "b",