diff --git a/AGENTS.md b/AGENTS.md index 14412fc4..c3381ba8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,14 @@ # AGENTS GUIDE +## Positioning + +Two layers, one mental model: + +- **`devframe`** — *the container for one devtool integration, portable across viewers.* Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. +- **`@vitejs/devtools-kit`** — *the hub that unites many devtools integrations.* Owns docking, the command palette, toasts, terminal sessions — anything that only makes sense when more than one tool shares a UI. Provides `createPluginFromDevframe(devtoolApp)` so a portable devframe definition drops into Vite DevTools as a Vite plugin, with the dock entry auto-derived from the definition's metadata. + +When deciding where something belongs: if a single-app standalone CLI would still need it, it lives in devframe; if it only matters once you have multiple integrations or a host UI, it lives in the kit. + ## Stack & Structure Monorepo (`pnpm` workspaces + `turbo`). ESM TypeScript; bundled with `tsdown`. Path aliases in `alias.ts` (propagated to `tsconfig.base.json` — do not edit manually). @@ -8,14 +17,14 @@ Monorepo (`pnpm` workspaces + `turbo`). ESM TypeScript; bundled with `tsdown`. P | Package | npm | Description | |---------|-----|-------------| -| `packages/devframe` | `devframe` | Framework-neutral foundation — RPC layer (birpc + valibot + WS presets), host classes, createHostContext, six adapters at `devframe/adapters/*` (cli/build/spa/vite/kit/embedded), connectDevtool client | -| `packages/core` | `@vitejs/devtools` | Vite plugin, CLI, standalone/webcomponents client. Wraps devframe's createHostContext with the Vite plugin scan | -| `packages/kit` | `@vitejs/devtools-kit` | Vite-specific superset of devframe — adds PluginWithDevTools, ViteDevToolsNodeContext, and re-exports devframe's public types | -| `packages/ui` | `@vitejs/devtools-ui` | Shared UI components, composables, and UnoCSS preset (`presetDevToolsUI`). Private, not published | -| `packages/rolldown` | `@vitejs/devtools-rolldown` | Nuxt UI for Rolldown build data. Serves at `/__devtools-rolldown/` | -| `packages/vite` | `@vitejs/devtools-vite` | Nuxt UI for Vite DevTools (WIP). Serves at `/__devtools-vite/` | -| `packages/self-inspect` | `@vitejs/devtools-self-inspect` | Meta-introspection — DevTools for the DevTools. Serves at `/__devtools-self-inspect/` | -| `packages/webext` | — | Browser extension scaffolding (ancillary) | +| `devframe/packages/devframe` | `devframe` | Framework-neutral container for one devtool integration. RPC layer (birpc + valibot + WS presets), `createHostContext`, six deployment adapters at `devframe/adapters/*` (cli/dev/build/vite/embedded/mcp), `connectDevtool` client. No docks, no terminals, no command palette — those are hub concerns. | +| `packages/kit` | `@vitejs/devtools-kit` | The hub. `createKitContext` wraps devframe's context with `docks` / `terminals` / `messages` / `commands` host subsystems plus the Vite-augmented context type. `createPluginFromDevframe` bridges a portable devframe app into a `Plugin.devtools.setup` Vite plugin, auto-deriving its iframe dock entry from the definition. | +| `packages/core` | `@vitejs/devtools` | Vite plugin + CLI + standalone/webcomponents client for Vite DevTools itself. Calls kit's `createKitContext`, scans Vite plugins for `.devtools.setup`, and serves the dock UI. | +| `packages/ui` | `@vitejs/devtools-ui` | Shared UI components, composables, and UnoCSS preset (`presetDevToolsUI`). Private, not published. | +| `packages/rolldown` | `@vitejs/devtools-rolldown` | Nuxt UI for Rolldown build data. Hub-mounted via `Plugin.devtools.setup`. Serves at `/__devtools-rolldown/`. | +| `packages/vite` | `@vitejs/devtools-vite` | Nuxt UI for Vite DevTools (WIP). Hub-mounted via `Plugin.devtools.setup`. Serves at `/__devtools-vite/`. | +| `packages/self-inspect` | `@vitejs/devtools-self-inspect` | Meta-introspection — DevTools for the DevTools. Hub-mounted via `Plugin.devtools.setup`. Serves at `/__devtools-self-inspect/`. | +| `packages/webext` | — | Browser extension scaffolding (ancillary). | Other top-level directories: - `docs/` — VitePress docs; guides in `docs/guide/` @@ -34,15 +43,19 @@ flowchart TD ## Dep Boundary -`packages/devframe` is the lowest-level package in this monorepo and is positioned to be extracted into its own repo. It MUST NOT import from `vite` or any `@vitejs/*` package — not as a `dependencies` entry, not as an inlined dep, not as a source import. `packages/kit` and above build on top of devframe, never the reverse. +`devframe/packages/devframe` is the lowest-level package in this monorepo and is positioned to be extracted into its own repo. It MUST NOT import from `vite`, `@vitejs/*`, or any hub-only concept (docks, terminals, messages, commands) — not as a `dependencies` entry, not as an inlined dep, not as a source import. `packages/kit` and above build on top of devframe; never the reverse. If a feature requires multi-integration awareness, it goes in kit. + +`devframe/internal` is a marked-internal subpath that exposes a small set of helpers (`getInternalContext`, `resolveBasePath`) for first-party adapters that need to reach into devframe's private machinery — kit's relocated `DocksHost` uses it for remote-dock token allocation. End users should not import it. ## Architecture -- **Entry**: `createDevToolsContext` (`packages/core/src/node/context.ts`) builds `DevToolsNodeContext` with hosts for RPC, docks, views, terminals. Invokes `plugin.devtools.setup` hooks. -- **Node context**: server-side (cwd, vite config, mode, hosts, auth storage at `node_modules/.vite/devtools/auth.json`). +- **Devframe context** (`devframe/packages/devframe/src/node/context.ts`): `createHostContext` returns a `DevToolsNodeContext` carrying `rpc`, `views` (HTTP file-serving via `hostStatic`), `diagnostics`, `agent`, plus `cwd`/`workspaceRoot`/`mode`/`host`/`createJsonRenderer`. No docks, no terminals. +- **Kit context** (`packages/kit/src/node/context.ts`): `createKitContext` wraps `createHostContext` and attaches the four hub hosts — `docks`, `terminals`, `messages`, `commands`. Optionally surfaces `viteConfig`/`viteServer` when mounted inside Vite DevTools. Wires the `'devframe:docks'` / `'devframe:commands'` shared-state sync. +- **Bridge** (`packages/kit/src/node/create-plugin-from-devframe.ts`): `createPluginFromDevframe(d, opts?)` returns `PluginWithDevTools`; in its `setup`, mounts the SPA via `views.hostStatic`, auto-registers an iframe dock entry from `id`/`name`/`icon`/`basePath`, runs `d.setup(ctx)` for the devframe-level wiring, then runs `opts.setup?.(ctx)` for kit-only extensions. +- **Vite DevTools entry** (`packages/core/src/node/context.ts`): `createDevToolsContext` calls `createKitContext`, registers Vite-specific commands (`vite:open-in-editor`, `vite:open-in-finder`), then scans Vite plugins for `.devtools.setup` hooks (which now receive the kit-augmented context). - **Client context**: webcomponents/Nuxt UI state (`packages/core/src/client/webcomponents/state/*`) — dock entries, panels, RPC client. Two modes: `embedded` (overlay in host app) and `standalone` (independent page). - **WS server** (`packages/core/src/node/ws.ts`): RPC via `devframe/rpc/transports/ws-server`. Auth skipped in build mode or when `devtools.clientAuth` is `false`. -- **Nuxt UI plugins** (rolldown, vite, self-inspect): each registers RPC functions and hosts static Nuxt SPA at its own base path. +- **Hub-mounted Nuxt UI plugins** (rolldown, vite, self-inspect): each implements `Plugin.devtools.setup`, receives a `KitNodeContext`, registers RPC functions, hosts a static Nuxt SPA, and registers its dock entry. ## Development @@ -69,14 +82,23 @@ pnpm -C docs run docs # docs dev server ### Devframe design principles -These apply to everything inside `packages/devframe` and to how host packages (`kit`, `core`, etc.) layer on top. When in doubt, err on the side of "devframe provides hooks, the app decides UX". +These apply to everything inside `devframe/packages/devframe` and reinforce its positioning as "the container for one devtool integration, portable to multiple viewers". When in doubt, err on the side of "devframe provides primitives, the hub provides UX". +- **Single-integration scope.** Devframe describes one tool. If a feature only makes sense when multiple tools share a UI — docking, a unified command palette, cross-tool toasts, terminal aggregation — it lives in `@vitejs/devtools-kit`, not here. - **Headless by default.** No default startup banners, no opinionated logging to stdout, no default styling. Provide hooks (`onReady`, `cli.configure`, etc.); let the application print its own branding. Structured diagnostics via `logs-sdk` are fine — ad-hoc `console.log`s baked into adapters are not. - **File watching is the app's job, not devframe's.** Don't add a generic watcher primitive. Authors wire chokidar / fs.watch / watchman themselves and signal change via `ctx.rpc.sharedState.set(...)` or event-type RPCs. devframe stays out of the filesystem-observation business. -- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `kit`, `embedded`) and `/` for *standalone* adapters (`cli`, `spa`, `build`). Authors override via `DevtoolDefinition.basePath`. Don't hardcode `DEVTOOLS_MOUNT_PATH` in adapter code paths that may run standalone. -- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim — no HTML rewriting, no build-time `--base` injection. The client (`connectDevtool`) resolves `__connection.json` relative to the runtime base automatically. +- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`, kit's `createPluginFromDevframe`) and `/` for *standalone* adapters (`cli`, `spa`, `build`). Authors override via `DevtoolDefinition.basePath`. Don't hardcode `DEVTOOLS_MOUNT_PATH` in adapter code paths that may run standalone. +- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim — no HTML rewriting, no build-time `--base` injection. The client (`connectDevtool`) resolves `.connection.json` relative to the runtime base automatically. - **CLI flags compose from both sides.** The `cac` instance backing `createCli` is exposed both to the `DevtoolDefinition` (`cli.configure(cli)`) — for capabilities contributed by the tool itself — and to the `createCli` caller — for flags added at the final assembly stage. Parsed flag values are forwarded to `setup(ctx, { flags })`. Never hardcode domain-specific flags into `createCli`. +### Kit design principles + +The kit is the integration hub. When adding to it, the question is "does this help unify multiple devtools?" — not "is this useful in general?". + +- **Hub-only features.** `docks`, `terminals`, `messages`, `commands`, the auto-derived dock entry in `createPluginFromDevframe`, the unified user-settings shared state — these only have meaning across integrations and stay kit-side. +- **Devframe definitions stay portable.** `createPluginFromDevframe(devtoolApp, opts?)` is the bridge. The devtool's own `setup(ctx)` should not assume kit context; if it needs hub features, contribute them via `opts.setup` or via a kit-only Vite plugin that augments the same context. +- **Auto-derive what you can, override via options.** `createPluginFromDevframe` synthesizes the iframe dock entry from `id`/`name`/`icon`/`basePath`. Callers customise via `opts.dock` (category, when-clause, custom icon override) or `opts.setup` (terminals, additional dock entries). Don't push these into the portable `DevtoolDefinition`. + ## Structured Diagnostics (Error Codes) All node-side warnings and errors use structured diagnostics via [`logs-sdk`](https://github.com/vercel-labs/logs-sdk). Never use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages in node-side code — always define a coded diagnostic. @@ -85,11 +107,13 @@ All node-side warnings and errors use structured diagnostics via [`logs-sdk`](ht | Prefix | Package(s) | Diagnostics file | |--------|-----------|-----------------| -| `DF` | `packages/devframe` | `packages/devframe/src/node/diagnostics.ts`, `packages/devframe/src/rpc/diagnostics.ts` | -| `DTK` | `packages/core` (Vite-specific remainder) | `packages/core/src/node/diagnostics.ts` | +| `DF` | `devframe/packages/devframe` | `devframe/packages/devframe/src/node/diagnostics.ts`, `devframe/packages/devframe/src/rpc/diagnostics.ts` | +| `DTK` | `packages/kit` + `packages/core` (shared codespace, hub-side) | `packages/kit/src/node/diagnostics.ts`, `packages/core/src/node/diagnostics.ts` | | `RDDT` | `packages/rolldown` | `packages/rolldown/src/node/diagnostics.ts` | | `VDT` | `packages/vite` (reserved) | — | +`DTK` is shared between core and kit because they're sibling layers of the Vite DevTools hub. Coordinate code numbers across both files: kit currently reserves `DTK0050+`; core's existing codes top out below that. + Codes are sequential 4-digit numbers per prefix (e.g. `DTK0033`, `RDDT0003`). Check the existing diagnostics file to find the next available number. ### Adding a new error diff --git a/alias.ts b/alias.ts index 504c9940..36461c33 100644 --- a/alias.ts +++ b/alias.ts @@ -15,6 +15,7 @@ export const alias = { 'devframe/types': df('devframe/src/types/index.ts'), 'devframe/node': df('devframe/src/node/index.ts'), 'devframe/constants': df('devframe/src/constants.ts'), + 'devframe/internal': df('devframe/src/internal/index.ts'), 'devframe/utils/events': df('devframe/src/utils/events.ts'), 'devframe/utils/human-id': df('devframe/src/utils/human-id.ts'), 'devframe/utils/nanoid': df('devframe/src/utils/nanoid.ts'), @@ -27,7 +28,6 @@ export const alias = { 'devframe/adapters/dev': df('devframe/src/adapters/dev.ts'), 'devframe/adapters/build': df('devframe/src/adapters/build.ts'), 'devframe/adapters/vite': df('devframe/src/adapters/vite.ts'), - 'devframe/adapters/kit': df('devframe/src/adapters/kit.ts'), 'devframe/adapters/embedded': df('devframe/src/adapters/embedded.ts'), 'devframe/adapters/mcp': df('devframe/src/adapters/mcp.ts'), '@devframes/nuxt/runtime/plugin.client': df('nuxt/src/runtime/plugin.client.ts'), diff --git a/devframe/docs/.vitepress/config.ts b/devframe/docs/.vitepress/config.ts index 5499cfcf..c4de4abf 100644 --- a/devframe/docs/.vitepress/config.ts +++ b/devframe/docs/.vitepress/config.ts @@ -20,12 +20,8 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] { { text: 'RPC', link: `${prefix}/guide/rpc` }, { text: 'Shared State', link: `${prefix}/guide/shared-state` }, { text: 'Streaming', link: `${prefix}/guide/streaming` }, - { text: 'Dock System', link: `${prefix}/guide/dock-system` }, - { text: 'Commands', link: `${prefix}/guide/commands` }, { text: 'When Clauses', link: `${prefix}/guide/when-clauses` }, - { text: 'Messages & Notifications', link: `${prefix}/guide/messages` }, { text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` }, - { text: 'Terminals', link: `${prefix}/guide/terminals` }, { text: 'Client', link: `${prefix}/guide/client` }, { text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` }, { text: 'Nuxt Helper', link: `${prefix}/guide/nuxt` }, diff --git a/devframe/docs/errors/DF0001.md b/devframe/docs/errors/DF0001.md deleted file mode 100644 index 6c58744f..00000000 --- a/devframe/docs/errors/DF0001.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0001: Dock Already Registered - -## Message - -> Dock with id "`{id}`" is already registered - -## Cause - -`DevToolsDockHost.register()` rejects duplicate ids by default. - -## Fix - -Pass `force: true` as the second argument to intentionally overwrite, or choose a unique dock id. - -## Source - -- [`packages/devframe/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-docks.ts) — `DevToolsDockHost.register()` rejects duplicate dock ids; throws `DF0001` when one already exists. diff --git a/devframe/docs/errors/DF0002.md b/devframe/docs/errors/DF0002.md deleted file mode 100644 index dc936eb2..00000000 --- a/devframe/docs/errors/DF0002.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0002: Cannot Change Dock ID - -## Message - -> Cannot change the id of a dock. Use register() to add new docks. - -## Cause - -`DevToolsDockHost.update()` rejects id changes because docks are keyed by id. - -## Fix - -Keep the `id` on `update(patch)` identical to the registered value, or call `register()` with a new id (and optionally `force: true`). - -## Source - -- [`packages/devframe/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-docks.ts) — `DevToolsDockHost.update()` rejects id changes because docks are keyed by id; throws `DF0002` when the patch's `id` differs from the registered value. diff --git a/devframe/docs/errors/DF0003.md b/devframe/docs/errors/DF0003.md deleted file mode 100644 index 8bab38f5..00000000 --- a/devframe/docs/errors/DF0003.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0003: Dock Not Registered - -## Message - -> Dock with id "`{id}`" is not registered. Use register() to add new docks. - -## Cause - -`DevToolsDockHost.update()` was called with an id that has no matching registration. - -## Fix - -Call `register()` first, or check the id against `dockHost.values()` before updating. - -## Source - -- [`packages/devframe/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-docks.ts) — `DevToolsDockHost.update()` throws `DF0003` when called with an id that has no matching registration. diff --git a/devframe/docs/errors/DF0004.md b/devframe/docs/errors/DF0004.md deleted file mode 100644 index b1edf39a..00000000 --- a/devframe/docs/errors/DF0004.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0004: Terminal Session Already Registered - -## Message - -> Terminal session with id "`{id}`" already registered - -## Cause - -`DevToolsTerminalHost.register()` already has a session under that id. - -## Fix - -Use a unique session id or call `unregister(id)` first. - -## Source - -- [`packages/devframe/src/node/host-terminals.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-terminals.ts) — `DevToolsTerminalHost.register()` (and the internal terminal-creation path) throw `DF0004` when a session id is already in use. diff --git a/devframe/docs/errors/DF0005.md b/devframe/docs/errors/DF0005.md deleted file mode 100644 index 5823c8d2..00000000 --- a/devframe/docs/errors/DF0005.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0005: Terminal Session Not Registered - -## Message - -> Terminal session with id "`{id}`" not registered - -## Cause - -A terminal operation (update, read, close) referenced an id that the host does not know about. - -## Fix - -Ensure the session was created via `register()` before calling update/read/close operations on it. - -## Source - -- [`packages/devframe/src/node/host-terminals.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-terminals.ts) — terminal update/read/close operations throw `DF0005` when the referenced session id is not registered. diff --git a/devframe/docs/errors/DF0009.md b/devframe/docs/errors/DF0009.md deleted file mode 100644 index e20423c5..00000000 --- a/devframe/docs/errors/DF0009.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0009: Command Already Registered - -## Message - -> Command "`{id}`" is already registered - -## Cause - -`DevToolsCommandsHost.register()` rejects duplicate command ids. - -## Fix - -Use a unique id or call `unregister(id)` first. - -## Source - -- [`packages/devframe/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` throws `DF0009` when a command id is already in use. diff --git a/devframe/docs/errors/DF0010.md b/devframe/docs/errors/DF0010.md deleted file mode 100644 index 4eccd55d..00000000 --- a/devframe/docs/errors/DF0010.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0010: Cannot Change Command ID - -## Message - -> Cannot change the id of a command. Use register() to add new commands. - -## Cause - -`DevToolsCommandsHost.update()` rejects id changes because commands are keyed by id. - -## Fix - -Keep the `id` identical on `update(patch)`, or call `register()` to create a new command. - -## Source - -- [`packages/devframe/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-commands.ts) — `DevToolsCommandsHost.update()` throws `DF0010` when the patch's `id` differs from the registered value. diff --git a/devframe/docs/errors/DF0011.md b/devframe/docs/errors/DF0011.md deleted file mode 100644 index 5361eca3..00000000 --- a/devframe/docs/errors/DF0011.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -outline: deep ---- - -# DF0011: Command Not Registered - -## Message - -> Command "`{id}`" is not registered - -## Cause - -`DevToolsCommandsHost.update()` or `.unregister()` was called with an unknown id. - -## Fix - -Register the command first, or check `commandsHost.list()` before operating on it. - -## Source - -- [`packages/devframe/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-commands.ts) — `DevToolsCommandsHost.update()` and `unregister()` both throw `DF0011` when called with an unknown command id. diff --git a/devframe/docs/errors/DF0018.md b/devframe/docs/errors/DF0018.md deleted file mode 100644 index bb572b49..00000000 --- a/devframe/docs/errors/DF0018.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -outline: deep ---- - -# DF0018: `ctx.logs` Deprecated - -## Message - -> `` `ctx.logs` is deprecated and will be removed in a future release. Use `ctx.messages` instead. `` - -## Cause - -The user-facing message subsystem has been renamed from `logs` to `messages` to disambiguate it from the structured diagnostics surface (`ctx.diagnostics`, powered by [`logs-sdk`](https://github.com/vercel-labs/logs-sdk)). - -`ctx.logs` continues to work as an alias of `ctx.messages` for one release cycle, but emits this warning the first time it is accessed in a given process. - -## Example - -Code that triggers it: - -```ts -ctx.logs.add({ message: 'something happened', level: 'info' }) -``` - -## Fix - -Replace `ctx.logs` with `ctx.messages`: - -```ts -ctx.messages.add({ message: 'something happened', level: 'info' }) -``` - -The runtime behavior is identical — the same host instance backs both fields. - -The associated type names have been renamed too (with deprecated aliases kept for one release): - -| Old | New | -|-----|-----| -| `DevToolsLogsHost` | `DevToolsMessagesHost` | -| `DevToolsLogsClient` | `DevToolsMessagesClient` | -| `DevToolsLogEntry` | `DevToolsMessageEntry` | -| `DevToolsLogEntryInput` | `DevToolsMessageEntryInput` | -| `DevToolsLogHandle` | `DevToolsMessageHandle` | -| `DevToolsLogLevel` | `DevToolsMessageLevel` | - -The event names emitted by the host have changed from `log:added` / `log:updated` / `log:removed` / `log:cleared` to `message:added` / `message:updated` / `message:removed` / `message:cleared`. - -## Source - -- [`packages/devframe/src/node/context.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/context.ts) — the `ctx.logs` getter logs `DF0018` once when first accessed, then forwards to `ctx.messages`. diff --git a/devframe/docs/errors/index.md b/devframe/docs/errors/index.md index 7188d213..93fe604c 100644 --- a/devframe/docs/errors/index.md +++ b/devframe/docs/errors/index.md @@ -18,24 +18,15 @@ Emitted by `devframe` — framework-neutral host / shared-state / auth surface. | Code | Level | Title | |------|-------|-------| -| [DF0001](./DF0001) | error | Dock Already Registered | -| [DF0002](./DF0002) | error | Cannot Change Dock ID | -| [DF0003](./DF0003) | error | Dock Not Registered | -| [DF0004](./DF0004) | error | Terminal Session Already Registered | -| [DF0005](./DF0005) | error | Terminal Session Not Registered | | [DF0006](./DF0006) | error | RPC Function Not Registered | | [DF0007](./DF0007) | error | AsyncLocalStorage Not Set | | [DF0008](./DF0008) | error | View distDir Not Found | -| [DF0009](./DF0009) | error | Command Already Registered | -| [DF0010](./DF0010) | error | Cannot Change Command ID | -| [DF0011](./DF0011) | error | Command Not Registered | | [DF0012](./DF0012) | warn | Storage Parse Failed | | [DF0013](./DF0013) | error | Shared State Not Found | | [DF0014](./DF0014) | error | Invalid Agent Field | | [DF0015](./DF0015) | error | Agent Tool Already Registered | | [DF0016](./DF0016) | error | Agent Resource Already Registered | | [DF0017](./DF0017) | error | MCP Server Start Failure | -| [DF0018](./DF0018) | warn | `ctx.logs` Deprecated | | [DF0019](./DF0019) | error | Agent Requires JSON-Serializable RPC | | [DF0020](./DF0020) | error | Non-JSON Value in JSON-Serializable RPC | | [DF0021](./DF0021) | error | RPC Function Already Registered | diff --git a/devframe/docs/guide/adapters.md b/devframe/docs/guide/adapters.md index 544229a7..d0c63453 100644 --- a/devframe/docs/guide/adapters.md +++ b/devframe/docs/guide/adapters.md @@ -16,7 +16,7 @@ All adapter factories share the same shape: `createXxx(devtoolDef, options?)`. | [`dev`](#dev) | `devframe/adapters/dev` | `createDevServer(def, options?)` | Run the dev server programmatically — drive it from any CLI framework | | [`vite`](#vite) | `devframe/adapters/vite` | `createVitePlugin(def, options?)` | Mount a tool's UI inside an existing Vite dev server | | [`build`](#build) | `devframe/adapters/build` | `createBuild(def, options?)` | Offline reports, CI artifacts, deployable SPA snapshots | -| [`kit`](#kit) | `devframe/adapters/kit` | `createKitPlugin(def, options?)` | Integrating into Vite DevTools Kit | +| [`kit`](#kit) | `@vitejs/devtools-kit/node` | `createPluginFromDevframe(def, options?)` | Integrating into Vite DevTools Kit | | [`embedded`](#embedded) | `devframe/adapters/embedded` | `createEmbedded(def, { ctx })` | Runtime registration into an already-running host | | [`mcp`](#mcp) | `devframe/adapters/mcp` | `createMcpServer(def, options?)` | Exposing a devtool to coding agents | @@ -256,23 +256,25 @@ When `def.spa` is set on the definition, `createBuild` also writes `spa-loader.j ## Kit -Wraps a `DevtoolDefinition` so that Vite DevTools Kit's plugin-scan picks it up. +Wraps a `DevtoolDefinition` so that Vite DevTools Kit's plugin-scan picks it up. The factory lives in `@vitejs/devtools-kit/node` (kit owns docking + process management; devframe stays portable). ```ts -import type { Plugin } from 'vite' -import { createKitPlugin } from 'devframe/adapters/kit' +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' import devtool from './devtool' -export default function myVitePlugin(): Plugin { - return createKitPlugin(devtool) as unknown as Plugin +export default function myVitePlugin() { + return createPluginFromDevframe(devtool) } ``` -The returned object has the shape `{ name, devtools: { setup, capabilities } }`. Use this adapter when your devtool should live inside the Vite DevTools dock alongside other integrations. For a Vite-specific plugin guide, see the [DevTools Kit → DevTools Plugin](https://devtools.vite.dev/kit/devtools-plugin) page. +The returned object has the shape `{ name, devtools: { setup, capabilities } }`. Use this adapter when your devtool should live inside the Vite DevTools dock alongside other integrations. The kit synthesises an iframe dock entry from the definition's `id` / `name` / `icon` / `basePath` automatically; for richer kit-specific behaviour (extra terminals, commands, dock overrides) pass `options.setup`. For a Vite-specific plugin guide, see the [DevTools Kit → DevTools Plugin](https://devtools.vite.dev/kit/devtools-plugin) page. | Option | Default | Description | |--------|---------|-------------| | `name` | `devframe:` | Override the Vite plugin name. | +| `base` | `def.basePath ?? /.${id}/` | Mount path override. | +| `dock` | `{}` | Overrides for the synthesized iframe dock entry (category, icon, when). | +| `setup` | — | Additional kit-only setup hook; receives the kit-augmented context. | ## Embedded diff --git a/devframe/docs/guide/client.md b/devframe/docs/guide/client.md index e57847af..f5d9e24a 100644 --- a/devframe/docs/guide/client.md +++ b/devframe/docs/guide/client.md @@ -183,7 +183,7 @@ await connectDevtool({ ## Remote Docks -Remote docks (see [Dock System → Remote Docks](./dock-system#remote-docks)) work by DevFrame injecting a connection descriptor into the iframe URL. On the hosted page, `connectDevtool` auto-detects the descriptor from the URL fragment / query string — no code change required beyond calling it as usual: +Remote docks are a kit-side feature (see [Vite DevTools Kit → Remote Client](https://devtools.vite.dev/kit/remote-client)). The kit injects a connection descriptor into the iframe URL; on the hosted page, `connectDevtool` auto-detects the descriptor from the URL fragment / query string — no code change required beyond calling it as usual: ```ts import { connectDevtool } from 'devframe/client' diff --git a/devframe/docs/guide/commands.md b/devframe/docs/guide/commands.md deleted file mode 100644 index 2a8ee156..00000000 --- a/devframe/docs/guide/commands.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -outline: deep ---- - -# Commands - -The commands system registers executable actions that appear in the global command palette. Users can discover them via search, trigger them through keyboard shortcuts, or drill down into grouped children. - -## Registering a Command - -```ts -import { defineCommand, defineDevtool } from 'devframe' - -const clearCache = defineCommand({ - id: 'my-devtool:clear-cache', - title: 'Clear Build Cache', - description: 'Remove all cached build artifacts', - icon: 'ph:trash-duotone', - category: 'tools', - handler: async () => { - await fs.rm('.cache', { recursive: true }) - }, -}) - -export default defineDevtool({ - id: 'my-devtool', - name: 'My Devtool', - setup(ctx) { - ctx.commands.register(clearCache) - }, -}) -``` - -## Options - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `string` | **Required.** Globally unique, namespaced (`:`). | -| `title` | `string` | **Required.** Human-readable label. | -| `description` | `string` | Shown under the title in the palette. | -| `icon` | `string` | Iconify name, e.g. `ph:trash-duotone`. | -| `category` | `string` | Free-form grouping. | -| `showInPalette` | `boolean \| 'without-children'` | Default `true`. Use `'without-children'` to show the parent but keep children under drill-down only. | -| `when` | `string` | Visibility / executability expression — see [When Clauses](./when-clauses). | -| `keybindings` | `DevToolsCommandKeybinding[]` | Default keyboard shortcuts. | -| `handler` | `Function` | Server-side handler. Optional if the command only groups children. | -| `children` | `DevToolsServerCommandInput[]` | Static sub-commands (two levels max). | - -## Handle - -`register()` returns a handle for live updates and unregistration: - -```ts -const handle = ctx.commands.register({ - id: 'my-devtool:status', - title: 'Show Status', - handler: async () => {}, -}) - -handle.update({ title: 'Show Status (3 items)' }) -handle.unregister() -``` - -## Keybindings - -Use `Mod` for platform-aware modifier keys (Cmd on macOS, Ctrl elsewhere): - -```ts -ctx.commands.register({ - id: 'my-devtool:open-panel', - title: 'Open Devtool Panel', - keybindings: [ - { key: 'Mod+K Mod+D' }, // chord - { key: 'Alt+D' }, - ], - handler: async () => openPanel(), -}) -``` - -Users can override keybindings from the settings UI — overrides land in shared state under `commandShortcuts`. - -## Sub-Commands - -Commands can have static children forming a two-level hierarchy. In the palette, selecting a parent drills into its children: - -```ts -ctx.commands.register({ - id: 'my-devtool:routes', - title: 'Routes', - icon: 'ph:signpost-duotone', - children: [ - { - id: 'my-devtool:routes:rebuild', - title: 'Rebuild Route Manifest', - handler: async () => rebuildRoutes(), - }, - { - id: 'my-devtool:routes:open', - title: 'Open Routes File', - handler: async () => openInEditor('routes.ts'), - }, - ], -}) -``` - -Children must have globally unique `id`s. The palette flattens them into top-level search results by default; pass `showInPalette: 'without-children'` on the parent to hide children from search. - -## When-Clause Gating - -Attach a `when` expression to hide / disable commands based on UI state: - -```ts -ctx.commands.register({ - id: 'my-devtool:focus-devtool', - title: 'Focus Devtool Panel', - when: 'clientType == embedded && !paletteOpen', - handler: async () => focusPanel(), -}) -``` - -The expression is evaluated at palette-render time and before execution. Full syntax in [When Clauses](./when-clauses). - -## Executing Programmatically - -Any code with access to the context can trigger a command by id: - -```ts -await ctx.commands.execute('my-devtool:clear-cache') - -// With arguments: -await ctx.commands.execute('my-devtool:open-file', '/src/main.ts') -``` - -`execute` throws if the command isn't registered or has no handler. - -## Client Commands - -Commands registered on the server run in the node process. For actions that must run in the browser (DOM manipulation, copying to clipboard, etc.), register on the client host instead: - -```ts -import { connectDevtool } from 'devframe/client' - -const rpc = await connectDevtool() - -rpc.client.register(/* defineRpcFunction with a mirroring shape */) -``` - -The command palette merges server and client commands transparently. See [Client](./client) for the full client-side API. - -## Listing & Introspection - -The host exposes a `list()` method returning serializable command entries (without handlers) — useful when implementing your own palette or exporting the current command set: - -```ts -const commands = ctx.commands.list() -for (const cmd of commands) { - console.log(cmd.id, cmd.title, cmd.keybindings) -} -``` - -Events let you react to registrations: - -```ts -ctx.commands.events.on('command:registered', (cmd) => { - console.log('new command:', cmd.id) -}) -``` diff --git a/devframe/docs/guide/devtool-definition.md b/devframe/docs/guide/devtool-definition.md index 4257c682..c29c9b72 100644 --- a/devframe/docs/guide/devtool-definition.md +++ b/devframe/docs/guide/devtool-definition.md @@ -4,29 +4,32 @@ outline: deep # Devtool Definition -Every DevFrame tool starts with a single `defineDevtool` call. The returned `DevtoolDefinition` is a portable value that any of the seven [adapters](./adapters) can consume — the same definition runs under `createCli`, `createKitPlugin`, `createBuild`, `createMcpServer`, and so on. +Every DevFrame tool starts with a single `defineDevtool` call. The returned `DevtoolDefinition` is a portable value that any of the [adapters](./adapters) can consume — the same definition runs under `createCli`, `createBuild`, `createMcpServer`, kit's `createPluginFromDevframe`, and so on. ## Minimal Definition ```ts twoslash -import { defineDevtool } from 'devframe' +import { defineDevtool, defineRpcFunction } from 'devframe' +import * as v from 'valibot' export default defineDevtool({ id: 'my-devtool', name: 'My Devtool', + icon: 'ph:gauge-duotone', setup(ctx) { - // Register RPC, docks, commands, logs, terminals, agents here. - ctx.docks.register({ - id: 'my-devtool:main', - title: 'My Devtool', - icon: 'ph:gauge-duotone', - type: 'iframe', - url: '/__devtools/', - }) + // Register your RPC functions, shared state, etc. here. + ctx.rpc.register(defineRpcFunction({ + name: 'my-devtool:hello', + type: 'static', + jsonSerializable: true, + handler: () => ({ message: 'hello' }), + })) }, }) ``` +> The dock entry / iframe mount is auto-derived from `id`, `name`, `icon`, and `basePath` when the devtool is mounted into Vite DevTools via [`createPluginFromDevframe`](./adapters#kit). Hub-level features like `docks`, `terminals`, `messages`, and `commands` belong to the kit-augmented context — not to the devframe-level `setup`. + ## Definition Fields | Field | Type | Description | @@ -35,7 +38,7 @@ export default defineDevtool({ | `name` | `string` | **Required.** Display name shown in the dock and agent manifests. | | `icon` | `string \| { light, dark }` | Optional Iconify name or URL; supports light/dark pairs. | | `version` | `string` | Optional version string surfaced to clients. | -| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `spa` / `build`), `/__/` for hosted (`vite` / `kit` / `embedded`). | +| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `spa` / `build`), `/./` for hosted (`vite` / `kit` / `embedded`). | | `capabilities` | `{ dev?, build?, spa? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. | | `setup` | `(ctx, info?) => void \| Promise` | **Required.** Server-side entry point. Runs in every runtime. The optional second argument carries runtime metadata — most notably the parsed CLI `flags` when running under `createCli`. | | `setupBrowser` | `(ctx) => void \| Promise` | Browser-only entry used by the SPA adapter. | @@ -75,28 +78,22 @@ interface DevToolsNodeContext { host: DevToolsHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir) rpc: RpcFunctionsHost // register + broadcast + sharedState - docks: DevToolsDockHost // dock entries - views: DevToolsViewHost // static file hosting - terminals: DevToolsTerminalHost - messages: DevToolsMessagesHost + views: DevToolsViewHost // static file hosting (`hostStatic`) diagnostics: DevToolsDiagnosticsHost - commands: DevToolsCommandsHost agent: DevToolsAgentHost // experimental createJsonRenderer: (spec) => JsonRenderer - utils: DevToolsNodeUtils } ``` +Hub-level subsystems — `docks`, `terminals`, `messages`, `commands` — are owned by `@vitejs/devtools-kit` and only present on the kit-augmented context. A devframe app that wants to register kit-only behavior does so via the optional `setup` hook on `createPluginFromDevframe`. + Each host has a dedicated page: - [RPC](./rpc) — `ctx.rpc` - [Shared State](./shared-state) — `ctx.rpc.sharedState` -- [Dock System](./dock-system) — `ctx.docks`, `ctx.views` -- [Commands](./commands) — `ctx.commands` -- [Messages](./messages) — `ctx.messages` - [Diagnostics](./diagnostics) — `ctx.diagnostics` -- [Terminals](./terminals) — `ctx.terminals` - [Agent-Native](./agent-native) — `ctx.agent` +- Hub-side surfaces — [Dock System](https://devtools.vite.dev/kit/dock-system), [Commands](https://devtools.vite.dev/kit/commands), [Messages](https://devtools.vite.dev/kit/messages), [Terminals](https://devtools.vite.dev/kit/terminals) — live in the [Vite DevTools Kit](https://devtools.vite.dev/kit/) docs. ## Browser Setup @@ -179,9 +176,9 @@ See [Adapters](./adapters) for how each adapter consumes these. Because the definition is a plain value, you can wire it into multiple adapters from the same file: ```ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' import { createBuild } from 'devframe/adapters/build' import { createCli } from 'devframe/adapters/cli' -import { createKitPlugin } from 'devframe/adapters/kit' const devtool = defineDevtool({ id: 'my-devtool', name: 'My Devtool', setup() {} }) @@ -189,7 +186,7 @@ const devtool = defineDevtool({ id: 'my-devtool', name: 'My Devtool', setup() {} await createCli(devtool).parse() // 2. Embedded in a Vite project (from `vite.config.ts`): -export const myPlugin = () => createKitPlugin(devtool) +export const myPlugin = () => createPluginFromDevframe(devtool) // 3. Offline snapshot: await createBuild(devtool, { outDir: 'dist-static' }) @@ -199,4 +196,4 @@ await createBuild(devtool, { outDir: 'dist-static' }) - [Adapters](./adapters) — pick a deployment target - [RPC](./rpc) — register server functions -- [Dock System](./dock-system) — add UI surfaces +- [Vite DevTools Kit](https://devtools.vite.dev/kit/) — mount your devtool into the multi-integration hub diff --git a/devframe/docs/guide/diagnostics.md b/devframe/docs/guide/diagnostics.md index f27e08c9..5d14dda4 100644 --- a/devframe/docs/guide/diagnostics.md +++ b/devframe/docs/guide/diagnostics.md @@ -6,12 +6,12 @@ outline: deep `ctx.diagnostics` is a thin layer over [`logs-sdk`](https://github.com/vercel-labs/logs-sdk) that lets integrations register their own coded errors and warnings into a shared logger — without taking a direct dependency on `logs-sdk`. -Use it for *author-defined coded diagnostics* (errors, warnings, deprecations) that have a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](./messages) instead. +Use it for *author-defined coded diagnostics* (errors, warnings, deprecations) that have a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](https://devtools.vite.dev/kit/messages) instead. | Surface | Purpose | Example | |---------|---------|---------| | `ctx.diagnostics` | Coded errors and warnings emitted from node-side plugin code | `MYP0001: Plugin foo not configured` | -| [`ctx.messages`](./messages) | Free-form, user-facing notifications shown in the Messages panel | `'Audit complete — 3 issues found'` | +| [`ctx.messages`](https://devtools.vite.dev/kit/messages) | Free-form, user-facing notifications shown in the Messages panel | `'Audit complete — 3 issues found'` | ## Shape diff --git a/devframe/docs/guide/dock-system.md b/devframe/docs/guide/dock-system.md deleted file mode 100644 index a4c427e5..00000000 --- a/devframe/docs/guide/dock-system.md +++ /dev/null @@ -1,231 +0,0 @@ ---- -outline: deep ---- - -# Dock System - -Dock entries are the surfaces users interact with — the clickable icons in the DevTools dock and the panels that open when selected. A devtool typically registers one or more docks during `setup`. - -## Entry Types - -| Type | What it renders | Best for | -|------|-----------------|----------| -| `iframe` | An iframe pointing at a URL you provide. | Full-featured UIs, dashboards, framework apps. | -| `action` | Runs a client-side script when clicked. | Inspectors, toggles, one-shot actions. | -| `custom-render` | Mounts a client script into the panel DOM. | Framework apps that need direct DOM access. | -| `launcher` | A setup card with a server-triggered launch button. | Initialization flows before heavier tools. | -| `json-render` | UI described as a JSON spec — no client bundle needed. | Config viewers, data panels, simple forms. | - -## Iframe Panels - -The most common shape — point at a URL and let the iframe host your SPA. - -### Bundled SPA - -Host your built client through the view host so the same build serves every adapter: - -```ts -import { fileURLToPath } from 'node:url' -import { defineDevtool } from 'devframe' - -const clientDist = fileURLToPath(new URL('../client/dist', import.meta.url)) - -export default defineDevtool({ - id: 'my-devtool', - name: 'My Devtool', - setup(ctx) { - ctx.views.hostStatic('/__my-devtool/', clientDist) - - ctx.docks.register({ - id: 'my-devtool:main', - title: 'My Devtool', - icon: 'ph:gauge-duotone', - type: 'iframe', - url: '/__my-devtool/', - }) - }, -}) -``` - -`ctx.views.hostStatic(baseUrl, distDir)` does the right thing per mode: - -- **Dev** — mounts middleware on the underlying runtime (h3 for the CLI adapter, Vite dev server for the Kit adapter). -- **Build** — copies `distDir` into the output so the static snapshot serves the same assets. - -### External URL - -If your UI is hosted elsewhere, point the iframe straight at it: - -```ts -ctx.docks.register({ - id: 'my-devtool:hosted', - title: 'My Devtool', - icon: 'ph:cloud-duotone', - type: 'iframe', - url: 'https://example.com/my-devtool', -}) -``` - -### Remote Docks - -Set `remote: true` on an iframe dock to turn a hosted page into a live DevFrame client — DevFrame injects an auth-approved connection descriptor into the iframe URL so the hosted page can open a WebSocket back to the local dev server. - -```ts -ctx.docks.register({ - id: 'my-devtool:hosted', - title: 'My Devtool', - icon: 'ph:cloud-duotone', - type: 'iframe', - url: 'https://example.com/my-devtool', - remote: true, -}) -``` - -On the hosted page, [`connectDevtool`](./client) parses the descriptor and returns a fully connected RPC client. See the [Client](./client) page for the connection model. - -| Option | Default | Description | -|--------|---------|-------------| -| `remote.transport` | `'fragment'` | `'fragment'` keeps the descriptor out of access logs / `Referer`. `'query'` when your SPA router consumes the fragment. | -| `remote.originLock` | `true` | Reject WebSocket handshakes whose `Origin` doesn't match the registered dock URL. | - -Remote docks only work in dev mode (no WebSocket server exists in a static build). - -## Action Buttons - -Action docks run a client script when clicked — no panel is opened. Perfect for inspector overlays, toggles, and one-shot actions. - -```ts -ctx.docks.register({ - id: 'my-devtool:inspect', - title: 'Inspect Element', - icon: 'ph:cursor-duotone', - type: 'action', - action: { - importFrom: 'my-devtool/action', - importName: 'default', - }, -}) -``` - -The client script runs in the user's page. It receives a context object with the RPC client and an event emitter for `entry:activated` / `entry:deactivated`. - -## Custom Renderers - -Custom render docks hand you the panel DOM element and let you render whatever you want: - -```ts -ctx.docks.register({ - id: 'my-devtool:custom', - title: 'Custom View', - icon: 'ph:code-duotone', - type: 'custom-render', - renderer: { - importFrom: 'my-devtool/renderer', - importName: 'default', - }, -}) -``` - -The renderer script listens for `dom:panel:mounted` and mounts a Vue / React / Svelte app — or just vanilla DOM — into the provided element. - -## Launcher - -Launcher docks show a setup card with a button that triggers a server-side callback: - -```ts -ctx.docks.register({ - id: 'my-devtool:setup', - title: 'My Setup', - icon: 'ph:rocket-launch-duotone', - type: 'launcher', - launcher: { - title: 'Initialize Integration', - description: 'Run initial setup before opening tools', - onLaunch: async () => { - await runSetup() - }, - }, -}) -``` - -Use launchers when a devtool has a heavy initialization step — generate artifacts, kick off a long terminal command, authenticate against an external service — before its main panels become useful. - -## JSON Render - -JSON render docks describe their UI as a JSON spec — the client interprets it directly, so you don't need to ship any dock-specific JavaScript: - -```ts -defineDevtool({ - id: 'my-devtool', - name: 'My Devtool', - setup(ctx) { - const ui = ctx.createJsonRenderer({ - root: 'root', - elements: { - root: { - type: 'Stack', - props: { direction: 'vertical', gap: 12 }, - children: ['heading', 'info'], - }, - heading: { type: 'Text', props: { content: 'Hello', variant: 'heading' } }, - info: { - type: 'KeyValueTable', - props: { - entries: [ - { key: 'Version', value: '1.0.0' }, - { key: 'Status', value: 'Running' }, - ], - }, - }, - }, - }) - - ctx.docks.register({ - id: 'my-devtool:panel', - title: 'Panel', - icon: 'ph:chart-bar-duotone', - type: 'json-render', - ui, - }) - }, -}) -``` - -Update the spec or state at runtime via `ui.updateSpec(newSpec)` / `ui.updateState({ key: value })`. - -## Common Options - -Every dock type accepts these base fields: - -| Field | Type | Description | -|-------|------|-------------| -| `id` | `string` | Unique, namespaced. | -| `title` | `string` | Label shown in the dock. | -| `icon` | `string \| { light, dark }` | Iconify name, URL, data URI, or light/dark pair. | -| `category` | `'app' \| 'framework' \| 'web' \| 'advanced' \| 'default'` | Grouping in the dock panel. | -| `defaultOrder` | `number` | Higher numbers appear first. Default `0`. | -| `when` | `string` | Visibility expression — see [When Clauses](./when-clauses). | -| `badge` | `string` | Short text badge (e.g. unread count). | - -## Update & Unregister - -`register()` returns a handle with `update(patch)`: - -```ts -const handle = ctx.docks.register({ /* … */ }) - -// Live update -handle.update({ badge: '3' }) -``` - -The handle only supports `update`. Docks are not individually unregisterable today. - -## View Host - -`ctx.views` is a thin helper used for hosting static assets. You'll typically only call `hostStatic`: - -```ts -ctx.views.hostStatic('/__my-devtool/', clientDist) -``` - -Internal state (`buildStaticDirs`) is used by the build adapter — treat it as an implementation detail. diff --git a/devframe/docs/guide/index.md b/devframe/docs/guide/index.md index ed9570b5..e826c618 100644 --- a/devframe/docs/guide/index.md +++ b/devframe/docs/guide/index.md @@ -4,20 +4,21 @@ outline: deep # DevFrame -**DevFrame** is the framework-neutral foundation that powers Vite DevTools. It provides the RPC layer, host abstractions, and seven runtime adapters needed to build a devtool — and it does so without depending on Vite or any framework. +**DevFrame** is *the container for one devtool integration, portable across viewers.* You describe a single tool — its RPC surface, its data model, its SPA, its CLI shape — and DevFrame deploys the same definition through any number of runtime adapters: a standalone CLI, a self-contained static report, an embedded SPA, an MCP server, or mounted inside a multi-integration hub. -If you are writing a Vite plugin and want a Vite-specific entry point, see the [DevTools Kit](https://devtools.vite.dev/kit/) — Kit is a Vite-specific superset of DevFrame. If you want to build a standalone inspector, bundle a static snapshot of your data, drive a devtool from a CLI, or expose your tool to coding agents over MCP, start here. +DevFrame deliberately stops at the boundary of one tool. **Docking, the command palette, terminal aggregation, cross-tool toasts** — anything that only makes sense when *multiple* devtools share a UI — lives in [`@vitejs/devtools-kit`](https://devtools.vite.dev/kit/), the hub layer. To drop a DevFrame app into Vite DevTools, wrap it with `createPluginFromDevframe` from `@vitejs/devtools-kit/node`; the kit synthesizes the dock entry from your definition's `id` / `name` / `icon` / `basePath` and routes its hub-level ctx fields (`docks`, `terminals`, …) accordingly. > [!WARNING] Experimental > The DevFrame API is still in development and may change between versions. The agent-native surface (`agent` field on `defineRpcFunction`, `ctx.agent`, and the MCP adapter) is additionally flagged as experimental. ## Design Principles -DevFrame keeps its surface small and pushes UX decisions to the application consuming it: +DevFrame keeps its surface small and pushes hub-level UX to the kit consuming it: +- **Single-integration scope.** DevFrame describes one tool. Anything that only matters across tools — docks, palette, cross-tool toasts, unified terminals — is the [DevTools Kit's](https://devtools.vite.dev/kit/) job, not DevFrame's. - **Headless.** No default startup banners, logging, or styling. Hook into `onReady`, `cli.configure`, and friends to print your own output. - **App-owned file watching.** Wire your own watcher (chokidar, fs.watch, …) and signal change via `ctx.rpc.sharedState.set(...)` or event-type RPCs. DevFrame does not ship a watcher primitive. -- **Context-aware mount paths.** Standalone adapters (`cli`, `spa`, `build`) serve at `/` by default; hosted adapters (`vite`, `kit`, `embedded`) serve at `/__/`. Override via `DevtoolDefinition.basePath`. +- **Context-aware mount paths.** Standalone adapters (`cli`, `spa`, `build`) serve at `/` by default; hosted adapters (`vite`, `embedded`, kit's `createPluginFromDevframe`) serve at `/./`. Override via `DevtoolDefinition.basePath`. - **SPAs own their base at runtime.** Build with relative asset paths (`vite.base: './'`); `connectDevtool` discovers the effective base from the executing script's location. No HTML rewrites at build time. - **CLI flags compose.** The `cac` instance is exposed to both the devtool (`cli.configure`) and the caller of `createCli`, so capability flags and app flags merge cleanly. @@ -25,17 +26,17 @@ DevFrame keeps its surface small and pushes UX decisions to the application cons | Subsystem | What it does | |-----------|--------------| -| **[Devtool Definition](./devtool-definition)** | One `defineDevtool` call describes your tool once; seven adapters deploy it anywhere. | +| **[Devtool Definition](./devtool-definition)** | One `defineDevtool` call describes your tool once; the adapters deploy it anywhere. | | **[RPC](./rpc)** | Type-safe bidirectional calls built on birpc + valibot. Supports `query`, `static`, `action`, and `event` types. | | **[Shared State](./shared-state)** | Observable, patch-synced state that survives reconnects and bridges server ↔ browser. | -| **[Dock System](./dock-system)** | Register panels (iframe / action / custom / launcher / json-render) with visibility rules. | -| **[Commands](./commands)** | Command palette entries with keybindings, children, and when-clause gating. | +| **[Diagnostics](./diagnostics)** | Coded warnings/errors via `logs-sdk` — registered into the host logger so adapters and consumers share the same surface. | +| **[Streaming](./streaming)** | One-way (RPC streaming) and two-way (uploads) channel primitives for long-running data. | | **[When Clauses](./when-clauses)** | VS Code-style conditional expressions for docks, commands, and custom UI. | -| **[Logs](./logs)** | Structured log entries with file/element positions, toasts, and live updates. | -| **[Terminals](./terminals)** | Spawn child processes and stream output into an xterm.js UI. | -| **[Client](./client)** | Browser-side RPC client with auto-auth and WebSocket / static modes. | +| **[Client](./client)** | Browser-side RPC client (`connectDevtool`) with auto-auth and WebSocket / static modes. | | **[Agent-Native](./agent-native)** | Opt-in exposure of your tool's surface to coding agents over MCP. | +> Hub-only subsystems — **[Dock System](https://devtools.vite.dev/kit/dock-system)**, **[Commands](https://devtools.vite.dev/kit/commands)**, **[Messages](https://devtools.vite.dev/kit/messages)**, **[Terminals](https://devtools.vite.dev/kit/terminals)** — are documented in the [Vite DevTools Kit](https://devtools.vite.dev/kit/) since they only matter when multiple integrations share a UI. + ## Architecture ```mermaid @@ -55,17 +56,15 @@ flowchart TB Adapters --> Ctx["DevToolsNodeContext"] - subgraph Ctx["DevToolsNodeContext (server)"] + subgraph Ctx["DevToolsNodeContext (devframe / single-integration)"] direction TB RPC["rpc"] - Docks["docks"] - Views["views"] - Terminals["terminals"] - Logs["logs"] - Commands["commands"] + Views["views (hostStatic)"] + Diagnostics["diagnostics"] Agent["agent"] end + Ctx -.->|kit augments
via createKitContext| Hub["KitNodeContext
+ docks · terminals · messages · commands"] Ctx <-->|WebSocket or static| Client["DevToolsRpcClient
(browser)"] ``` @@ -82,29 +81,41 @@ pnpm add devframe A minimal devtool with a CLI entry point: ```ts twoslash -import { defineDevtool } from 'devframe' +import { defineDevtool, defineRpcFunction } from 'devframe' import { createCli } from 'devframe/adapters/cli' const devtool = defineDevtool({ id: 'my-devtool', name: 'My Devtool', + icon: 'ph:gauge-duotone', cli: { distDir: 'client/dist', }, setup(ctx) { - ctx.docks.register({ - id: 'my-devtool:main', - title: 'My Devtool', - icon: 'ph:gauge-duotone', - type: 'iframe', - url: '/__devtools/', - }) + ctx.rpc.register(defineRpcFunction({ + name: 'my-devtool:hello', + type: 'static', + jsonSerializable: true, + handler: () => ({ message: 'hello' }), + })) }, }) await createCli(devtool).parse() ``` +Drop the same definition into Vite DevTools — the kit auto-derives the iframe dock entry from `id` / `name` / `icon` / `basePath`: + +```ts +// vite.config.ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' +import devtool from './my-devtool' + +export default { + plugins: [createPluginFromDevframe(devtool)], +} +``` + Run it: ```sh @@ -113,31 +124,31 @@ node ./my-devtool.js build # self-contained static deploy in dist-static/ node ./my-devtool.js mcp # stdio MCP server (experimental) ``` -The CLI adapter serves the SPA at `/` by default. When the same devtool is embedded inside a host (`vite`, `kit`, `embedded`), the default becomes `/__my-devtool/`. Override either side via `defineDevtool({ basePath })`. +The CLI adapter serves the SPA at `/` by default. When the same devtool is embedded inside a host (`vite`, `kit`, `embedded`), the default becomes `/.my-devtool/`. Override either side via `defineDevtool({ basePath })`. ## Adapters at a Glance -DevFrame deploys the same `DevtoolDefinition` through one of six adapters: +DevFrame deploys the same `DevtoolDefinition` through one of these adapters: | Adapter | Entry | Target | |---------|-------|--------| | `cli` | `createCli(d).parse()` | Standalone CLI with dev / build / mcp subcommands | -| `vite` | `createVitePlugin(d, opts?)` | Plain Vite plugin — mounts the SPA only (no RPC server) | +| `vite` | `createVitePlugin(d, opts?)` | Plain Vite plugin — mounts the SPA only (no RPC server, no hub) | | `build` | `createBuild(d, opts?)` | Self-contained static deploy with baked RPC dumps | -| `kit` | `createKitPlugin(d, opts?)` | Vite DevTools Kit plugin | +| **kit (bridge)** | `createPluginFromDevframe(d, opts?)` *(from `@vitejs/devtools-kit/node`)* | Mount the devtool into Vite DevTools' hub UI | | `embedded` | `createEmbedded(d, { ctx })` | Runtime registration into an existing host | | `mcp` | `createMcpServer(d, opts)` | Model Context Protocol server | -See [Adapters](./adapters) for the full reference. +`createPluginFromDevframe` lives in the kit, not in DevFrame, because mounting *into a multi-integration hub* is by definition a kit responsibility. See [Adapters](./adapters) for the full reference. ## Dependency Boundary -DevFrame is the lowest-level package in the Vite DevTools monorepo and is positioned to be extracted into its own repo. It **must not** import from Vite or any `@vitejs/*` package — neither as a dependency nor as a source import. Consumers layer on top: +DevFrame is the lowest-level package in the Vite DevTools monorepo and is positioned to be extracted into its own repo. It **must not** import from Vite or any `@vitejs/*` package — neither as a dependency nor as a source import. Hub-only concepts (docks, terminals, messages, commands) belong in the layers above: -- `@vitejs/devtools-kit` — Vite-specific superset of DevFrame. -- `@vitejs/devtools` — The Vite plugin that wraps Kit + DevFrame. +- `@vitejs/devtools-kit` — *the hub*. Owns docking, terminals, messages, the command palette; provides `createPluginFromDevframe` to bridge a DevFrame app into Vite DevTools. +- `@vitejs/devtools` — *the integration*. The Vite plugin that wraps the kit and exposes Vite DevTools' own UI. -If you are porting an existing inspector, prefer the [`cli`](./adapters#cli) adapter for standalone use and the [`kit`](./adapters#kit) adapter to surface inside Vite DevTools. +If you are porting an existing inspector, prefer the [`cli`](./adapters#cli) adapter for standalone use and `createPluginFromDevframe` (from `@vitejs/devtools-kit/node`) to surface it inside Vite DevTools. ## What's Next diff --git a/devframe/docs/guide/messages.md b/devframe/docs/guide/messages.md deleted file mode 100644 index 61ce6369..00000000 --- a/devframe/docs/guide/messages.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -outline: deep ---- - -# Messages & Notifications - -`ctx.messages` is a structured message store with live updates, toasts, and positional hints that link a message entry back to a DOM element or source file. Use it to surface a11y findings, lint errors, runtime failures, or short-lived notifications like "URL copied". - -The same API works from the server and the browser: each call is a Promise, but most callers fire-and-forget. - -> **Note:** Previously named `ctx.logs`. The old field still works as a deprecated alias for one release cycle — see [DF0018](/errors/DF0018) for migration details. - -## Entry Fields - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `message` | `string` | ✓ | Short title or summary. | -| `level` | `'info' \| 'warn' \| 'error' \| 'success' \| 'debug'` | ✓ | Severity — drives color and icon. | -| `description` | `string` | | Longer explanation shown in the detail panel. | -| `stacktrace` | `string` | | Formatted stack trace. | -| `filePosition` | `{ file, line?, column? }` | | Source file (clickable — opens in editor). | -| `elementPosition` | `{ selector?, boundingBox?, description? }` | | DOM anchor (for a11y, layout issues). | -| `notify` | `boolean` | | Also emit as a toast. | -| `category` | `string` | | Grouping key (e.g. `'a11y'`, `'lint'`). | -| `labels` | `string[]` | | Free-form tags for filtering. | -| `autoDismiss` | `number` | | Toast auto-dismiss in ms. | -| `autoDelete` | `number` | | Server-side auto-delete in ms. | -| `status` | `'loading' \| 'idle'` | | Spinner indicator when `'loading'`. | -| `id` | `string` | | Explicit id — re-adding with the same id updates the existing entry. | - -`from` is automatically set to `'server'` or `'browser'` by the host. - -## Fire-and-Forget - -```ts -ctx.messages.add({ - message: 'Plugin initialized', - level: 'info', -}) -``` - -## With a Handle - -`await` the call to get a handle for live updates: - -```ts -const handle = await ctx.messages.add({ - id: 'my-devtool:build', - message: 'Building…', - level: 'info', - status: 'loading', -}) - -// later: -await handle.update({ - message: 'Build complete', - level: 'success', - status: 'idle', -}) - -await handle.dismiss() -``` - -Re-adding with the same `id` updates the existing entry — use this to replace `update` + `dismiss` with a single call site. - -## Toasts - -Set `notify: true` to also render the message as a toast: - -```ts -ctx.messages.add({ - message: 'URL copied to clipboard', - level: 'success', - notify: true, - autoDismiss: 3000, -}) -``` - -`autoDismiss` controls how long the toast stays on screen; the message entry persists in the panel until explicitly removed or `autoDelete` fires. - -## Positional Hints - -### File Position - -Linking a message to a source file makes it clickable — clicking opens the file in the user's editor: - -```ts -ctx.messages.add({ - message: 'Unused import', - level: 'warn', - category: 'lint', - filePosition: { file: '/src/App.vue', line: 12, column: 4 }, -}) -``` - -### Element Position - -DOM anchors are rendered as a highlight overlay when the user hovers the message entry: - -```ts -// Typically from a browser-side audit: -ctx.messages.add({ - message: 'Button missing accessible name', - level: 'warn', - category: 'a11y', - elementPosition: { - selector: 'button.cta', - boundingBox: { x: 100, y: 200, width: 120, height: 40 }, - description: 'Call-to-action button', - }, -}) -``` - -## Worked Examples - -### Long-running operation - -```ts -async function rebuild(ctx) { - const handle = await ctx.messages.add({ - id: 'my-devtool:rebuild', - message: 'Rebuilding…', - level: 'info', - status: 'loading', - }) - - try { - await doRebuild() - await handle.update({ message: 'Rebuild complete', level: 'success', status: 'idle' }) - } - catch (error) { - await handle.update({ - message: 'Rebuild failed', - level: 'error', - description: error.message, - stacktrace: error.stack, - status: 'idle', - }) - } -} -``` - -### Category filter - -```ts -ctx.messages.events.on('message:added', (entry) => { - if (entry.category === 'a11y') { - console.log('a11y finding:', entry.message) - } -}) -``` - -## Removing Entries - -```ts -await ctx.messages.remove('my-devtool:build') -await ctx.messages.clear() // all entries -``` - -## Events - -The host emits events for anyone who wants to observe the message stream: - -```ts -ctx.messages.events.on('message:added', (entry) => { /* … */ }) -ctx.messages.events.on('message:updated', (entry) => { /* … */ }) -ctx.messages.events.on('message:removed', (id) => { /* … */ }) -ctx.messages.events.on('message:cleared', () => { /* … */ }) -``` - -Use this to bridge messages into external tools — e.g. mirror them into a structured log file or forward certain categories to your own reporter. - -## Server vs Browser - -Both sides share the same API. Browser-side calls go through the RPC client (— more idiomatically — the exported `DevToolsMessagesClient` interface). Entries carry a `from` field so the UI can distinguish server-originated messages from browser-originated ones. - -> [!NOTE] -> The separate, Node-side structured diagnostics system used for DevFrame's own warnings / errors (`DF`-prefixed codes) is distinct from `ctx.messages`. See the [Diagnostics guide](./diagnostics) for `ctx.diagnostics`, the host-level wrapper around [`logs-sdk`](https://github.com/vercel-labs/logs-sdk). diff --git a/devframe/docs/guide/terminals.md b/devframe/docs/guide/terminals.md deleted file mode 100644 index fbe8628a..00000000 --- a/devframe/docs/guide/terminals.md +++ /dev/null @@ -1,127 +0,0 @@ ---- -outline: deep ---- - -# Terminals - -`ctx.terminals` lets a devtool spawn and manage child processes. Output is streamed in real time to an xterm.js terminal in the Terminals panel, and each session gets lifecycle controls (terminate / restart). - -## Spawning a Child Process - -`ctx.terminals.startChildProcess(executeOptions, terminalMeta)` creates a new session: - -```ts -const session = await ctx.terminals.startChildProcess( - { - command: 'vite', - args: ['build', '--watch'], - cwd: process.cwd(), - env: { NODE_ENV: 'development' }, - }, - { - id: 'my-devtool:build-watcher', - title: 'Build Watcher', - icon: 'ph:terminal-duotone', - }, -) -``` - -The first argument describes the process: - -| Field | Type | Description | -|-------|------|-------------| -| `command` | `string` | Executable name or path. | -| `args` | `string[]` | Command-line arguments. | -| `cwd` | `string` | Working directory. Defaults to the process cwd. | -| `env` | `Record` | Environment variable overrides. | - -The second argument is the terminal metadata (id, title, optional description/icon) — it lets users identify the session in the dock. - -> [!NOTE] -> Color output is enabled automatically — `FORCE_COLOR` and `COLORS` default to `'true'` so tools like Vite, esbuild, and vitest render colored output in the panel. - -## Session Lifecycle - -`startChildProcess` returns a `DevToolsChildProcessTerminalSession`: - -```ts -// Terminate (SIGTERM, then SIGKILL on timeout) -await session.terminate() - -// Kill + re-spawn with the same execute options -await session.restart() - -// Access the underlying Node.js ChildProcess -const cp = session.getChildProcess() -``` - -The session also carries a streaming `buffer` / `stream` of output chunks, plus a `status` field (`'running' | 'stopped' | 'error'`) that updates automatically. - -## Registering an External Session - -If you manage the process yourself (e.g. a long-running worker that predates your devtool), register it via `ctx.terminals.register`: - -```ts -ctx.terminals.register({ - id: 'my-devtool:worker', - title: 'Worker', - icon: 'ph:gear-duotone', - status: 'running', - buffer: [], // fill with existing output - stream: myReadableStream, // ReadableStream -}) -``` - -Emit chunks by pushing to the stream the host subscribed to — the Terminals panel will render them as they arrive. - -## Combining with Launcher Docks - -Launchers pair naturally with terminals: the launcher button kicks off a child process, and the panel surfaces its live output. See the [Dock System → Launcher](./dock-system#launcher) section. - -```ts -ctx.docks.register({ - id: 'my-devtool:setup', - title: 'My Setup', - icon: 'ph:rocket-launch-duotone', - type: 'launcher', - launcher: { - title: 'Run Dev Server', - onLaunch: async () => { - await ctx.terminals.startChildProcess( - { command: 'npm', args: ['run', 'dev'] }, - { id: 'my-devtool:dev', title: 'npm run dev', icon: 'ph:terminal-duotone' }, - ) - }, - }, -}) -``` - -## Events - -```ts -ctx.terminals.events.on('terminal:session:updated', (session) => { - console.log(session.id, session.status) -}) -``` - -Output chunks aren't delivered as host events anymore — terminals now use the [streaming](./streaming) channel `devframe:terminals` keyed by session id. From the browser: - -```ts -const reader = rpc.streaming.subscribe( - 'devframe:terminals', - sessionId, -) -for await (const chunk of reader) writeToTerminal(chunk) -``` - -A server-side bridge inside `DevToolsTerminalHost` pipes each session's `ReadableStream` straight into the channel — you don't need to wire anything yourself unless you want a custom client-side renderer. - -## Inspection - -```ts -for (const session of ctx.terminals.sessions.values()) { - console.log(session.id, session.title, session.status) -} -``` - -`ctx.terminals.sessions` is a live `Map` — handy for diagnostics, testing, and for building custom terminal UIs that mirror the built-in panel. diff --git a/devframe/examples/devframe-files-inspector/README.md b/devframe/examples/devframe-files-inspector/README.md index 30b0e494..792adc4f 100644 --- a/devframe/examples/devframe-files-inspector/README.md +++ b/devframe/examples/devframe-files-inspector/README.md @@ -27,7 +27,7 @@ pnpm -C examples/devframe-files-inspector run test # E2E tests | Path | Purpose | |------|---------| | `src/devtool.ts` | The single `DevtoolDefinition` consumed by every adapter. | -| `src/plugin.ts` | `createKitPlugin(devtool)` re-export for `@vitejs/devtools`. | +| `src/plugin.ts` | `createPluginFromDevframe(devtool)` re-export for `@vitejs/devtools`. | | `src/client/` | Preact SPA: `index.html`, `main.tsx`, `app.tsx`, `routes/*`, `vite.config.ts`. | | `bin.mjs` | `createCli(devtool).parse()` — exposes `dev`, `build`, `spa`, `mcp`. | | `tests/` | E2E tests for dev server, static build, and kit plugin shape. | diff --git a/devframe/examples/devframe-files-inspector/package.json b/devframe/examples/devframe-files-inspector/package.json index 174fbc1d..5e728b90 100644 --- a/devframe/examples/devframe-files-inspector/package.json +++ b/devframe/examples/devframe-files-inspector/package.json @@ -15,6 +15,7 @@ "test": "vitest run" }, "dependencies": { + "@vitejs/devtools-kit": "workspace:*", "devframe": "workspace:*", "preact": "catalog:frontend", "tinyglobby": "catalog:deps" diff --git a/devframe/examples/devframe-files-inspector/src/devtool.ts b/devframe/examples/devframe-files-inspector/src/devtool.ts index 75481b2e..02ac8410 100644 --- a/devframe/examples/devframe-files-inspector/src/devtool.ts +++ b/devframe/examples/devframe-files-inspector/src/devtool.ts @@ -35,14 +35,5 @@ export default defineDevtool({ }, snapshot: true, })) - - ctx.views.hostStatic(BASE_PATH, distDir) - ctx.docks.register({ - id: 'devframe-files-inspector', - title: 'Files Inspector', - icon: 'ph:folder-open-duotone', - type: 'iframe', - url: BASE_PATH, - }) }, }) diff --git a/devframe/examples/devframe-files-inspector/src/plugin.ts b/devframe/examples/devframe-files-inspector/src/plugin.ts index 9c2281f9..5d3e3873 100644 --- a/devframe/examples/devframe-files-inspector/src/plugin.ts +++ b/devframe/examples/devframe-files-inspector/src/plugin.ts @@ -1,4 +1,4 @@ -import { createKitPlugin } from 'devframe/adapters/kit' +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' import devtool from './devtool' -export default createKitPlugin(devtool) +export default createPluginFromDevframe(devtool) diff --git a/devframe/examples/devframe-files-inspector/tests/_utils.ts b/devframe/examples/devframe-files-inspector/tests/_utils.ts index af047b55..4a343532 100644 --- a/devframe/examples/devframe-files-inspector/tests/_utils.ts +++ b/devframe/examples/devframe-files-inspector/tests/_utils.ts @@ -72,8 +72,9 @@ export async function startInspectorServer( const h3Host = createH3DevToolsHost({ origin, appName: devtool.id, - mount: (base, dir) => - app.use(base, fromNodeMiddleware(sirv(dir, { dev: true, single: true }))), + mount: (base, dir) => { + app.use(base, fromNodeMiddleware(sirv(dir, { dev: true, single: true }))) + }, }) const ctx = await createHostContext({ cwd, mode: 'dev', host: h3Host }) diff --git a/devframe/examples/devframe-files-inspector/tests/kit-plugin.test.ts b/devframe/examples/devframe-files-inspector/tests/kit-plugin.test.ts index bbe8ec12..d6ad2fd4 100644 --- a/devframe/examples/devframe-files-inspector/tests/kit-plugin.test.ts +++ b/devframe/examples/devframe-files-inspector/tests/kit-plugin.test.ts @@ -1,5 +1,6 @@ -import type { DevToolsViewIframe } from 'devframe/types' -import { createH3DevToolsHost, createHostContext } from 'devframe/node' +import type { DevToolsViewIframe } from '@vitejs/devtools-kit' +import { createKitContext } from '@vitejs/devtools-kit/node' +import { createH3DevToolsHost } from 'devframe/node' import { describe, expect, it, vi } from 'vitest' import devtool from '../src/devtool' import kitPlugin from '../src/plugin' @@ -7,7 +8,7 @@ import kitPlugin from '../src/plugin' describe('kit-plugin (Vite DevTools dock surface)', () => { it('exposes the expected Vite plugin shape', () => { expect(kitPlugin.name).toBe('devframe:devframe-files-inspector') - expect(typeof kitPlugin.devtools.setup).toBe('function') + expect(typeof kitPlugin.devtools!.setup).toBe('function') }) it('registers a dock entry, both RPC functions, and mounts static UI on setup', async () => { @@ -17,9 +18,9 @@ describe('kit-plugin (Vite DevTools dock surface)', () => { appName: devtool.id, mount, }) - const ctx = await createHostContext({ cwd: process.cwd(), mode: 'dev', host }) + const ctx = await createKitContext({ cwd: process.cwd(), mode: 'dev', host }) - await kitPlugin.devtools.setup!(ctx) + await kitPlugin.devtools!.setup!(ctx) expect(ctx.rpc.definitions.has('devframe-files-inspector:get-cwd')).toBe(true) expect(ctx.rpc.definitions.has('devframe-files-inspector:list-files')).toBe(true) diff --git a/devframe/examples/devframe-streaming-chat/src/devtool.ts b/devframe/examples/devframe-streaming-chat/src/devtool.ts index 10f526ef..9dd4f033 100644 --- a/devframe/examples/devframe-streaming-chat/src/devtool.ts +++ b/devframe/examples/devframe-streaming-chat/src/devtool.ts @@ -212,14 +212,5 @@ export default defineDevtool({ }) }, })) - - ctx.views.hostStatic(BASE_PATH, distDir) - ctx.docks.register({ - id: 'devframe-streaming-chat', - title: 'Streaming Chat', - icon: 'ph:chat-circle-dots-duotone', - type: 'iframe', - url: BASE_PATH, - }) }, }) diff --git a/devframe/packages/devframe/package.json b/devframe/packages/devframe/package.json index b3dfa510..cf1166d0 100644 --- a/devframe/packages/devframe/package.json +++ b/devframe/packages/devframe/package.json @@ -24,11 +24,11 @@ "./adapters/cli": "./dist/adapters/cli.mjs", "./adapters/dev": "./dist/adapters/dev.mjs", "./adapters/embedded": "./dist/adapters/embedded.mjs", - "./adapters/kit": "./dist/adapters/kit.mjs", "./adapters/mcp": "./dist/adapters/mcp.mjs", "./adapters/vite": "./dist/adapters/vite.mjs", "./client": "./dist/client/index.mjs", "./constants": "./dist/constants.mjs", + "./internal": "./dist/internal/index.mjs", "./node": "./dist/node/index.mjs", "./recipes/open-helpers": "./dist/recipes/open-helpers.mjs", "./rpc": "./dist/rpc/index.mjs", @@ -88,7 +88,6 @@ "whenexpr": "catalog:deps" }, "inlinedDependencies": { - "acorn": "8.16.0", "bundle-name": "4.1.0", "default-browser": "5.4.0", "default-browser-id": "5.0.1", @@ -99,14 +98,12 @@ "is-in-ssh": "1.0.0", "is-inside-container": "1.0.0", "is-wsl": "3.1.0", - "mlly": "1.8.2", "obug": "2.1.1", "open": "11.0.0", "p-limit": "7.3.0", "perfect-debounce": "2.1.0", "powershell-utils": "0.1.0", "run-applescript": "7.1.0", - "tinyexec": "1.1.2", "ua-parser-modern": "0.1.1", "whenexpr": "0.1.2", "wsl-utils": "0.3.1", diff --git a/devframe/packages/devframe/src/adapters/kit.ts b/devframe/packages/devframe/src/adapters/kit.ts deleted file mode 100644 index 75dfc37f..00000000 --- a/devframe/packages/devframe/src/adapters/kit.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { DevtoolDefinition } from '../types/devtool' - -export interface CreateKitPluginOptions { - /** - * Optional plugin name override. Defaults to `devframe:`. - */ - name?: string -} - -export interface KitPlugin { - name: string - devtools: { - setup: DevtoolDefinition['setup'] - capabilities?: DevtoolDefinition['capabilities'] - } -} - -/** - * Produce a Vite plugin object that Kit's plugin-scan picks up via - * `Plugin.devtools`. - */ -export function createKitPlugin(d: DevtoolDefinition, options: CreateKitPluginOptions = {}): KitPlugin { - return { - name: options.name ?? `devframe:${d.id}`, - devtools: { - setup: d.setup, - capabilities: d.capabilities, - }, - } -} diff --git a/devframe/packages/devframe/src/adapters/vite.ts b/devframe/packages/devframe/src/adapters/vite.ts index 8bc7aacd..558a7d29 100644 --- a/devframe/packages/devframe/src/adapters/vite.ts +++ b/devframe/packages/devframe/src/adapters/vite.ts @@ -26,7 +26,8 @@ export interface DevframeVitePlugin { * pulling the full Vite DevTools Kit. * * Note: this does not yet spin up the RPC WS server — for the full - * RPC path, use `createKitPlugin` alongside `@vitejs/devtools`, or the + * RPC path, use `createPluginFromDevframe` from + * `@vitejs/devtools-kit/node` alongside `@vitejs/devtools`, or the * standalone `createCli`. */ export function createVitePlugin(d: DevtoolDefinition, options: CreateVitePluginOptions = {}): DevframeVitePlugin { diff --git a/devframe/packages/devframe/src/client/index.ts b/devframe/packages/devframe/src/client/index.ts index 77862cf9..2f09828b 100644 --- a/devframe/packages/devframe/src/client/index.ts +++ b/devframe/packages/devframe/src/client/index.ts @@ -1,5 +1,3 @@ -export * from './context' -export * from './docks' export * from './rpc' export { getDevToolsRpcClient as connectDevtool } from './rpc' export * from './rpc-streaming' diff --git a/devframe/packages/devframe/src/client/rpc-ws.ts b/devframe/packages/devframe/src/client/rpc-ws.ts index 61183d34..4fab9854 100644 --- a/devframe/packages/devframe/src/client/rpc-ws.ts +++ b/devframe/packages/devframe/src/client/rpc-ws.ts @@ -1,6 +1,5 @@ import type { ConnectionMeta, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, EventEmitter } from 'devframe/types' -import type { DevToolsClientRpcHost, RpcClientEvents } from './docks' -import type { DevToolsRpcClientMode, DevToolsRpcClientOptions } from './rpc' +import type { DevToolsClientRpcHost, DevToolsRpcClientMode, DevToolsRpcClientOptions, RpcClientEvents } from './rpc' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' import { promiseWithResolver } from 'devframe/utils/promise' diff --git a/devframe/packages/devframe/src/client/rpc.ts b/devframe/packages/devframe/src/client/rpc.ts index 893a569f..a81e2cbb 100644 --- a/devframe/packages/devframe/src/client/rpc.ts +++ b/devframe/packages/devframe/src/client/rpc.ts @@ -1,8 +1,7 @@ import type { BirpcOptions, BirpcReturn } from 'birpc' -import type { RpcCacheOptions } from 'devframe/rpc' +import type { RpcCacheOptions, RpcFunctionsCollector } from 'devframe/rpc' import type { WsRpcChannelOptions } from 'devframe/rpc/transports/ws-client' import type { ConnectionMeta, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, EventEmitter, RpcSharedStateHost } from 'devframe/types' -import type { DevToolsClientRpcHost, DevToolsRpcContext, RpcClientEvents } from './docks' import type { RpcStreamingClientHost } from './rpc-streaming' import { DEVTOOLS_CONNECTION_META_FILENAME, @@ -15,6 +14,19 @@ import { createStaticRpcClientMode } from './rpc-static' import { createRpcStreamingClientHost } from './rpc-streaming' import { createWsRpcClientMode } from './rpc-ws' +export interface DevToolsRpcContext { + /** + * The RPC client to interact with the server + */ + readonly rpc: DevToolsRpcClient +} + +export type DevToolsClientRpcHost = RpcFunctionsCollector + +export interface RpcClientEvents { + 'rpc:is-trusted:updated': (isTrusted: boolean) => void +} + const CONNECTION_META_KEY = '__VITE_DEVTOOLS_CONNECTION_META__' const CONNECTION_AUTH_TOKEN_KEY = '__VITE_DEVTOOLS_CONNECTION_AUTH_TOKEN__' diff --git a/devframe/packages/devframe/src/constants.ts b/devframe/packages/devframe/src/constants.ts index f9f26987..c04f0b3f 100644 --- a/devframe/packages/devframe/src/constants.ts +++ b/devframe/packages/devframe/src/constants.ts @@ -1,5 +1,3 @@ -import type { DevToolsDockEntryCategory, DevToolsDocksUserSettings } from './types' - // DevTools runtime routes and static output conventions. export const DEVTOOLS_MOUNT_PATH = '/__devtools/' export const DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH = '/__devtools' @@ -12,27 +10,8 @@ export const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID = '/__devtools-client-imports.js' export const DEVTOOLS_RPC_DUMP_DIRNAME = '__rpc-dump' /** - * URL fragment / query parameter name carrying the {@link RemoteConnectionInfo} - * descriptor injected into remote-UI iframe dock URLs. + * URL fragment / query parameter name carrying the remote dock + * connection descriptor (defined as `RemoteConnectionInfo` in + * `@vitejs/devtools-kit`) injected into remote-UI iframe dock URLs. */ export const REMOTE_CONNECTION_KEY = 'vite-devtools-kit-connection' - -export const DEFAULT_CATEGORIES_ORDER: Record = { - '~viteplus': -1000, - 'default': 0, - 'app': 100, - 'framework': 200, - 'web': 300, - 'advanced': 400, - '~builtin': 1000, -} satisfies Record - -export const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings = () => ({ - docksHidden: [], - docksCategoriesHidden: [], - docksPinned: [], - docksCustomOrder: {}, - showIframeAddressBar: false, - closeOnOutsideClick: false, - commandShortcuts: {}, -}) diff --git a/devframe/packages/devframe/src/define.ts b/devframe/packages/devframe/src/define.ts index b82ec8ed..83e521f9 100644 --- a/devframe/packages/devframe/src/define.ts +++ b/devframe/packages/devframe/src/define.ts @@ -1,12 +1,8 @@ -import type { DevToolsNodeContext, DevToolsServerCommandInput, JsonRenderSpec } from 'devframe/types' +import type { DevToolsNodeContext, JsonRenderSpec } from 'devframe/types' import { createDefineWrapperWithContext } from 'devframe/rpc' export const defineRpcFunction = createDefineWrapperWithContext() -export function defineCommand(command: DevToolsServerCommandInput): DevToolsServerCommandInput { - return command -} - export function defineJsonRenderSpec(spec: JsonRenderSpec): JsonRenderSpec { return spec } diff --git a/devframe/packages/devframe/src/internal/index.ts b/devframe/packages/devframe/src/internal/index.ts new file mode 100644 index 00000000..ae710c45 --- /dev/null +++ b/devframe/packages/devframe/src/internal/index.ts @@ -0,0 +1,26 @@ +/** + * Reserved for `@vitejs/devtools-kit` and other first-party adapters + * that reach into devframe's private machinery (currently the + * remote-dock token bridge required by the relocated `DocksHost`). + * + * End users should not import from this subpath. The surface is + * unstable and may change without a major bump. + * + * @internal + */ + +export { + normalizeBasePath, + resolveBasePath, +} from '../adapters/_shared' + +export { + getInternalContext, + internalContextMap, +} from '../node/context-internal' + +export type { + DevToolsInternalContext, + InternalAnonymousAuthStorage, + RemoteTokenRecord, +} from '../node/context-internal' diff --git a/devframe/packages/devframe/src/node/context-utils.ts b/devframe/packages/devframe/src/node/context-utils.ts deleted file mode 100644 index 7303a2c9..00000000 --- a/devframe/packages/devframe/src/node/context-utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { ClientScriptEntry } from 'devframe/types' -import { toDataURL } from 'mlly' - -export const ContextUtils = { - createSimpleClientScript(fn: string | ((ctx: any) => void)): ClientScriptEntry { - const code = `const fn = ${fn.toString()}; export default fn` - return { - importFrom: toDataURL(code), - importName: 'default', - } - }, -} diff --git a/devframe/packages/devframe/src/node/context.ts b/devframe/packages/devframe/src/node/context.ts index e4bcab18..8558454b 100644 --- a/devframe/packages/devframe/src/node/context.ts +++ b/devframe/packages/devframe/src/node/context.ts @@ -1,16 +1,10 @@ import type { RpcFunctionDefinitionAny } from 'devframe/rpc' import type { DevToolsHost, DevToolsNodeContext, JsonRenderer, JsonRenderSpec } from 'devframe/types' -import { debounce } from 'perfect-debounce' import { diagnostics as rpcDiagnostics } from '../rpc/diagnostics' -import { ContextUtils } from './context-utils' -import { diagnostics as devframeDiagnostics, logger } from './diagnostics' +import { diagnostics as devframeDiagnostics } from './diagnostics' import { DevToolsAgentHost } from './host-agent' -import { DevToolsCommandsHost } from './host-commands' import { DevToolsDiagnosticsHost } from './host-diagnostics' -import { DevToolsDockHost } from './host-docks' import { RpcFunctionsHost } from './host-functions' -import { DevToolsMessagesHost } from './host-messages' -import { DevToolsTerminalHost } from './host-terminals' import { DevToolsViewHost } from './host-views' import { BUILTIN_AGENT_RPC } from './rpc' @@ -28,11 +22,13 @@ export interface CreateHostContextOptions { } /** - * Framework-neutral core of the DevTools node context. Wires the six - * host subsystems, the JSON-render factory, and the standard shared-state - * bookkeeping. Framework-specific extensions (Vite plugin scan, Vite - * config/server fields, built-in RPCs like `open-in-editor`) layer on - * top via wrapper functions such as kit's `createDevToolsContext`. + * Framework-neutral core of the DevTools node context. Wires the RPC + * host, view (HTTP file-serving) host, diagnostics, and agent + * subsystems plus the JSON-render factory. Hub-level subsystems + * (`docks`, `terminals`, `messages`, `commands`) are owned by + * `@vitejs/devtools-kit` — its `createKitContext` wraps this and + * attaches them when the devtool is mounted into a multi-integration + * hub. */ export async function createHostContext(options: CreateHostContextOptions): Promise { const { cwd, workspaceRoot = cwd, mode, host, builtinRpcDeclarations = [] } = options @@ -43,45 +39,19 @@ export async function createHostContext(options: CreateHostContextOptions): Prom mode, host, rpc: undefined!, - docks: undefined!, views: undefined!, - terminals: undefined!, - messages: undefined!, diagnostics: undefined!, - commands: undefined!, agent: undefined!, - utils: ContextUtils, createJsonRenderer: undefined!, } as unknown as DevToolsNodeContext const rpcHost = new RpcFunctionsHost(context) - const docksHost = new DevToolsDockHost(context) const viewsHost = new DevToolsViewHost(context) - const terminalsHost = new DevToolsTerminalHost(context) - const messagesHost = new DevToolsMessagesHost(context) const diagnosticsHost = new DevToolsDiagnosticsHost(context, [devframeDiagnostics, rpcDiagnostics]) - const commandsHost = new DevToolsCommandsHost(context) context.rpc = rpcHost - context.docks = docksHost context.views = viewsHost - context.terminals = terminalsHost - context.messages = messagesHost context.diagnostics = diagnosticsHost - context.commands = commandsHost - // Deprecated `ctx.logs` getter — emits DF0018 once per process when accessed. - let _warnedLogs = false - Object.defineProperty(context, 'logs', { - get() { - if (!_warnedLogs) { - _warnedLogs = true - logger.DF0018().log() - } - return messagesHost - }, - enumerable: false, - configurable: true, - }) // Agent host must be constructed after `rpcHost` so it can subscribe // to `onChanged` — it auto-discovers RPC functions flagged with // the `agent` field. @@ -121,41 +91,5 @@ export async function createHostContext(options: CreateHostContextOptions): Prom rpcHost.register(fn) } - await docksHost.init() - - const docksSharedState = await rpcHost.sharedState.get('devframe:docks', { initialValue: [] }) - - docksHost.events.on('dock:entry:updated', debounce(() => { - docksSharedState.mutate(() => context.docks.values()) - }, mode === 'build' ? 0 : 10)) - - terminalsHost.events.on('terminal:session:updated', debounce(() => { - rpcHost.broadcast({ - method: 'devframe:terminals:updated', - args: [], - }) - docksSharedState.mutate(() => context.docks.values()) - }, mode === 'build' ? 0 : 10)) - - const debouncedMessagesUpdate = debounce(() => { - rpcHost.broadcast({ - method: 'devframe:messages:updated', - args: [], - }) - docksSharedState.mutate(() => context.docks.values()) - }, mode === 'build' ? 0 : 10) - - messagesHost.events.on('message:added', () => debouncedMessagesUpdate()) - messagesHost.events.on('message:updated', () => debouncedMessagesUpdate()) - messagesHost.events.on('message:removed', () => debouncedMessagesUpdate()) - messagesHost.events.on('message:cleared', () => debouncedMessagesUpdate()) - - const commandsSharedState = await rpcHost.sharedState.get('devframe:commands', { initialValue: [] }) - const debouncedCommandsSync = debounce(() => { - commandsSharedState.mutate(() => commandsHost.list()) - }, mode === 'build' ? 0 : 10) - commandsHost.events.on('command:registered', () => debouncedCommandsSync()) - commandsHost.events.on('command:unregistered', () => debouncedCommandsSync()) - return context } diff --git a/devframe/packages/devframe/src/node/diagnostics.ts b/devframe/packages/devframe/src/node/diagnostics.ts index 967036d0..769bf15b 100644 --- a/devframe/packages/devframe/src/node/diagnostics.ts +++ b/devframe/packages/devframe/src/node/diagnostics.ts @@ -5,22 +5,6 @@ import { ansiFormatter } from 'logs-sdk/formatters/ansi' export const diagnostics = defineDiagnostics({ docsBase: 'https://devfra.me/errors', codes: { - DF0001: { - message: (p: { id: string }) => `Dock with id "${p.id}" is already registered`, - hint: 'Use the `force` parameter to overwrite an existing registration.', - }, - DF0002: { - message: 'Cannot change the id of a dock. Use register() to add new docks.', - }, - DF0003: { - message: (p: { id: string }) => `Dock with id "${p.id}" is not registered. Use register() to add new docks.`, - }, - DF0004: { - message: (p: { id: string }) => `Terminal session with id "${p.id}" already registered`, - }, - DF0005: { - message: (p: { id: string }) => `Terminal session with id "${p.id}" not registered`, - }, DF0006: { message: (p: { name: string }) => `RPC function "${p.name}" is not registered`, }, @@ -30,15 +14,6 @@ export const diagnostics = defineDiagnostics({ DF0008: { message: (p: { distDir: string }) => `distDir ${p.distDir} does not exist`, }, - DF0009: { - message: (p: { id: string }) => `Command "${p.id}" is already registered`, - }, - DF0010: { - message: 'Cannot change the id of a command. Use register() to add new commands.', - }, - DF0011: { - message: (p: { id: string }) => `Command "${p.id}" is not registered`, - }, DF0012: { message: (p: { filepath: string }) => `Failed to parse storage file: ${p.filepath}, falling back to defaults.`, level: 'warn', @@ -60,11 +35,6 @@ export const diagnostics = defineDiagnostics({ DF0017: { message: (p: { transport: string, reason: string }) => `Failed to start MCP server (${p.transport}): ${p.reason}`, }, - DF0018: { - message: '`ctx.logs` is deprecated and will be removed in a future release. Use `ctx.messages` instead.', - hint: 'Replace any access to `ctx.logs` (or `context.logs`) with `ctx.messages`. The runtime behavior is identical.', - level: 'warn', - }, DF0029: { message: (p: { channel: string, id: string, dropped: number }) => `Stream "${p.channel}#${p.id}" dropped ${p.dropped} chunk(s) after exceeding the client high-water mark.`, diff --git a/devframe/packages/devframe/src/node/index.ts b/devframe/packages/devframe/src/node/index.ts index 13766b48..49001bc8 100644 --- a/devframe/packages/devframe/src/node/index.ts +++ b/devframe/packages/devframe/src/node/index.ts @@ -3,15 +3,10 @@ export * from './auth-revoke' export * from './auth-state' export * from './context' export * from './context-internal' -export * from './context-utils' export * from './host-agent' -export * from './host-commands' export * from './host-diagnostics' -export * from './host-docks' export * from './host-functions' export * from './host-h3' -export * from './host-messages' -export * from './host-terminals' export * from './host-views' export * from './rpc-shared-state' export * from './rpc-streaming' diff --git a/devframe/packages/devframe/src/types/context.ts b/devframe/packages/devframe/src/types/context.ts index ab69eb42..e6df5680 100644 --- a/devframe/packages/devframe/src/types/context.ts +++ b/devframe/packages/devframe/src/types/context.ts @@ -1,10 +1,7 @@ import type { DevToolsAgentHost } from './agent' -import type { DevToolsCommandsHost } from './commands' import type { DevToolsDiagnosticsHost } from './diagnostics' -import type { ClientScriptEntry, DevToolsDockHost, JsonRenderer, JsonRenderSpec } from './docks' import type { DevToolsHost } from './host' -import type { DevToolsMessagesHost } from './messages' -import type { DevToolsTerminalHost } from './terminals' +import type { JsonRenderer, JsonRenderSpec } from './json-render' import type { DevToolsViewHost } from './views' export interface DevToolsCapabilities { @@ -12,6 +9,13 @@ export interface DevToolsCapabilities { views?: boolean } +/** + * Framework-neutral node context — RPC + diagnostics + agent + the + * view-host (HTTP file-serving). Hub-level subsystems (docks, + * terminals, messages, commands) are not part of this surface; they are + * added by `@vitejs/devtools-kit`'s `createKitContext` when the devtool + * is mounted into a multi-integration hub. + */ export interface DevToolsNodeContext { readonly workspaceRoot: string readonly cwd: string @@ -36,26 +40,12 @@ export interface DevToolsNodeContext { */ host: DevToolsHost rpc: import('./rpc').RpcFunctionsHost - docks: DevToolsDockHost views: DevToolsViewHost - utils: DevToolsNodeUtils - terminals: DevToolsTerminalHost - /** - * User-facing message subsystem — toast notifications and the - * "Messages & Notifications" dock panel. Plugins call - * `ctx.messages.add({ ... })` to surface activity to users. - */ - messages: DevToolsMessagesHost - /** - * @deprecated Use `ctx.messages` instead. Will be removed in a future release. - */ - readonly logs: DevToolsMessagesHost /** * Structured diagnostics host — wraps `logs-sdk` and lets integrations * register their own coded errors/warnings into the shared logger. */ diagnostics: DevToolsDiagnosticsHost - commands: DevToolsCommandsHost /** * Agent host — aggregates the agent-exposed surface of this devtool. * @@ -68,16 +58,6 @@ export interface DevToolsNodeContext { createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer } -export interface DevToolsNodeUtils { - /** - * Create a simple client script from a function or stringified code. - * - * @deprecated testing helper; prefer a proper importable module. - * @experimental - */ - createSimpleClientScript: (fn: string | ((ctx: any) => void)) => ClientScriptEntry -} - export interface ConnectionMeta { backend: 'websocket' | 'static' websocket?: number | string @@ -91,11 +71,3 @@ export interface ConnectionMeta { */ jsonSerializableMethods?: string[] } - -export interface RemoteConnectionInfo extends ConnectionMeta { - backend: 'websocket' - websocket: string - v: 1 - authToken: string - origin: string -} diff --git a/devframe/packages/devframe/src/types/index.ts b/devframe/packages/devframe/src/types/index.ts index 00589589..90331373 100644 --- a/devframe/packages/devframe/src/types/index.ts +++ b/devframe/packages/devframe/src/types/index.ts @@ -1,15 +1,11 @@ export * from './agent' -export * from './commands' export * from './context' export * from './devtool' export * from './diagnostics' -export * from './docks' export * from './events' export * from './host' -export * from './messages' +export * from './json-render' export * from './rpc' export * from './rpc-augments' -export * from './settings' -export * from './terminals' export * from './utils' export * from './views' diff --git a/devframe/packages/devframe/src/types/json-render.ts b/devframe/packages/devframe/src/types/json-render.ts new file mode 100644 index 00000000..336af7cc --- /dev/null +++ b/devframe/packages/devframe/src/types/json-render.ts @@ -0,0 +1,29 @@ +export interface JsonRenderElement { + type: string + props?: Record + children?: string[] + /** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */ + on?: Record + /** json-render visibility condition */ + visible?: unknown + /** json-render repeat binding */ + repeat?: unknown + /** Allow additional json-render element fields */ + [key: string]: unknown +} + +export interface JsonRenderSpec { + root: string + elements: Record + /** Initial client-side state model for $state/$bindState expressions */ + state?: Record +} + +export interface JsonRenderer { + /** Replace the entire spec */ + updateSpec: (spec: JsonRenderSpec) => void | Promise + /** Update json-render state values (shallow merge into spec.state) */ + updateState: (state: Record) => void | Promise + /** Internal: shared state key used by the client to subscribe */ + readonly _stateKey: string +} diff --git a/devframe/packages/devframe/tsdown.config.ts b/devframe/packages/devframe/tsdown.config.ts index 3b9de877..6ed92ef2 100644 --- a/devframe/packages/devframe/tsdown.config.ts +++ b/devframe/packages/devframe/tsdown.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ 'types/index': 'src/types/index.ts', 'node/index': 'src/node/index.ts', 'constants': 'src/constants.ts', + 'internal/index': 'src/internal/index.ts', 'utils/events': 'src/utils/events.ts', 'utils/human-id': 'src/utils/human-id.ts', 'utils/nanoid': 'src/utils/nanoid.ts', @@ -23,7 +24,6 @@ export default defineConfig({ 'adapters/dev': 'src/adapters/dev.ts', 'adapters/build': 'src/adapters/build.ts', 'adapters/vite': 'src/adapters/vite.ts', - 'adapters/kit': 'src/adapters/kit.ts', 'adapters/embedded': 'src/adapters/embedded.ts', 'adapters/mcp': 'src/adapters/mcp.ts', 'client/index': 'src/client/index.ts', diff --git a/devframe/skills/devframe/SKILL.md b/devframe/skills/devframe/SKILL.md index bb052a1e..4dec9e10 100644 --- a/devframe/skills/devframe/SKILL.md +++ b/devframe/skills/devframe/SKILL.md @@ -1,20 +1,25 @@ --- name: devframe description: > - Use when building devtools with devframe — the framework-neutral - foundation that powers Vite DevTools. Covers DevtoolDefinition, - picking the right adapter (cli / build / spa / vite / kit / embedded / - mcp), designing RPC contracts, registering docks / commands / logs / - terminals, exposing an agent-native surface over MCP, and wiring the - author's SPA client. Triggers on `devframe` imports, `defineDevtool`, - `createCli`, `createMcpServer`, `connectDevtool`, and on migrations of - existing inspectors (eslint-config-inspector, unocss-inspector, - node-modules-inspector-style tools) to devframe. + Use when building one devtool integration with devframe — the + portable, framework-neutral container for a single tool. Covers + DevtoolDefinition, picking the right deployment adapter + (cli / build / spa / vite / embedded / mcp), designing RPC + contracts, exposing an agent-native surface over MCP, and wiring + the author's SPA client. Hub-only concerns (docks, terminals, + commands, the unified messages dock) belong to + `@vitejs/devtools-kit` — see the `vite-devtools-kit` skill for + those. Triggers on `devframe` imports, `defineDevtool`, + `createCli`, `createMcpServer`, `connectDevtool`, and on + migrations of existing inspectors (eslint-config-inspector, + unocss-inspector, node-modules-inspector-style tools) to devframe. --- # devframe skill -A devtool built on devframe is a **single `DevtoolDefinition`** plus an author-provided SPA. Use one of seven adapters to ship it. `devframe` must not depend on Vite or any `@vitejs/*` package — it's the lowest-level layer in the monorepo, and the Kit / core packages build on top. +**Devframe is the container for one devtool integration, portable across viewers.** A devtool built on devframe is a single `DevtoolDefinition` plus an author-provided SPA — the same definition deploys as a standalone CLI, a static report, an embedded SPA, an MCP server, or as a dock entry inside the Vite DevTools hub via `createPluginFromDevframe`. + +Devframe deliberately stops at the boundary of one tool. Anything that only matters across multiple integrations — docks, terminals, command palette, cross-tool toasts — lives in `@vitejs/devtools-kit`, the hub layer. `devframe` must not depend on Vite, any `@vitejs/*` package, or hub-only concepts; it's the lowest-level layer in the monorepo. Full reference: [devfra.me/](https://devfra.me/). @@ -28,7 +33,7 @@ All adapter factories share the shape `createXxx(devtoolDef, options?)`. | Run the dev server programmatically (any CLI framework) | `createDevServer(def, options?)` | `devframe/adapters/dev` | | Mount a SPA in an existing Vite dev server | `createVitePlugin(def, options?)` | `devframe/adapters/vite` | | Self-contained static deploy with baked data | `createBuild(def, options?)` | `devframe/adapters/build` | -| Integrate into Vite DevTools | `createKitPlugin(def, options?)` | `devframe/adapters/kit` | +| Integrate into Vite DevTools | `createPluginFromDevframe(def, options?)` | `@vitejs/devtools-kit/node` | | Register dynamically at runtime | `createEmbedded(def, { ctx })` | `devframe/adapters/embedded` | | Expose to coding agents (MCP) | `createMcpServer(def, options?)` | `devframe/adapters/mcp` *(experimental)* | @@ -50,17 +55,12 @@ export default defineDevtool({ type: 'static', handler: () => ({ count: 42 }), })) - ctx.docks.register({ - id: 'my-inspector', - title: 'My Inspector', - icon: 'ph:magnifying-glass-duotone', - type: 'iframe', - url: '/__devtools/', - }) }, }) ``` +`setup(ctx)` registers RPC functions, shared state, diagnostics, and any other devframe-level wiring. It does **not** receive `docks` / `terminals` / `messages` / `commands` — those are hub features. When mounted into Vite DevTools via `createPluginFromDevframe(d)`, the kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`; for richer hub-side behaviour (custom-render, terminals, palette commands) pass `options.setup` to `createPluginFromDevframe`. + See `templates/counter-devtool.ts` for a runnable counter example, `templates/spa-devtool.ts` for an SPA-ready shape, and `templates/vite-client.ts` for the author's client entry. ## Namespacing @@ -75,21 +75,19 @@ See `templates/counter-devtool.ts` for a runnable counter example, `templates/sp ## DevToolsNodeContext at a glance -`setup(ctx)` receives the full server-side surface. Each host corresponds to a [docs](https://devfra.me/) page: +`setup(ctx)` receives the framework-neutral server-side surface. Each host corresponds to a [docs](https://devfra.me/) page: | Host | Purpose | |------|---------| -| `ctx.rpc` | Register RPC functions, broadcast, shared state | -| `ctx.docks` | Dock entries (iframe / action / custom-render / launcher / json-render) | +| `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels | | `ctx.views` | Serve static files via `hostStatic(base, distDir)` | -| `ctx.commands` | Command palette entries with keybindings + `when` gating | -| `ctx.messages` | Structured message entries, toasts, file / element positions | | `ctx.diagnostics` | Structured diagnostics host (logs-sdk) — register custom error codes | -| `ctx.terminals` | Spawn and stream child processes | | `ctx.agent` | Expose tools + resources to coding agents (experimental) | | `ctx.host` | Runtime abstraction — `mountStatic`, `resolveOrigin`, `getStorageDir` | | `ctx.mode` | `'dev'` or `'build'` — gate setup work per runtime | +> Hub-only hosts (`ctx.docks`, `ctx.terminals`, `ctx.messages`, `ctx.commands`) only exist when the devtool is mounted into Vite DevTools via `createPluginFromDevframe`. See the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit) for those. + ## RPC contracts ```ts @@ -240,84 +238,36 @@ Readable.fromWeb(reader.readable).pipe(targetNodeWritable) For chat-style UIs that combine both: keep the **conversation log** in shared state (survives reconnects), and use a streaming channel for **active responses**. The action that starts a response appends a placeholder to shared state; on producer close, commit the joined content back to shared state. Working example: [`devframe/examples/devframe-streaming-chat`](https://github.com/vitejs/devtools/tree/main/devframe/examples/devframe-streaming-chat). -## Dock entries +## Mounting into Vite DevTools -Five entry types: `iframe` (full panel), `action` (client script on click), `custom-render` (mount into panel DOM), `launcher` (setup card + server callback), `json-render` (UI from a JSON spec, zero client code). +A portable devframe definition is dropped into the Vite DevTools hub via `createPluginFromDevframe`: ```ts -// Iframe — most common: -ctx.docks.register({ - id: 'my-inspector', - title: 'My Inspector', - icon: 'ph:magnifying-glass-duotone', - type: 'iframe', - url: '/__my-inspector/', -}) - -ctx.views.hostStatic('/__my-inspector/', clientDist) -``` - -All entries accept `when` for conditional visibility and `badge` for short indicator text. See `/devframe/dock-system` for the full type reference. - -### Remote docks - -Set `remote: true` on an iframe dock to turn a hosted URL into a live DevFrame client. DevFrame injects an auth-approved connection descriptor into the iframe URL; on the hosted page, `connectDevtool()` parses it and returns a fully connected client — no extra wiring. Dev-mode only. - -## Commands - -```ts -import { defineCommand } from 'devframe' - -ctx.commands.register(defineCommand({ - id: 'my-inspector:clear-cache', - title: 'Clear Cache', - icon: 'ph:trash-duotone', - keybindings: [{ key: 'Mod+Shift+C' }], - when: 'clientType == embedded', - handler: async () => clearCache(), -})) +// vite.config.ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' +import myInspector from './my-inspector' + +export default { + plugins: [ + createPluginFromDevframe(myInspector, { + // Optional kit-only setup — runs after the auto-derived dock entry. + setup(kitCtx) { + kitCtx.commands.register({ + id: 'my-inspector:clear-cache', + title: 'Clear Cache', + handler: () => { /* ... */ }, + }) + }, + }), + ], +} ``` -- Two-level hierarchy (parent + `children`) max. -- Use `Mod` for platform-aware modifier (Cmd on macOS, Ctrl elsewhere). -- `ctx.commands.execute(id, ...args)` runs a command programmatically. -- `when` is a whenexpr expression — see below. - -## Logs & notifications - -```ts -// Fire-and-forget -ctx.messages.add({ message: 'Scan complete', level: 'success', notify: true }) - -// With handle for in-place updates -const handle = await ctx.messages.add({ - id: 'my-inspector:build', - message: 'Building…', - level: 'info', - status: 'loading', -}) -await handle.update({ message: 'Built', level: 'success', status: 'idle' }) -``` - -`notify: true` also renders a toast. `filePosition: { file, line, column }` makes the entry click-to-editor. `elementPosition: { selector, boundingBox }` highlights a DOM element. Re-adding with the same `id` updates the existing entry (deduplication pattern). - -## Terminals - -```ts -const session = await ctx.terminals.startChildProcess( - { command: 'vite', args: ['build', '--watch'], cwd: process.cwd() }, - { id: 'my-inspector:build', title: 'Build', icon: 'ph:terminal-duotone' }, -) - -await session.terminate() -await session.restart() -``` - -Color is enabled automatically (`FORCE_COLOR=true`). Output streams into the built-in Terminals panel and is buffered for late-joining clients. +The kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For dock variations (custom-render, launcher, action, json-render), terminals, palette commands, and toasts, use the `options.setup` hook — those APIs live on the kit-augmented context, not on the devframe-level `setup`. See the [`vite-devtools-kit` skill](../../skills/vite-devtools-kit) for the hub-side reference. ## When clauses -Gate dock / command visibility with VS Code-style expressions (evaluated by the external `whenexpr` package): +Gate kit-side dock / command visibility with VS Code-style expressions evaluated by the external `whenexpr` package. The runtime + types ship from `devframe/utils/when`, but the consumers (`when` field on docks and commands) live in the kit: ```ts when: 'clientType == embedded' @@ -387,7 +337,7 @@ const rpc = await connectDevtool() const data = await rpc.call('my-inspector:get-stats', { limit: 10 }) ``` -`connectDevtool` auto-detects the backend via `/__devtools/__connection.json`: +`connectDevtool` auto-detects the backend via `/.devtools/.connection.json`: - **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `vite-devtools-auth` BroadcastChannel. - **static** (build / spa output) — read-only, resolves calls from the baked RPC dump. @@ -420,7 +370,7 @@ At runtime, static clients look up the argument hash in the dump; misses resolve | Subcommand | Action | |------------|--------| -| *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/__devtools/` | +| *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/.devtools/` | | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) | | `spa` | Deployable SPA → `./dist-spa/` | | `mcp` | stdio MCP server (experimental) | @@ -431,22 +381,26 @@ At runtime, static clients look up the argument hash in the dump; misses resolve - Unit-test host classes with fake contexts. - Run `templates/counter-devtool.ts` under each adapter for integration coverage. -- Snapshot the build-static RPC dump (`/__devtools/__rpc-dump/index.json`) to catch accidental drift in `static` function outputs. +- Snapshot the build-static RPC dump (`/.devtools/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs. ## Further reading -All of the above has a dedicated page at [devfra.me](https://devfra.me/): +Devframe-level pages (one-tool, portable surface): - [Devtool Definition](https://devfra.me/devtool-definition) — fields, runtime flags, multi-adapter wiring -- [Adapters](https://devfra.me/adapters) — full reference for all seven adapters +- [Adapters](https://devfra.me/adapters) — full reference for all deployment adapters - [RPC](https://devfra.me/rpc) — types, schema, broadcasts, dumps - [Shared State](https://devfra.me/shared-state) — patches, events, client-side mutation - [Streaming](https://devfra.me/streaming) — chunked feeds, uploads, replay, Web/Node Streams interop -- [Dock System](https://devfra.me/dock-system) — every entry type + remote docks -- [Commands](https://devfra.me/commands) — palette, keybindings, sub-commands - [When Clauses](https://devfra.me/when-clauses) — syntax, context, type-safe wrappers -- [Messages & Notifications](https://devfra.me/messages) — entry fields, positional hints - [Structured Diagnostics](https://devfra.me/diagnostics) — coded errors via `ctx.diagnostics`, register custom codes -- [Terminals](https://devfra.me/terminals) — child processes, external sessions - [Client](https://devfra.me/client) — auth handshake, modes, discovery - [Agent-Native](https://devfra.me/agent-native) — agent field, tools/resources, MCP + Claude Desktop + +Hub-only surfaces (Vite DevTools Kit — only available when mounted into the hub): + +- [Vite DevTools Kit overview](https://devtools.vite.dev/kit/) +- [Dock System](https://devtools.vite.dev/kit/dock-system) — every entry type + remote docks +- [Commands](https://devtools.vite.dev/kit/commands) — palette, keybindings, sub-commands +- [Messages & Notifications](https://devtools.vite.dev/kit/messages) — entry fields, positional hints +- [Terminals](https://devtools.vite.dev/kit/terminals) — child processes, external sessions diff --git a/devframe/skills/devframe/templates/counter-devtool.ts b/devframe/skills/devframe/templates/counter-devtool.ts index fe6b9466..a193d80d 100644 --- a/devframe/skills/devframe/templates/counter-devtool.ts +++ b/devframe/skills/devframe/templates/counter-devtool.ts @@ -1,4 +1,6 @@ -// Smallest possible devtool. +// Smallest possible devtool. The dock entry is auto-derived from +// `id` / `name` / `icon` when this definition is mounted into Vite +// DevTools via `createPluginFromDevframe(devtool)`. import { defineDevtool, defineRpcFunction } from 'devframe' let counter = 0 @@ -18,12 +20,5 @@ export default defineDevtool({ type: 'action', handler: () => ({ count: ++counter }), })) - ctx.docks.register({ - id: 'counter', - title: 'Counter', - icon: 'ph:counter-duotone', - type: 'iframe', - url: '/counter/', - }) }, }) diff --git a/devframe/skills/devframe/templates/spa-devtool.ts b/devframe/skills/devframe/templates/spa-devtool.ts index b24c3161..632caadc 100644 --- a/devframe/skills/devframe/templates/spa-devtool.ts +++ b/devframe/skills/devframe/templates/spa-devtool.ts @@ -1,4 +1,6 @@ // Devtool with setupBrowser + SPA query-loader — deployable as a static site. +// When mounted into Vite DevTools via `createPluginFromDevframe`, the kit +// auto-derives an iframe dock from `id` / `name` / `icon`. import { defineDevtool, defineRpcFunction } from 'devframe' import * as v from 'valibot' @@ -16,13 +18,6 @@ export default defineDevtool({ return { url, verdict: 'ok' as const } }, })) - ctx.docks.register({ - id: 'my-inspector', - title: 'My Inspector', - icon: 'ph:magnifying-glass-duotone', - type: 'iframe', - url: '/my-inspector/', - }) }, setupBrowser() { // Browser-side implementation — used by the SPA adapter so the diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.d.ts deleted file mode 100644 index 2393a12a..00000000 --- a/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `devframe/adapters/kit` - */ -// #region Interfaces -export interface CreateKitPluginOptions { - name?: string; -} -export interface KitPlugin { - name: string; - devtools: { - setup: DevtoolDefinition['setup']; - capabilities?: DevtoolDefinition['capabilities']; - }; -} -// #endregion - -// #region Functions -export declare function createKitPlugin(_: DevtoolDefinition, _?: CreateKitPluginOptions): KitPlugin; -// #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.js deleted file mode 100644 index e555611a..00000000 --- a/devframe/tests/__snapshots__/tsnapi/devframe/adapters/kit.snapshot.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `devframe/adapters/kit` - */ -// #region Functions -export function createKitPlugin(_, _) {} -// #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts index 261871c6..78261078 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts @@ -2,15 +2,6 @@ * Generated by tsnapi — public API snapshot of `devframe/client` */ // #region Interfaces -export interface CommandsContext { - readonly commands: DevToolsCommandEntry[]; - readonly paletteCommands: DevToolsCommandEntry[]; - register: (_: DevToolsClientCommand | DevToolsClientCommand[]) => () => void; - execute: (_: string, ..._: any[]) => Promise; - getKeybindings: (_: string) => DevToolsCommandKeybinding[]; - settings: SharedState; - paletteOpen: boolean; -} export interface DevToolsRpcClient { events: EventEmitter; readonly isTrusted: boolean | null; @@ -46,56 +37,6 @@ export interface DevToolsRpcClientOptions { export interface DevToolsRpcContext { readonly rpc: DevToolsRpcClient; } -export interface DockEntryState { - entryMeta: DevToolsDockEntry; - readonly isActive: boolean; - domElements: { - iframe?: HTMLIFrameElement | null; - panel?: HTMLDivElement | null; - }; - events: EventEmitter; -} -export interface DockEntryStateEvents { - 'entry:activated': () => void; - 'entry:deactivated': () => void; - 'entry:updated': (_: DevToolsDockUserEntry) => void; - 'dom:panel:mounted': (_: HTMLDivElement) => void; - 'dom:iframe:mounted': (_: HTMLIFrameElement) => void; -} -export interface DockPanelStorage { - mode: 'float' | 'edge'; - width: number; - height: number; - top: number; - left: number; - position: 'left' | 'right' | 'bottom' | 'top'; - open: boolean; - inactiveTimeout: number; -} -export interface DocksContext extends DevToolsRpcContext { - readonly clientType: 'embedded' | 'standalone'; - readonly panel: DocksPanelContext; - readonly docks: DocksEntriesContext; - readonly commands: CommandsContext; - readonly when: WhenClauseContext; -} -export interface DocksEntriesContext { - selectedId: string | null; - readonly selected: DevToolsDockEntry | null; - entries: DevToolsDockEntry[]; - entryToStateMap: Map; - groupedEntries: DevToolsDockEntriesGrouped; - settings: SharedState; - getStateById: (_: string) => DockEntryState | undefined; - switchEntry: (_?: string | null) => Promise; - toggleEntry: (_: string) => Promise; -} -export interface DocksPanelContext { - store: DockPanelStorage; - isDragging: boolean; - isResizing: boolean; - readonly isVertical: boolean; -} export interface RpcClientEvents { 'rpc:is-trusted:updated': (_: boolean) => void; } @@ -106,27 +47,17 @@ export interface RpcStreamingClientHost { export interface StreamingSubscribeOptions { highWaterMark?: number; } -export interface WhenClauseContext { - readonly context: WhenContext; -} // #endregion // #region Types -export type DevToolsClientContext = DocksContext; export type DevToolsClientRpcHost = RpcFunctionsCollector; export type DevToolsRpcClientCall = BirpcReturn['$call']; export type DevToolsRpcClientCallEvent = BirpcReturn['$callEvent']; export type DevToolsRpcClientCallOptional = BirpcReturn['$callOptional']; -export type DockClientType = 'embedded' | 'standalone'; // #endregion // #region Functions export declare function connectDevtool(_?: DevToolsRpcClientOptions): Promise; export declare function createRpcStreamingClientHost(_: DevToolsRpcClient): RpcStreamingClientHost; -export declare function getDevToolsClientContext(): DevToolsClientContext | undefined; export declare function getDevToolsRpcClient(_?: DevToolsRpcClientOptions): Promise; -// #endregion - -// #region Variables -export declare const CLIENT_CONTEXT_KEY: string; // #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.js index 8f0526e8..812ca3ff 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.js +++ b/devframe/tests/__snapshots__/tsnapi/devframe/client.snapshot.js @@ -4,10 +4,5 @@ // #region Functions export async function connectDevtool(_) {} export function createRpcStreamingClientHost(_) {} -export function getDevToolsClientContext() {} export async function getDevToolsRpcClient(_) {} -// #endregion - -// #region Variables -export var CLIENT_CONTEXT_KEY /* const */ // #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts index 448e4b16..089b9731 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts @@ -2,8 +2,6 @@ * Generated by tsnapi — public API snapshot of `devframe/constants` */ // #region Variables -export declare const DEFAULT_CATEGORIES_ORDER: Record; -export declare const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings; export declare const DEVTOOLS_CONNECTION_META_FILENAME: string; export declare const DEVTOOLS_DIRNAME: string; export declare const DEVTOOLS_DOCK_IMPORTS_FILENAME: string; diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js index 1c91fc1f..bc40e7f8 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js +++ b/devframe/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js @@ -2,8 +2,6 @@ * Generated by tsnapi — public API snapshot of `devframe/constants` */ // #region Variables -export var DEFAULT_CATEGORIES_ORDER /* const */ -export var DEFAULT_STATE_USER_SETTINGS /* const */ export var DEVTOOLS_CONNECTION_META_FILENAME /* const */ export var DEVTOOLS_DIRNAME /* const */ export var DEVTOOLS_DOCK_IMPORTS_FILENAME /* const */ diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index d4b4787a..d14c446d 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -2,7 +2,6 @@ * Generated by tsnapi — public API snapshot of `devframe` */ // #region Functions -export declare function defineCommand(_: DevToolsServerCommandInput): DevToolsServerCommandInput; export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; // #endregion @@ -18,7 +17,6 @@ export { AgentResourceContent } export { AgentResourceInput } export { AgentTool } export { AgentToolInput } -export { ClientScriptEntry } export { ConnectionMeta } export { defineDevtool } export { DevtoolBrowserContext } @@ -29,69 +27,19 @@ export { DevtoolRuntime } export { DevToolsAgentHost } export { DevToolsAgentHostEvents } export { DevToolsCapabilities } -export { DevToolsChildProcessExecuteOptions } -export { DevToolsChildProcessTerminalSession } -export { DevToolsClientCommand } -export { DevToolsCommandBase } -export { DevToolsCommandEntry } -export { DevToolsCommandHandle } -export { DevToolsCommandKeybinding } -export { DevToolsCommandShortcutOverrides } -export { DevToolsCommandsHost } -export { DevToolsCommandsHostEvents } export { DevToolsDiagnosticsDefinition } export { DevToolsDiagnosticsHost } export { DevToolsDiagnosticsLogger } -export { DevToolsDockEntriesGrouped } -export { DevToolsDockEntry } -export { DevToolsDockEntryBase } -export { DevToolsDockEntryCategory } -export { DevToolsDockEntryIcon } -export { DevToolsDockHost } -export { DevToolsDocksUserSettings } -export { DevToolsDockUserEntry } export { DevtoolSetupInfo } export { DevToolsHost } -export { DevToolsLogElementPosition } -export { DevToolsLogEntry } -export { DevToolsLogEntryFrom } -export { DevToolsLogEntryInput } -export { DevToolsLogFilePosition } -export { DevToolsLogHandle } -export { DevToolsLogLevel } -export { DevToolsLogsClient } -export { DevToolsLogsHost } -export { DevToolsMessageElementPosition } -export { DevToolsMessageEntry } -export { DevToolsMessageEntryFrom } -export { DevToolsMessageEntryInput } -export { DevToolsMessageFilePosition } -export { DevToolsMessageHandle } -export { DevToolsMessageLevel } -export { DevToolsMessagesClient } -export { DevToolsMessagesHost } export { DevToolsNodeContext } export { DevToolsNodeRpcSession } export { DevToolsNodeRpcSessionMeta } -export { DevToolsNodeUtils } export { DevtoolSpaOptions } export { DevToolsRpcClientFunctions } export { DevToolsRpcServerFunctions } export { DevToolsRpcSharedStates } -export { DevToolsServerCommandEntry } -export { DevToolsServerCommandInput } -export { DevToolsTerminalHost } -export { DevToolsTerminalSession } -export { DevToolsTerminalSessionBase } -export { DevToolsTerminalStatus } -export { DevToolsViewAction } -export { DevToolsViewBuiltin } -export { DevToolsViewCustomRender } export { DevToolsViewHost } -export { DevToolsViewIframe } -export { DevToolsViewJsonRender } -export { DevToolsViewLauncher } -export { DevToolsViewLauncherStatus } export { EntriesToObject } export { EventEmitter } export { EventsMap } @@ -100,8 +48,6 @@ export { JsonRenderElement } export { JsonRenderer } export { JsonRenderSpec } export { PartialWithoutId } -export { RemoteConnectionInfo } -export { RemoteDockOptions } export { RpcBroadcastOptions } export { RpcFunctionAgentOptions } export { RpcFunctionsHost } diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.js index 75eacbd2..b3980b4c 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.js +++ b/devframe/tests/__snapshots__/tsnapi/devframe/index.snapshot.js @@ -2,7 +2,6 @@ * Generated by tsnapi — public API snapshot of `devframe` */ // #region Functions -export function defineCommand(_) {} export function defineJsonRenderSpec(_) {} // #endregion diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts new file mode 100644 index 00000000..b1cfab64 --- /dev/null +++ b/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts @@ -0,0 +1,15 @@ +/** + * Generated by tsnapi — public API snapshot of `devframe/internal` + */ +// #region Functions +export declare function normalizeBasePath(_: string): string; +export declare function resolveBasePath(_: DevtoolDefinition, _: DevtoolDeploymentKind): string; +// #endregion + +// #region Other +export { DevToolsInternalContext } +export { getInternalContext } +export { InternalAnonymousAuthStorage } +export { internalContextMap } +export { RemoteTokenRecord } +// #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js new file mode 100644 index 00000000..d83b7e7e --- /dev/null +++ b/devframe/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js @@ -0,0 +1,9 @@ +/** + * Generated by tsnapi — public API snapshot of `devframe/internal` + */ +// #region Other +export { getInternalContext } +export { internalContextMap } +export { normalizeBasePath } +export { resolveBasePath } +// #endregion \ No newline at end of file diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts index fed46d3a..9c3d8e16 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts @@ -22,28 +22,6 @@ export interface CreateStorageOptions { mergeInitialValue?: false | ((_: T, _: T) => T); debounce?: number; } -export interface DevToolsInternalContext { - storage: { - auth: SharedState; - }; - revokeAuthToken: (_: string) => Promise; - remoteTokens: Map; - allocateRemoteToken: (_: string, _: string, _: boolean) => string; - revokeRemoteToken: (_: string) => void; - revokeRemoteTokensForDock: (_: string) => void; - isRemoteTokenTrusted: (_: string, _?: string) => boolean; - wsEndpoint?: { - url: string; - }; -} -export interface InternalAnonymousAuthStorage { - trusted: Record; -} export interface PendingAuthRequest { clientAuthToken: string; session: DevToolsNodeRpcSession; @@ -55,11 +33,6 @@ export interface PendingAuthRequest { abortController: AbortController; timeout: ReturnType; } -export interface RemoteTokenRecord { - dockId: string; - origin: string; - originLock: boolean; -} export interface StaticRpcDumpCollection { manifest: StaticRpcDumpManifest; files: Record; @@ -112,18 +85,6 @@ export declare class DevToolsAgentHost implements DevToolsAgentHost$1 { private _findRpcDefinition; private _coercePositionalArgs; } -export declare class DevToolsCommandsHost implements DevToolsCommandsHost$1 { - readonly context: DevToolsNodeContext; - readonly commands: DevToolsCommandsHost$1['commands']; - readonly events: DevToolsCommandsHost$1['events']; - constructor(_: DevToolsNodeContext); - register(_: DevToolsServerCommandInput): DevToolsCommandHandle; - unregister(_: string): boolean; - execute(_: string, ..._: any[]): Promise; - list(): DevToolsServerCommandEntry[]; - private findCommand; - private toSerializable; -} export declare class DevToolsDiagnosticsHost implements DevToolsDiagnosticsHost$1 { readonly context: DevToolsNodeContext; private _definitions; @@ -135,60 +96,6 @@ export declare class DevToolsDiagnosticsHost implements DevToolsDiagnosticsHost$ register(_: unknown): void; private _rebuild; } -export declare class DevToolsDockHost implements DevToolsDockHost$1 { - readonly context: DevToolsNodeContext; - readonly views: DevToolsDockHost$1['views']; - readonly events: DevToolsDockHost$1['events']; - userSettings: SharedState; - private readonly remoteDocks; - constructor(_: DevToolsNodeContext); - init(): Promise; - values({ - includeBuiltin - }?: { - includeBuiltin?: boolean; - }): DevToolsDockEntry[]; - private projectView; - private resolveDevServerOrigin; - register(_: T, _?: boolean): { - update: (_: Partial) => void; - }; - update(_: DevToolsDockUserEntry): void; - private prepareRemoteRegistration; -} -export declare class DevToolsMessagesHost implements DevToolsMessagesHost$1 { - readonly context: DevToolsNodeContext; - readonly entries: DevToolsMessagesHost$1['entries']; - readonly events: DevToolsMessagesHost$1['events']; - readonly lastModified: Map; - readonly removals: Array<{ - id: string; - time: number; - }>; - private _autoDeleteTimers; - private _clock; - private _tick; - constructor(_: DevToolsNodeContext); - add(_: DevToolsMessageEntryInput): Promise; - update(_: string, _: Partial): Promise; - remove(_: string): Promise; - clear(): Promise; - private _createHandle; -} -export declare class DevToolsTerminalHost implements DevToolsTerminalHost$1 { - readonly context: DevToolsNodeContext; - readonly sessions: DevToolsTerminalHost$1['sessions']; - readonly events: DevToolsTerminalHost$1['events']; - private _boundStreams; - private _channel?; - constructor(_: DevToolsNodeContext); - private getStreamingChannel; - register(_: DevToolsTerminalSession): DevToolsTerminalSession; - update(_: PartialWithoutId): void; - remove(_: DevToolsTerminalSession): void; - private bindStream; - startChildProcess(_: DevToolsChildProcessExecuteOptions, _: Omit): Promise; -} export declare class DevToolsViewHost implements DevToolsViewHost$1 { readonly context: DevToolsNodeContext; buildStaticDirs: { @@ -220,7 +127,6 @@ export declare function createHostContext(_: CreateHostContextOptions): Promise< export declare function createRpcSharedStateServerHost(_: RpcFunctionsHost$1): RpcSharedStateHost; export declare function createRpcStreamingServerHost(_: RpcFunctionsHost$1): RpcStreamingHost; export declare function createStorage(_: CreateStorageOptions): SharedState; -export declare function getInternalContext(_: DevToolsNodeContext): DevToolsInternalContext; export declare function getPendingAuth(): PendingAuthRequest | null; export declare function getTempAuthToken(): string; export declare function isObject(_: unknown): value is Record; @@ -231,14 +137,12 @@ export declare function revokeAuthToken(_: DevToolsNodeContext, _: SharedState void)): ClientScriptEntry; -}; -export declare const internalContextMap: WeakMap; -// #endregion - // #region Other +export { DevToolsInternalContext } +export { getInternalContext } +export { InternalAnonymousAuthStorage } +export { internalContextMap } +export { RemoteTokenRecord } export { StartedServer } export { startHttpAndWs } export { StartHttpAndWsOptions } diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.js b/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.js index bb7a0b84..53e4d9b0 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.js +++ b/devframe/tests/__snapshots__/tsnapi/devframe/node.snapshot.js @@ -14,18 +14,13 @@ export function setPendingAuth(_) {} // #region Other export { collectStaticRpcDump } -export { ContextUtils } export { createH3DevToolsHost } export { createHostContext } export { createRpcSharedStateServerHost } export { createRpcStreamingServerHost } export { createStorage } export { DevToolsAgentHost } -export { DevToolsCommandsHost } export { DevToolsDiagnosticsHost } -export { DevToolsDockHost } -export { DevToolsMessagesHost } -export { DevToolsTerminalHost } export { DevToolsViewHost } export { getInternalContext } export { internalContextMap } diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 9639c657..361dafd7 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -9,7 +9,6 @@ export { AgentResourceContent } export { AgentResourceInput } export { AgentTool } export { AgentToolInput } -export { ClientScriptEntry } export { ConnectionMeta } export { defineDevtool } export { DevtoolBrowserContext } @@ -20,69 +19,19 @@ export { DevtoolRuntime } export { DevToolsAgentHost } export { DevToolsAgentHostEvents } export { DevToolsCapabilities } -export { DevToolsChildProcessExecuteOptions } -export { DevToolsChildProcessTerminalSession } -export { DevToolsClientCommand } -export { DevToolsCommandBase } -export { DevToolsCommandEntry } -export { DevToolsCommandHandle } -export { DevToolsCommandKeybinding } -export { DevToolsCommandShortcutOverrides } -export { DevToolsCommandsHost } -export { DevToolsCommandsHostEvents } export { DevToolsDiagnosticsDefinition } export { DevToolsDiagnosticsHost } export { DevToolsDiagnosticsLogger } -export { DevToolsDockEntriesGrouped } -export { DevToolsDockEntry } -export { DevToolsDockEntryBase } -export { DevToolsDockEntryCategory } -export { DevToolsDockEntryIcon } -export { DevToolsDockHost } -export { DevToolsDocksUserSettings } -export { DevToolsDockUserEntry } export { DevtoolSetupInfo } export { DevToolsHost } -export { DevToolsLogElementPosition } -export { DevToolsLogEntry } -export { DevToolsLogEntryFrom } -export { DevToolsLogEntryInput } -export { DevToolsLogFilePosition } -export { DevToolsLogHandle } -export { DevToolsLogLevel } -export { DevToolsLogsClient } -export { DevToolsLogsHost } -export { DevToolsMessageElementPosition } -export { DevToolsMessageEntry } -export { DevToolsMessageEntryFrom } -export { DevToolsMessageEntryInput } -export { DevToolsMessageFilePosition } -export { DevToolsMessageHandle } -export { DevToolsMessageLevel } -export { DevToolsMessagesClient } -export { DevToolsMessagesHost } export { DevToolsNodeContext } export { DevToolsNodeRpcSession } export { DevToolsNodeRpcSessionMeta } -export { DevToolsNodeUtils } export { DevtoolSpaOptions } export { DevToolsRpcClientFunctions } export { DevToolsRpcServerFunctions } export { DevToolsRpcSharedStates } -export { DevToolsServerCommandEntry } -export { DevToolsServerCommandInput } -export { DevToolsTerminalHost } -export { DevToolsTerminalSession } -export { DevToolsTerminalSessionBase } -export { DevToolsTerminalStatus } -export { DevToolsViewAction } -export { DevToolsViewBuiltin } -export { DevToolsViewCustomRender } export { DevToolsViewHost } -export { DevToolsViewIframe } -export { DevToolsViewJsonRender } -export { DevToolsViewLauncher } -export { DevToolsViewLauncherStatus } export { EntriesToObject } export { EventEmitter } export { EventsMap } @@ -91,8 +40,6 @@ export { JsonRenderElement } export { JsonRenderer } export { JsonRenderSpec } export { PartialWithoutId } -export { RemoteConnectionInfo } -export { RemoteDockOptions } export { RpcBroadcastOptions } export { RpcFunctionAgentOptions } export { RpcFunctionsHost } diff --git a/devframe/tests/__snapshots__/tsnapi/devframe/utils/when.snapshot.d.ts b/devframe/tests/__snapshots__/tsnapi/devframe/utils/when.snapshot.d.ts index e29233ef..77f6f990 100644 --- a/devframe/tests/__snapshots__/tsnapi/devframe/utils/when.snapshot.d.ts +++ b/devframe/tests/__snapshots__/tsnapi/devframe/utils/when.snapshot.d.ts @@ -1,9 +1,21 @@ /** * Generated by tsnapi — public API snapshot of `devframe/utils/when` */ -// #region Other -export { evaluateWhen } -export { resolveContextValue } -export { WhenContext } -export { WhenExpression } +// #region Interfaces +export interface WhenContext { + clientType: 'embedded' | 'standalone'; + dockOpen: boolean; + paletteOpen: boolean; + dockSelectedId: string; + [key: string]: unknown; +} +// #endregion + +// #region Types +export type WhenExpression = S & ValidateExpression; +// #endregion + +// #region Functions +export declare function evaluateWhen(_: E & ValidateExpression, _: T, _?: EvaluateOptions): boolean; +export declare function resolveContextValue>(_: string, _: T): unknown; // #endregion \ No newline at end of file diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index e00d3819..5ecf1748 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -2,26 +2,17 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { transformerTwoslash } from '@shikijs/vitepress-twoslash' import { extendConfig } from '@voidzero-dev/vitepress-theme/config' -import { globSync } from 'tinyglobby' import { defineConfig } from 'vitepress' import { groupIconMdPlugin, groupIconVitePlugin, } from 'vitepress-plugin-group-icons' import { withMermaid } from 'vitepress-plugin-mermaid' -import { devframeNav, devframeSidebar } from '../../devframe/docs/.vitepress/config' +import devframeSidebar from '../../devframe/docs/.vitepress/sidebar' import { version } from '../../package.json' const repoRoot = resolve(fileURLToPath(new URL('.', import.meta.url)), '../..') -const errorsDir = fileURLToPath(new URL('../errors/', import.meta.url)) - -function listErrorCodes(prefix: string): string[] { - return globSync(`${prefix}*.md`, { cwd: errorsDir }) - .map(f => f.replace(/\.md$/, '')) - .sort() -} - const DevToolsKitNav = [ { text: 'Introduction', link: '/kit/' }, { text: 'DevTools Plugin', link: '/kit/devtools-plugin' }, @@ -98,7 +89,7 @@ export default extendConfig(withMermaid(defineConfig({ text: 'DevTools Kit', items: DevToolsKitNav, }, - { text: 'DevFrame', items: devframeNav('/devframe') }, + { text: 'DevFrame', link: '/devframe/' }, { text: `v${version}`, items: [ @@ -151,18 +142,28 @@ export default extendConfig(withMermaid(defineConfig({ { text: 'DevTools Kit (DTK)', collapsed: true, - items: listErrorCodes('DTK').map(code => ({ - text: code, - link: `/errors/${code}`, - })), + items: [ + ...Array.from({ length: 32 }, (_, i) => i + 1), + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + ].map((n) => { + const code = `DTK${String(n).padStart(4, '0')}` + return { text: code, link: `/errors/${code}` } + }), }, { text: 'Rolldown DevTools (RDDT)', collapsed: true, - items: listErrorCodes('RDDT').map(code => ({ - text: code, - link: `/errors/${code}`, - })), + items: [ + { text: 'RDDT0001', link: '/errors/RDDT0001' }, + { text: 'RDDT0002', link: '/errors/RDDT0002' }, + ], }, ], }, diff --git a/docs/errors/DTK0050.md b/docs/errors/DTK0050.md new file mode 100644 index 00000000..d99c5b51 --- /dev/null +++ b/docs/errors/DTK0050.md @@ -0,0 +1,29 @@ +--- +outline: deep +--- + +# DTK0050: Dock Already Registered + +## Message + +> Dock with id "`{id}`" is already registered + +## Cause + +`ctx.docks.register(view)` is called twice with the same `id`. The kit's `DocksHost` enforces unique ids so each dock entry has a stable handle. + +## Example + +```ts +ctx.docks.register({ id: 'my-plugin:main', title: 'My Plugin', type: 'iframe', url: '/__foo/' }) +ctx.docks.register({ id: 'my-plugin:main', title: 'My Plugin (again)', type: 'iframe', url: '/__foo/' }) +// Throws DTK0050 +``` + +## Fix + +Pick a different `id`, or call `ctx.docks.update(...)` to mutate the existing entry. Pass `force: true` as the second arg to `register` to intentionally replace the existing entry — this is what the auto-derived dock from `createPluginFromDevframe` uses internally. + +## Source + +- [`packages/kit/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-docks.ts) — `DevToolsDockHost.register` throws `DTK0050` when an id is already in `views` and `force` was not set. diff --git a/docs/errors/DTK0051.md b/docs/errors/DTK0051.md new file mode 100644 index 00000000..d42ced5f --- /dev/null +++ b/docs/errors/DTK0051.md @@ -0,0 +1,27 @@ +--- +outline: deep +--- + +# DTK0051: Cannot Change Dock ID + +## Message + +> Cannot change the id of a dock. Use register() to add new docks. + +## Cause + +The handle returned by `ctx.docks.register({ id })` exposes an `update(patch)` method. Passing a different `id` in that patch is rejected — ids identify the entry and must remain stable for the kit client to track it across reactivity updates. + +## Fix + +To "rename" a dock, call `ctx.docks.register(...)` with the new id. To change other fields, leave `id` out of the patch: + +```ts +const handle = ctx.docks.register({ id: 'my-plugin', title: 'Old', /* ... */ }) +handle.update({ title: 'New' }) // ✓ +handle.update({ id: 'renamed' }) // ✗ DTK0051 +``` + +## Source + +- [`packages/kit/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-docks.ts) — the `update` closure returned from `register` throws `DTK0051` when the patch carries a different `id`. diff --git a/docs/errors/DTK0052.md b/docs/errors/DTK0052.md new file mode 100644 index 00000000..3cb2495c --- /dev/null +++ b/docs/errors/DTK0052.md @@ -0,0 +1,26 @@ +--- +outline: deep +--- + +# DTK0052: Dock Not Registered + +## Message + +> Dock with id "`{id}`" is not registered. Use register() to add new docks. + +## Cause + +`ctx.docks.update(view)` was called with an `id` that has not been registered. Updates require the dock to exist first. + +## Fix + +Call `ctx.docks.register(...)` before `update`, or guard with `ctx.docks.views.has(id)`: + +```ts +if (ctx.docks.views.has('my-plugin')) + ctx.docks.update({ id: 'my-plugin', badge: '3' }) +``` + +## Source + +- [`packages/kit/src/node/host-docks.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-docks.ts) — `DevToolsDockHost.update` throws `DTK0052` when the supplied entry's id isn't in `views`. diff --git a/docs/errors/DTK0053.md b/docs/errors/DTK0053.md new file mode 100644 index 00000000..f59cb7f1 --- /dev/null +++ b/docs/errors/DTK0053.md @@ -0,0 +1,21 @@ +--- +outline: deep +--- + +# DTK0053: Terminal Session Already Registered + +## Message + +> Terminal session with id "`{id}`" already registered + +## Cause + +`ctx.terminals.register(session)` (or the `startChildProcess` shortcut) was called with an `id` that already exists in the session map. + +## Fix + +Pick a unique id for each session, or call `ctx.terminals.remove(...)` on the existing one before registering a replacement. For long-running processes, prefer `session.restart()` over re-registering. + +## Source + +- [`packages/kit/src/node/host-terminals.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-terminals.ts) — both `DevToolsTerminalHost.register` and `startChildProcess` throw `DTK0053` when `sessions` already has the id. diff --git a/docs/errors/DTK0054.md b/docs/errors/DTK0054.md new file mode 100644 index 00000000..d33ddadf --- /dev/null +++ b/docs/errors/DTK0054.md @@ -0,0 +1,21 @@ +--- +outline: deep +--- + +# DTK0054: Terminal Session Not Registered + +## Message + +> Terminal session with id "`{id}`" not registered + +## Cause + +`ctx.terminals.update(patch)` was called with an `id` that has no corresponding session. Updates can only target a session that is still alive in the kit's terminal map. + +## Fix + +Register the session via `ctx.terminals.register(...)` (or `ctx.terminals.startChildProcess(...)`) first. Guard updates with `ctx.terminals.sessions.has(id)` if a session may have been removed concurrently. + +## Source + +- [`packages/kit/src/node/host-terminals.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-terminals.ts) — `DevToolsTerminalHost.update` throws `DTK0054` when the patch's id isn't in `sessions`. diff --git a/docs/errors/DTK0055.md b/docs/errors/DTK0055.md new file mode 100644 index 00000000..491acf0e --- /dev/null +++ b/docs/errors/DTK0055.md @@ -0,0 +1,26 @@ +--- +outline: deep +--- + +# DTK0055: Command Already Registered + +## Message + +> Command "`{id}`" is already registered + +## Cause + +`ctx.commands.register(command)` was called twice with the same `id`. Command ids must be globally unique across all registered integrations because the palette merges them by id. + +## Fix + +Pick a different namespaced id (e.g. `my-plugin:do-thing`), or use the handle returned from the original `register` call to mutate the existing command: + +```ts +const handle = ctx.commands.register({ id: 'my-plugin:open', title: 'Open', handler: () => {} }) +handle.update({ title: 'Open File' }) +``` + +## Source + +- [`packages/kit/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-commands.ts) — `DevToolsCommandsHost.register` throws `DTK0055` when `commands` already has the id. diff --git a/docs/errors/DTK0056.md b/docs/errors/DTK0056.md new file mode 100644 index 00000000..4a2fe290 --- /dev/null +++ b/docs/errors/DTK0056.md @@ -0,0 +1,27 @@ +--- +outline: deep +--- + +# DTK0056: Cannot Change Command ID + +## Message + +> Cannot change the id of a command. Use register() to add new commands. + +## Cause + +The handle returned by `ctx.commands.register(...)` exposes an `update(patch)` method that intentionally rejects an `id` field — palette state, keybinding overrides, and the palette renderer all key off the id. + +## Fix + +To rename a command, unregister the old one and register a new one. Otherwise, omit `id` from the patch: + +```ts +const handle = ctx.commands.register({ id: 'my-plugin:run', title: 'Run', handler }) +handle.update({ title: 'Run again' }) // ✓ +handle.update({ id: 'renamed' }) // ✗ DTK0056 +``` + +## Source + +- [`packages/kit/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-commands.ts) — the `update` closure returned from `register` throws `DTK0056` when the patch carries an `id` field. diff --git a/docs/errors/DTK0057.md b/docs/errors/DTK0057.md new file mode 100644 index 00000000..02189ec1 --- /dev/null +++ b/docs/errors/DTK0057.md @@ -0,0 +1,32 @@ +--- +outline: deep +--- + +# DTK0057: Command Not Registered + +## Message + +> Command "`{id}`" is not registered + +## Cause + +The kit's commands host throws this error from two paths: + +1. The handle's `update(patch)` was called after the underlying command was already unregistered. +2. `ctx.commands.execute(id, ...args)` was called with an id that doesn't match any registered command (top-level or child). + +## Fix + +Verify the id exists before executing: + +```ts +const cmd = ctx.commands.commands.get('my-plugin:run') +if (cmd?.handler) + await ctx.commands.execute('my-plugin:run') +``` + +For palette executions surfaced via the client, this typically indicates a race between the client palette UI and a server-side `unregister`. Re-render the palette after the next `command:registered` / `command:unregistered` event. + +## Source + +- [`packages/kit/src/node/host-commands.ts`](https://github.com/vitejs/devtools/blob/main/packages/kit/src/node/host-commands.ts) — `execute` and the `update` closure both throw `DTK0057` when the command id can't be resolved via `findCommand` / `commands.get`. diff --git a/docs/errors/index.md b/docs/errors/index.md index 6ea927a9..536f0a1e 100644 --- a/docs/errors/index.md +++ b/docs/errors/index.md @@ -31,6 +31,14 @@ Emitted by `@vitejs/devtools` and `@vitejs/devtools-kit`. | [DTK0030](./DTK0030) | error | Dock Entry Not Found | | [DTK0031](./DTK0031) | error | Dock Entry Not a Launcher | | [DTK0032](./DTK0032) | error | Dock Launch Error | +| [DTK0050](./DTK0050) | error | Dock Already Registered | +| [DTK0051](./DTK0051) | error | Cannot Change Dock ID | +| [DTK0052](./DTK0052) | error | Dock Not Registered | +| [DTK0053](./DTK0053) | error | Terminal Session Already Registered | +| [DTK0054](./DTK0054) | error | Terminal Session Not Registered | +| [DTK0055](./DTK0055) | error | Command Already Registered | +| [DTK0056](./DTK0056) | error | Cannot Change Command ID | +| [DTK0057](./DTK0057) | error | Command Not Registered | ## Rolldown DevTools (RDDT) diff --git a/docs/kit/commands.md b/docs/kit/commands.md index 333fb6f3..6972c657 100644 --- a/docs/kit/commands.md +++ b/docs/kit/commands.md @@ -254,6 +254,41 @@ context.commands.register({ }) ``` +## Executing Programmatically + +Any code with access to the kit context can trigger a command by id: + +```ts +await ctx.commands.execute('my-plugin:clear-cache') + +// With arguments: +await ctx.commands.execute('my-plugin:open-file', '/src/main.ts') +``` + +`execute` throws if the command isn't registered or has no handler. It searches both top-level commands and children. + +## Listing & Introspection + +The host exposes a `list()` method returning serializable command entries (without handlers) — useful when implementing your own palette UI or exporting the current command set: + +```ts +const commands = ctx.commands.list() +for (const cmd of commands) { + console.log(cmd.id, cmd.title, cmd.keybindings) +} +``` + +Subscribe to lifecycle events to react to registrations: + +```ts +ctx.commands.events.on('command:registered', (cmd) => { + console.log('new command:', cmd.id) +}) +ctx.commands.events.on('command:unregistered', (id) => { + console.log('removed:', id) +}) +``` + ## Complete Example ::: code-group diff --git a/docs/kit/devtools-plugin.md b/docs/kit/devtools-plugin.md index 8d303e4d..1dc5a7b0 100644 --- a/docs/kit/devtools-plugin.md +++ b/docs/kit/devtools-plugin.md @@ -4,7 +4,14 @@ outline: deep # DevTools Plugin -A DevTools plugin is a **superset** of a Vite plugin—meaning any Vite plugin can become a DevTools plugin by simply adding a `devtools` hook. This allows you to extend the DevTools infrastructure with custom data visualizations, actions, and integrations. +A DevTools plugin is a **superset** of a Vite plugin — any Vite plugin can become one by adding a `devtools` hook. The hook's `setup(ctx)` receives the **kit-augmented context** (`KitNodeContext`) — DevFrame's framework-neutral surface plus the hub-level subsystems the kit owns: `docks`, `terminals`, `messages`, `commands`. + +There are two common ways to author one: + +- **From a portable [DevFrame](https://devfra.me/guide/) app.** Wrap the definition with `createPluginFromDevframe(d, opts?)` from `@vitejs/devtools-kit/node`. The kit auto-mounts the SPA via `views.hostStatic`, synthesizes an iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`, runs the devtool's own `setup`, then runs your optional kit-only `opts.setup` for hub features (terminals, commands, custom dock metadata). +- **As a Vite-specific plugin from scratch.** Implement the `Plugin.devtools.setup` hook directly — useful when the integration is intrinsically tied to Vite's lifecycle (e.g. inspecting the resolved config, reading the dev-server middleware stack). + +The rest of this page covers the manual hook approach. See `createPluginFromDevframe` for the portable path. ## Installation diff --git a/docs/kit/dock-system.md b/docs/kit/dock-system.md index 2ded6cec..5d46a328 100644 --- a/docs/kit/dock-system.md +++ b/docs/kit/dock-system.md @@ -351,6 +351,33 @@ ctx.docks.register({ See the [JSON Render](/kit/json-render) page for the full component reference, dynamic updates, actions, state bindings, and examples. +## Common Options + +Every dock type accepts these base fields: + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Unique, namespaced. | +| `title` | `string` | Label shown in the dock. | +| `icon` | `string \| { light, dark }` | Iconify name, URL, data URI, or light/dark pair. | +| `category` | `'app' \| 'framework' \| 'web' \| 'advanced' \| 'default'` | Grouping in the dock panel. Defaults to `'default'`. | +| `defaultOrder` | `number` | Higher numbers appear first. Default `0`. | +| `when` | `string` | Visibility expression — see [When Clauses](/kit/when-clauses). | +| `badge` | `string` | Short text badge (e.g. unread count). | + +## Update & Unregister + +`register()` returns a handle with an `update(patch)` method: + +```ts +const handle = ctx.docks.register({ /* ... */ }) + +// Live update (e.g. refresh the badge) +handle.update({ badge: '3' }) +``` + +The handle only supports `update`. Docks are not individually unregisterable today. + ## Communication with Server All client scripts (actions and custom renderers) can communicate with the server using [RPC](./rpc): diff --git a/docs/kit/index.md b/docs/kit/index.md index b3c92560..35f10fee 100644 --- a/docs/kit/index.md +++ b/docs/kit/index.md @@ -7,7 +7,9 @@ outline: deep > [!WARNING] Experimental > The API is still in development and may change in any version. If you are building on top of it, please mind the version of packages you are using and warn your users about the experimental status. -DevTools Kit is the **Vite-specific superset** of [DevFrame](https://devfra.me/guide/) — the framework-neutral foundation. Everything in DevFrame (RPC, hosts, docks, shared state, agents) is available through Kit, plus Vite-specific glue: the `devtools` hook on Vite plugins, Vite dev-server integration, and the Vite DevTools client. If you're building a tool that doesn't need Vite at all, start from [DevFrame](https://devfra.me/guide/) directly. +DevTools Kit is **the hub that unites many DevTools integrations**. While [DevFrame](https://devfra.me/guide/) describes one tool — its RPC, its data, its SPA — Kit is the layer that takes many of those tools and gives them a single home: the dock, the command palette, terminal aggregation, cross-tool toasts, and the Vite plugin glue (`Plugin.devtools.setup`) that ties it all together. + +If you have a portable DevFrame app, drop it in via `createPluginFromDevframe(d)` from `@vitejs/devtools-kit/node` — the kit auto-derives an iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`. If you're authoring a fresh Vite-specific integration that needs hub features (terminals, palette, custom-render docks), reach for the `Plugin.devtools.setup` hook directly. If you have a tool that doesn't need a hub at all, stay in [DevFrame](https://devfra.me/guide/). The vision of DevTools Kit is to provide a unified foundation for building custom developer tools that integrate seamlessly with Vite and frameworks built on top of it. @@ -19,15 +21,18 @@ If you are interested in more details, you can also check out [Anthony Fu's talk ## What DevTools Kit Provides -DevTools Kit offers a complete toolkit for building DevTools integrations: +DevTools Kit owns the **hub-level surface** — the things that only make sense once you have multiple integrations sharing a UI: | Feature | Description | |---------|-------------| -| **[Extensible Architecture](./devtools-plugin)** | Simple, well-typed APIs for registering custom visualizations, actions, and interactions | -| **[Dock System](./dock-system)** | A unified entry point where users can discover and switch between all DevTools integrations | -| **[Built-in RPC Layer](./rpc)** | Type-safe bidirectional communication between Node.js server and browser clients | -| **[Shared State](./shared-state)** | Share data between server and client with automatic synchronization | -| **Isomorphic Views** | Deploy your UI as embedded panels, browser extensions, or standalone webpages | +| **[DevTools Plugin](./devtools-plugin)** | The `Plugin.devtools.setup` hook, plus `createPluginFromDevframe` for porting DevFrame apps into the hub. | +| **[Dock System](./dock-system)** | The unified dock — iframe / action / custom / launcher / json-render entries — with categories, when-clauses, and remote dock support. | +| **[Commands](./commands)** | The shared command palette: keybindings, children, when-gating across every integration. | +| **[Messages](./messages)** | Cross-tool toast notifications and the unified messages dock. | +| **[Terminals](./terminals)** | Aggregate terminal output from any integration into one xterm.js view. | +| **[RPC](./rpc)** | Type-safe bidirectional RPC backed by DevFrame's birpc + valibot. | +| **[Shared State](./shared-state)** | Patch-synced state that bridges server ↔ client across every integration. | +| **Isomorphic Views** | Deploy your UI as embedded panels, browser extensions, or standalone webpages. | ## Architecture Overview @@ -67,7 +72,32 @@ Whether you're building a framework-specific inspector, a build analysis tool, o ## Quick Example -Here's a minimal example to add a DevTools panel to your Vite plugin: +Two paths into the hub. + +**Have a portable DevFrame app already?** Wrap it once. The kit auto-derives an iframe dock from the definition: + +```ts +// vite.config.ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' +import devtool from './my-devtool' + +export default { + plugins: [ + createPluginFromDevframe(devtool, { + // Optional kit-only setup for hub features: + setup(ctx) { + ctx.commands.register({ + id: 'my-devtool:clear-cache', + title: 'Clear Cache', + handler: () => { /* ... */ }, + }) + }, + }), + ], +} +``` + +**Authoring a fresh Vite-specific integration?** Reach for the hook directly: ```ts /// @@ -78,7 +108,7 @@ export default function myPlugin(): Plugin { name: 'my-plugin', devtools: { setup(ctx) { - // Register a dock entry that shows an iframe + // ctx is the kit-augmented context: rpc + docks + terminals + messages + commands ctx.docks.register({ id: 'my-plugin', title: 'My Plugin', @@ -94,12 +124,10 @@ export default function myPlugin(): Plugin { ## Getting Started -If you're building a Vite plugin and want to add DevTools capabilities, or if you're creating a framework-specific DevTools integration, DevTools Kit makes it straightforward: - -1. **[DevTools Plugin](./devtools-plugin)** - Learn how to create a DevTools plugin and understand the setup context -2. **[Dock System](./dock-system)** - Create UI panels, action buttons, or custom renderers -3. **[RPC](./rpc)** - Enable bidirectional communication between server and client -4. **[Shared State](./shared-state)** - Share data between server and client with automatic synchronization +1. **[DevTools Plugin](./devtools-plugin)** — Learn how to register a hub plugin and understand the kit-augmented context +2. **[Dock System](./dock-system)** — Iframe panels, action buttons, custom renderers, launchers, json-render specs +3. **[RPC](./rpc)** — Bidirectional, type-safe communication between server and client +4. **[Shared State](./shared-state)** — Patch-synced cross-integration state > [!TIP] Help Us Improve > If you are building something on top of Vite DevTools Kit, we invite you to label your repository with `vite-devtools` on GitHub to help us track usage and improve the project. Thank you! diff --git a/docs/kit/messages.md b/docs/kit/messages.md index c886cace..850a5654 100644 --- a/docs/kit/messages.md +++ b/docs/kit/messages.md @@ -2,8 +2,6 @@ The Messages system allows plugins to emit structured message entries from both the server (Node.js) and client (browser) contexts. Messages are displayed in the built-in **Messages** panel in the DevTools dock, and can optionally appear as toast notifications. -> **Note:** This subsystem was previously named `logs`. The `ctx.logs` field is still available as a deprecated alias for one release cycle — see [DF0018](https://devfra.me/errors/DF0018) for migration details. - > For *coded* errors and warnings with stable codes and docs URLs, see [Structured Diagnostics](./diagnostics) (`ctx.diagnostics`) instead. ## Use Cases @@ -173,3 +171,52 @@ Messages have a maximum capacity of 1000 entries. When the limit is reached, the ## Dock Badge The Messages dock icon automatically shows a badge with the total message count. The icon is hidden when there are no messages. + +## Events + +The host emits events for anyone who wants to observe the message stream: + +```ts +ctx.messages.events.on('message:added', (entry) => { /* ... */ }) +ctx.messages.events.on('message:updated', (entry) => { /* ... */ }) +ctx.messages.events.on('message:removed', (id) => { /* ... */ }) +ctx.messages.events.on('message:cleared', () => { /* ... */ }) +``` + +Use this to bridge messages into external tools — e.g. mirror them into a structured log file or forward certain categories to your own reporter: + +```ts +ctx.messages.events.on('message:added', (entry) => { + if (entry.category === 'a11y') + console.log('a11y finding:', entry.message) +}) +``` + +## Long-Running Operation Pattern + +Combine `id`-based deduplication with `status: 'loading'` to drive a single message through a multi-step lifecycle: + +```ts +async function rebuild(ctx) { + const handle = await ctx.messages.add({ + id: 'my-plugin:rebuild', + message: 'Rebuilding...', + level: 'info', + status: 'loading', + }) + + try { + await doRebuild() + await handle.update({ message: 'Rebuild complete', level: 'success', status: 'idle' }) + } + catch (error) { + await handle.update({ + message: 'Rebuild failed', + level: 'error', + description: (error as Error).message, + stacktrace: (error as Error).stack, + status: 'idle', + }) + } +} +``` diff --git a/docs/kit/terminals.md b/docs/kit/terminals.md index 50c48dda..d39e0b95 100644 --- a/docs/kit/terminals.md +++ b/docs/kit/terminals.md @@ -130,3 +130,35 @@ ctx.terminals.update({ title: 'Build Watcher (done)', }) ``` + +## Events + +Subscribe to lifecycle changes (register, update, remove) via the host event emitter: + +```ts +ctx.terminals.events.on('terminal:session:updated', (session) => { + console.log(session.id, session.status) +}) +``` + +Output chunks aren't delivered as host events — terminals stream via the [streaming channel](/kit/streaming) `devframe:terminals`, keyed by session id. From the browser: + +```ts +const reader = rpc.streaming.subscribe( + 'devframe:terminals', + sessionId, +) +for await (const chunk of reader) writeToTerminal(chunk) +``` + +A server-side bridge inside the kit's `DevToolsTerminalHost` pipes each session's `ReadableStream` straight into the channel — you don't need to wire anything yourself unless you're building a custom terminal renderer. + +## Inspection + +```ts +for (const session of ctx.terminals.sessions.values()) { + console.log(session.id, session.title, session.status) +} +``` + +`ctx.terminals.sessions` is a live `Map` — handy for diagnostics, testing, and for building custom terminal UIs that mirror the built-in panel. diff --git a/packages/core/playground/vite.config.ts b/packages/core/playground/vite.config.ts index 04fb77a0..529da1ce 100644 --- a/packages/core/playground/vite.config.ts +++ b/packages/core/playground/vite.config.ts @@ -1,4 +1,5 @@ import process from 'node:process' +import { createSimpleClientScript } from '@vitejs/devtools-kit/node' import Vue from '@vitejs/plugin-vue' import UnoCSS from 'unocss/vite' import { defineConfig } from 'vite' @@ -59,7 +60,7 @@ export default defineConfig({ async setup(ctx) { ctx.docks.register({ type: 'action', - action: ctx.utils.createSimpleClientScript((ctx) => { + action: createSimpleClientScript((ctx) => { // eslint-disable-next-line no-alert alert('Hello, world! For the first time!') ctx.current.events.on('entry:activated', () => { @@ -74,7 +75,7 @@ export default defineConfig({ ctx.docks.register({ type: 'custom-render', - renderer: ctx.utils.createSimpleClientScript((ctx) => { + renderer: createSimpleClientScript((ctx) => { ctx.current.events.on('dom:panel:mounted', (panel: any) => { const el = document.createElement('div') el.style.padding = '16px' @@ -101,7 +102,7 @@ export default defineConfig({ icon: 'material-symbols:counter-1', title: 'Counter', // TODO: HMR - action: ctx.utils.createSimpleClientScript(() => {}), + action: createSimpleClientScript(() => {}), }) ctx.docks.register({ @@ -165,7 +166,7 @@ export default defineConfig({ type: 'action', icon: `material-symbols:counter-${newState.count}`, title: `Counter ${newState.count}`, - action: ctx.utils.createSimpleClientScript(`() => { + action: createSimpleClientScript(`() => { alert('Counter ${newState.count}') }`), }) @@ -182,7 +183,7 @@ export default defineConfig({ // icon: `material-symbols:counter-${count}`, // title: `Counter ${count}`, // // TODO: HMR? - // action: ctx.utils.createSimpleClientScript(`() => { + // action: createSimpleClientScript(`() => { // alert('Counter ${count}') // }`), // }) diff --git a/packages/core/src/node/context.ts b/packages/core/src/node/context.ts index 7abf6134..c6b4e421 100644 --- a/packages/core/src/node/context.ts +++ b/packages/core/src/node/context.ts @@ -1,8 +1,8 @@ -import type { DevToolsNodeContext } from '@vitejs/devtools-kit' +import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit' import type { RpcFunctionsHost } from 'devframe/node' import type { ResolvedConfig, ViteDevServer } from 'vite' -import { createViteDevToolsHost } from '@vitejs/devtools-kit/node' -import { createHostContext, isObject } from 'devframe/node' +import { createKitContext, createViteDevToolsHost } from '@vitejs/devtools-kit/node' +import { isObject } from 'devframe/node' import { createDebug } from 'obug' import { diagnostics, logger } from './diagnostics' import { builtinRpcDeclarations } from './rpc' @@ -24,29 +24,27 @@ function shouldSkipSetupByCapabilities( export async function createDevToolsContext( viteConfig: ResolvedConfig, viteServer?: ViteDevServer, -): Promise { +): Promise { const cwd = viteConfig.root const { searchForWorkspaceRoot } = await import('vite') const mode = viteConfig.command === 'serve' ? 'dev' : 'build' const workspaceRoot = searchForWorkspaceRoot(cwd) ?? cwd - const context = await createHostContext({ + const context = (await createKitContext({ cwd, workspaceRoot, mode, host: createViteDevToolsHost({ viteConfig, viteServer, workspaceRoot }), builtinRpcDeclarations, - }) + viteConfig, + viteServer, + })) as ViteDevToolsNodeContext // Fold the core (Vite) diagnostics into the shared host logger so plugin // setup() hooks can reference DTK codes via `ctx.diagnostics.logger`. context.diagnostics.register(diagnostics) - // Attach the Vite-specific fields on top of the framework-neutral context. - ;(context as any).viteConfig = viteConfig - ;(context as any).viteServer = viteServer - // Vite-specific built-in server commands. const rpcHost = context.rpc as RpcFunctionsHost context.commands.register({ @@ -77,12 +75,12 @@ export async function createDevToolsContext( } try { debugSetup(`setting up plugin ${JSON.stringify(plugin.name)}`) - await plugin.devtools?.setup?.(context as DevToolsNodeContext) + await plugin.devtools?.setup?.(context) } catch (error) { throw logger.DTK0014({ name: plugin.name }, { cause: error }).throw() } } - return context as DevToolsNodeContext + return context } diff --git a/packages/core/src/node/rpc/internal/messages-list.ts b/packages/core/src/node/rpc/internal/messages-list.ts index 59050736..9aebd246 100644 --- a/packages/core/src/node/rpc/internal/messages-list.ts +++ b/packages/core/src/node/rpc/internal/messages-list.ts @@ -1,5 +1,4 @@ -import type { DevToolsMessageEntry } from '@vitejs/devtools-kit' -import type { DevToolsMessagesHost } from 'devframe/node' +import type { DevToolsMessageEntry, DevToolsMessagesHost } from '@vitejs/devtools-kit' import { defineRpcFunction } from '@vitejs/devtools-kit' export interface MessagesListResult { diff --git a/packages/kit/package.json b/packages/kit/package.json index 2dc1f739..50cb826d 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -44,10 +44,16 @@ "vite": "*" }, "dependencies": { + "ansis": "catalog:deps", "birpc": "catalog:deps", "devframe": "workspace:*", + "logs-sdk": "catalog:deps", + "mlly": "catalog:deps", "ohash": "catalog:deps", - "sirv": "catalog:deps" + "pathe": "catalog:deps", + "perfect-debounce": "catalog:deps", + "sirv": "catalog:deps", + "tinyexec": "catalog:deps" }, "devDependencies": { "human-id": "catalog:inlined", diff --git a/packages/kit/src/client/client-script.ts b/packages/kit/src/client/client-script.ts index 125b3694..fa976ab1 100644 --- a/packages/kit/src/client/client-script.ts +++ b/packages/kit/src/client/client-script.ts @@ -1,5 +1,5 @@ -import type { DockEntryState, DocksContext } from 'devframe/client' -import type { DevToolsMessagesClient } from 'devframe/types' +import type { DevToolsMessagesClient } from '../types/messages' +import type { DockEntryState, DocksContext } from './docks' /** * Context for client scripts running in dock entries diff --git a/devframe/packages/devframe/src/client/context.ts b/packages/kit/src/client/context.ts similarity index 100% rename from devframe/packages/devframe/src/client/context.ts rename to packages/kit/src/client/context.ts diff --git a/devframe/packages/devframe/src/client/docks.ts b/packages/kit/src/client/docks.ts similarity index 85% rename from devframe/packages/devframe/src/client/docks.ts rename to packages/kit/src/client/docks.ts index 9ba626b6..9ae43915 100644 --- a/devframe/packages/devframe/src/client/docks.ts +++ b/packages/kit/src/client/docks.ts @@ -1,8 +1,12 @@ -import type { RpcFunctionsCollector } from 'devframe/rpc' -import type { DevToolsClientCommand, DevToolsCommandEntry, DevToolsCommandKeybinding, DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsDocksUserSettings, DevToolsDockUserEntry, DevToolsRpcClientFunctions, EventEmitter } from 'devframe/types' +import type { DevToolsRpcContext } from 'devframe/client' +import type { EventEmitter } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { WhenContext } from 'devframe/utils/when' -import type { DevToolsRpcClient } from './rpc' +import type { DevToolsClientCommand, DevToolsCommandEntry, DevToolsCommandKeybinding } from '../types/commands' +import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsDockUserEntry } from '../types/docks' +import type { DevToolsDocksUserSettings } from '../types/settings' + +export type { DevToolsClientRpcHost, RpcClientEvents } from 'devframe/client' export interface DockPanelStorage { mode: 'float' | 'edge' @@ -17,13 +21,6 @@ export interface DockPanelStorage { export type DockClientType = 'embedded' | 'standalone' -export interface DevToolsRpcContext { - /** - * The RPC client to interact with the server - */ - readonly rpc: DevToolsRpcClient -} - export interface DocksContext extends DevToolsRpcContext { /** * Type of the client environment @@ -58,8 +55,6 @@ export interface WhenClauseContext { readonly context: WhenContext } -export type DevToolsClientRpcHost = RpcFunctionsCollector - export type DevToolsClientContext = DocksContext export interface DocksPanelContext { @@ -112,10 +107,6 @@ export interface DockEntryStateEvents { 'dom:iframe:mounted': (iframe: HTMLIFrameElement) => void } -export interface RpcClientEvents { - 'rpc:is-trusted:updated': (isTrusted: boolean) => void -} - export interface CommandsContext { /** * All commands (server + client) diff --git a/packages/kit/src/client/index.ts b/packages/kit/src/client/index.ts index 7c55ddb5..475395f9 100644 --- a/packages/kit/src/client/index.ts +++ b/packages/kit/src/client/index.ts @@ -1,3 +1,5 @@ export * from './client-script' +export * from './context' +export * from './docks' export * from './remote' export * from 'devframe/client' diff --git a/packages/kit/src/constants.ts b/packages/kit/src/constants.ts index 361f1778..71f9acc7 100644 --- a/packages/kit/src/constants.ts +++ b/packages/kit/src/constants.ts @@ -1 +1,24 @@ +import type { DevToolsDockEntryCategory } from './types/docks' +import type { DevToolsDocksUserSettings } from './types/settings' + export * from 'devframe/constants' + +export const DEFAULT_CATEGORIES_ORDER: Record = { + '~viteplus': -1000, + 'default': 0, + 'app': 100, + 'framework': 200, + 'web': 300, + 'advanced': 400, + '~builtin': 1000, +} satisfies Record + +export const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings = () => ({ + docksHidden: [], + docksCategoriesHidden: [], + docksPinned: [], + docksCustomOrder: {}, + showIframeAddressBar: false, + closeOnOutsideClick: false, + commandShortcuts: {}, +}) diff --git a/devframe/packages/devframe/src/node/__tests__/host-docks.test.ts b/packages/kit/src/node/__tests__/host-docks.test.ts similarity index 97% rename from devframe/packages/devframe/src/node/__tests__/host-docks.test.ts rename to packages/kit/src/node/__tests__/host-docks.test.ts index df2611ab..11da28f0 100644 --- a/devframe/packages/devframe/src/node/__tests__/host-docks.test.ts +++ b/packages/kit/src/node/__tests__/host-docks.test.ts @@ -1,11 +1,12 @@ -import type { DevToolsDockUserEntry, DevToolsNodeContext, DevToolsViewIframe, RemoteConnectionInfo } from 'devframe/types' +import type { DevToolsDockUserEntry, DevToolsViewIframe, RemoteConnectionInfo } from '../../types/docks' +import type { KitNodeContext } from '../context' import { Buffer } from 'node:buffer' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' +import { getInternalContext, internalContextMap } from 'devframe/internal' import { describe, expect, it } from 'vitest' -import { getInternalContext, internalContextMap } from '../context-internal' import { DevToolsDockHost } from '../host-docks' -function createMockContext(): DevToolsNodeContext { +function createMockContext(): KitNodeContext { return { viteConfig: { server: { host: 'localhost', port: 5173, https: false }, @@ -17,7 +18,7 @@ function createMockContext(): DevToolsNodeContext { resolveOrigin: () => 'http://localhost:5173', getStorageDir: () => '/tmp/devframe-test-storage', }, - } as unknown as DevToolsNodeContext + } as unknown as KitNodeContext } function decodeDescriptor(url: string): RemoteConnectionInfo { @@ -37,7 +38,7 @@ describe('devToolsDockHost', () => { resolveOrigin: () => 'http://localhost:5173', getStorageDir: () => '/tmp/devframe-test-storage', }, - } as unknown as DevToolsNodeContext + } as unknown as KitNodeContext describe('builtin entries', () => { it('does not include popup in builtin docks', () => { diff --git a/devframe/packages/devframe/src/node/__tests__/host-messages.test.ts b/packages/kit/src/node/__tests__/host-messages.test.ts similarity index 99% rename from devframe/packages/devframe/src/node/__tests__/host-messages.test.ts rename to packages/kit/src/node/__tests__/host-messages.test.ts index bdc2bfd5..71521b3b 100644 --- a/devframe/packages/devframe/src/node/__tests__/host-messages.test.ts +++ b/packages/kit/src/node/__tests__/host-messages.test.ts @@ -1,9 +1,9 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { KitNodeContext } from '../context' import { describe, expect, it, vi } from 'vitest' import { DevToolsMessagesHost } from '../host-messages' describe('devToolsMessagesHost', () => { - const mockContext = {} as DevToolsNodeContext + const mockContext = {} as KitNodeContext function createHost() { return new DevToolsMessagesHost(mockContext) diff --git a/devframe/packages/devframe/src/node/__tests__/host-terminals.test.ts b/packages/kit/src/node/__tests__/host-terminals.test.ts similarity index 78% rename from devframe/packages/devframe/src/node/__tests__/host-terminals.test.ts rename to packages/kit/src/node/__tests__/host-terminals.test.ts index 5cd52aa3..b3203f00 100644 --- a/devframe/packages/devframe/src/node/__tests__/host-terminals.test.ts +++ b/packages/kit/src/node/__tests__/host-terminals.test.ts @@ -1,10 +1,11 @@ -import type { DevToolsNodeContext, DevToolsTerminalSession } from 'devframe/types' +import type { DevToolsTerminalSession } from '../../types/terminals' +import type { KitNodeContext } from '../context' import { describe, expect, it, vi } from 'vitest' import { DevToolsTerminalHost } from '../host-terminals' describe('devToolsTerminalHost', () => { it('disposes bound stream entry on remove', () => { - const host = new DevToolsTerminalHost({} as DevToolsNodeContext) + const host = new DevToolsTerminalHost({} as KitNodeContext) const session: DevToolsTerminalSession = { id: 'terminal-1', title: 'Terminal 1', diff --git a/packages/kit/src/node/context.ts b/packages/kit/src/node/context.ts new file mode 100644 index 00000000..4a932f6f --- /dev/null +++ b/packages/kit/src/node/context.ts @@ -0,0 +1,102 @@ +import type { CreateHostContextOptions } from 'devframe/node' +import type { DevToolsNodeContext } from 'devframe/types' +import type { ResolvedConfig, ViteDevServer } from 'vite' +import type { DevToolsCommandsHost } from '../types/commands' +import type { DevToolsDockHost } from '../types/docks' +import type { DevToolsMessagesHost } from '../types/messages' +import type { DevToolsTerminalHost } from '../types/terminals' +import { createHostContext } from 'devframe/node' +import { debounce } from 'perfect-debounce' +import { DevToolsCommandsHost as CommandsHostImpl } from './host-commands' +import { DevToolsDockHost as DocksHostImpl } from './host-docks' +import { DevToolsMessagesHost as MessagesHostImpl } from './host-messages' +import { DevToolsTerminalHost as TerminalsHostImpl } from './host-terminals' + +/** + * Kit-augmented node context — extends devframe's framework-neutral + * `DevToolsNodeContext` with the hub-level subsystems (`docks`, + * `terminals`, `messages`, `commands`) that are owned by + * `@vitejs/devtools-kit`. When kit hosts the devtool inside Vite + * DevTools, also exposes the underlying Vite handles. + */ +export interface KitNodeContext extends DevToolsNodeContext { + docks: DevToolsDockHost + terminals: DevToolsTerminalHost + messages: DevToolsMessagesHost + commands: DevToolsCommandsHost + readonly viteConfig?: ResolvedConfig + readonly viteServer?: ViteDevServer +} + +export interface CreateKitContextOptions extends CreateHostContextOptions { + /** Optional Vite resolved config to surface on the context (for Vite-mounted hubs). */ + viteConfig?: ResolvedConfig + /** Optional Vite dev server to surface on the context. */ + viteServer?: ViteDevServer +} + +/** + * Create a kit-level node context: wraps devframe's `createHostContext`, + * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), + * and wires the shared-state synchronization that powers the unified + * client UI. + */ +export async function createKitContext(options: CreateKitContextOptions): Promise { + const baseContext = await createHostContext(options) + const context = baseContext as KitNodeContext + + const docks = new DocksHostImpl(context) + const terminals = new TerminalsHostImpl(context) + const messages = new MessagesHostImpl(context) + const commands = new CommandsHostImpl(context) + + context.docks = docks + context.terminals = terminals + context.messages = messages + context.commands = commands + + if (options.viteConfig) + Object.defineProperty(context, 'viteConfig', { value: options.viteConfig, enumerable: true }) + if (options.viteServer) + Object.defineProperty(context, 'viteServer', { value: options.viteServer, enumerable: true }) + + await docks.init() + + const debounceMs = options.mode === 'build' ? 0 : 10 + + const docksSharedState = await context.rpc.sharedState.get('devframe:docks', { initialValue: [] }) + const refreshDocks = debounce(() => { + docksSharedState.mutate(() => docks.values()) + }, debounceMs) + docks.events.on('dock:entry:updated', refreshDocks) + + const broadcastTerminals = debounce(() => { + context.rpc.broadcast({ + method: 'devframe:terminals:updated', + args: [], + }) + docksSharedState.mutate(() => docks.values()) + }, debounceMs) + terminals.events.on('terminal:session:updated', broadcastTerminals) + + const broadcastMessages = debounce(() => { + context.rpc.broadcast({ + method: 'devframe:messages:updated', + args: [], + }) + docksSharedState.mutate(() => docks.values()) + }, debounceMs) + messages.events.on('message:added', broadcastMessages) + messages.events.on('message:updated', broadcastMessages) + messages.events.on('message:removed', broadcastMessages) + messages.events.on('message:cleared', broadcastMessages) + + const commandsSharedState = await context.rpc.sharedState.get('devframe:commands', { initialValue: [] }) + const syncCommands = debounce(() => { + commandsSharedState.mutate(() => commands.list()) + }, debounceMs) + commands.events.on('command:registered', syncCommands) + commands.events.on('command:unregistered', syncCommands) + + return context +} diff --git a/packages/kit/src/node/create-plugin-from-devframe.ts b/packages/kit/src/node/create-plugin-from-devframe.ts new file mode 100644 index 00000000..7abe9f4e --- /dev/null +++ b/packages/kit/src/node/create-plugin-from-devframe.ts @@ -0,0 +1,85 @@ +import type { DevtoolDefinition, DevToolsCapabilities } from 'devframe/types' +import type { DevToolsViewIframe } from '../types/docks' +import type { PluginWithDevTools } from '../types/vite-augment' +import type { KitNodeContext } from './context' +import { resolveBasePath } from 'devframe/internal' +import { resolve } from 'pathe' + +export interface CreatePluginFromDevframeOptions { + /** + * Vite plugin name override. Defaults to `devframe:${d.id}`. + */ + name?: string + /** + * Mount path override. Defaults to `d.basePath` or `/__${d.id}/`. + */ + base?: string + /** + * Overrides for the auto-synthesized iframe dock entry. Use this to + * customize the entry's `category`, override the icon, hide it via + * `when`, etc. Cannot change `id`, `type`, or `url` — those are + * derived from the devtool definition. + */ + dock?: Partial> + /** + * Capability flags forwarded onto the kit plugin's `devtools` slot. + * Defaults to `d.capabilities`. + */ + capabilities?: DevToolsCapabilities | { dev?: DevToolsCapabilities | boolean, build?: DevToolsCapabilities | boolean } + /** + * Additional kit-only setup hook. Runs after the devframe-level + * `d.setup(ctx)` and after the auto-derived dock entry has been + * registered. Use this for kit-specific behavior that should not + * bleed into the portable {@link DevtoolDefinition} — e.g. + * registering terminals/commands/messages, or enriching the + * synthesized dock entry. + */ + setup?: (ctx: KitNodeContext) => void | Promise +} + +/** + * Wrap a {@link DevtoolDefinition} as a Vite plugin that mounts inside + * `@vitejs/devtools` (Vite DevTools). The kit takes care of mounting + * the SPA at the resolved base path, synthesizing an iframe dock entry + * from the definition's metadata, and threading the kit-augmented + * context into both the devframe-level `d.setup` and the optional + * `options.setup` hook. + * + * For richer kit-specific behavior (registering terminals/commands, + * adding additional dock entries), use `options.setup`. + */ +export function createPluginFromDevframe( + d: DevtoolDefinition, + options: CreatePluginFromDevframeOptions = {}, +): PluginWithDevTools { + const base = options.base ?? resolveBasePath(d, 'hosted') + + return { + name: options.name ?? `devframe:${d.id}`, + devtools: { + capabilities: options.capabilities ?? (d.capabilities as any), + async setup(rawCtx) { + const ctx = rawCtx as KitNodeContext + + if (d.cli?.distDir) { + ctx.views.hostStatic(base, resolve(d.cli.distDir)) + } + + ctx.docks.register({ + id: d.id, + title: d.name, + icon: d.icon ?? 'ph:plug-duotone', + ...options.dock, + type: 'iframe', + url: base, + } as DevToolsViewIframe) + + await d.setup(ctx) + + if (options.setup) { + await options.setup(ctx) + } + }, + }, + } +} diff --git a/packages/kit/src/node/diagnostics.ts b/packages/kit/src/node/diagnostics.ts new file mode 100644 index 00000000..e640ce51 --- /dev/null +++ b/packages/kit/src/node/diagnostics.ts @@ -0,0 +1,44 @@ +import c from 'ansis' +import { consoleReporter, createLogger, defineDiagnostics } from 'logs-sdk' +import { ansiFormatter } from 'logs-sdk/formatters/ansi' + +// Kit-side diagnostics for the hub subsystems (docks, terminals, commands, +// messages). The `DTK` prefix is shared with `@vitejs/devtools` (core); +// numbers must not collide. Kit reserves 0050+; core's codes top out +// below that today. +export const diagnostics = defineDiagnostics({ + docsBase: 'https://devtools.vite.dev/errors', + codes: { + DTK0050: { + message: (p: { id: string }) => `Dock with id "${p.id}" is already registered`, + hint: 'Use the `force` parameter to overwrite an existing registration.', + }, + DTK0051: { + message: 'Cannot change the id of a dock. Use register() to add new docks.', + }, + DTK0052: { + message: (p: { id: string }) => `Dock with id "${p.id}" is not registered. Use register() to add new docks.`, + }, + DTK0053: { + message: (p: { id: string }) => `Terminal session with id "${p.id}" already registered`, + }, + DTK0054: { + message: (p: { id: string }) => `Terminal session with id "${p.id}" not registered`, + }, + DTK0055: { + message: (p: { id: string }) => `Command "${p.id}" is already registered`, + }, + DTK0056: { + message: 'Cannot change the id of a command. Use register() to add new commands.', + }, + DTK0057: { + message: (p: { id: string }) => `Command "${p.id}" is not registered`, + }, + }, +}) + +export const logger = createLogger({ + diagnostics: [diagnostics], + formatter: ansiFormatter(c), + reporters: consoleReporter, +}) diff --git a/devframe/packages/devframe/src/node/host-commands.ts b/packages/kit/src/node/host-commands.ts similarity index 83% rename from devframe/packages/devframe/src/node/host-commands.ts rename to packages/kit/src/node/host-commands.ts index 6596b67b..bc77e30b 100644 --- a/devframe/packages/devframe/src/node/host-commands.ts +++ b/packages/kit/src/node/host-commands.ts @@ -1,4 +1,10 @@ -import type { DevToolsCommandHandle, DevToolsCommandsHost as DevToolsCommandsHostType, DevToolsNodeContext, DevToolsServerCommandEntry, DevToolsServerCommandInput } from 'devframe/types' +import type { + DevToolsCommandHandle, + DevToolsCommandsHost as DevToolsCommandsHostType, + DevToolsServerCommandEntry, + DevToolsServerCommandInput, +} from '../types/commands' +import type { KitNodeContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { logger } from './diagnostics' @@ -7,12 +13,12 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { public readonly events: DevToolsCommandsHostType['events'] = createEventEmitter() constructor( - public readonly context: DevToolsNodeContext, + public readonly context: KitNodeContext, ) {} register(command: DevToolsServerCommandInput): DevToolsCommandHandle { if (this.commands.has(command.id)) { - throw logger.DF0009({ id: command.id }).throw() + throw logger.DTK0055({ id: command.id }).throw() } this.commands.set(command.id, command) this.events.emit('command:registered', this.toSerializable(command)) @@ -21,11 +27,11 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { id: command.id, update: (patch: Partial>) => { if ('id' in patch) { - throw logger.DF0010().throw() + throw logger.DTK0056().throw() } const existing = this.commands.get(command.id) if (!existing) { - throw logger.DF0011({ id: command.id }).throw() + throw logger.DTK0057({ id: command.id }).throw() } Object.assign(existing, patch) this.events.emit('command:registered', this.toSerializable(existing)) @@ -45,7 +51,7 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { async execute(id: string, ...args: any[]): Promise { const found = this.findCommand(id) if (!found) { - throw logger.DF0011({ id }).throw() + throw logger.DTK0057({ id }).throw() } if (!found.handler) { throw new Error(`Command "${id}" has no handler (group-only command)`) diff --git a/devframe/packages/devframe/src/node/host-docks.ts b/packages/kit/src/node/host-docks.ts similarity index 85% rename from devframe/packages/devframe/src/node/host-docks.ts rename to packages/kit/src/node/host-docks.ts index d93b9edf..89c26c68 100644 --- a/devframe/packages/devframe/src/node/host-docks.ts +++ b/packages/kit/src/node/host-docks.ts @@ -1,11 +1,23 @@ -import type { DevToolsDockEntry, DevToolsDockHost as DevToolsDockHostType, DevToolsDocksUserSettings, DevToolsDockUserEntry, DevToolsNodeContext, DevToolsViewBuiltin, DevToolsViewIframe, RemoteConnectionInfo, RemoteDockOptions } from 'devframe/types' +import type { DevToolsNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' -import { DEFAULT_STATE_USER_SETTINGS, REMOTE_CONNECTION_KEY } from 'devframe/constants' +import type { + DevToolsDockEntry, + DevToolsDockHost as DevToolsDockHostType, + DevToolsDockUserEntry, + DevToolsViewBuiltin, + DevToolsViewIframe, + RemoteConnectionInfo, + RemoteDockOptions, +} from '../types/docks' +import type { DevToolsDocksUserSettings } from '../types/settings' +import type { KitNodeContext } from './context' +import { REMOTE_CONNECTION_KEY } from 'devframe/constants' +import { getInternalContext } from 'devframe/internal' +import { createStorage } from 'devframe/node' import { createEventEmitter } from 'devframe/utils/events' import { join } from 'pathe' -import { getInternalContext } from './context-internal' +import { DEFAULT_STATE_USER_SETTINGS } from '../constants' import { logger } from './diagnostics' -import { createStorage } from './storage' interface RemoteDockRecord { token: string @@ -59,7 +71,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { private readonly remoteDocks = new Map() constructor( - public readonly context: DevToolsNodeContext, + public readonly context: KitNodeContext, ) { } @@ -120,7 +132,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { if (view.type !== 'iframe' || !view.remote) return view const record = this.remoteDocks.get(view.id) - const endpoint = getInternalContext(this.context).wsEndpoint + const endpoint = getInternalContext(this.context as DevToolsNodeContext).wsEndpoint if (!record || !endpoint) return view @@ -145,7 +157,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { update: (patch: Partial) => void } { if (this.views.has(view.id) && !force) { - throw logger.DF0001({ id: view.id }).throw() + throw logger.DTK0050({ id: view.id }).throw() } this.prepareRemoteRegistration(view) this.views.set(view.id, view) @@ -154,7 +166,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { return { update: (patch) => { if (patch.id && patch.id !== view.id) { - throw logger.DF0002().throw() + throw logger.DTK0051().throw() } this.update(Object.assign(this.views.get(view.id)!, patch)) }, @@ -163,7 +175,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { update(view: DevToolsDockUserEntry): void { if (!this.views.has(view.id)) { - throw logger.DF0003({ id: view.id }).throw() + throw logger.DTK0052({ id: view.id }).throw() } this.prepareRemoteRegistration(view) this.views.set(view.id, view) @@ -171,7 +183,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { } private prepareRemoteRegistration(view: DevToolsDockUserEntry): void { - const internal = getInternalContext(this.context) + const internal = getInternalContext(this.context as DevToolsNodeContext) // Always revoke any previously allocated token for this dock id — covers // force re-registration and update() paths. internal.revokeRemoteTokensForDock(view.id) diff --git a/devframe/packages/devframe/src/node/host-messages.ts b/packages/kit/src/node/host-messages.ts similarity index 93% rename from devframe/packages/devframe/src/node/host-messages.ts rename to packages/kit/src/node/host-messages.ts index bc3f0b51..79001c41 100644 --- a/devframe/packages/devframe/src/node/host-messages.ts +++ b/packages/kit/src/node/host-messages.ts @@ -1,4 +1,10 @@ -import type { DevToolsMessageEntry, DevToolsMessageEntryInput, DevToolsMessageHandle, DevToolsMessagesHost as DevToolsMessagesHostType, DevToolsNodeContext } from 'devframe/types' +import type { + DevToolsMessageEntry, + DevToolsMessageEntryInput, + DevToolsMessageHandle, + DevToolsMessagesHost as DevToolsMessagesHostType, +} from '../types/messages' +import type { KitNodeContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { nanoid } from 'devframe/utils/nanoid' @@ -21,7 +27,7 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { } constructor( - public readonly context: DevToolsNodeContext, + public readonly context: KitNodeContext, ) {} async add(input: DevToolsMessageEntryInput): Promise { diff --git a/devframe/packages/devframe/src/node/host-terminals.ts b/packages/kit/src/node/host-terminals.ts similarity index 87% rename from devframe/packages/devframe/src/node/host-terminals.ts rename to packages/kit/src/node/host-terminals.ts index 78686f38..f7e36821 100644 --- a/devframe/packages/devframe/src/node/host-terminals.ts +++ b/packages/kit/src/node/host-terminals.ts @@ -1,13 +1,23 @@ -import type { DevToolsChildProcessExecuteOptions, DevToolsChildProcessTerminalSession, DevToolsNodeContext, DevToolsTerminalHost as DevToolsTerminalHostType, DevToolsTerminalSession, DevToolsTerminalSessionBase, PartialWithoutId, RpcStreamingChannel } from 'devframe/types' +import type { RpcStreamingChannel } from 'devframe/types' import type { Result as TinyExecResult } from 'tinyexec' +import type { + DevToolsChildProcessExecuteOptions, + DevToolsChildProcessTerminalSession, + DevToolsTerminalHost as DevToolsTerminalHostType, + DevToolsTerminalSession, + DevToolsTerminalSessionBase, +} from '../types/terminals' +import type { KitNodeContext } from './context' import process from 'node:process' import { createEventEmitter } from 'devframe/utils/events' import { logger } from './diagnostics' +type PartialWithoutId = Partial & { id: string } + /** - * Channel name used for terminal stream output. Built into devframe so the + * Channel name used for terminal stream output. Stable, well-known so the * standalone client (`packages/core/src/client/webcomponents/state/terminals.ts`) - * can subscribe by a stable, well-known name. + * can subscribe by name. */ const TERMINAL_STREAM_CHANNEL = 'devframe:terminals' as const const TERMINAL_REPLAY_WINDOW = 1000 @@ -24,7 +34,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { private _channel?: RpcStreamingChannel constructor( - public readonly context: DevToolsNodeContext, + public readonly context: KitNodeContext, ) { } @@ -47,7 +57,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { register(session: DevToolsTerminalSession): DevToolsTerminalSession { if (this.sessions.has(session.id)) { - throw logger.DF0004({ id: session.id }).throw() + throw logger.DTK0053({ id: session.id }).throw() } this.sessions.set(session.id, session) this.bindStream(session) @@ -57,7 +67,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { update(patch: PartialWithoutId): void { if (!this.sessions.has(patch.id)) { - throw logger.DF0005({ id: patch.id }).throw() + throw logger.DTK0054({ id: patch.id }).throw() } const session = this.sessions.get(patch.id)! Object.assign(session, patch) @@ -126,7 +136,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { terminal: Omit, ): Promise { if (this.sessions.has(terminal.id)) { - throw logger.DF0004({ id: terminal.id }).throw() + throw logger.DTK0053({ id: terminal.id }).throw() } const { exec } = await import('tinyexec') diff --git a/packages/kit/src/node/index.ts b/packages/kit/src/node/index.ts index cb1edbde..af160279 100644 --- a/packages/kit/src/node/index.ts +++ b/packages/kit/src/node/index.ts @@ -1 +1,8 @@ +export * from './context' +export * from './create-plugin-from-devframe' +export * from './host-commands' +export * from './host-docks' +export * from './host-messages' +export * from './host-terminals' +export * from './utils' export * from './vite-host' diff --git a/packages/kit/src/node/utils.ts b/packages/kit/src/node/utils.ts new file mode 100644 index 00000000..2c3d0b27 --- /dev/null +++ b/packages/kit/src/node/utils.ts @@ -0,0 +1,17 @@ +import type { ClientScriptEntry } from '../types/docks' +import { toDataURL } from 'mlly' + +/** + * Create a quick `ClientScriptEntry` from an inline function or + * stringified code. Useful for prototyping `action` / `renderer` + * dock entries without setting up a separate importable module. + * + * @experimental Prefer a proper importable module for production use. + */ +export function createSimpleClientScript(fn: string | ((ctx: any) => void)): ClientScriptEntry { + const code = `const fn = ${fn.toString()}; export default fn` + return { + importFrom: toDataURL(code), + importName: 'default', + } +} diff --git a/devframe/packages/devframe/src/types/commands.ts b/packages/kit/src/types/commands.ts similarity index 98% rename from devframe/packages/devframe/src/types/commands.ts rename to packages/kit/src/types/commands.ts index 6f34f055..96c48113 100644 --- a/devframe/packages/devframe/src/types/commands.ts +++ b/packages/kit/src/types/commands.ts @@ -1,4 +1,4 @@ -import type { EventEmitter } from './events' +import type { EventEmitter } from 'devframe/types' // --- Keybinding --- diff --git a/devframe/packages/devframe/src/types/docks.ts b/packages/kit/src/types/docks.ts similarity index 81% rename from devframe/packages/devframe/src/types/docks.ts rename to packages/kit/src/types/docks.ts index 37ce67bd..89642d78 100644 --- a/devframe/packages/devframe/src/types/docks.ts +++ b/packages/kit/src/types/docks.ts @@ -1,4 +1,4 @@ -import type { EventEmitter } from './events' +import type { ConnectionMeta, EventEmitter } from 'devframe/types' export interface DevToolsDockHost { readonly views: Map @@ -41,7 +41,7 @@ export interface DevToolsDockEntryBase { * Set to `'false'` to unconditionally hide the entry. * * @example 'clientType == embedded' - * @see {@link import('../utils/when').evaluateWhen} + * @see {@link import('devframe/utils/when').evaluateWhen} */ when?: string /** @@ -118,6 +118,14 @@ export interface RemoteDockOptions { originLock?: boolean } +export interface RemoteConnectionInfo extends ConnectionMeta { + backend: 'websocket' + websocket: string + v: 1 + authToken: string + origin: string +} + export type DevToolsViewLauncherStatus = 'idle' | 'loading' | 'success' | 'error' export interface DevToolsViewLauncher extends DevToolsDockEntryBase { @@ -149,40 +157,10 @@ export interface DevToolsViewBuiltin extends DevToolsDockEntryBase { id: '~terminals' | '~messages' | '~client-auth-notice' | '~settings' | '~popup' } -export interface JsonRenderElement { - type: string - props?: Record - children?: string[] - /** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */ - on?: Record - /** json-render visibility condition */ - visible?: unknown - /** json-render repeat binding */ - repeat?: unknown - /** Allow additional json-render element fields */ - [key: string]: unknown -} - -export interface JsonRenderSpec { - root: string - elements: Record - /** Initial client-side state model for $state/$bindState expressions */ - state?: Record -} - -export interface JsonRenderer { - /** Replace the entire spec */ - updateSpec: (spec: JsonRenderSpec) => void | Promise - /** Update json-render state values (shallow merge into spec.state) */ - updateState: (state: Record) => void | Promise - /** Internal: shared state key used by the client to subscribe */ - readonly _stateKey: string -} - export interface DevToolsViewJsonRender extends DevToolsDockEntryBase { type: 'json-render' /** JsonRenderer handle created by ctx.createJsonRenderer() */ - ui: JsonRenderer + ui: import('devframe/types').JsonRenderer } export type DevToolsDockUserEntry = DevToolsViewIframe | DevToolsViewAction | DevToolsViewCustomRender | DevToolsViewLauncher | DevToolsViewJsonRender diff --git a/packages/kit/src/types/index.ts b/packages/kit/src/types/index.ts index 6428a5e4..af2d0b59 100644 --- a/packages/kit/src/types/index.ts +++ b/packages/kit/src/types/index.ts @@ -1,7 +1,16 @@ -export type { WhenContext, WhenExpression } from '../utils/when' +// Re-export the kit-augmented context type so consumers can import it +// from the kit's main `types` barrel. +export type { CreateKitContextOptions, KitNodeContext } from '../node/context' +export type { WhenContext, WhenExpression } from '../utils/when' +export * from './commands' +export * from './docks' +export * from './messages' +export * from './settings' +export * from './terminals' export * from './vite-augment' export * from './vite-plugin' + export type { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from 'devframe/rpc' // NOTE: we re-export devframe's types individually rather than using @@ -10,68 +19,17 @@ export type { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from 'devframe/r // from external packages (tsdown 0.21 / rolldown-plugin-dts 0.23). // Revisit once upstream supports it. export type { - ClientScriptEntry, ConnectionMeta, DevToolsCapabilities, - DevToolsChildProcessExecuteOptions, - DevToolsChildProcessTerminalSession, - DevToolsClientCommand, - DevToolsCommandBase, - DevToolsCommandEntry, - DevToolsCommandHandle, - DevToolsCommandKeybinding, - DevToolsCommandShortcutOverrides, - DevToolsCommandsHost, - DevToolsCommandsHostEvents, DevToolsDiagnosticsDefinition, DevToolsDiagnosticsHost, DevToolsDiagnosticsLogger, - DevToolsDockEntriesGrouped, - DevToolsDockEntry, - DevToolsDockEntryBase, - DevToolsDockEntryCategory, - DevToolsDockEntryIcon, - DevToolsDockHost, - DevToolsDocksUserSettings, - DevToolsDockUserEntry, DevToolsHost, - DevToolsLogElementPosition, - DevToolsLogEntry, - DevToolsLogEntryFrom, - DevToolsLogEntryInput, - DevToolsLogFilePosition, - DevToolsLogHandle, - DevToolsLogLevel, - DevToolsLogsClient, - DevToolsLogsHost, - DevToolsMessageElementPosition, - DevToolsMessageEntry, - DevToolsMessageEntryFrom, - DevToolsMessageEntryInput, - DevToolsMessageFilePosition, - DevToolsMessageHandle, - DevToolsMessageLevel, - DevToolsMessagesClient, - DevToolsMessagesHost, DevToolsNodeRpcSession, - DevToolsNodeUtils, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, DevToolsRpcSharedStates, - DevToolsServerCommandEntry, - DevToolsServerCommandInput, - DevToolsTerminalHost, - DevToolsTerminalSession, - DevToolsTerminalSessionBase, - DevToolsTerminalStatus, - DevToolsViewAction, - DevToolsViewBuiltin, - DevToolsViewCustomRender, DevToolsViewHost, - DevToolsViewIframe, - DevToolsViewJsonRender, - DevToolsViewLauncher, - DevToolsViewLauncherStatus, EntriesToObject, EventEmitter, EventsMap, @@ -80,8 +38,6 @@ export type { JsonRenderer, JsonRenderSpec, PartialWithoutId, - RemoteConnectionInfo, - RemoteDockOptions, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, diff --git a/devframe/packages/devframe/src/types/messages.ts b/packages/kit/src/types/messages.ts similarity index 99% rename from devframe/packages/devframe/src/types/messages.ts rename to packages/kit/src/types/messages.ts index 3a8fedd8..87c87faf 100644 --- a/devframe/packages/devframe/src/types/messages.ts +++ b/packages/kit/src/types/messages.ts @@ -1,4 +1,4 @@ -import type { EventEmitter } from './events' +import type { EventEmitter } from 'devframe/types' export type DevToolsMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug' export type DevToolsMessageEntryFrom = 'server' | 'browser' diff --git a/devframe/packages/devframe/src/types/settings.ts b/packages/kit/src/types/settings.ts similarity index 100% rename from devframe/packages/devframe/src/types/settings.ts rename to packages/kit/src/types/settings.ts diff --git a/devframe/packages/devframe/src/types/terminals.ts b/packages/kit/src/types/terminals.ts similarity index 96% rename from devframe/packages/devframe/src/types/terminals.ts rename to packages/kit/src/types/terminals.ts index 77edb457..396c036c 100644 --- a/devframe/packages/devframe/src/types/terminals.ts +++ b/packages/kit/src/types/terminals.ts @@ -1,6 +1,6 @@ +import type { EventEmitter } from 'devframe/types' import type { ChildProcess } from 'node:child_process' import type { DevToolsDockEntryIcon } from './docks' -import type { EventEmitter } from './events' export interface DevToolsTerminalHost { readonly sessions: Map diff --git a/packages/kit/src/types/vite-plugin.ts b/packages/kit/src/types/vite-plugin.ts index d2e130fb..b1a959b6 100644 --- a/packages/kit/src/types/vite-plugin.ts +++ b/packages/kit/src/types/vite-plugin.ts @@ -1,5 +1,6 @@ -import type { DevToolsCapabilities, DevToolsNodeContext as DevToolsNodeContextBase } from 'devframe/types' +import type { DevToolsCapabilities } from 'devframe/types' import type { ResolvedConfig, ViteDevServer } from 'vite' +import type { KitNodeContext } from '../node/context' export interface DevToolsPluginOptions { capabilities?: { @@ -10,12 +11,14 @@ export interface DevToolsPluginOptions { } /** - * Vite-extended node context — framework-neutral {@link DevToolsNodeContextBase} - * plus the Vite-specific slots consumers can rely on when running under - * `@vitejs/devtools`. Callers that want portability should target the base - * type from `devframe/types`. + * Vite-extended node context — kit-augmented context with the four hub + * subsystems (`docks`, `terminals`, `messages`, `commands`) plus the + * Vite-specific slots (`viteConfig`, `viteServer`). Plugins running + * under `@vitejs/devtools` rely on this surface; portable devframe + * apps should target {@link KitNodeContext} or the framework-neutral + * `DevToolsNodeContext` from `devframe/types`. */ -export interface ViteDevToolsNodeContext extends DevToolsNodeContextBase { +export interface ViteDevToolsNodeContext extends KitNodeContext { readonly viteConfig: ResolvedConfig readonly viteServer?: ViteDevServer } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5887e327..cddd825f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -610,6 +610,9 @@ importers: devframe/examples/devframe-files-inspector: dependencies: + '@vitejs/devtools-kit': + specifier: workspace:* + version: link:../../../packages/kit devframe: specifier: workspace:* version: link:../../packages/devframe @@ -963,18 +966,36 @@ importers: packages/kit: dependencies: + ansis: + specifier: catalog:deps + version: 4.2.0 birpc: specifier: catalog:deps version: 4.0.0 devframe: specifier: workspace:* version: link:../../devframe/packages/devframe + logs-sdk: + specifier: catalog:deps + version: 0.0.6 + mlly: + specifier: catalog:deps + version: 1.8.2 ohash: specifier: catalog:deps version: 2.0.11 + pathe: + specifier: catalog:deps + version: 2.0.3 + perfect-debounce: + specifier: catalog:deps + version: 2.1.0 sirv: specifier: catalog:deps version: 3.0.2 + tinyexec: + specifier: catalog:deps + version: 1.1.2 devDependencies: human-id: specifier: catalog:inlined @@ -9130,7 +9151,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.1.1 + tinyexec: 1.1.2 '@antfu/ni@30.1.0': dependencies: @@ -9941,7 +9962,7 @@ snapshots: srvx: 0.11.15 std-env: 4.1.0 tinyclip: 0.1.12 - tinyexec: 1.1.1 + tinyexec: 1.1.2 ufo: 1.6.4 youch: 4.1.1 optionalDependencies: @@ -16039,13 +16060,13 @@ snapshots: dependencies: citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.1.1 + tinyexec: 1.1.2 nypm@0.6.6: dependencies: citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.1.1 + tinyexec: 1.1.2 object-assign@4.1.1: {} diff --git a/skills/vite-devtools-kit/SKILL.md b/skills/vite-devtools-kit/SKILL.md index cfd7c291..1ac7059e 100644 --- a/skills/vite-devtools-kit/SKILL.md +++ b/skills/vite-devtools-kit/SKILL.md @@ -1,38 +1,70 @@ --- name: writing-vite-devtools-integrations description: > - Creates devtools integrations for Vite using @vitejs/devtools-kit. - Use when building Vite plugins with devtools panels, RPC functions, - dock entries, shared state, logs/notifications, or any devtools-related - functionality. Applies to files importing from @vitejs/devtools-kit or - containing devtools.setup hooks in Vite plugins. + Creates devtools integrations that mount inside the Vite DevTools + hub via @vitejs/devtools-kit. Use when building Vite plugins with + devtools panels, RPC functions, dock entries, shared state, + messages/notifications, terminals, command palette entries, or any + hub-level integration. Applies to files importing from + @vitejs/devtools-kit or containing devtools.setup hooks in Vite + plugins. For building one portable devtool integration without a + hub (CLI, static deploy, MCP), see the `devframe` skill instead. --- # Vite DevTools Kit -Build custom developer tools that integrate with Vite DevTools using `@vitejs/devtools-kit`. +**`@vitejs/devtools-kit` is the hub that unites many devtools integrations.** It owns the cross-tool surface — docking, the command palette, terminal aggregation, cross-tool toasts — and wraps the framework-neutral [DevFrame](https://devfra.me/) container with the Vite-specific glue (`Plugin.devtools.setup`). + +If you have a portable DevFrame app already, drop it in via `createPluginFromDevframe(d)` from `@vitejs/devtools-kit/node` and the kit auto-derives the iframe dock entry. If you're authoring a Vite-specific integration that needs hub features directly, reach for `Plugin.devtools.setup`. ## Core Concepts -A DevTools plugin extends a Vite plugin with a `devtools.setup(ctx)` hook. The context provides: - -| Property | Purpose | -|----------|---------| -| `ctx.docks` | Register dock entries (iframe, action, custom-render, launcher, json-render) | -| `ctx.views` | Host static files for UI | -| `ctx.rpc` | Register RPC functions, broadcast to clients | -| `ctx.rpc.sharedState` | Synchronized server-client state | -| `ctx.rpc.streaming` | Streaming channels — chunk-style server↔client data with cancellation, replay, Web Streams interop | -| `ctx.messages` | Emit structured message entries and toast notifications | -| `ctx.diagnostics` | Structured diagnostics host (logs-sdk) — register custom error codes | -| `ctx.terminals` | Spawn and manage child processes with streaming terminal output | -| `ctx.createJsonRenderer` | Create server-side JSON render specs for zero-client-code UIs | -| `ctx.commands` | Register executable commands with keyboard shortcuts and palette visibility | -| `ctx.viteConfig` | Resolved Vite configuration | -| `ctx.viteServer` | Dev server instance (dev mode only) | -| `ctx.mode` | `'dev'` or `'build'` | - -## Quick Start: Minimal Plugin +A DevTools plugin extends a Vite plugin with a `devtools.setup(ctx)` hook. The context is the **kit-augmented context** (`KitNodeContext` extended with Vite-specific fields) — it carries DevFrame's portable surface plus the hub-only subsystems the kit owns: + +| Property | Layer | Purpose | +|----------|-------|---------| +| `ctx.docks` | **kit** | Register dock entries (iframe, action, custom-render, launcher, json-render) | +| `ctx.terminals` | **kit** | Spawn and manage child processes with streaming terminal output | +| `ctx.messages` | **kit** | Emit structured message entries and toast notifications | +| `ctx.commands` | **kit** | Register executable commands with keyboard shortcuts and palette visibility | +| `ctx.rpc` | devframe | Register RPC functions, broadcast to clients | +| `ctx.rpc.sharedState` | devframe | Synchronized server-client state | +| `ctx.rpc.streaming` | devframe | Streaming channels — chunk-style server↔client data with cancellation, replay, Web Streams interop | +| `ctx.views` | devframe | Host static files for UI (`hostStatic(base, distDir)`) | +| `ctx.diagnostics` | devframe | Structured diagnostics host (logs-sdk) — register custom error codes | +| `ctx.createJsonRenderer` | devframe | Create server-side JSON render specs for zero-client-code UIs | +| `ctx.viteConfig` | core | Resolved Vite configuration | +| `ctx.viteServer` | core | Dev server instance (dev mode only) | +| `ctx.mode` | devframe | `'dev'` or `'build'` | + +## Quick Start: Bridge a DevFrame App + +If you already have a portable DevFrame definition, this is the one-liner. The kit synthesises the iframe dock entry from the definition's `id` / `name` / `icon` / `basePath`, mounts the SPA via `views.hostStatic`, runs the devtool's own `setup`, then runs the optional kit-only `options.setup`. + +```ts +// vite.config.ts +import { createPluginFromDevframe } from '@vitejs/devtools-kit/node' +import devtool from './my-devtool' + +export default { + plugins: [ + createPluginFromDevframe(devtool, { + // Optional kit-only setup for hub features: + setup(ctx) { + ctx.commands.register({ + id: 'my-devtool:clear-cache', + title: 'Clear Cache', + handler: () => {/* ... */}, + }) + }, + }), + ], +} +``` + +## Quick Start: Minimal Hub-Native Plugin + +When the integration is intrinsically tied to Vite (it inspects the resolved config, augments middleware, etc.), reach for `Plugin.devtools.setup` directly: ```ts /// diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts index 346b5475..650170b9 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts @@ -2,22 +2,96 @@ * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/client` */ // #region Interfaces +export interface CommandsContext { + readonly commands: DevToolsCommandEntry[]; + readonly paletteCommands: DevToolsCommandEntry[]; + register: (_: DevToolsClientCommand | DevToolsClientCommand[]) => () => void; + execute: (_: string, ..._: any[]) => Promise; + getKeybindings: (_: string) => DevToolsCommandKeybinding[]; + settings: SharedState; + paletteOpen: boolean; +} export interface DockClientScriptContext extends DocksContext { current: DockEntryState; messages: DevToolsMessagesClient; readonly logs: DevToolsMessagesClient; } +export interface DockEntryState { + entryMeta: DevToolsDockEntry; + readonly isActive: boolean; + domElements: { + iframe?: HTMLIFrameElement | null; + panel?: HTMLDivElement | null; + }; + events: EventEmitter; +} +export interface DockEntryStateEvents { + 'entry:activated': () => void; + 'entry:deactivated': () => void; + 'entry:updated': (_: DevToolsDockUserEntry) => void; + 'dom:panel:mounted': (_: HTMLDivElement) => void; + 'dom:iframe:mounted': (_: HTMLIFrameElement) => void; +} +export interface DockPanelStorage { + mode: 'float' | 'edge'; + width: number; + height: number; + top: number; + left: number; + position: 'left' | 'right' | 'bottom' | 'top'; + open: boolean; + inactiveTimeout: number; +} +export interface DocksContext extends DevToolsRpcContext { + readonly clientType: 'embedded' | 'standalone'; + readonly panel: DocksPanelContext; + readonly docks: DocksEntriesContext; + readonly commands: CommandsContext; + readonly when: WhenClauseContext; +} +export interface DocksEntriesContext { + selectedId: string | null; + readonly selected: DevToolsDockEntry | null; + entries: DevToolsDockEntry[]; + entryToStateMap: Map; + groupedEntries: DevToolsDockEntriesGrouped; + settings: SharedState; + getStateById: (_: string) => DockEntryState | undefined; + switchEntry: (_?: string | null) => Promise; + toggleEntry: (_: string) => Promise; +} +export interface DocksPanelContext { + store: DockPanelStorage; + isDragging: boolean; + isResizing: boolean; + readonly isVertical: boolean; +} +export interface WhenClauseContext { + readonly context: WhenContext; +} // #endregion // #region Types export type ConnectRemoteDevToolsOptions = Omit; +export type DevToolsClientContext = DocksContext; +export type DockClientType = 'embedded' | 'standalone'; // #endregion // #region Functions export declare function connectRemoteDevTools(_?: ConnectRemoteDevToolsOptions): Promise; +export declare function getDevToolsClientContext(): DevToolsClientContext | undefined; export declare function parseRemoteConnection(_?: string): RemoteConnectionInfo | null; // #endregion +// #region Variables +export declare const CLIENT_CONTEXT_KEY: string; +// #endregion + // #region Re-exports export * from "devframe/client"; +// #endregion + +// #region Other +export { DevToolsClientRpcHost } +export { RpcClientEvents } // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js index 4fed5576..bfc1f93c 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js @@ -3,9 +3,14 @@ */ // #region Functions export async function connectRemoteDevTools(_) {} +export function getDevToolsClientContext() {} export function parseRemoteConnection(_) {} // #endregion +// #region Variables +export var CLIENT_CONTEXT_KEY /* const */ +// #endregion + // #region Re-exports export * from "devframe/client"; // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts index 0a08f810..02e84179 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts @@ -1,6 +1,11 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/constants` */ +// #region Variables +export declare const DEFAULT_CATEGORIES_ORDER: Record; +export declare const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings; +// #endregion + // #region Re-exports export * from "devframe/constants"; // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js index 0a08f810..ecc6f929 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js @@ -1,6 +1,11 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/constants` */ +// #region Variables +export var DEFAULT_CATEGORIES_ORDER /* const */ +export var DEFAULT_STATE_USER_SETTINGS /* const */ +// #endregion + // #region Re-exports export * from "devframe/constants"; // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts index 87467e46..248be7f5 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts @@ -18,6 +18,7 @@ export declare const defineRpcFunction: >; + capabilities?: DevToolsCapabilities | { + dev?: DevToolsCapabilities | boolean; + build?: DevToolsCapabilities | boolean; + }; + setup?: (_: KitNodeContext) => void | Promise; +} export interface CreateViteDevToolsHostOptions { viteConfig: ResolvedConfig; viteServer?: ViteDevServer; @@ -9,6 +19,83 @@ export interface CreateViteDevToolsHostOptions { } // #endregion +// #region Classes +export declare class DevToolsCommandsHost implements DevToolsCommandsHost$1 { + readonly context: KitNodeContext; + readonly commands: DevToolsCommandsHost$1['commands']; + readonly events: DevToolsCommandsHost$1['events']; + constructor(_: KitNodeContext); + register(_: DevToolsServerCommandInput): DevToolsCommandHandle; + unregister(_: string): boolean; + execute(_: string, ..._: any[]): Promise; + list(): DevToolsServerCommandEntry[]; + private findCommand; + private toSerializable; +} +export declare class DevToolsDockHost implements DevToolsDockHost$1 { + readonly context: KitNodeContext; + readonly views: DevToolsDockHost$1['views']; + readonly events: DevToolsDockHost$1['events']; + userSettings: SharedState; + private readonly remoteDocks; + constructor(_: KitNodeContext); + init(): Promise; + values({ + includeBuiltin + }?: { + includeBuiltin?: boolean; + }): DevToolsDockEntry[]; + private projectView; + private resolveDevServerOrigin; + register(_: T, _?: boolean): { + update: (_: Partial) => void; + }; + update(_: DevToolsDockUserEntry): void; + private prepareRemoteRegistration; +} +export declare class DevToolsMessagesHost implements DevToolsMessagesHost$1 { + readonly context: KitNodeContext; + readonly entries: DevToolsMessagesHost$1['entries']; + readonly events: DevToolsMessagesHost$1['events']; + readonly lastModified: Map; + readonly removals: Array<{ + id: string; + time: number; + }>; + private _autoDeleteTimers; + private _clock; + private _tick; + constructor(_: KitNodeContext); + add(_: DevToolsMessageEntryInput): Promise; + update(_: string, _: Partial): Promise; + remove(_: string): Promise; + clear(): Promise; + private _createHandle; +} +export declare class DevToolsTerminalHost implements DevToolsTerminalHost$1 { + readonly context: KitNodeContext; + readonly sessions: DevToolsTerminalHost$1['sessions']; + readonly events: DevToolsTerminalHost$1['events']; + private _boundStreams; + private _channel?; + constructor(_: KitNodeContext); + private getStreamingChannel; + register(_: DevToolsTerminalSession): DevToolsTerminalSession; + update(_: PartialWithoutId): void; + remove(_: DevToolsTerminalSession): void; + private bindStream; + startChildProcess(_: DevToolsChildProcessExecuteOptions, _: Omit): Promise; +} +// #endregion + // #region Functions +export declare function createPluginFromDevframe(_: DevtoolDefinition, _?: CreatePluginFromDevframeOptions): PluginWithDevTools; +export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; export declare function createViteDevToolsHost(_: CreateViteDevToolsHostOptions): DevToolsHost; +// #endregion + +// #region Other +export { createKitContext } +export { CreateKitContextOptions } +export { KitNodeContext } // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js index 9bb907d0..8ec7a60b 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js @@ -1,6 +1,65 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/node` */ +// #region Classes +export class DevToolsCommandsHost { + commands + events + constructor(_) {} + register(_) {} + unregister(_) {} + async execute(_, ..._) {} + list() {} + findCommand(_) {} + toSerializable(_) {} +} +export class DevToolsDockHost { + views + events + userSettings + remoteDocks + constructor(_) {} + async init() {} + values(_) {} + projectView(_) {} + resolveDevServerOrigin() {} + register(_, _) {} + update(_) {} + prepareRemoteRegistration(_) {} +} +export class DevToolsMessagesHost { + entries + events + lastModified + removals + _autoDeleteTimers + _clock + _tick() {} + constructor(_) {} + async add(_) {} + async update(_, _) {} + async remove(_) {} + async clear() {} + _createHandle(_) {} +} +export class DevToolsTerminalHost { + sessions + events + _boundStreams + _channel + constructor(_) {} + getStreamingChannel() {} + register(_) {} + update(_) {} + remove(_) {} + bindStream(_) {} + async startChildProcess(_, _) {} +} +// #endregion + // #region Functions +export async function createKitContext(_) {} +export function createPluginFromDevframe(_, _) {} +export function createSimpleClientScript(_) {} export function createViteDevToolsHost(_) {} // #endregion \ No newline at end of file diff --git a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts index f909cec0..4cc13faf 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools/index.snapshot.d.ts @@ -6,7 +6,7 @@ export type BuiltinServerFunctions = RpcDefinitionsToFunctions; +export declare function createDevToolsContext(_: ResolvedConfig, _?: ViteDevServer): Promise; export declare function createDevToolsMiddleware(_: CreateWsServerOptions): Promise<{ h3: _$h3.App; rpc: _$birpc.BirpcGroup<_$devframe.DevToolsRpcClientFunctions, _$devframe.DevToolsRpcServerFunctions, false>; diff --git a/tsconfig.base.json b/tsconfig.base.json index a040f256..25ee5e26 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -33,6 +33,9 @@ "devframe/constants": [ "./devframe/packages/devframe/src/constants.ts" ], + "devframe/internal": [ + "./devframe/packages/devframe/src/internal/index.ts" + ], "devframe/utils/events": [ "./devframe/packages/devframe/src/utils/events.ts" ], @@ -69,9 +72,6 @@ "devframe/adapters/vite": [ "./devframe/packages/devframe/src/adapters/vite.ts" ], - "devframe/adapters/kit": [ - "./devframe/packages/devframe/src/adapters/kit.ts" - ], "devframe/adapters/embedded": [ "./devframe/packages/devframe/src/adapters/embedded.ts" ],