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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/rude-cows-cheat.md
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions .changeset/witty-taxes-itch.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
"
`);
});
});
5 changes: 5 additions & 0 deletions packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
Loading
Loading