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..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"); @@ -325,30 +289,31 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { expect(extendExpirySpy).toHaveBeenCalledTimes(1); }); - test("registers tunnel shortcuts even without tunnel config", ({ - expect, - }) => { - const mockContext = new PluginContext({ - hasShownWorkerConfigWarnings: false, - restartingDevServerCount: 0, - tunnelHostnames: new Set(), - }); + test("display tunnel shortcut hint", ({ expect }) => { + vi.spyOn(tunnelPlugin, "isTunnelOpen") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); - addTunnelShortcut(mockServer, mockContext); + addShortcuts(mockServer, createMockContext()); - expect(mockServer.bindCLIShortcuts).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), - }, - ], - }); + 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" + ); }); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts index 85a1a554f3..f8a3551e3b 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"; @@ -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,64 +96,67 @@ export function addExplorerShortcut( ); }); }, - } satisfies vite.CLIShortcut; + } satisfies vite.CLIShortcut; + + const toggleTunnelShortcut = { + key: "t", + description: "start or close tunnel", + action: () => { + void toggleTunnel(server, ctx).catch((error) => { + const message = error instanceof Error ? error.message : String(error); + server.config.logger.error(colors.red(`Error: ${message}`)); + }); + }, + } satisfies vite.CLIShortcut; + const extendTunnelExpiryShortcut = { + key: "a", + description: "extend tunnel by 1 hour", + action: () => { + extendTunnelExpiry(); + }, + } 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 - > - ) => { + server.bindCLIShortcuts = (options?: vite.BindCLIShortcutsOptions) => { if ( server.httpServer && process.stdin.isTTY && !process.env.CI && options?.print ) { + 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 ${isTunnelOpen() ? "close tunnel" : "start tunnel"}`) + ); } bindCLIShortcuts(options); }; server.bindCLIShortcuts({ - customShortcuts: [openExplorerShortcut], - }); -} - -export function addTunnelShortcut( - server: vite.ViteDevServer | vite.PreviewServer, - ctx: PluginContext -) { - if (!process.stdin.isTTY) { - return; - } - - const toggleTunnelShortcut = { - key: "t", - description: "start or close tunnel", - action: () => { - void toggleTunnel(server, ctx).catch((error) => { - const message = error instanceof Error ? error.message : String(error); - server.config.logger.error(colors.red(`Error: ${message}`)); - }); - }, - } satisfies vite.CLIShortcut; - const extendTunnelExpiryShortcut = { - key: "a", - description: "extend tunnel by 1 hour", - action: () => { - extendTunnelExpiry(); - }, - } satisfies vite.CLIShortcut; - - server.bindCLIShortcuts({ - customShortcuts: [toggleTunnelShortcut, extendTunnelExpiryShortcut], + customShortcuts: [ + printBindingsShortcut, + openExplorerShortcut, + 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