From c2e5e432ec08672faf72b338b960ba36305bbed1 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Tue, 31 Mar 2026 19:50:26 +1100 Subject: [PATCH 1/3] add local explorer hotkey for vite --- .changeset/add-explorer-shortcut-vite.md | 8 +++ packages/create-cloudflare/package.json | 2 +- packages/vite-plugin-cloudflare/package.json | 1 + .../bindings/__tests__/shortcuts.spec.ts | 34 +++++++++- .../src/plugins/shortcuts.ts | 66 ++++++++++++++++++- packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 47 ++++--------- pnpm-workspace.yaml | 1 + 8 files changed, 121 insertions(+), 40 deletions(-) create mode 100644 .changeset/add-explorer-shortcut-vite.md diff --git a/.changeset/add-explorer-shortcut-vite.md b/.changeset/add-explorer-shortcut-vite.md new file mode 100644 index 0000000000..fb1041ccb5 --- /dev/null +++ b/.changeset/add-explorer-shortcut-vite.md @@ -0,0 +1,8 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Add `e` hotkey to open local explorer during dev + +Press `e` during `vite dev` to open the local explorer UI at `/cdn-cgi/explorer`, which allows you to inspect the state of your D1, R2, KV, DO and Workflow bindings. + diff --git a/packages/create-cloudflare/package.json b/packages/create-cloudflare/package.json index bc78d19c68..14a5957aef 100644 --- a/packages/create-cloudflare/package.json +++ b/packages/create-cloudflare/package.json @@ -72,7 +72,7 @@ "indent-string": "^5.0.0", "jsonc-parser": "catalog:default", "magic-string": "^0.30.5", - "open": "^8.4.0", + "open": "catalog:default", "recast": "^0.23.11", "semver": "^7.7.1", "smol-toml": "catalog:default", diff --git a/packages/vite-plugin-cloudflare/package.json b/packages/vite-plugin-cloudflare/package.json index ccd07e7098..65203c2f88 100644 --- a/packages/vite-plugin-cloudflare/package.json +++ b/packages/vite-plugin-cloudflare/package.json @@ -66,6 +66,7 @@ "get-port": "^7.1.0", "magic-string": "^0.30.12", "mlly": "^1.7.4", + "open": "catalog:default", "picocolors": "^1.1.1", "semver": "^7.7.1", "tinyglobby": "^0.2.12", 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 06e8992e22..437aa0caaf 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/shortcuts.spec.ts @@ -3,7 +3,10 @@ import { stripVTControlCharacters } from "node:util"; import { afterAll, beforeAll, describe, test, vi } from "vitest"; import { PluginContext } from "../../../src/context"; import { resolvePluginConfig } from "../../../src/plugin-config"; -import { addBindingsShortcut } from "../../../src/plugins/shortcuts"; +import { + addBindingsShortcut, + addExplorerShortcut, +} from "../../../src/plugins/shortcuts"; import { resetServerLogs, satisfiesViteVersion, @@ -186,4 +189,33 @@ describe.skipIf(!satisfiesViteVersion("7.2.7"))("shortcuts", () => { " `); }); + + test("registers explorer shortcut with correct URL", async ({ expect }) => { + const mockOpen = vi.hoisted(() => vi.fn(() => ({ on: vi.fn() }))); + vi.mock("open", () => ({ default: mockOpen })); + + const mockBindCLIShortcuts = vi.spyOn(viteServer, "bindCLIShortcuts"); + + addExplorerShortcut(viteServer); + + expect(mockBindCLIShortcuts).toHaveBeenCalledWith({ + customShortcuts: [ + { + key: "e", + description: "open local explorer", + action: expect.any(Function), + }, + ], + }); + + const { customShortcuts } = mockBindCLIShortcuts.mock.calls[0]?.[0] ?? {}; + const explorerShortcut = customShortcuts?.find((s) => s.key === "e"); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await explorerShortcut?.action?.(viteServer as any); + + expect(mockOpen).toHaveBeenCalledWith( + expect.stringMatching(/^http:\/\/localhost:\d+\/cdn-cgi\/explorer$/) + ); + }); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts index 578edc3e46..af1f17d633 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/shortcuts.ts @@ -1,4 +1,9 @@ -import { getDefaultDevRegistryPath, getWorkerRegistry } from "miniflare"; +import { + CorePaths, + getDefaultDevRegistryPath, + getWorkerRegistry, +} from "miniflare"; +import open from "open"; import colors from "picocolors"; import * as wrangler from "wrangler"; import { assertIsNotPreview, assertIsPreview } from "../context"; @@ -19,6 +24,7 @@ export const shortcutsPlugin = createPlugin("shortcuts", (ctx) => { assertIsNotPreview(ctx); addBindingsShortcut(viteDevServer, ctx); + addExplorerShortcut(viteDevServer); }, async configurePreviewServer(vitePreviewServer) { if (!isCustomShortcutsSupported) { @@ -27,6 +33,7 @@ export const shortcutsPlugin = createPlugin("shortcuts", (ctx) => { assertIsPreview(ctx); addBindingsShortcut(vitePreviewServer, ctx); + addExplorerShortcut(vitePreviewServer); }, }; }); @@ -107,3 +114,60 @@ export function addBindingsShortcut( customShortcuts: [printBindingsShortcut], }); } + +export function addExplorerShortcut( + server: vite.ViteDevServer | vite.PreviewServer +) { + if (!process.stdin.isTTY) { + return; + } + + const openExplorerShortcut = { + key: "e", + description: "open local explorer", + action: async (viteServer) => { + const url = viteServer.resolvedUrls?.local[0]; + if (!url) { + viteServer.config.logger.warn("No local URL available"); + return; + } + + const explorerUrl = new URL(CorePaths.EXPLORER, url).href; + const childProcess = await open(explorerUrl); + childProcess.on("error", () => { + viteServer.config.logger.warn( + "Failed to open browser, the local explorer can be accessed at " + + explorerUrl + ); + }); + }, + } 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], + }); +} diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 94fedd3d0b..3c6e63d812 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -140,7 +140,7 @@ "mock-socket": "^9.3.1", "msw": "catalog:default", "node-forge": "^1.3.2", - "open": "^8.4.0", + "open": "catalog:default", "p-queue": "^9.0.0", "patch-console": "^1.0.0", "pretty-bytes": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4df39526e8..ec33f7b3d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,6 +42,9 @@ catalogs: msw: specifier: 2.12.4 version: 2.12.4 + open: + specifier: ^11.0.0 + version: 11.0.0 playwright-chromium: specifier: ^1.56.1 version: 1.56.1 @@ -1783,8 +1786,8 @@ importers: specifier: ^0.30.5 version: 0.30.14 open: - specifier: ^8.4.0 - version: 8.4.0 + specifier: catalog:default + version: 11.0.0 recast: specifier: ^0.23.11 version: 0.23.11 @@ -2455,6 +2458,9 @@ importers: mlly: specifier: ^1.7.4 version: 1.7.4 + open: + specifier: catalog:default + version: 11.0.0 picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -4163,8 +4169,8 @@ importers: specifier: ^1.3.2 version: 1.3.3 open: - specifier: ^8.4.0 - version: 8.4.0 + specifier: catalog:default + version: 11.0.0 p-queue: specifier: ^9.0.0 version: 9.0.0 @@ -9958,10 +9964,6 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} @@ -11152,11 +11154,6 @@ packages: is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -11312,10 +11309,6 @@ packages: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -12233,10 +12226,6 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} - open@8.4.0: - resolution: {integrity: sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==} - engines: {node: '>=12'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -14539,7 +14528,7 @@ packages: '@vitest/ui': 4.1.0 happy-dom: '*' jsdom: '*' - vite: 7.1.12 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -20755,8 +20744,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} define-properties@1.2.1: @@ -22123,8 +22110,6 @@ snapshots: is-deflate@1.0.0: {} - is-docker@2.2.1: {} - is-docker@3.0.0: {} is-even@1.0.0: @@ -22251,10 +22236,6 @@ snapshots: is-windows@1.0.2: {} - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -23091,12 +23072,6 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - open@8.4.0: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e96f290dbe..6dea50cded 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -41,6 +41,7 @@ catalog: "capnp-es": "^0.0.14" "capnweb": "^0.5.0" "ci-info": "^4.4.0" + "open": "^11.0.0" # CAUTION: Most usage of @cloudflare/vitest-pool-workers in this monorepo should use workspace:* instead of this catalog version # However, some packages (pages-shared, workers-shared, etc...) need to be tested using vitest-pool-workers but are themselves # ultimately included in vitest-pool-workers (through Wrangler), causing a circular dependency. From ff6fd17664568c732f0912fe298f17875d63a96e Mon Sep 17 00:00:00 2001 From: emily-shen Date: Tue, 31 Mar 2026 21:03:14 +1100 Subject: [PATCH 2/3] wrangler fixups for open v11 --- packages/wrangler/src/dev/inspect.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/wrangler/src/dev/inspect.ts b/packages/wrangler/src/dev/inspect.ts index 542d54264e..c8efc68274 100644 --- a/packages/wrangler/src/dev/inspect.ts +++ b/packages/wrangler/src/dev/inspect.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath, URL } from "node:url"; -import open from "open"; +import open, { apps } from "open"; import { isAllowedSourceMapPath, isAllowedSourcePath, @@ -276,16 +276,16 @@ export const openInspector = async ( const childProcess = await open(url, { app: [ { - name: open.apps.chrome, + name: apps.chrome, }, { name: braveBrowser, }, { - name: open.apps.edge, + name: apps.edge, }, { - name: open.apps.firefox, + name: apps.firefox, }, ], }); From 0e4fb82ec4161557d3fbc8b22d9f63d748408de3 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Thu, 2 Apr 2026 17:56:50 +1100 Subject: [PATCH 3/3] Unhide wrangler hotkey --- .changeset/silly-pears-kick.md | 7 +++++++ fixtures/interactive-dev-tests/tests/index.test.ts | 6 +----- .../test/plugins/local-explorer/index.spec.ts | 12 ++++++++++++ packages/wrangler/src/dev/hotkeys.ts | 3 +-- 4 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 .changeset/silly-pears-kick.md diff --git a/.changeset/silly-pears-kick.md b/.changeset/silly-pears-kick.md new file mode 100644 index 0000000000..157a27e387 --- /dev/null +++ b/.changeset/silly-pears-kick.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +explorer: expose the local explorer hotkey + +List the local explorer's hotkey `[e]` in wrangler dev output. \ No newline at end of file diff --git a/fixtures/interactive-dev-tests/tests/index.test.ts b/fixtures/interactive-dev-tests/tests/index.test.ts index 746a296f07..caa3f49e73 100644 --- a/fixtures/interactive-dev-tests/tests/index.test.ts +++ b/fixtures/interactive-dev-tests/tests/index.test.ts @@ -266,6 +266,7 @@ if (process.platform === "win32") { expect(wrangler.stdout).toContain("open devtools"); expect(wrangler.stdout).toContain("clear console"); expect(wrangler.stdout).toContain("to exit"); + expect(wrangler.stdout).toContain("open local explorer"); expect(wrangler.stdout).not.toContain("rebuild container"); }); it("should not show hotkeys with --show-interactive-dev-session=false", async () => { @@ -279,11 +280,6 @@ if (process.platform === "win32") { expect(wrangler.stdout).not.toContain("clear console"); expect(wrangler.stdout).not.toContain("to exit"); expect(wrangler.stdout).not.toContain("rebuild container"); - }); - // TODO: update this when we release properly - it("should not show local explorer hotkey by default", async () => { - const wrangler = await startWranglerDev(args); - wrangler.pty.kill(); expect(wrangler.stdout).not.toContain("open local explorer"); }); }); diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 17c32335bd..526c574296 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -255,6 +255,18 @@ describe("Local Explorer API validation", () => { }); describe("routing", () => { + test("serves OpenAPI spec at /cdn-cgi/explorer/api", async ({ expect }) => { + const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer/api"); + expect(res.status).toBe(200); + expect(res.headers.get("Content-Type")).toContain("application/json"); + + const spec = await res.json(); + expect(spec).toMatchObject({ + openapi: "3.0.3", + info: { title: "Local Explorer API" }, + }); + }); + test("serves explorer UI at /cdn-cgi/explorer", async ({ expect }) => { const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer"); expect(res.status).toBe(200); diff --git a/packages/wrangler/src/dev/hotkeys.ts b/packages/wrangler/src/dev/hotkeys.ts index e627acae92..80d34b8b6d 100644 --- a/packages/wrangler/src/dev/hotkeys.ts +++ b/packages/wrangler/src/dev/hotkeys.ts @@ -48,8 +48,7 @@ export default function registerDevHotKeys( }, { keys: ["e"], - // This makes the label hidden but still enabled - // label: "open local explorer", + label: "open local explorer", handler: async () => { const { url } = await primaryDevEnv.proxy.ready.promise; const explorerUrl = new URL(CorePaths.EXPLORER, url);