From e9593699099caef60468baefca3dcfc98a1b0961 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 14 May 2026 20:49:06 +0100 Subject: [PATCH 1/2] feat(vite-plugin-cloudflare): add tunnel shortcut hint --- .changeset/sour-falcons-share.md | 7 ++++ .../src/__tests__/shortcuts.spec.ts | 37 ++++++++++++++++++- .../src/plugins/shortcuts.ts | 29 ++++++++++++++- .../src/plugins/tunnel.ts | 4 ++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .changeset/sour-falcons-share.md diff --git a/.changeset/sour-falcons-share.md b/.changeset/sour-falcons-share.md new file mode 100644 index 0000000000..e85d42a9ad --- /dev/null +++ b/.changeset/sour-falcons-share.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Add a tunnel shortcut hint when CLI shortcuts are printed + +The Cloudflare Vite plugin now includes a `t + enter` tunnel hint alongside the other CLI shortcuts it prints. diff --git a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts index a75d8799f0..6f71934a7d 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts @@ -325,9 +325,43 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { expect(extendExpirySpy).toHaveBeenCalledTimes(1); }); + test("display tunnel shortcut hint", ({ expect }) => { + const mockContext = new PluginContext({ + hasShownWorkerConfigWarnings: false, + restartingDevServerCount: 0, + tunnelHostnames: new Set(), + }); + vi.spyOn(tunnelPlugin, "isTunnelOpen") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + + addTunnelShortcut(mockServer, mockContext); + + serverLogs.info = []; + mockServer.bindCLIShortcuts(); + + expect(normalize(serverLogs.info)).not.toMatch( + "press t + enter to start tunnel" + ); + + mockServer.bindCLIShortcuts({ print: true }); + + expect(normalize(serverLogs.info)).toMatch( + "press t + enter to start tunnel" + ); + + serverLogs.info = []; + mockServer.bindCLIShortcuts({ print: true }); + + expect(normalize(serverLogs.info)).toMatch( + "press t + enter to close tunnel" + ); + }); + test("registers tunnel shortcuts even without tunnel config", ({ expect, }) => { + const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); const mockContext = new PluginContext({ hasShownWorkerConfigWarnings: false, restartingDevServerCount: 0, @@ -336,7 +370,8 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { addTunnelShortcut(mockServer, mockContext); - expect(mockServer.bindCLIShortcuts).toHaveBeenCalledWith({ + expect(mockServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); + expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ customShortcuts: [ { key: "t", diff --git a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts index 85a1a554f3..2f6d189dce 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts @@ -8,7 +8,7 @@ import colors from "picocolors"; import * as wrangler from "wrangler"; import { assertIsNotPreview, assertIsPreview } from "../context"; import { createPlugin, satisfiesMinimumViteVersion } from "../utils"; -import { extendTunnelExpiry, toggleTunnel } from "./tunnel"; +import { extendTunnelExpiry, isTunnelOpen, toggleTunnel } from "./tunnel"; import type { PluginContext } from "../context"; import type * as vite from "vite"; @@ -201,6 +201,33 @@ export function addTunnelShortcut( }, } satisfies vite.CLIShortcut; + const bindCLIShortcuts = server.bindCLIShortcuts.bind(server); + server.bindCLIShortcuts = ( + options?: vite.BindCLIShortcutsOptions< + vite.ViteDevServer | vite.PreviewServer + > + ) => { + if ( + server.httpServer && + process.stdin.isTTY && + !process.env.CI && + options?.print + ) { + const tunnelHintDescription = isTunnelOpen() + ? "close tunnel" + : "start tunnel"; + + server.config.logger.info( + colors.dim(colors.green(" ➜")) + + colors.dim(" press ") + + colors.bold(`${toggleTunnelShortcut.key} + enter`) + + colors.dim(` to ${tunnelHintDescription}`) + ); + } + + bindCLIShortcuts(options); + }; + server.bindCLIShortcuts({ customShortcuts: [toggleTunnelShortcut, extendTunnelExpiryShortcut], }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/tunnel.ts b/packages/vite-plugin-cloudflare/src/plugins/tunnel.ts index 77285fb2e7..a9652f92fd 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/tunnel.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/tunnel.ts @@ -307,6 +307,10 @@ export function extendTunnelExpiry() { tunnelManager?.extendExpiry(); } +export function isTunnelOpen() { + return tunnelManager?.isOpen() ?? false; +} + export async function toggleTunnel( server: vite.ViteDevServer | vite.PreviewServer, ctx: PluginContext From 23a9b7ddb31bcf4e1f738eb8e31aea7acd437d74 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 14 May 2026 21:08:03 +0100 Subject: [PATCH 2/2] refactor: shortcut plugin --- .../src/__tests__/shortcuts.spec.ts | 180 ++++++------------ .../src/plugins/shortcuts.ts | 136 ++++--------- 2 files changed, 87 insertions(+), 229 deletions(-) diff --git a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts index 6f71934a7d..33aec9f1ea 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts @@ -6,11 +6,7 @@ import { removeDirSync } from "@cloudflare/workers-utils"; import { afterAll, beforeAll, beforeEach, describe, test, vi } from "vitest"; import { PluginContext } from "../context"; import { resolvePluginConfig } from "../plugin-config"; -import { - addBindingsShortcut, - addExplorerShortcut, - addTunnelShortcut, -} from "../plugins/shortcuts"; +import { addShortcuts } from "../plugins/shortcuts"; import * as tunnelPlugin from "../plugins/tunnel"; import { satisfiesMinimumViteVersion } from "../utils"; import type * as vite from "vite"; @@ -124,62 +120,84 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { return () => removeDirSync(tempDir); }); - test("display binding shortcut hint", ({ expect }) => { + function createMockContext(options?: { + auxiliaryWorkers?: Array<{ configPath: string }>; + }) { const mockContext = new PluginContext({ hasShownWorkerConfigWarnings: false, restartingDevServerCount: 0, tunnelHostnames: new Set(), }); + mockContext.setResolvedPluginConfig( resolvePluginConfig( - { configPath: primaryConfigPath }, + { + configPath: primaryConfigPath, + auxiliaryWorkers: options?.auxiliaryWorkers, + }, {}, { command: "serve", mode: "development" } ) ); - addBindingsShortcut(mockServer, mockContext); + + return mockContext; + } + + test("prints shortcut hints in registration order", ({ expect }) => { + vi.spyOn(tunnelPlugin, "isTunnelOpen").mockReturnValue(false); + addShortcuts(mockServer, createMockContext()); serverLogs.info = []; mockServer.bindCLIShortcuts(); - expect(normalize(serverLogs.info)).not.toMatch( - "press b + enter to list configured Cloudflare bindings" - ); + expect(normalize(serverLogs.info)).toBe(""); mockServer.bindCLIShortcuts({ print: true }); - expect(normalize(serverLogs.info)).toMatch( - "press b + enter to list configured Cloudflare bindings" + expect(normalize(serverLogs.info)).toBe( + [ + "➜ press b + enter to list configured Cloudflare bindings", + "➜ press e + enter to open local explorer", + "➜ press t + enter to start tunnel", + ].join("\n") ); }); - test("prints bindings with a single Worker", ({ expect }) => { + test("registers custom shortcuts in order", ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); - mockContext.setResolvedPluginConfig( - resolvePluginConfig( - { configPath: primaryConfigPath }, - {}, - { command: "serve", mode: "development" } - ) - ); + addShortcuts(mockServer, createMockContext()); - addBindingsShortcut(mockServer, mockContext); expect(mockServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); - expect(mockBindCLIShortcuts).toHaveBeenCalledExactlyOnceWith({ + expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ customShortcuts: [ { key: "b", description: "list configured Cloudflare bindings", action: expect.any(Function), }, + { + key: "e", + description: "open local explorer", + action: expect.any(Function), + }, + { + key: "t", + description: "start or close tunnel", + action: expect.any(Function), + }, + { + key: "a", + description: "extend tunnel by 1 hour", + action: expect.any(Function), + }, ], }); + }); + + test("prints bindings with a single Worker", ({ expect }) => { + const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); + addShortcuts(mockServer, createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const printBindingShortcut = customShortcuts?.find((s) => s.key === "b"); @@ -204,35 +222,13 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { test("prints bindings with multi Workers", ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); - - mockContext.setResolvedPluginConfig( - resolvePluginConfig( - { - configPath: primaryConfigPath, - auxiliaryWorkers: [{ configPath: auxiliaryConfigPath }], - }, - {}, - { command: "serve", mode: "development" } - ) + addShortcuts( + mockServer, + createMockContext({ + auxiliaryWorkers: [{ configPath: auxiliaryConfigPath }], + }) ); - addBindingsShortcut(mockServer, mockContext); - expect(mockServer.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"); @@ -260,18 +256,7 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { test("registers explorer shortcut with correct URL", async ({ expect }) => { const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - - addExplorerShortcut(mockServer); - - expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ - customShortcuts: [ - { - key: "e", - description: "open local explorer", - action: expect.any(Function), - }, - ], - }); + addShortcuts(mockServer, createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const explorerShortcut = customShortcuts?.find((s) => s.key === "e"); @@ -284,35 +269,14 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { }); test("registers tunnel shortcut and extends expiry", async ({ expect }) => { - const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); const toggleTunnelSpy = vi .spyOn(tunnelPlugin, "toggleTunnel") .mockResolvedValue(undefined); const extendExpirySpy = vi .spyOn(tunnelPlugin, "extendTunnelExpiry") .mockImplementation(() => {}); - - addTunnelShortcut(mockServer, mockContext); - - expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ - customShortcuts: [ - { - key: "t", - description: "start or close tunnel", - action: expect.any(Function), - }, - { - key: "a", - description: "extend tunnel by 1 hour", - action: expect.any(Function), - }, - ], - }); + const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); + addShortcuts(mockServer, createMockContext()); const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; const toggleShortcut = customShortcuts?.find((s) => s.key === "t"); @@ -326,16 +290,11 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { }); test("display tunnel shortcut hint", ({ expect }) => { - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); vi.spyOn(tunnelPlugin, "isTunnelOpen") .mockReturnValueOnce(false) .mockReturnValueOnce(true); - addTunnelShortcut(mockServer, mockContext); + addShortcuts(mockServer, createMockContext()); serverLogs.info = []; mockServer.bindCLIShortcuts(); @@ -357,33 +316,4 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { "press t + enter to close tunnel" ); }); - - test("registers tunnel shortcuts even without tunnel config", ({ - expect, - }) => { - const mockBindCLIShortcuts = vi.spyOn(mockServer, "bindCLIShortcuts"); - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); - - addTunnelShortcut(mockServer, mockContext); - - expect(mockServer.bindCLIShortcuts).not.toBe(mockBindCLIShortcuts); - expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ - customShortcuts: [ - { - key: "t", - description: "start or close tunnel", - action: expect.any(Function), - }, - { - key: "a", - description: "extend tunnel by 1 hour", - action: expect.any(Function), - }, - ], - }); - }); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts index 2f6d189dce..f8a3551e3b 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts @@ -24,9 +24,7 @@ export const shortcutsPlugin = createPlugin("shortcuts", (ctx) => { } assertIsNotPreview(ctx); - addBindingsShortcut(viteDevServer, ctx); - addExplorerShortcut(viteDevServer); - addTunnelShortcut(viteDevServer, ctx); + addShortcuts(viteDevServer, ctx); }, async configurePreviewServer(vitePreviewServer) { if (!isCustomShortcutsSupported) { @@ -34,24 +32,15 @@ export const shortcutsPlugin = createPlugin("shortcuts", (ctx) => { } assertIsPreview(ctx); - addBindingsShortcut(vitePreviewServer, ctx); - addExplorerShortcut(vitePreviewServer); - addTunnelShortcut(vitePreviewServer, ctx); + addShortcuts(vitePreviewServer, ctx); }, }; }); -export function addBindingsShortcut( +export function addShortcuts( server: vite.ViteDevServer | vite.PreviewServer, ctx: PluginContext ) { - const workerConfigs = ctx.allWorkerConfigs; - - if (workerConfigs.length === 0) { - return; - } - - // Interactive shortcuts should only be registered in a TTY environment if (!process.stdin.isTTY) { return; } @@ -63,6 +52,8 @@ export function addBindingsShortcut( action: (viteServer) => { viteServer.config.logger.info(""); + const workerConfigs = ctx.allWorkerConfigs; + for (const workerConfig of workerConfigs) { const bindings = wrangler.unstable_convertConfigBindingsToStartWorkerBindings( @@ -84,46 +75,7 @@ export function addBindingsShortcut( ); } }, - } satisfies vite.CLIShortcut; - - // Update the bindCLIShortcuts method to print our shortcut hint first - const bindCLIShortcuts = server.bindCLIShortcuts.bind(server); - server.bindCLIShortcuts = ( - options?: vite.BindCLIShortcutsOptions< - vite.ViteDevServer | vite.PreviewServer - > - ) => { - if ( - // Vite will not print shortcuts if not in a TTY or in CI - // @see https://github.com/vitejs/vite/blob/fa3753a0f3a6c12659d8a68eefbd055c5ab90552/packages/vite/src/node/shortcuts.ts#L28-L35 - server.httpServer && - process.stdin.isTTY && - !process.env.CI && - options?.print - ) { - server.config.logger.info( - colors.dim(colors.green(" ➜")) + - colors.dim(" press ") + - colors.bold(`${printBindingsShortcut.key} + enter`) + - colors.dim(` to ${printBindingsShortcut.description}`) - ); - } - - bindCLIShortcuts(options); - }; - - // Add the custom binding shortcut - server.bindCLIShortcuts({ - customShortcuts: [printBindingsShortcut], - }); -} - -export function addExplorerShortcut( - server: vite.ViteDevServer | vite.PreviewServer -) { - if (!process.stdin.isTTY) { - return; - } + } satisfies vite.CLIShortcut; const openExplorerShortcut = { key: "e", @@ -144,44 +96,7 @@ export function addExplorerShortcut( ); }); }, - } satisfies vite.CLIShortcut; - - // Wrap bindCLIShortcuts to print our shortcut hint - const bindCLIShortcuts = server.bindCLIShortcuts.bind(server); - server.bindCLIShortcuts = ( - options?: vite.BindCLIShortcutsOptions< - vite.ViteDevServer | vite.PreviewServer - > - ) => { - if ( - server.httpServer && - process.stdin.isTTY && - !process.env.CI && - options?.print - ) { - server.config.logger.info( - colors.dim(colors.green(" ➜")) + - colors.dim(" press ") + - colors.bold(`${openExplorerShortcut.key} + enter`) + - colors.dim(` to ${openExplorerShortcut.description}`) - ); - } - - bindCLIShortcuts(options); - }; - - server.bindCLIShortcuts({ - customShortcuts: [openExplorerShortcut], - }); -} - -export function addTunnelShortcut( - server: vite.ViteDevServer | vite.PreviewServer, - ctx: PluginContext -) { - if (!process.stdin.isTTY) { - return; - } + } satisfies vite.CLIShortcut; const toggleTunnelShortcut = { key: "t", @@ -192,36 +107,44 @@ export function addTunnelShortcut( server.config.logger.error(colors.red(`Error: ${message}`)); }); }, - } satisfies vite.CLIShortcut; + } satisfies vite.CLIShortcut; const extendTunnelExpiryShortcut = { key: "a", description: "extend tunnel by 1 hour", action: () => { extendTunnelExpiry(); }, - } satisfies vite.CLIShortcut; + } satisfies vite.CLIShortcut; const bindCLIShortcuts = server.bindCLIShortcuts.bind(server); - server.bindCLIShortcuts = ( - options?: vite.BindCLIShortcutsOptions< - vite.ViteDevServer | vite.PreviewServer - > - ) => { + server.bindCLIShortcuts = (options?: vite.BindCLIShortcutsOptions) => { if ( server.httpServer && process.stdin.isTTY && !process.env.CI && options?.print ) { - const tunnelHintDescription = isTunnelOpen() - ? "close tunnel" - : "start tunnel"; + if (ctx.allWorkerConfigs.length > 0) { + server.config.logger.info( + colors.dim(colors.green(" ➜")) + + colors.dim(" press ") + + colors.bold(`${printBindingsShortcut.key} + enter`) + + colors.dim(` to ${printBindingsShortcut.description}`) + ); + } + + server.config.logger.info( + colors.dim(colors.green(" ➜")) + + colors.dim(" press ") + + colors.bold(`${openExplorerShortcut.key} + enter`) + + colors.dim(` to ${openExplorerShortcut.description}`) + ); server.config.logger.info( colors.dim(colors.green(" ➜")) + colors.dim(" press ") + colors.bold(`${toggleTunnelShortcut.key} + enter`) + - colors.dim(` to ${tunnelHintDescription}`) + colors.dim(` to ${isTunnelOpen() ? "close tunnel" : "start tunnel"}`) ); } @@ -229,6 +152,11 @@ export function addTunnelShortcut( }; server.bindCLIShortcuts({ - customShortcuts: [toggleTunnelShortcut, extendTunnelExpiryShortcut], + customShortcuts: [ + printBindingsShortcut, + openExplorerShortcut, + toggleTunnelShortcut, + extendTunnelExpiryShortcut, + ], }); }