diff --git a/AGENTS.md b/AGENTS.md index c2299b6f..3d1b2191 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,13 @@ ## Positioning -Two layers, one mental model: +Three layers, one mental model: - **`devframe`** — *the container for one devtool integration, portable across viewers.* External project; lives at [`github.com/devframes/devframe`](https://github.com/devframes/devframe), docs at [`devfra.me`](https://devfra.me). Consumed here as an npm dependency (`catalog:deps`). -- **`@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(devframeApp)` so a portable devframe definition drops into Vite DevTools as a Vite plugin, with the dock entry auto-derived from the definition's metadata. +- **`@devframes/hub`** — *the framework-neutral hub layer on top of devframe.* Owns docks, terminals, messages, commands, the `mountDevframe` primitive, and the json-render factory — anything that only matters once a host wants to combine multiple devframes into one UI. External project, same repo as devframe; consumed via npm. +- **`@vitejs/devtools-kit`** — *the Vite-flavored skin over `@devframes/hub`.* Re-exports hub's hosts and primitives under the kit's `DevTools*` names, adds the Vite-specific extensions (`ViteDevToolsNodeContext`, `PluginWithDevTools`, `DevToolsPluginOptions`, `createViteDevToolsHost`, the `~viteplus` dock category), pins the kit-side mount path at `/__devtools/`, and ships `createPluginFromDevframe` to drop a portable devframe into Vite DevTools as a Vite plugin. -When deciding where something belongs: if a single-app standalone CLI would still need it, it belongs upstream in devframe; if it only matters once you have multiple integrations or a host UI, it lives in the kit. +When deciding where something belongs: if a single-app standalone CLI would still need it, it belongs upstream in devframe; if it only matters once a host combines multiple integrations, it belongs in `@devframes/hub` (or in `@vitejs/devtools-kit` if it's Vite-specific). ## Stack & Structure @@ -17,7 +18,7 @@ Monorepo (`pnpm` workspaces + `turbo`). ESM TypeScript; bundled with `tsdown`. P | Package | npm | Description | |---------|-----|-------------| -| `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/kit` | `@vitejs/devtools-kit` | Vite-flavored skin over `@devframes/hub`. `createKitContext` wraps hub's `createHubContext` and surfaces the Vite-augmented context type (`viteConfig`/`viteServer`); hub hosts (`docks` / `terminals` / `messages` / `commands`) and the `mountDevframe` primitive are re-exported under the kit's `DevTools*` aliases. `createPluginFromDevframe` delegates to `mountDevframe` and wraps it in a `Plugin.devtools.setup` Vite plugin shell. | | `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/`. | @@ -31,7 +32,8 @@ Other top-level directories: ```mermaid flowchart TD - kit --> devframe + hub --> devframe + kit --> hub core --> kit core --> rolldown & vite & self-inspect rolldown --> kit & ui @@ -42,15 +44,16 @@ flowchart TD ## Dep Boundary -`devframe` is an external package consumed via `catalog:deps` — contribute upstream at [github.com/devframes/devframe](https://github.com/devframes/devframe). `packages/kit` and above build on top of it. Features that require multi-integration awareness (docks, terminals, messages, commands) belong in kit. +`devframe` and `@devframes/hub` are external packages consumed via `catalog:deps` — contribute upstream at [github.com/devframes/devframe](https://github.com/devframes/devframe). `packages/kit` and above build on top of them. Features that require multi-integration awareness (docks, terminals, messages, commands) belong upstream in `@devframes/hub`. Features that only matter to Vite — `ViteDevToolsNodeContext`, `PluginWithDevTools`, the `~viteplus` category, the kit-pinned `/__devtools/` mount path, the `vite:open-in-editor`/`vite:open-in-finder` commands — stay in `@vitejs/devtools-kit` and `@vitejs/devtools`. -`devframe/node/internal` is a marked-internal subpath exposing a small set of helpers (`getInternalContext`, `resolveBasePath`) for first-party adapters reaching into devframe's private machinery — kit's `DocksHost` uses it for remote-dock token allocation. End users should not import it. +`devframe/node/hub-internals` is a marked-public-but-low-level subpath exposing a small set of helpers (`getInternalContext`, `resolveBasePath`) for first-party adapters reaching into devframe's hub-side machinery — kit's adapters use `getInternalContext` for remote-dock token allocation and WS-endpoint metadata. End users should not import it. ## Architecture -- **Devframe context** (external — see [devfra.me](https://devfra.me)): `createHostContext` returns a `DevToolsNodeContext` carrying `rpc`, `views` (HTTP file-serving via `hostStatic`), `diagnostics`, `agent`, plus `cwd`/`workspaceRoot`/`mode`/`host`. No docks, no terminals, no json-render. -- **Kit context** (`packages/kit/src/node/context.ts`): `createKitContext` wraps `createHostContext` and attaches the four hub hosts — `docks`, `terminals`, `messages`, `commands` — plus the `createJsonRenderer` factory. 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. +- **Devframe context** (external — see [devfra.me](https://devfra.me)): `createHostContext` returns a `DevframeNodeContext` carrying `rpc`, `views` (HTTP file-serving via `hostStatic`), `diagnostics`, `agent`, plus `cwd`/`workspaceRoot`/`mode`/`host`. No docks, no terminals, no json-render. +- **Hub context** (external `@devframes/hub/node`): `createHubContext` wraps `createHostContext` and attaches the four hub hosts — `docks`, `terminals`, `messages`, `commands` — plus the `createJsonRenderer` factory. Wires the `'devframe:docks'` / `'devframe:commands'` shared-state sync and seeds the built-in `~terminals` / `~messages` / `~settings` docks. Also ships `mountDevframe(ctx, def)` — the framework-neutral primitive that registers any `DevframeDefinition` as a dock. +- **Kit context** (`packages/kit/src/node/context.ts`): `createKitContext` wraps `createHubContext` and surfaces Vite-specific `viteConfig`/`viteServer` slots when mounted inside Vite DevTools. `KitNodeContext` extends `DevframeHubContext` so all the hub hosts come along for free. +- **Bridge** (`packages/kit/src/node/create-plugin-from-devframe.ts`): `createPluginFromDevframe(d, opts?)` returns `PluginWithDevTools`; in its `setup`, delegates to `mountDevframe(ctx, d, opts)` to mount the SPA, register the auto-derived iframe dock entry, and run `d.setup(ctx)`, 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`. @@ -97,13 +100,13 @@ All node-side warnings and errors use structured diagnostics via [`nostics`](htt | Prefix | Package(s) | Diagnostics file | |--------|-----------|-----------------| -| `DTK` | `packages/kit` + `packages/core` (shared codespace, hub-side) | `packages/kit/src/node/diagnostics.ts`, `packages/core/src/node/diagnostics.ts` | +| `DTK` | `packages/kit` + `packages/core` (shared codespace, Vite-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) | — | -`DF` codes belong to the upstream devframe project — file new ones there. +`DF` codes belong to the upstream devframe/hub projects — file new ones there. The `DF8xxx` sub-range covers `@devframes/hub` (DF8100–DF8199 docks, DF8200–DF8299 terminals, DF8300–DF8399 messages, DF8400–DF8499 commands). -`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. +`DTK` is shared between core and kit because they're sibling layers of Vite DevTools. Coordinate code numbers across both files: kit reserves `DTK0050+` for Vite-specific kit codes; core's existing codes top out below that. The hub-domain DTK codes (DTK0050–DTK0057) retired when their conditions moved upstream to `DF8100`–`DF8403`. Codes are sequential 4-digit numbers per prefix (e.g. `DTK0033`, `RDDT0003`). Check the existing diagnostics file to find the next available number. diff --git a/MIGRATION.md b/MIGRATION.md index eca20944..73c5379a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,5 +1,39 @@ # Migration Guide +## 0.2 → 0.3 (devframe 0.5.x + `@devframes/hub`) + +`@vitejs/devtools-kit` now thins onto [`@devframes/hub`](https://www.npmjs.com/package/@devframes/hub) for the hub primitives (docks, terminals, messages, commands, `mountDevframe`, json-render factory). The kit's public `DevTools*` surface stays in place via re-export aliases — most plugin authors don't need to change anything. + +### What changes for plugin authors + +- **Nothing at the import level.** `DevToolsRpcClient`, `DevToolsDockEntry`, `DevToolsNodeContext`, `PluginWithDevTools`, `defineRpcFunction`, `getDevToolsRpcClient`, `connectRemoteDevTools`, the `DEVTOOLS_MOUNT_PATH` constant — all keep their `@vitejs/devtools-kit` import paths and identifier names. +- **One-time re-auth.** The anonymous-auth RPC scope moved from `vite:anonymous:` to `devframe:anonymous:` and the WebSocket auth-token query parameter from `vite_devtools_auth_token` to `devframe_auth_token` (both following devframe's internal rename). Auth tokens stored by older clients become invalid — users re-authorize once on first connect after upgrading. +- **`DTK0050`–`DTK0057` retire.** These dock/terminal/command diagnostic codes now ship from `@devframes/hub` as `DF8100`–`DF8403`. Error messages and docs URLs change accordingly; the conditions that trigger them are unchanged. + +### What stays the same + +- The Vite DevTools SPA continues to serve at **`/__devtools/`**. The kit pins this mount path independently of devframe's new `/__devframe/` default. +- `~viteplus` remains a valid dock category. +- `vite:open-in-editor` and `vite:open-in-finder` server commands keep their existing IDs. + +### If you import from `devframe` directly + +The `devframe/node/internal` subpath was renamed to `devframe/node/hub-internals` in devframe v0.5.x. Update direct imports: + +```diff +- import { getInternalContext } from 'devframe/node/internal' ++ import { getInternalContext } from 'devframe/node/hub-internals' +``` + +And the type rename: + +```diff +- import type { DevToolsInternalContext } from 'devframe/node/internal' ++ import type { DevframeInternalContext } from 'devframe/node/hub-internals' +``` + +Inside the kit's own re-export (`@vitejs/devtools/internal`), the alias `DevToolsInternalContext` remains. + ## 0.1 → 0.2 ### `@vitejs/devtools-kit` diff --git a/docs/errors/DTK0013.md b/docs/errors/DTK0013.md index 0d7e929c..6fed2dc5 100644 --- a/docs/errors/DTK0013.md +++ b/docs/errors/DTK0013.md @@ -10,12 +10,12 @@ outline: deep ## Cause -This error is thrown by the RPC resolver in `createWsServer()` when an untrusted WebSocket client attempts to call a non-anonymous RPC method. Methods whose names start with `vite:anonymous:` are exempt from authentication and can be called by any client; all other methods require the client to be trusted. +This error is thrown by the RPC resolver in `createWsServer()` when an untrusted WebSocket client attempts to call a non-anonymous RPC method. Methods whose names start with `devframe:anonymous:` are exempt from authentication and can be called by any client; all other methods require the client to be trusted. A client becomes trusted through one of these mechanisms: 1. **Client auth is disabled** (build mode, `clientAuth: false`, or `VITE_DEVTOOLS_DISABLE_CLIENT_AUTH=true`) -- all clients are auto-trusted. -2. **Auth token in storage** -- the client provides a `vite_devtools_auth_token` query parameter that matches a token stored in `node_modules/.vite/devtools/auth.json`. +2. **Auth token in storage** -- the client provides a `devframe_auth_token` query parameter that matches a token stored in `node_modules/.vite/devtools/auth.json`. 3. **Static auth tokens** -- the token matches one of the tokens listed in `devtools.config.clientAuthTokens`. If none of these conditions are met, the client is untrusted and any call to a non-anonymous method triggers this error. @@ -41,7 +41,7 @@ Provide a valid authentication token when connecting: ```ts // Client-side: connecting with a valid auth token const ws = new WebSocket( - 'ws://localhost:7812?vite_devtools_auth_token=your-token-here' + 'ws://localhost:7812?devframe_auth_token=your-token-here' ) ``` @@ -67,4 +67,4 @@ If you are developing locally and want to skip authentication entirely, see [DTK ## Source -- [`packages/core/src/node/ws.ts`](https://github.com/vitejs/devtools/blob/main/packages/core/src/node/ws.ts) — the WebSocket RPC resolver in `createWsServer()` throws `DTK0013` when an untrusted client invokes a method whose name is not prefixed with `vite:anonymous:`. +- [`packages/core/src/node/ws.ts`](https://github.com/vitejs/devtools/blob/main/packages/core/src/node/ws.ts) — the WebSocket RPC resolver in `createWsServer()` throws `DTK0013` when an untrusted client invokes a method whose name is not prefixed with `devframe:anonymous:`. diff --git a/docs/errors/DTK0050.md b/docs/errors/DTK0050.md deleted file mode 100644 index d99c5b51..00000000 --- a/docs/errors/DTK0050.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -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 deleted file mode 100644 index d42ced5f..00000000 --- a/docs/errors/DTK0051.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -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 deleted file mode 100644 index 3cb2495c..00000000 --- a/docs/errors/DTK0052.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 deleted file mode 100644 index f59cb7f1..00000000 --- a/docs/errors/DTK0053.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -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 deleted file mode 100644 index d33ddadf..00000000 --- a/docs/errors/DTK0054.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -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 deleted file mode 100644 index 491acf0e..00000000 --- a/docs/errors/DTK0055.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 deleted file mode 100644 index 4a2fe290..00000000 --- a/docs/errors/DTK0056.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -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 deleted file mode 100644 index 02189ec1..00000000 --- a/docs/errors/DTK0057.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -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 54660d62..10004303 100644 --- a/docs/errors/index.md +++ b/docs/errors/index.md @@ -31,14 +31,8 @@ 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 | + +Hub-side diagnostics for docks, terminals, messages, and commands live upstream in `@devframes/hub` under the `DF8xxx` range — see the [Devframe error reference](https://devfra.me/errors/). ## Rolldown DevTools (RDDT) diff --git a/docs/kit/remote-client.md b/docs/kit/remote-client.md index 9181d800..8a4fce25 100644 --- a/docs/kit/remote-client.md +++ b/docs/kit/remote-client.md @@ -35,7 +35,7 @@ sequenceDiagram Core->>Core: createWsServer → wsEndpoint=ws://localhost:7812 User->>Core: open dock Core->>Hosted: iframe src + connection descriptor - Hosted->>Core: WS connect (?vite_devtools_auth_token=...) + Hosted->>Core: WS connect (?devframe_auth_token=...) Core->>Core: verify token + Origin Core-->>Hosted: trusted session Hosted->>Core: rpc.call('my-plugin:…') diff --git a/packages/core/package.json b/packages/core/package.json index b2592739..fb93da51 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,6 +56,7 @@ "vite": "*" }, "dependencies": { + "@devframes/hub": "catalog:deps", "@vitejs/devtools-kit": "workspace:*", "@vitejs/devtools-rolldown": "workspace:*", "birpc": "catalog:deps", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0aa40d11..2b0db643 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,4 +3,7 @@ export { DevTools } from './node/plugins' export type { BuiltinServerFunctions } from './node/rpc' export { createDevToolsMiddleware } from './node/server' export type { DevToolsMiddleware } from './node/server' -export type { DevToolsInternalContext, InternalAnonymousAuthStorage } from 'devframe/node/internal' +export type { + DevframeInternalContext as DevToolsInternalContext, + InternalAnonymousAuthStorage, +} from 'devframe/node/hub-internals' diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts index ca0d2ebb..da31e4cc 100644 --- a/packages/core/src/internal.ts +++ b/packages/core/src/internal.ts @@ -1,2 +1,5 @@ -export { getInternalContext } from 'devframe/node/internal' -export type { DevToolsInternalContext, InternalAnonymousAuthStorage } from 'devframe/node/internal' +export { getInternalContext } from 'devframe/node/hub-internals' +export type { + DevframeInternalContext as DevToolsInternalContext, + InternalAnonymousAuthStorage, +} from 'devframe/node/hub-internals' diff --git a/packages/core/src/node/plugins/integration.ts b/packages/core/src/node/plugins/integration.ts index a578091d..456aae6e 100644 --- a/packages/core/src/node/plugins/integration.ts +++ b/packages/core/src/node/plugins/integration.ts @@ -26,7 +26,7 @@ export async function runDevTools(builder: unknown) { const config = (builder as ViteBuilder).config for (const _environment of getDevToolsEnvironments(config)) { try { - const { start } = await import('@vitejs/devtools/cli-commands') + const { start } = await import('../cli-commands') await start(config.devtools.config) } catch (error: any) { diff --git a/packages/core/src/node/rpc/anonymous/auth.ts b/packages/core/src/node/rpc/anonymous/auth.ts index f8586e2e..6a5c582f 100644 --- a/packages/core/src/node/rpc/anonymous/auth.ts +++ b/packages/core/src/node/rpc/anonymous/auth.ts @@ -3,7 +3,7 @@ import process from 'node:process' import * as p from '@clack/prompts' import { defineRpcFunction } from '@vitejs/devtools-kit' import { abortPendingAuth, getTempAuthToken, refreshTempAuthToken, setPendingAuth } from 'devframe/node/auth' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { colors as c } from 'devframe/utils/colors' import { MARK_INFO } from '../../constants' @@ -20,7 +20,7 @@ export interface DevToolsAuthReturn { const AUTH_TIMEOUT_MS = 60_000 export const anonymousAuth = defineRpcFunction({ - name: 'vite:anonymous:auth', + name: 'devframe:anonymous:auth', type: 'action', jsonSerializable: true, setup: (context) => { diff --git a/packages/core/src/node/server.ts b/packages/core/src/node/server.ts index b278dd58..1df11b4d 100644 --- a/packages/core/src/node/server.ts +++ b/packages/core/src/node/server.ts @@ -2,7 +2,7 @@ import type { NodeHandler } from 'h3' import type { CreateWsServerOptions } from './ws' import { DEVTOOLS_CONNECTION_META_FILENAME } from '@vitejs/devtools-kit/constants' import { consumeTempAuthToken } from 'devframe/node/auth' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { mountStaticHandler } from 'devframe/utils/serve-static' import { defineHandler, getQuery, H3, toNodeHandler } from 'h3' import { dirClientStandalone } from '../dirs' @@ -44,10 +44,10 @@ function generateAuthPageHtml(): string { const data = await r.json() const authToken = data.authToken - localStorage.setItem('__VITE_DEVTOOLS_CONNECTION_AUTH_TOKEN__', authToken) + localStorage.setItem('__DEVFRAME_CONNECTION_AUTH_TOKEN__', authToken) try { - const bc = new BroadcastChannel('vite-devtools-auth') + const bc = new BroadcastChannel('devframe-auth') bc.postMessage({ type: 'auth-update', authToken: authToken }) } catch {} diff --git a/packages/core/src/node/ws.ts b/packages/core/src/node/ws.ts index b8e07c48..ffb0cbe7 100644 --- a/packages/core/src/node/ws.ts +++ b/packages/core/src/node/ws.ts @@ -4,7 +4,7 @@ import type { RpcFunctionsHost } from 'devframe/node' import type { WebSocket } from 'ws' import { AsyncLocalStorage } from 'node:async_hooks' import process from 'node:process' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { createRpcServer } from 'devframe/rpc/server' import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server' import { colors as c } from 'devframe/utils/colors' @@ -26,7 +26,7 @@ export interface CreateWsServerOptions { context: ViteDevToolsNodeContext } -const ANONYMOUS_SCOPE = 'vite:anonymous:' +const ANONYMOUS_SCOPE = 'devframe:anonymous:' function buildWsUrl({ host, port, https }: { host: string, port: number, https: boolean }): string { // 0.0.0.0 / :: / unspecified bindings listen on all interfaces, but a hosted @@ -116,7 +116,7 @@ export async function createWsServer(options: CreateWsServerOptions) { definitions: rpcHost.definitions, onConnected: (ws, req, meta) => { const url = new URL(req.url ?? '', 'http://localhost') - const authToken = url.searchParams.get('vite_devtools_auth_token') ?? undefined + const authToken = url.searchParams.get('devframe_auth_token') ?? undefined const requestOrigin = req.headers.origin if (isClientAuthDisabled) { meta.isTrusted = true diff --git a/packages/kit/package.json b/packages/kit/package.json index fc37518c..9fae2bbd 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -44,6 +44,7 @@ "vite": "*" }, "dependencies": { + "@devframes/hub": "catalog:deps", "birpc": "catalog:deps", "devframe": "catalog:deps", "mlly": "catalog:deps", diff --git a/packages/kit/src/client/client-script.ts b/packages/kit/src/client/client-script.ts index 6f90555f..c284ac52 100644 --- a/packages/kit/src/client/client-script.ts +++ b/packages/kit/src/client/client-script.ts @@ -1,16 +1 @@ -import type { DevToolsMessagesClient } from '../types/messages' -import type { DockEntryState, DocksContext } from './docks' - -/** - * Context for client scripts running in dock entries - */ -export interface DockClientScriptContext extends DocksContext { - /** - * The state of the current dock entry - */ - current: DockEntryState - /** - * Messages client scoped to this dock entry's source - */ - messages: DevToolsMessagesClient -} +export type { DockClientScriptContext } from '@devframes/hub/client' diff --git a/packages/kit/src/client/context.ts b/packages/kit/src/client/context.ts index 2aad9126..c858338e 100644 --- a/packages/kit/src/client/context.ts +++ b/packages/kit/src/client/context.ts @@ -1,5 +1,8 @@ import type { DevToolsClientContext } from './docks' +// The kit owns this global key — the webcomponents host writes to it and +// dock client scripts read from it. Independent of devframe-hub's own +// `CLIENT_CONTEXT_KEY` (which points at a different global). const CLIENT_CONTEXT_KEY = '__VITE_DEVTOOLS_CLIENT_CONTEXT__' /** diff --git a/packages/kit/src/client/docks.ts b/packages/kit/src/client/docks.ts index 9ae43915..27aea0e3 100644 --- a/packages/kit/src/client/docks.ts +++ b/packages/kit/src/client/docks.ts @@ -1,139 +1,14 @@ -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 { 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' - width: number - height: number - top: number - left: number - position: 'left' | 'right' | 'bottom' | 'top' - open: boolean - inactiveTimeout: number -} - -export type DockClientType = 'embedded' | 'standalone' - -export interface DocksContext extends DevToolsRpcContext { - /** - * Type of the client environment - * - * 'embedded' - running inside an embedded floating panel - * 'standalone' - running inside a standalone window (no user app) - */ - readonly clientType: 'embedded' | 'standalone' - /** - * The panel context - */ - readonly panel: DocksPanelContext - /** - * The docks entries context - */ - readonly docks: DocksEntriesContext - /** - * The commands context for command palette and shortcuts - */ - readonly commands: CommandsContext - /** - * The when-clause context for conditional visibility - */ - readonly when: WhenClauseContext -} - -export interface WhenClauseContext { - /** - * Get the current when-clause context snapshot. - * Returns a reactive object with built-in variables and any custom plugin variables. - */ - readonly context: WhenContext -} - -export type DevToolsClientContext = DocksContext - -export interface DocksPanelContext { - store: DockPanelStorage - isDragging: boolean - isResizing: boolean - readonly isVertical: boolean -} - -export interface DocksEntriesContext { - selectedId: string | null - readonly selected: DevToolsDockEntry | null - entries: DevToolsDockEntry[] - entryToStateMap: Map - groupedEntries: DevToolsDockEntriesGrouped - settings: SharedState - /** - * Get the state of a dock entry by its ID - */ - getStateById: (id: string) => DockEntryState | undefined - /** - * Switch to the selected dock entry, pass `null` to clear the selection - * - * @returns Whether the selection was changed successfully - */ - switchEntry: (id?: string | null) => Promise - /** - * Toggle the selected dock entry - * - * @returns Whether the selection was changed successfully - */ - toggleEntry: (id: string) => Promise -} - -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': (newMeta: DevToolsDockUserEntry) => void - 'dom:panel:mounted': (panel: HTMLDivElement) => void - 'dom:iframe:mounted': (iframe: HTMLIFrameElement) => void -} - -export interface CommandsContext { - /** - * All commands (server + client) - */ - readonly commands: DevToolsCommandEntry[] - /** - * Palette-visible commands only (filtered by `showInPalette !== false`) - */ - readonly paletteCommands: DevToolsCommandEntry[] - /** - * Register client-side command(s). Returns cleanup function. - */ - register: (cmd: DevToolsClientCommand | DevToolsClientCommand[]) => () => void - /** - * Execute a command by ID. Delegates to RPC for server commands. - */ - execute: (id: string, ...args: any[]) => Promise - /** - * Get effective keybindings for a command (defaults merged with overrides) - */ - getKeybindings: (id: string) => DevToolsCommandKeybinding[] - /** - * User settings store (persisted, includes command shortcuts) - */ - settings: SharedState - /** - * Whether the command palette is open - */ - paletteOpen: boolean -} +export type { + CommandsContext, + DevframeClientContext as DevToolsClientContext, + DevframeClientRpcHost as DevToolsClientRpcHost, + DockClientType, + DockEntryState, + DockEntryStateEvents, + DockPanelStorage, + DocksContext, + DocksEntriesContext, + DocksPanelContext, + RpcClientEvents, + WhenClauseContext, +} from '@devframes/hub/client' diff --git a/packages/kit/src/client/index.ts b/packages/kit/src/client/index.ts index 475395f9..17553e99 100644 --- a/packages/kit/src/client/index.ts +++ b/packages/kit/src/client/index.ts @@ -2,4 +2,17 @@ export * from './client-script' export * from './context' export * from './docks' export * from './remote' -export * from 'devframe/client' + +export { + type DevframeClientRpcHost as DevToolsClientRpcHost, + type DevframeRpcClient as DevToolsRpcClient, + type DevframeRpcClientCall as DevToolsRpcClientCall, + type DevframeRpcClientCallEvent as DevToolsRpcClientCallEvent, + type DevframeRpcClientCallOptional as DevToolsRpcClientCallOptional, + type DevframeRpcClientMode as DevToolsRpcClientMode, + type DevframeRpcClientOptions as DevToolsRpcClientOptions, + type DevframeRpcContext as DevToolsRpcContext, + getDevframeRpcClient as getDevToolsRpcClient, + type RpcStreamingClientHost, + type StreamingSubscribeOptions, +} from '@devframes/hub/client' diff --git a/packages/kit/src/client/remote.ts b/packages/kit/src/client/remote.ts index e38e407c..196b6ed3 100644 --- a/packages/kit/src/client/remote.ts +++ b/packages/kit/src/client/remote.ts @@ -1,122 +1,5 @@ -import type { DevToolsRpcClient, DevToolsRpcClientOptions } from 'devframe/client' -import type { RemoteConnectionInfo } from '../types' -import { getDevToolsRpcClient } from 'devframe/client' -import { REMOTE_CONNECTION_KEY } from 'devframe/constants' - -export type ConnectRemoteDevToolsOptions = Omit - -function base64UrlDecode(value: string): string { - const padLen = (4 - value.length % 4) % 4 - const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(padLen) - const binary = atob(padded) - const bytes = new Uint8Array(binary.length) - for (let i = 0; i < binary.length; i++) - bytes[i] = binary.charCodeAt(i) - return new TextDecoder().decode(bytes) -} - -function extractKeyFromFragment(hash: string): string | null { - if (!hash) - return null - const raw = hash.startsWith('#') ? hash.slice(1) : hash - for (const part of raw.split('&')) { - const [k, v = ''] = part.split('=') - if (k === REMOTE_CONNECTION_KEY) - return decodeURIComponent(v) - } - return null -} - -function extractKeyFromQuery(search: string): string | null { - if (!search) - return null - const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search) - return params.get(REMOTE_CONNECTION_KEY) -} - -/** - * Parse a {@link RemoteConnectionInfo} descriptor from the current page's URL - * (or a provided URL/string). Checks the URL fragment first, then the query. - * - * Returns `null` if no descriptor is present. - * Throws if the descriptor is malformed or its schema version is unsupported. - */ -export function parseRemoteConnection(input?: string): RemoteConnectionInfo | null { - let hash = '' - let search = '' - if (input === undefined) { - if (typeof location === 'undefined') - return null - hash = location.hash - search = location.search - } - else { - try { - const parsed = new URL(input, 'http://_') - hash = parsed.hash - search = parsed.search - } - catch { - // Treat as raw fragment or query string. - if (input.startsWith('#')) - hash = input - else if (input.startsWith('?')) - search = input - else - return null - } - } - - const encoded = extractKeyFromFragment(hash) ?? extractKeyFromQuery(search) - if (!encoded) - return null - - let payload: unknown - try { - payload = JSON.parse(base64UrlDecode(encoded)) - } - catch (cause) { - throw new Error('[vite-devtools-kit] Failed to decode remote connection descriptor.', { cause }) - } - - if (!payload || typeof payload !== 'object') - throw new Error('[vite-devtools-kit] Remote connection descriptor must be an object.') - - const info = payload as Partial - if (info.v !== 1) - throw new Error(`[vite-devtools-kit] Unsupported remote connection descriptor version: ${String(info.v)}`) - if (info.backend !== 'websocket' || typeof info.websocket !== 'string' || !info.websocket) - throw new Error('[vite-devtools-kit] Remote connection descriptor must carry a websocket URL.') - if (typeof info.authToken !== 'string' || !info.authToken) - throw new Error('[vite-devtools-kit] Remote connection descriptor must carry an auth token.') - if (typeof info.origin !== 'string') - throw new Error('[vite-devtools-kit] Remote connection descriptor must carry an origin.') - - return info as RemoteConnectionInfo -} - -/** - * One-liner for a hosted DevTools page: reads the connection descriptor from - * the current URL and returns a connected {@link DevToolsRpcClient}. - * - * Pairs with `remote: true` on a `DevToolsViewIframe` registered on the node - * side — the core injects the descriptor into the iframe URL. - * - * @throws if no descriptor is present in the URL. - */ -export async function connectRemoteDevTools( - options: ConnectRemoteDevToolsOptions = {}, -): Promise { - const info = parseRemoteConnection() - if (!info) { - throw new Error( - `[vite-devtools-kit] No remote connection descriptor found in the URL. ` - + `Open this page through a Vite DevTools dock registered with \`remote: true\`.`, - ) - } - return getDevToolsRpcClient({ - ...options, - connectionMeta: info, - authToken: info.authToken, - }) -} +export { + connectRemoteDevframe as connectRemoteDevTools, + type ConnectRemoteDevframeOptions as ConnectRemoteDevToolsOptions, + parseRemoteConnection, +} from '@devframes/hub/client' diff --git a/packages/kit/src/constants.ts b/packages/kit/src/constants.ts index 71f9acc7..a3f94d68 100644 --- a/packages/kit/src/constants.ts +++ b/packages/kit/src/constants.ts @@ -1,7 +1,26 @@ import type { DevToolsDockEntryCategory } from './types/docks' import type { DevToolsDocksUserSettings } from './types/settings' -export * from 'devframe/constants' +// Filename / dirname constants whose *values* are unchanged across the +// devframe `DevTools*` → `Devframe*` rename. Re-export them under both +// names so downstream code that imports `DEVTOOLS_*` keeps compiling. +export { + DEFAULT_STATE_USER_SETTINGS, + DEVFRAME_CONNECTION_META_FILENAME as DEVTOOLS_CONNECTION_META_FILENAME, + DEVFRAME_DOCK_IMPORTS_FILENAME as DEVTOOLS_DOCK_IMPORTS_FILENAME, + DEVFRAME_RPC_DUMP_DIRNAME as DEVTOOLS_RPC_DUMP_DIRNAME, + DEVFRAME_RPC_DUMP_MANIFEST_FILENAME as DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME, + REMOTE_CONNECTION_KEY, +} from '@devframes/hub/constants' + +// Kit-side mount path is pinned at `/__devtools/` regardless of devframe's +// new `/__devframe/` default. The hosted (Vite-mounted) flow always passes +// the base path explicitly to `ctx.views.hostStatic()` and to the Vite +// middleware, so the kit owns the value. +export const DEVTOOLS_MOUNT_PATH = '/__devtools/' +export const DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH = '/__devtools' +export const DEVTOOLS_DIRNAME = '__devtools' +export const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID = '/__devtools-client-imports.js' export const DEFAULT_CATEGORIES_ORDER: Record = { '~viteplus': -1000, @@ -13,12 +32,4 @@ export const DEFAULT_CATEGORIES_ORDER: Record = { '~builtin': 1000, } satisfies Record -export const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings = () => ({ - docksHidden: [], - docksCategoriesHidden: [], - docksPinned: [], - docksCustomOrder: {}, - showIframeAddressBar: false, - closeOnOutsideClick: false, - commandShortcuts: {}, -}) +export type { DevToolsDocksUserSettings } diff --git a/packages/kit/src/define.ts b/packages/kit/src/define.ts index 1c5da49b..e7173c5c 100644 --- a/packages/kit/src/define.ts +++ b/packages/kit/src/define.ts @@ -1,24 +1,6 @@ -import type { WhenContext, WhenExpression } from 'devframe/utils/when' -import type { DevToolsDockUserEntry, DevToolsServerCommandInput, JsonRenderSpec, ViteDevToolsNodeContext } from './types' +import type { ViteDevToolsNodeContext } from './types' import { createDefineWrapperWithContext } from 'devframe/rpc' -export const defineRpcFunction = createDefineWrapperWithContext() - -export function defineCommand( - command: Omit & { when?: WhenExpression }, -): DevToolsServerCommandInput { - return command as DevToolsServerCommandInput -} +export { defineCommand, defineDockEntry, defineJsonRenderSpec } from '@devframes/hub' -export function defineDockEntry< - const T extends DevToolsDockUserEntry, - const W extends string = '', ->( - entry: Omit & { when?: WhenExpression }, -): T { - return entry as unknown as T -} - -export function defineJsonRenderSpec(spec: JsonRenderSpec): JsonRenderSpec { - return spec -} +export const defineRpcFunction = createDefineWrapperWithContext() diff --git a/packages/kit/src/node/__tests__/host-docks.test.ts b/packages/kit/src/node/__tests__/host-docks.test.ts deleted file mode 100644 index 1977aea8..00000000 --- a/packages/kit/src/node/__tests__/host-docks.test.ts +++ /dev/null @@ -1,445 +0,0 @@ -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/node/internal' -import { describe, expect, it } from 'vitest' -import { DevToolsDockHost } from '../host-docks' - -function createMockContext(): KitNodeContext { - return { - viteConfig: { - server: { host: 'localhost', port: 5173, https: false }, - }, - viteServer: undefined, - mode: 'dev', - host: { - mountStatic: () => {}, - resolveOrigin: () => 'http://localhost:5173', - getStorageDir: () => '/tmp/devframe-test-storage', - }, - } as unknown as KitNodeContext -} - -function decodeDescriptor(url: string): RemoteConnectionInfo { - const match = url.match(new RegExp(`[#&?]${REMOTE_CONNECTION_KEY}=([^&]+)`)) - if (!match) - throw new Error(`No descriptor in URL: ${url}`) - const encoded = match[1]! - const padLen = (4 - encoded.length % 4) % 4 - const padded = encoded.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat(padLen) - return JSON.parse(Buffer.from(padded, 'base64').toString('utf8')) -} - -describe('devToolsDockHost', () => { - const mockContext = { - host: { - mountStatic: () => {}, - resolveOrigin: () => 'http://localhost:5173', - getStorageDir: () => '/tmp/devframe-test-storage', - }, - } as unknown as KitNodeContext - - describe('builtin entries', () => { - it('does not include popup in builtin docks', () => { - const host = new DevToolsDockHost(mockContext) - const builtinEntries = host.values().filter(entry => entry.type === '~builtin') - const builtinIds = builtinEntries.map(entry => entry.id) - - expect(builtinIds).not.toContain('~popup') - }) - }) - - describe('register() collision detection', () => { - it('should register a new dock successfully', () => { - const host = new DevToolsDockHost(mockContext) - const dock: DevToolsDockUserEntry = { - type: 'iframe', - id: 'test-dock', - title: 'Test Dock', - icon: 'test-icon', - url: 'http://localhost:3000', - } - - expect(() => host.register(dock)).not.toThrow() - expect(host.views.has('test-dock')).toBe(true) - }) - - it('should throw error when registering duplicate dock ID', () => { - const host = new DevToolsDockHost(mockContext) - const dock1: DevToolsDockUserEntry = { - type: 'iframe', - id: 'duplicate-dock', - title: 'First Dock', - icon: 'icon1', - url: 'http://localhost:3001', - } - const dock2: DevToolsDockUserEntry = { - type: 'iframe', - id: 'duplicate-dock', - title: 'Second Dock', - icon: 'icon2', - url: 'http://localhost:3002', - } - - host.register(dock1) - - expect(() => host.register(dock2)).toThrow() - expect(() => host.register(dock2)).toThrow('duplicate-dock') - expect(() => host.register(dock2)).toThrow('already registered') - }) - - it('should include the duplicate ID in error message', () => { - const host = new DevToolsDockHost(mockContext) - const dock: DevToolsDockUserEntry = { - type: 'custom-render', - id: 'my-special-panel', - title: 'Special Panel', - icon: 'special', - renderer: { - importFrom: './component.js', - importName: 'MyComponent', - }, - } - - host.register(dock) - - expect(() => host.register(dock)).toThrow('my-special-panel') - }) - - it('should allow different dock IDs', () => { - const host = new DevToolsDockHost(mockContext) - - host.register({ - type: 'iframe', - id: 'dock-1', - title: 'Dock 1', - icon: 'icon1', - url: 'http://localhost:3001', - }) - - host.register({ - type: 'iframe', - id: 'dock-2', - title: 'Dock 2', - icon: 'icon2', - url: 'http://localhost:3002', - }) - - expect(host.views.size).toBe(2) - }) - }) - - describe('update() existence validation', () => { - it('should throw error when updating non-existent dock', () => { - const host = new DevToolsDockHost(mockContext) - const dock: DevToolsDockUserEntry = { - type: 'iframe', - id: 'nonexistent', - title: 'Does Not Exist', - icon: 'icon', - url: 'http://localhost:3000', - } - - expect(() => host.update(dock)).toThrow() - expect(() => host.update(dock)).toThrow('nonexistent') - expect(() => host.update(dock)).toThrow('not registered') - expect(() => host.update(dock)).toThrow('Use register()') - }) - - it('should update existing dock successfully', () => { - const host = new DevToolsDockHost(mockContext) - const dock1: DevToolsDockUserEntry = { - type: 'iframe', - id: 'update-test', - title: 'Original Title', - icon: 'original', - url: 'http://localhost:3001', - } - const dock2: DevToolsDockUserEntry = { - type: 'iframe', - id: 'update-test', - title: 'Updated Title', - icon: 'updated', - url: 'http://localhost:3002', - } - - host.register(dock1) - expect(() => host.update(dock2)).not.toThrow() - - const updated = host.views.get('update-test') - expect(updated?.title).toBe('Updated Title') - if (updated?.type === 'iframe') { - expect(updated.url).toBe('http://localhost:3002') - } - }) - - it('should validate that update only works on existing entries', () => { - const host = new DevToolsDockHost(mockContext) - - // Register one dock - host.register({ - type: 'iframe', - id: 'exists', - title: 'Exists', - icon: 'icon', - url: 'http://localhost:3000', - }) - - // Update should work for existing - expect(() => - host.update({ - type: 'iframe', - id: 'exists', - title: 'Updated', - icon: 'icon', - url: 'http://localhost:3001', - }), - ).not.toThrow() - - // Update should fail for non-existing - expect(() => - host.update({ - type: 'iframe', - id: 'does-not-exist', - title: 'Failed', - icon: 'icon', - url: 'http://localhost:3002', - }), - ).toThrow() - }) - - it('should preserve dock in values() after update', () => { - const host = new DevToolsDockHost(mockContext) - - const { update } = host.register({ - type: 'iframe', - id: 'test', - title: 'Original', - icon: 'icon', - url: 'http://localhost:3000', - }) - - update({ - title: 'Updated', - icon: 'newicon', - url: 'http://localhost:3001', - }) - - const docks = host.values({ includeBuiltin: false }) - expect(docks.length).toBe(1) - expect(docks[0]?.title).toBe('Updated') - }) - }) - - describe('remote iframe docks', () => { - it('allocates a session token and enriches URL with a fragment descriptor', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - const internal = getInternalContext(ctx) - internal.wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/devtools', - remote: true, - }) - - expect(internal.remoteTokens.size).toBe(1) - - const projected = host.values({ includeBuiltin: false })[0] as DevToolsViewIframe - expect(projected.url).toContain('#') - expect(projected.url).toContain(REMOTE_CONNECTION_KEY) - expect(projected.url).not.toContain('?') - - const descriptor = decodeDescriptor(projected.url) - expect(descriptor).toMatchObject({ - v: 1, - backend: 'websocket', - websocket: 'ws://localhost:7812', - origin: 'http://localhost:5173', - }) - expect(descriptor.authToken).toBeTruthy() - - // The token in the descriptor is exactly the one allocated by the registry. - expect(internal.remoteTokens.has(descriptor.authToken)).toBe(true) - internalContextMap.delete(ctx) - }) - - it('emits a query transport when transport=query is requested', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - getInternalContext(ctx).wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/devtools', - remote: { transport: 'query' }, - }) - - const projected = host.values({ includeBuiltin: false })[0] as DevToolsViewIframe - expect(projected.url).toMatch(new RegExp(`\\?${REMOTE_CONNECTION_KEY}=`)) - expect(projected.url).not.toContain('#') - internalContextMap.delete(ctx) - }) - - it('does not inject a default `when` clause for remote docks', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - getInternalContext(ctx).wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/devtools', - remote: true, - }) - - expect(host.views.get('remote-dock')?.when).toBeUndefined() - internalContextMap.delete(ctx) - }) - - it('respects an explicit `when` clause from the author', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - getInternalContext(ctx).wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/devtools', - remote: true, - when: 'clientType == embedded', - }) - - expect(host.views.get('remote-dock')?.when).toBe('clientType == embedded') - internalContextMap.delete(ctx) - }) - - it('revokes the prior token when the dock is force-re-registered', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - const internal = getInternalContext(ctx) - internal.wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/v1', - remote: true, - }) - const firstToken = [...internal.remoteTokens.keys()][0]! - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/v2', - remote: true, - }, true) - - expect(internal.remoteTokens.has(firstToken)).toBe(false) - expect(internal.remoteTokens.size).toBe(1) - internalContextMap.delete(ctx) - }) - - it('leaves non-remote iframe URLs untouched', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - getInternalContext(ctx).wsEndpoint = { url: 'ws://localhost:7812' } - - host.register({ - type: 'iframe', - id: 'plain-dock', - title: 'Plain', - icon: 'ph:app-window-duotone', - url: '/__devtools-plain/', - }) - - const projected = host.values({ includeBuiltin: false })[0] as DevToolsViewIframe - expect(projected.url).toBe('/__devtools-plain/') - internalContextMap.delete(ctx) - }) - - it('returns non-enriched URL before the WS endpoint is known', () => { - const ctx = createMockContext() - const host = new DevToolsDockHost(ctx) - // Note: wsEndpoint intentionally NOT set. - - host.register({ - type: 'iframe', - id: 'remote-dock', - title: 'Remote', - icon: 'ph:globe-duotone', - url: 'https://example.com/devtools', - remote: true, - }) - - const projected = host.values({ includeBuiltin: false })[0] as DevToolsViewIframe - expect(projected.url).toBe('https://example.com/devtools') - internalContextMap.delete(ctx) - }) - }) - - describe('remote token trust', () => { - it('trusts a token when origin matches (originLock on)', () => { - const ctx = createMockContext() - const internal = getInternalContext(ctx) - - const token = internal.allocateRemoteToken('dock-a', 'https://example.com', true) - expect(internal.isRemoteTokenTrusted(token, 'https://example.com')).toBe(true) - expect(internal.isRemoteTokenTrusted(token, 'https://attacker.com')).toBe(false) - expect(internal.isRemoteTokenTrusted(token, undefined)).toBe(false) - internalContextMap.delete(ctx) - }) - - it('trusts a token regardless of origin when originLock is off', () => { - const ctx = createMockContext() - const internal = getInternalContext(ctx) - - const token = internal.allocateRemoteToken('dock-a', 'https://example.com', false) - expect(internal.isRemoteTokenTrusted(token, 'https://example.com')).toBe(true) - expect(internal.isRemoteTokenTrusted(token, 'https://another.com')).toBe(true) - expect(internal.isRemoteTokenTrusted(token, undefined)).toBe(true) - internalContextMap.delete(ctx) - }) - - it('rejects tokens after revocation', () => { - const ctx = createMockContext() - const internal = getInternalContext(ctx) - - const token = internal.allocateRemoteToken('dock-a', 'https://example.com', true) - internal.revokeRemoteToken(token) - expect(internal.isRemoteTokenTrusted(token, 'https://example.com')).toBe(false) - internalContextMap.delete(ctx) - }) - - it('revokeRemoteTokensForDock removes every token tied to the dock', () => { - const ctx = createMockContext() - const internal = getInternalContext(ctx) - - const t1 = internal.allocateRemoteToken('dock-a', 'https://example.com', true) - const t2 = internal.allocateRemoteToken('dock-a', 'https://example.com', true) - const t3 = internal.allocateRemoteToken('dock-b', 'https://example.com', true) - - internal.revokeRemoteTokensForDock('dock-a') - expect(internal.remoteTokens.has(t1)).toBe(false) - expect(internal.remoteTokens.has(t2)).toBe(false) - expect(internal.remoteTokens.has(t3)).toBe(true) - internalContextMap.delete(ctx) - }) - }) -}) diff --git a/packages/kit/src/node/__tests__/host-messages.test.ts b/packages/kit/src/node/__tests__/host-messages.test.ts deleted file mode 100644 index 71521b3b..00000000 --- a/packages/kit/src/node/__tests__/host-messages.test.ts +++ /dev/null @@ -1,325 +0,0 @@ -import type { KitNodeContext } from '../context' -import { describe, expect, it, vi } from 'vitest' -import { DevToolsMessagesHost } from '../host-messages' - -describe('devToolsMessagesHost', () => { - const mockContext = {} as KitNodeContext - - function createHost() { - return new DevToolsMessagesHost(mockContext) - } - - describe('add()', () => { - it('should add a message entry with auto-generated id and timestamp', async () => { - const host = createHost() - const handle = await host.add({ message: 'test', level: 'info' }) - - expect(handle.id).toBeDefined() - expect(handle.entry.message).toBe('test') - expect(handle.entry.level).toBe('info') - expect(handle.entry.timestamp).toBeTypeOf('number') - expect(handle.entry.from).toBe('server') - expect(host.entries.size).toBe(1) - }) - - it('should use provided id and timestamp', async () => { - const host = createHost() - const handle = await host.add({ id: 'my-id', message: 'test', level: 'warn', timestamp: 12345 }) - - expect(handle.id).toBe('my-id') - expect(handle.entry.timestamp).toBe(12345) - }) - - it('should emit message:added event', async () => { - const host = createHost() - const handler = vi.fn() - host.events.on('message:added', handler) - - const handle = await host.add({ message: 'test', level: 'info' }) - - expect(handler).toHaveBeenCalledWith(handle.entry) - }) - - it('should dedup by id — delegates to update() if id exists', async () => { - const host = createHost() - await host.add({ id: 'dup', message: 'first', level: 'info' }) - const handle = await host.add({ id: 'dup', message: 'second', level: 'warn' }) - - expect(host.entries.size).toBe(1) - expect(handle.entry.message).toBe('second') - expect(handle.entry.level).toBe('warn') - // Preserves original id, source, timestamp - expect(handle.id).toBe('dup') - expect(handle.entry.from).toBe('server') - }) - - it('should evict oldest entry when at capacity', async () => { - const host = createHost() - // Add 1000 entries (MAX_ENTRIES) - for (let i = 0; i < 1000; i++) - await host.add({ id: `entry-${i}`, message: `msg ${i}`, level: 'info' }) - - expect(host.entries.size).toBe(1000) - expect(host.entries.has('entry-0')).toBe(true) - - // Adding one more should evict the first - await host.add({ id: 'overflow', message: 'overflow', level: 'info' }) - expect(host.entries.size).toBe(1000) - expect(host.entries.has('entry-0')).toBe(false) - expect(host.entries.has('overflow')).toBe(true) - }) - }) - - describe('update()', () => { - it('should update an existing entry', async () => { - const host = createHost() - await host.add({ id: 'u1', message: 'original', level: 'info' }) - const updated = await host.update('u1', { message: 'changed', level: 'error' }) - - expect(updated).toBeDefined() - expect(updated!.message).toBe('changed') - expect(updated!.level).toBe('error') - // Preserved fields - expect(updated!.id).toBe('u1') - expect(updated!.from).toBe('server') - }) - - it('should return undefined for non-existent id', async () => { - const host = createHost() - expect(await host.update('nope', { message: 'x' })).toBeUndefined() - }) - - it('should emit message:updated event', async () => { - const host = createHost() - await host.add({ id: 'u2', message: 'a', level: 'info' }) - const handler = vi.fn() - host.events.on('message:updated', handler) - - await host.update('u2', { message: 'b' }) - - expect(handler).toHaveBeenCalledOnce() - expect(handler.mock.calls[0]![0].message).toBe('b') - }) - - it('should preserve id, source, and timestamp on update', async () => { - const host = createHost() - const handle = await host.add({ id: 'u3', message: 'orig', level: 'info' }) - const original = handle.entry - const updated = await host.update('u3', { message: 'new' }) - - expect(updated!.id).toBe(original.id) - expect(updated!.from).toBe(original.from) - expect(updated!.timestamp).toBe(original.timestamp) - }) - }) - - describe('remove()', () => { - it('should remove an entry', async () => { - const host = createHost() - await host.add({ id: 'r1', message: 'test', level: 'info' }) - await host.remove('r1') - - expect(host.entries.size).toBe(0) - }) - - it('should emit message:removed event', async () => { - const host = createHost() - await host.add({ id: 'r2', message: 'test', level: 'info' }) - const handler = vi.fn() - host.events.on('message:removed', handler) - - await host.remove('r2') - - expect(handler).toHaveBeenCalledWith('r2') - }) - }) - - describe('clear()', () => { - it('should remove all entries', async () => { - const host = createHost() - await host.add({ message: 'a', level: 'info' }) - await host.add({ message: 'b', level: 'warn' }) - await host.clear() - - expect(host.entries.size).toBe(0) - }) - - it('should emit message:cleared event', async () => { - const host = createHost() - await host.add({ message: 'a', level: 'info' }) - const handler = vi.fn() - host.events.on('message:cleared', handler) - - await host.clear() - - expect(handler).toHaveBeenCalledOnce() - }) - }) - - describe('autoDelete', () => { - it('should auto-delete entry after timeout', async () => { - vi.useFakeTimers() - const host = createHost() - await host.add({ id: 'ad1', message: 'temp', level: 'info', autoDelete: 1000 }) - - expect(host.entries.has('ad1')).toBe(true) - vi.advanceTimersByTime(1000) - expect(host.entries.has('ad1')).toBe(false) - - vi.useRealTimers() - }) - - it('should reset autoDelete timer on update', async () => { - vi.useFakeTimers() - const host = createHost() - await host.add({ id: 'ad2', message: 'temp', level: 'info', autoDelete: 1000 }) - - vi.advanceTimersByTime(500) - await host.update('ad2', { autoDelete: 2000 }) - - vi.advanceTimersByTime(500) - expect(host.entries.has('ad2')).toBe(true) - - vi.advanceTimersByTime(1500) - expect(host.entries.has('ad2')).toBe(false) - - vi.useRealTimers() - }) - - it('should clear autoDelete timer on remove', async () => { - vi.useFakeTimers() - const host = createHost() - await host.add({ id: 'ad3', message: 'temp', level: 'info', autoDelete: 1000 }) - await host.remove('ad3') - - // Should not throw or re-remove after timer fires - vi.advanceTimersByTime(1000) - expect(host.entries.has('ad3')).toBe(false) - - vi.useRealTimers() - }) - }) - - describe('incremental versioning', () => { - it('should track lastModified on add', async () => { - const host = createHost() - await host.add({ id: 'v1', message: 'a', level: 'info' }) - await host.add({ id: 'v2', message: 'b', level: 'info' }) - - const mod1 = host.lastModified.get('v1')! - const mod2 = host.lastModified.get('v2')! - expect(mod1).toBeLessThan(mod2) - }) - - it('should update lastModified on update', async () => { - const host = createHost() - await host.add({ id: 'v3', message: 'a', level: 'info' }) - const modBefore = host.lastModified.get('v3')! - - await host.update('v3', { message: 'b' }) - const modAfter = host.lastModified.get('v3')! - - expect(modAfter).toBeGreaterThan(modBefore) - }) - - it('should remove lastModified on remove', async () => { - const host = createHost() - await host.add({ id: 'v4', message: 'a', level: 'info' }) - await host.remove('v4') - - expect(host.lastModified.has('v4')).toBe(false) - }) - - it('should track removals', async () => { - const host = createHost() - await host.add({ id: 'v5', message: 'a', level: 'info' }) - await host.add({ id: 'v6', message: 'b', level: 'info' }) - await host.remove('v5') - - expect(host.removals).toHaveLength(1) - expect(host.removals[0]!.id).toBe('v5') - expect(host.removals[0]!.time).toBeGreaterThan(0) - }) - - it('should track all removals on clear', async () => { - const host = createHost() - await host.add({ id: 'c1', message: 'a', level: 'info' }) - await host.add({ id: 'c2', message: 'b', level: 'info' }) - await host.clear() - - expect(host.removals).toHaveLength(2) - const ids = host.removals.map(r => r.id) - expect(ids).toContain('c1') - expect(ids).toContain('c2') - }) - - it('should clear lastModified on clear', async () => { - const host = createHost() - await host.add({ id: 'c3', message: 'a', level: 'info' }) - await host.clear() - - expect(host.lastModified.size).toBe(0) - }) - - it('should allow filtering entries by version', async () => { - const host = createHost() - await host.add({ id: 'f1', message: 'a', level: 'info' }) - const versionAfterFirst = (host as any)._clock as number - - await host.add({ id: 'f2', message: 'b', level: 'info' }) - await host.update('f1', { message: 'a updated' }) - - // Entries modified after versionAfterFirst - const modified: string[] = [] - for (const [id] of host.entries) { - const mod = host.lastModified.get(id) - if (mod != null && mod > versionAfterFirst) - modified.push(id) - } - - // Both f2 (added after) and f1 (updated after) should be included - expect(modified).toContain('f1') - expect(modified).toContain('f2') - }) - - it('should allow filtering removals by version', async () => { - const host = createHost() - await host.add({ id: 'r1', message: 'a', level: 'info' }) - await host.add({ id: 'r2', message: 'b', level: 'info' }) - await host.remove('r1') - const versionAfterRemove = (host as any)._clock as number - - await host.add({ id: 'r3', message: 'c', level: 'info' }) - await host.remove('r2') - - // Removals after versionAfterRemove - const removedSince = host.removals - .filter(r => r.time > versionAfterRemove) - .map(r => r.id) - - expect(removedSince).toEqual(['r2']) - }) - }) - - describe('handle', () => { - it('should return a handle with live entry', async () => { - const host = createHost() - const handle = await host.add({ id: 'h1', message: 'initial', level: 'info' }) - - expect(handle.id).toBe('h1') - expect(handle.entry.message).toBe('initial') - - // Update via handle - await handle.update({ message: 'updated' }) - expect(handle.entry.message).toBe('updated') - }) - - it('should dismiss via handle', async () => { - const host = createHost() - const handle = await host.add({ id: 'h2', message: 'temp', level: 'info' }) - - await handle.dismiss() - expect(host.entries.has('h2')).toBe(false) - }) - }) -}) diff --git a/packages/kit/src/node/__tests__/host-terminals.test.ts b/packages/kit/src/node/__tests__/host-terminals.test.ts deleted file mode 100644 index b3203f00..00000000 --- a/packages/kit/src/node/__tests__/host-terminals.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -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 KitNodeContext) - const session: DevToolsTerminalSession = { - id: 'terminal-1', - title: 'Terminal 1', - status: 'running', - } - - const dispose = vi.fn() - - ;(host as any).sessions.set(session.id, session) - ;(host as any)._boundStreams.set(session.id, { - dispose, - stream: new ReadableStream(), - }) - - host.remove(session) - - expect(dispose).toHaveBeenCalledTimes(1) - expect((host as any)._boundStreams.has(session.id)).toBe(false) - }) -}) diff --git a/packages/kit/src/node/context.ts b/packages/kit/src/node/context.ts index 83c39a44..37d44455 100644 --- a/packages/kit/src/node/context.ts +++ b/packages/kit/src/node/context.ts @@ -1,40 +1,18 @@ -import type { CreateHostContextOptions } from 'devframe/node' -import type { DevToolsNodeContext } from 'devframe/types' +import type { CreateHubContextOptions, DevframeHubContext } from '@devframes/hub/node' import type { ResolvedConfig, ViteDevServer } from 'vite' -import type { DevToolsCommandsHost } from '../types/commands' -import type { DevToolsDockHost } from '../types/docks' -import type { JsonRenderer, JsonRenderSpec } from '../types/json-render' -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' +import { createHubContext } from '@devframes/hub/node' /** - * Kit-augmented node context — extends devframe's framework-neutral - * `DevToolsNodeContext` with the hub-level subsystems (`docks`, - * `terminals`, `messages`, `commands`) and the `createJsonRenderer` - * factory, all owned by `@vitejs/devtools-kit`. When kit hosts the - * devtool inside Vite DevTools, also exposes the underlying Vite - * handles. + * Kit-augmented node context — the framework-neutral hub context from + * `@devframes/hub`, plus the Vite-specific slots surfaced when kit hosts + * the devtool inside Vite DevTools. */ -export interface KitNodeContext extends DevToolsNodeContext { - docks: DevToolsDockHost - terminals: DevToolsTerminalHost - messages: DevToolsMessagesHost - commands: DevToolsCommandsHost - /** - * Create a JsonRenderer handle for building json-render powered UIs. - */ - createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer +export interface KitNodeContext extends DevframeHubContext { readonly viteConfig?: ResolvedConfig readonly viteServer?: ViteDevServer } -export interface CreateKitContextOptions extends CreateHostContextOptions { +export interface CreateKitContextOptions extends CreateHubContextOptions { /** Optional Vite resolved config to surface on the context (for Vite-mounted hubs). */ viteConfig?: ResolvedConfig /** Optional Vite dev server to surface on the context. */ @@ -42,89 +20,19 @@ export interface CreateKitContextOptions extends CreateHostContextOptions { } /** - * 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. + * Create a kit-level node context: wraps `@devframes/hub`'s + * `createHubContext` (which itself wraps devframe's `createHostContext`) + * and attaches the Vite-specific slots. The hub layer owns the + * docks/terminals/messages/commands subsystems and seeds the shared-state + * sync the unified client UI consumes. */ 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 + const context = await createHubContext(options) as KitNodeContext 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() - - let jrCounter = 0 - context.createJsonRenderer = (initialSpec: JsonRenderSpec): JsonRenderer => { - const stateKey = `devframe:json-render:${jrCounter++}` - const statePromise = context.rpc.sharedState.get(stateKey as any, { - initialValue: initialSpec as any, - }) - - return { - _stateKey: stateKey, - async updateSpec(spec) { - const state = await statePromise - state.mutate(() => spec as any) - }, - async updateState(newState) { - const state = await statePromise - state.mutate((draft: any) => { - draft.state = { ...draft.state, ...newState } - }) - }, - } - } - - 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 index 9e130037..41fba98f 100644 --- a/packages/kit/src/node/create-plugin-from-devframe.ts +++ b/packages/kit/src/node/create-plugin-from-devframe.ts @@ -1,9 +1,8 @@ -import type { DevframeDefinition, DevToolsCapabilities } from 'devframe/types' -import type { DevToolsViewIframe } from '../types/docks' +import type { DevframeCapabilities, DevframeViewIframe } from '@devframes/hub/types' +import type { DevframeDefinition } from 'devframe/types' import type { PluginWithDevTools } from '../types/vite-augment' import type { KitNodeContext } from './context' -import { resolveBasePath } from 'devframe/node/internal' -import { resolve } from 'pathe' +import { mountDevframe } from '@devframes/hub/node' export interface CreatePluginFromDevframeOptions { /** @@ -20,12 +19,12 @@ export interface CreatePluginFromDevframeOptions { * `when`, etc. Cannot change `id`, `type`, or `url` — those are * derived from the devframe definition. */ - dock?: Partial> + dock?: Partial> /** * Capability flags forwarded onto the kit plugin's `devtools` slot. * Defaults to `d.capabilities`. */ - capabilities?: DevToolsCapabilities | { dev?: DevToolsCapabilities | boolean, build?: DevToolsCapabilities | boolean } + capabilities?: DevframeCapabilities | { dev?: DevframeCapabilities | boolean, build?: DevframeCapabilities | boolean } /** * Additional kit-only setup hook. Runs after the devframe-level * `d.setup(ctx)` and after the auto-derived dock entry has been @@ -39,43 +38,25 @@ export interface CreatePluginFromDevframeOptions { /** * Wrap a {@link DevframeDefinition} 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`. + * `@vitejs/devtools` (Vite DevTools). Delegates the mount work + * (serving the SPA, registering the iframe dock entry, calling + * `d.setup(ctx)`) to `@devframes/hub`'s `mountDevframe`, then runs the + * optional kit-only `options.setup` hook. */ export function createPluginFromDevframe( d: DevframeDefinition, 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) - + await mountDevframe(ctx, d, { + base: options.base, + dock: options.dock, + }) if (options.setup) { await options.setup(ctx) } diff --git a/packages/kit/src/node/diagnostics.ts b/packages/kit/src/node/diagnostics.ts index 42e3f49f..cb842b54 100644 --- a/packages/kit/src/node/diagnostics.ts +++ b/packages/kit/src/node/diagnostics.ts @@ -1,37 +1,11 @@ import { defineDiagnostics, reporterLog } from 'nostics' -// 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. +// Kit-side diagnostics. The hub-domain codes (DTK0050-DTK0057) have +// moved upstream into `@devframes/hub` as DF8100-DF8403 since hub now +// owns the docks/terminals/commands hosts. Kit-only codes can be added +// here in the DTK0050+ range as needed. export const diagnostics = defineDiagnostics({ docsBase: 'https://devtools.vite.dev/errors', reporters: [reporterLog], - codes: { - DTK0050: { - why: (p: { id: string }) => `Dock with id "${p.id}" is already registered`, - fix: 'Use the `force` parameter to overwrite an existing registration.', - }, - DTK0051: { - why: 'Cannot change the id of a dock. Use register() to add new docks.', - }, - DTK0052: { - why: (p: { id: string }) => `Dock with id "${p.id}" is not registered. Use register() to add new docks.`, - }, - DTK0053: { - why: (p: { id: string }) => `Terminal session with id "${p.id}" already registered`, - }, - DTK0054: { - why: (p: { id: string }) => `Terminal session with id "${p.id}" not registered`, - }, - DTK0055: { - why: (p: { id: string }) => `Command "${p.id}" is already registered`, - }, - DTK0056: { - why: 'Cannot change the id of a command. Use register() to add new commands.', - }, - DTK0057: { - why: (p: { id: string }) => `Command "${p.id}" is not registered`, - }, - }, + codes: {}, }) diff --git a/packages/kit/src/node/host-commands.ts b/packages/kit/src/node/host-commands.ts deleted file mode 100644 index 6b6d636e..00000000 --- a/packages/kit/src/node/host-commands.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { - DevToolsCommandHandle, - DevToolsCommandsHost as DevToolsCommandsHostType, - DevToolsServerCommandEntry, - DevToolsServerCommandInput, -} from '../types/commands' -import type { KitNodeContext } from './context' -import { createEventEmitter } from 'devframe/utils/events' -import { diagnostics } from './diagnostics' - -export class DevToolsCommandsHost implements DevToolsCommandsHostType { - public readonly commands: DevToolsCommandsHostType['commands'] = new Map() - public readonly events: DevToolsCommandsHostType['events'] = createEventEmitter() - - constructor( - public readonly context: KitNodeContext, - ) {} - - register(command: DevToolsServerCommandInput): DevToolsCommandHandle { - if (this.commands.has(command.id)) { - throw diagnostics.DTK0055({ id: command.id }) - } - this.commands.set(command.id, command) - this.events.emit('command:registered', this.toSerializable(command)) - - return { - id: command.id, - update: (patch: Partial>) => { - if ('id' in patch) { - throw diagnostics.DTK0056() - } - const existing = this.commands.get(command.id) - if (!existing) { - throw diagnostics.DTK0057({ id: command.id }) - } - Object.assign(existing, patch) - this.events.emit('command:registered', this.toSerializable(existing)) - }, - unregister: () => this.unregister(command.id), - } - } - - unregister(id: string): boolean { - const deleted = this.commands.delete(id) - if (deleted) { - this.events.emit('command:unregistered', id) - } - return deleted - } - - async execute(id: string, ...args: any[]): Promise { - const found = this.findCommand(id) - if (!found) { - throw diagnostics.DTK0057({ id }) - } - if (!found.handler) { - throw new Error(`Command "${id}" has no handler (group-only command)`) - } - return found.handler(...args) - } - - list(): DevToolsServerCommandEntry[] { - return Array.from(this.commands.values()).map(cmd => this.toSerializable(cmd)) - } - - private findCommand(id: string): DevToolsServerCommandInput | undefined { - // Check top-level - const topLevel = this.commands.get(id) - if (topLevel) - return topLevel - - // Search children - for (const cmd of this.commands.values()) { - if (cmd.children) { - const child = cmd.children.find((c: DevToolsServerCommandInput) => c.id === id) - if (child) - return child - } - } - - return undefined - } - - private toSerializable(cmd: DevToolsServerCommandInput): DevToolsServerCommandEntry { - const { handler: _, children, ...rest } = cmd - return { - ...rest, - source: 'server', - ...(children - ? { children: children.map((c: DevToolsServerCommandInput) => this.toSerializable(c)) } - : {} - ), - } - } -} diff --git a/packages/kit/src/node/host-docks.ts b/packages/kit/src/node/host-docks.ts deleted file mode 100644 index eb82a05b..00000000 --- a/packages/kit/src/node/host-docks.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { DevToolsNodeContext } from 'devframe/types' -import type { SharedState } from 'devframe/utils/shared-state' -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 { createStorage } from 'devframe/node' -import { getInternalContext } from 'devframe/node/internal' -import { createEventEmitter } from 'devframe/utils/events' -import { join } from 'pathe' -import { DEFAULT_STATE_USER_SETTINGS } from '../constants' -import { diagnostics } from './diagnostics' - -interface RemoteDockRecord { - token: string - options: Required -} - -function normaliseRemoteOptions(remote: true | RemoteDockOptions): Required { - const opts = remote === true ? {} : remote - return { - transport: opts.transport ?? 'fragment', - originLock: opts.originLock ?? true, - } -} - -function base64UrlEncode(value: string): string { - // URL-safe base64 without padding so the descriptor is compact and safe to - // drop into a URL without escaping. - const bytes = new TextEncoder().encode(value) - let binary = '' - for (const byte of bytes) - binary += String.fromCharCode(byte) - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') -} - -function buildRemoteUrl(baseUrl: string, payload: RemoteConnectionInfo, transport: 'fragment' | 'query'): string { - const encoded = base64UrlEncode(JSON.stringify(payload)) - const param = `${REMOTE_CONNECTION_KEY}=${encoded}` - if (transport === 'fragment') { - // Replace any existing fragment bearing our key; otherwise append. - const hashIdx = baseUrl.indexOf('#') - if (hashIdx === -1) - return `${baseUrl}#${param}` - const before = baseUrl.slice(0, hashIdx) - return `${before}#${param}` - } - // query - const qIdx = baseUrl.indexOf('?') - const hashIdx = baseUrl.indexOf('#') - const hash = hashIdx === -1 ? '' : baseUrl.slice(hashIdx) - const beforeHash = hashIdx === -1 ? baseUrl : baseUrl.slice(0, hashIdx) - const sep = qIdx === -1 || qIdx >= (hashIdx === -1 ? beforeHash.length : hashIdx) ? '?' : '&' - return `${beforeHash}${sep}${param}${hash}` -} - -export class DevToolsDockHost implements DevToolsDockHostType { - public readonly views: DevToolsDockHostType['views'] = new Map() - public readonly events: DevToolsDockHostType['events'] = createEventEmitter() - public userSettings: SharedState = undefined! - - /** Dock-id → allocated remote token + resolved options. */ - private readonly remoteDocks = new Map() - - constructor( - public readonly context: KitNodeContext, - ) { - - } - - async init() { - this.userSettings = await this.context.rpc.sharedState.get('devframe:user-settings', { - sharedState: createStorage({ - filepath: join(this.context.host.getStorageDir('workspace'), 'settings.json'), - initialValue: DEFAULT_STATE_USER_SETTINGS(), - }), - }) - } - - values({ - includeBuiltin = true, - }: { - includeBuiltin?: boolean - } = {}): DevToolsDockEntry[] { - const context = this.context - const builtinDocksEntries: DevToolsViewBuiltin[] = [ - { - type: '~builtin', - id: '~terminals', - title: 'Terminals', - icon: 'ph:terminal-duotone', - category: '~builtin', - get when() { - return context.terminals.sessions.size === 0 ? 'false' : undefined - }, - }, - { - type: '~builtin', - id: '~messages', - title: 'Messages & Notifications', - icon: 'ph:notification-duotone', - category: '~builtin', - get badge() { - const size = context.messages.entries.size - return size > 0 ? String(size) : undefined - }, - }, - { - type: '~builtin', - id: '~settings', - title: 'Settings', - category: '~builtin', - icon: 'ph:gear-duotone', - }, - ] - - return [ - ...Array.from(this.views.values(), view => this.projectView(view)), - ...(includeBuiltin ? builtinDocksEntries : []), - ] - } - - private projectView(view: DevToolsDockUserEntry): DevToolsDockUserEntry { - if (view.type !== 'iframe' || !view.remote) - return view - const record = this.remoteDocks.get(view.id) - const endpoint = getInternalContext(this.context as DevToolsNodeContext).wsEndpoint - if (!record || !endpoint) - return view - - const payload: RemoteConnectionInfo = { - v: 1, - backend: 'websocket', - websocket: endpoint.url, - authToken: record.token, - origin: this.resolveDevServerOrigin(), - } - return { - ...view, - url: buildRemoteUrl(view.url, payload, record.options.transport), - } satisfies DevToolsViewIframe - } - - private resolveDevServerOrigin(): string { - return this.context.host.resolveOrigin() - } - - register(view: T, force?: boolean): { - update: (patch: Partial) => void - } { - if (this.views.has(view.id) && !force) { - throw diagnostics.DTK0050({ id: view.id }) - } - this.prepareRemoteRegistration(view) - this.views.set(view.id, view) - this.events.emit('dock:entry:updated', view) - - return { - update: (patch) => { - if (patch.id && patch.id !== view.id) { - throw diagnostics.DTK0051() - } - this.update(Object.assign(this.views.get(view.id)!, patch)) - }, - } - } - - update(view: DevToolsDockUserEntry): void { - if (!this.views.has(view.id)) { - throw diagnostics.DTK0052({ id: view.id }) - } - this.prepareRemoteRegistration(view) - this.views.set(view.id, view) - this.events.emit('dock:entry:updated', view) - } - - private prepareRemoteRegistration(view: DevToolsDockUserEntry): void { - 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) - this.remoteDocks.delete(view.id) - - if (view.type !== 'iframe' || !view.remote) - return - - const options = normaliseRemoteOptions(view.remote) - let dockOrigin: string - try { - dockOrigin = new URL(view.url).origin - } - catch { - // Relative/invalid URL — origin-lock can't be enforced. Fall back to the - // dev-server origin; this still works because the iframe loads in the - // same browser anyway. - dockOrigin = this.resolveDevServerOrigin() - } - const token = internal.allocateRemoteToken(view.id, dockOrigin, options.originLock) - this.remoteDocks.set(view.id, { token, options }) - } -} diff --git a/packages/kit/src/node/host-messages.ts b/packages/kit/src/node/host-messages.ts deleted file mode 100644 index 79001c41..00000000 --- a/packages/kit/src/node/host-messages.ts +++ /dev/null @@ -1,134 +0,0 @@ -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' - -const MAX_ENTRIES = 1000 - -export class DevToolsMessagesHost implements DevToolsMessagesHostType { - public readonly entries: DevToolsMessagesHostType['entries'] = new Map() - public readonly events: DevToolsMessagesHostType['events'] = createEventEmitter() - - /** Tracks when each entry was last added or updated (monotonic) */ - readonly lastModified = new Map() - /** Tracks recently removed entry IDs with their removal time */ - readonly removals: Array<{ id: string, time: number }> = [] - - private _autoDeleteTimers = new Map>() - private _clock = 0 - - private _tick(): number { - return ++this._clock - } - - constructor( - public readonly context: KitNodeContext, - ) {} - - async add(input: DevToolsMessageEntryInput): Promise { - // Dedupe: if an entry with the same explicit id exists, update it instead - if (input.id && this.entries.has(input.id)) { - await this.update(input.id, input) - return this._createHandle(input.id) - } - - const entry: DevToolsMessageEntry = { - ...input, - id: input.id ?? nanoid(), - timestamp: input.timestamp ?? Date.now(), - from: (input as any).from ?? 'server', - } - - // FIFO eviction when at capacity - if (this.entries.size >= MAX_ENTRIES) { - const oldest = this.entries.keys().next().value! - await this.remove(oldest) - } - - this.entries.set(entry.id, entry) - this.lastModified.set(entry.id, this._tick()) - this.events.emit('message:added', entry) - - if (entry.autoDelete) { - this._autoDeleteTimers.set(entry.id, setTimeout(() => { - this.remove(entry.id) - }, entry.autoDelete)) - } - - return this._createHandle(entry.id) - } - - async update(id: string, patch: Partial): Promise { - const existing = this.entries.get(id) - if (!existing) - return undefined - - const updated: DevToolsMessageEntry = { - ...existing, - ...patch, - id: existing.id, - from: existing.from, - timestamp: existing.timestamp, - } - - this.entries.set(id, updated) - this.lastModified.set(id, this._tick()) - this.events.emit('message:updated', updated) - - // Reset autoDelete timer if changed - if (patch.autoDelete !== undefined) { - const timer = this._autoDeleteTimers.get(id) - if (timer) { - clearTimeout(timer) - this._autoDeleteTimers.delete(id) - } - if (patch.autoDelete) { - this._autoDeleteTimers.set(id, setTimeout(() => { - this.remove(id) - }, patch.autoDelete)) - } - } - - return updated - } - - async remove(id: string): Promise { - const timer = this._autoDeleteTimers.get(id) - if (timer) { - clearTimeout(timer) - this._autoDeleteTimers.delete(id) - } - this.entries.delete(id) - this.lastModified.delete(id) - this.removals.push({ id, time: this._tick() }) - this.events.emit('message:removed', id) - } - - async clear(): Promise { - for (const timer of this._autoDeleteTimers.values()) - clearTimeout(timer) - this._autoDeleteTimers.clear() - const tick = this._tick() - for (const id of this.entries.keys()) - this.removals.push({ id, time: tick }) - this.entries.clear() - this.lastModified.clear() - this.events.emit('message:cleared') - } - - private _createHandle(id: string): DevToolsMessageHandle { - // eslint-disable-next-line ts/no-this-alias - const host = this - return { - get entry() { return host.entries.get(id)! }, - get id() { return id }, - update: patch => host.update(id, patch), - dismiss: () => host.remove(id), - } - } -} diff --git a/packages/kit/src/node/host-terminals.ts b/packages/kit/src/node/host-terminals.ts deleted file mode 100644 index 1c29c8dc..00000000 --- a/packages/kit/src/node/host-terminals.ts +++ /dev/null @@ -1,201 +0,0 @@ -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 { diagnostics } from './diagnostics' - -type PartialWithoutId = Partial & { id: string } - -/** - * 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 name. - */ -const TERMINAL_STREAM_CHANNEL = 'devframe:terminals' as const -const TERMINAL_REPLAY_WINDOW = 1000 - -export class DevToolsTerminalHost implements DevToolsTerminalHostType { - public readonly sessions: DevToolsTerminalHostType['sessions'] = new Map() - public readonly events: DevToolsTerminalHostType['events'] = createEventEmitter() - - private _boundStreams = new Map void - stream: ReadableStream - }>() - - private _channel?: RpcStreamingChannel - - constructor( - public readonly context: KitNodeContext, - ) { - } - - /** - * Lazily acquire the streaming channel — `context.rpc` isn't assigned - * until after every host is constructed, so we can't grab it in the - * constructor. - */ - private getStreamingChannel(): RpcStreamingChannel | undefined { - if (this._channel) - return this._channel - if (!this.context.rpc?.streaming) - return undefined - this._channel = this.context.rpc.streaming.create( - TERMINAL_STREAM_CHANNEL, - { replayWindow: TERMINAL_REPLAY_WINDOW }, - ) - return this._channel - } - - register(session: DevToolsTerminalSession): DevToolsTerminalSession { - if (this.sessions.has(session.id)) { - throw diagnostics.DTK0053({ id: session.id }) - } - this.sessions.set(session.id, session) - this.bindStream(session) - this.events.emit('terminal:session:updated', session) - return session - } - - update(patch: PartialWithoutId): void { - if (!this.sessions.has(patch.id)) { - throw diagnostics.DTK0054({ id: patch.id }) - } - const session = this.sessions.get(patch.id)! - Object.assign(session, patch) - this.sessions.set(patch.id, session) - this.bindStream(session) - this.events.emit('terminal:session:updated', session) - } - - remove(session: DevToolsTerminalSession): void { - this._boundStreams.get(session.id)?.dispose() - this.sessions.delete(session.id) - this.events.emit('terminal:session:updated', session) - this._boundStreams.delete(session.id) - } - - private bindStream(session: DevToolsTerminalSession) { - // Skip when the same stream is already bound - if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) - return - - // Dispose the previous stream - this._boundStreams.get(session.id)?.dispose() - this._boundStreams.delete(session.id) - - // If new stream is not available, skip - if (!session.stream) - return - - session.buffer ||= [] - const sessionBuffer = session.buffer - - const channel = this.getStreamingChannel() - // The streaming channel reuses `session.id` as the stream id so clients - // can subscribe immediately after seeing the session in - // `devframe:terminals:list`. - const sink = channel?.start({ id: session.id }) - - const writer = new WritableStream({ - write(chunk) { - // Mirror to the legacy session.buffer used by `terminals:read` — - // unbounded history kept for the snapshot endpoint. - sessionBuffer.push(chunk) - sink?.write(chunk) - }, - close() { - sink?.close() - }, - abort(reason) { - sink?.error(reason) - }, - }) - session.stream.pipeTo(writer).catch(() => { - // pipeTo rejection surfaces via writer.abort -> sink.error already. - }) - this._boundStreams.set(session.id, { - dispose: () => { - if (sink && !sink.closed) - sink.close() - }, - stream: session.stream, - }) - } - - async startChildProcess( - executeOptions: DevToolsChildProcessExecuteOptions, - terminal: Omit, - ): Promise { - if (this.sessions.has(terminal.id)) { - throw diagnostics.DTK0053({ id: terminal.id }) - } - const { exec } = await import('tinyexec') - - let controller: ReadableStreamDefaultController | undefined - const stream = new ReadableStream({ - start(_controller) { - controller = _controller - }, - }) - - function createChildProcess() { - const cp = exec( - executeOptions.command, - executeOptions.args || [], - { - nodeOptions: { - env: { - COLORS: 'true', - FORCE_COLOR: 'true', - ...(executeOptions.env || {}), - }, - cwd: executeOptions.cwd ?? process.cwd(), - stdio: 'pipe', - }, - }, - ) - - ;(async () => { - for await (const chunk of cp) { - controller?.enqueue(chunk) - } - })() - - return cp - } - - let cp: TinyExecResult | undefined = createChildProcess() - - const restart = async () => { - cp?.kill() - cp = createChildProcess() - } - const terminate = async () => { - cp?.kill() - cp = undefined - } - - const session: DevToolsChildProcessTerminalSession = { - ...terminal, - status: 'running', - stream, - type: 'child-process', - executeOptions, - getChildProcess: () => cp?.process, - terminate, - restart, - } - this.register(session) - - return Promise.resolve(session) - } -} diff --git a/packages/kit/src/node/index.ts b/packages/kit/src/node/index.ts index af160279..f4cd38f0 100644 --- a/packages/kit/src/node/index.ts +++ b/packages/kit/src/node/index.ts @@ -1,8 +1,16 @@ 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' + +// The hub-side host implementations and the `mountDevframe` primitive +// power kit's `createKitContext` / `createPluginFromDevframe` under the +// hood. Re-export the class names under the legacy `DevTools*` aliases +// so downstream code that already imported them keeps compiling. +export { + DevframeCommandsHost as DevToolsCommandsHost, + DevframeDocksHost as DevToolsDockHost, + DevframeMessagesHost as DevToolsMessagesHost, + DevframeTerminalsHost as DevToolsTerminalHost, + mountDevframe, +} from '@devframes/hub/node' diff --git a/packages/kit/src/node/vite-host.ts b/packages/kit/src/node/vite-host.ts index ffb67814..23889ac7 100644 --- a/packages/kit/src/node/vite-host.ts +++ b/packages/kit/src/node/vite-host.ts @@ -1,4 +1,4 @@ -import type { DevToolsHost } from 'devframe/types' +import type { DevframeHost } from 'devframe/types' import type { ResolvedConfig, ViteDevServer } from 'vite' import { homedir } from 'node:os' import { join } from 'node:path' @@ -15,7 +15,7 @@ export interface CreateViteDevToolsHostOptions { workspaceRoot?: string } -export function createViteDevToolsHost(options: CreateViteDevToolsHostOptions): DevToolsHost { +export function createViteDevToolsHost(options: CreateViteDevToolsHostOptions): DevframeHost { const { viteConfig, viteServer } = options const workspaceRoot = options.workspaceRoot ?? viteConfig.root diff --git a/packages/kit/src/types/commands.ts b/packages/kit/src/types/commands.ts index 96c48113..63721798 100644 --- a/packages/kit/src/types/commands.ts +++ b/packages/kit/src/types/commands.ts @@ -1,142 +1,12 @@ -import type { EventEmitter } from 'devframe/types' - -// --- Keybinding --- - -export interface DevToolsCommandKeybinding { - /** - * Keyboard shortcut string. - * Use "Mod" for platform-aware modifier (Cmd on macOS, Ctrl elsewhere). - * Examples: "Mod+K", "Mod+Shift+P", "Alt+N" - */ - key: string -} - -// --- Command Entry --- - -export interface DevToolsCommandBase { - /** - * Unique namespaced ID, e.g. "vite:open-in-editor" - */ - id: string - title: string - description?: string - /** - * Iconify icon string, e.g. "ph:pencil-duotone" - */ - icon?: string - category?: string - /** - * Whether to show in command palette. Default: true - * - * - `true` — show the command and flatten its children into search results - * - `false` — hide the command entirely from the palette - * - `'without-children'` — show the command but don't flatten children into top-level search (children are still accessible via drill-down) - */ - showInPalette?: boolean | 'without-children' - /** - * Optional context expression for conditional visibility. - * When set, the command is only shown in the palette and only executable - * when the expression evaluates to true. - * - * Uses the same syntax as keybinding `when` clauses. - * @example 'clientType == embedded' - * @example 'dockOpen && !paletteOpen' - */ - when?: string - /** - * Default keyboard shortcut(s) for this command - */ - keybindings?: DevToolsCommandKeybinding[] -} - -/** - * Server command input — what plugins pass to `ctx.commands.register()`. - */ -export interface DevToolsServerCommandInput extends DevToolsCommandBase { - /** - * Handler for this command. Optional if the command only serves as a group for children. - */ - handler?: (...args: any[]) => any | Promise - /** - * Static sub-commands. Two levels max (parent → children). - * Each child must have a globally unique `id`. - */ - children?: DevToolsServerCommandInput[] -} - -/** - * Serializable server command entry — sent over RPC (no handler). - */ -export interface DevToolsServerCommandEntry extends DevToolsCommandBase { - source: 'server' - children?: DevToolsServerCommandEntry[] -} - -/** - * Client command — registered in the webcomponent context. - */ -export interface DevToolsClientCommand extends DevToolsCommandBase { - source: 'client' - /** - * Action for this command. Optional if the command only serves as a group for children. - * Return sub-commands for dynamic nested palette menus (runtime submenus). - */ - action?: (...args: any[]) => void | DevToolsClientCommand[] | Promise - /** - * Static sub-commands. Two levels max (parent → children). - */ - children?: DevToolsClientCommand[] -} - -/** - * Union of command entries visible in the palette. - */ -export type DevToolsCommandEntry = DevToolsServerCommandEntry | DevToolsClientCommand - -// --- Server Host --- - -export interface DevToolsCommandHandle { - readonly id: string - update: (patch: Partial>) => void - unregister: () => void -} - -export interface DevToolsCommandsHostEvents { - 'command:registered': (command: DevToolsServerCommandEntry) => void - 'command:unregistered': (id: string) => void -} - -export interface DevToolsCommandsHost { - readonly commands: Map - readonly events: EventEmitter - - /** - * Register a command (with optional children). - */ - register: (command: DevToolsServerCommandInput) => DevToolsCommandHandle - - /** - * Unregister a command by ID (removes parent and all children). - */ - unregister: (id: string) => boolean - - /** - * Execute a command by ID. Searches top-level and children. - * Throws if not found or if command has no handler. - */ - execute: (id: string, ...args: any[]) => Promise - - /** - * Returns serializable list (no handlers), preserving tree structure. - */ - list: () => DevToolsServerCommandEntry[] -} - -// --- Shortcut Overrides (shared state) --- - -export interface DevToolsCommandShortcutOverrides { - /** - * Command ID → keybinding overrides. Empty array = shortcut disabled. - */ - [commandId: string]: DevToolsCommandKeybinding[] -} +export type { + DevframeClientCommand as DevToolsClientCommand, + DevframeCommandBase as DevToolsCommandBase, + DevframeCommandEntry as DevToolsCommandEntry, + DevframeCommandHandle as DevToolsCommandHandle, + DevframeCommandKeybinding as DevToolsCommandKeybinding, + DevframeCommandShortcutOverrides as DevToolsCommandShortcutOverrides, + DevframeCommandsHost as DevToolsCommandsHost, + DevframeCommandsHostEvents as DevToolsCommandsHostEvents, + DevframeServerCommandEntry as DevToolsServerCommandEntry, + DevframeServerCommandInput as DevToolsServerCommandInput, +} from '@devframes/hub/types' diff --git a/packages/kit/src/types/docks.ts b/packages/kit/src/types/docks.ts index ae10370c..9a97d792 100644 --- a/packages/kit/src/types/docks.ts +++ b/packages/kit/src/types/docks.ts @@ -1,171 +1,27 @@ -import type { ConnectionMeta, EventEmitter } from 'devframe/types' -import type { JsonRenderer } from './json-render' - -export interface DevToolsDockHost { - readonly views: Map - readonly events: EventEmitter<{ - 'dock:entry:updated': (entry: DevToolsDockUserEntry) => void - }> - - register: (entry: T, force?: boolean) => { - update: (patch: Partial) => void - } - update: (entry: DevToolsDockUserEntry) => void - values: (options?: { includeBuiltin?: boolean }) => DevToolsDockEntry[] -} - -// TODO: refine categories more clearly -export type DevToolsDockEntryCategory = 'app' | 'framework' | 'web' | 'advanced' | 'default' | '~viteplus' | '~builtin' - -export type DevToolsDockEntryIcon = string | { light: string, dark: string } - -export interface DevToolsDockEntryBase { - id: string - title: string - icon: DevToolsDockEntryIcon - /** - * The default order of the entry in the dock. - * The higher the number the earlier it appears. - * @default 0 - */ - defaultOrder?: number - /** - * The category of the entry - * @default 'default' - */ - category?: DevToolsDockEntryCategory - /** - * Conditional visibility expression. - * When set, the dock entry is only visible when the expression evaluates to true. - * Uses the same syntax as command `when` clauses. - * - * Set to `'false'` to unconditionally hide the entry. - * - * @example 'clientType == embedded' - * @see {@link import('devframe/utils/when').evaluateWhen} - */ - when?: string - /** - * Badge text to display on the dock icon (e.g., unread count) - */ - badge?: string -} - -export interface ClientScriptEntry { - /** - * The filepath or module name to import from - */ - importFrom: string - /** - * The name to import the module as - * - * @default 'default' - */ - importName?: string -} - -export interface DevToolsViewIframe extends DevToolsDockEntryBase { - type: 'iframe' - url: string - /** - * The id of the iframe, if multiple tabs is assigned with the same id, the iframe will be shared. - * - * When not provided, it would be treated as a unique frame. - */ - frameId?: string - /** - * Optional client script to import into the iframe - */ - clientScript?: ClientScriptEntry - /** - * Enable remote-UI mode: the core injects a connection descriptor - * (WS URL + pre-approved auth token) into the iframe URL so a hosted - * page can connect back via `connectRemoteDevTools()` from - * `@vitejs/devtools-kit/client` — without needing to ship a dist with - * the plugin. - * - * Requires dev mode (no effect in build mode — no WS server exists). - * When enabled, the dock is automatically hidden in build mode unless - * the author provides an explicit `when` clause. - * - * @example - * remote: true - * remote: { transport: 'query', originLock: false } - */ - remote?: boolean | RemoteDockOptions -} - -export interface RemoteDockOptions { - /** - * How to pass the connection descriptor to the hosted page. - * - * - `'fragment'` (default): appended as `#vite-devtools-kit-connection=...`. - * Not sent in HTTP requests or Referer headers — safest for auth tokens. - * - `'query'`: appended as `?vite-devtools-kit-connection=...`. Use when - * your hosting platform rewrites fragments or your SPA router repurposes - * the fragment for navigation. The token will appear in server access - * logs and outbound Referer headers. - * - * @default 'fragment' - */ - transport?: 'fragment' | 'query' - /** - * Reject WS handshakes whose `Origin` header doesn't match the dock URL - * origin. Turn off when the same hosted app is served from multiple - * origins (e.g. preview deploys). - * - * @default true - */ - 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 { - type: 'launcher' - launcher: { - icon?: DevToolsDockEntryIcon - title: string - status?: DevToolsViewLauncherStatus - error?: string - description?: string - buttonStart?: string - buttonLoading?: string - onLaunch: () => Promise - } -} - -export interface DevToolsViewAction extends DevToolsDockEntryBase { - type: 'action' - action: ClientScriptEntry -} - -export interface DevToolsViewCustomRender extends DevToolsDockEntryBase { - type: 'custom-render' - renderer: ClientScriptEntry -} - -export interface DevToolsViewBuiltin extends DevToolsDockEntryBase { - type: '~builtin' - id: '~terminals' | '~messages' | '~client-auth-notice' | '~settings' | '~popup' -} - -export interface DevToolsViewJsonRender extends DevToolsDockEntryBase { - type: 'json-render' - /** JsonRenderer handle created by ctx.createJsonRenderer() */ - ui: JsonRenderer -} - -export type DevToolsDockUserEntry = DevToolsViewIframe | DevToolsViewAction | DevToolsViewCustomRender | DevToolsViewLauncher | DevToolsViewJsonRender - -export type DevToolsDockEntry = DevToolsDockUserEntry | DevToolsViewBuiltin - -export type DevToolsDockEntriesGrouped = [category: string, entries: DevToolsDockEntry[]][] +import type { DevframeDockEntryCategory } from '@devframes/hub/types' + +export type { + ClientScriptEntry, + DevframeDockEntriesGrouped as DevToolsDockEntriesGrouped, + DevframeDockEntry as DevToolsDockEntry, + DevframeDockEntryBase as DevToolsDockEntryBase, + DevframeDockEntryIcon as DevToolsDockEntryIcon, + DevframeDocksHost as DevToolsDockHost, + DevframeDockUserEntry as DevToolsDockUserEntry, + DevframeViewAction as DevToolsViewAction, + DevframeViewBuiltin as DevToolsViewBuiltin, + DevframeViewCustomRender as DevToolsViewCustomRender, + DevframeViewIframe as DevToolsViewIframe, + DevframeViewJsonRender as DevToolsViewJsonRender, + DevframeViewLauncher as DevToolsViewLauncher, + DevframeViewLauncherStatus as DevToolsViewLauncherStatus, + RemoteConnectionInfo, + RemoteDockOptions, +} from '@devframes/hub/types' + +/** + * The kit's dock-entry category union extends hub's framework-neutral set + * with the Vite-specific `~viteplus` slot used by Vite DevTools to group + * Vite Plus integrations above the default categories. + */ +export type DevToolsDockEntryCategory = DevframeDockEntryCategory | '~viteplus' diff --git a/packages/kit/src/types/index.ts b/packages/kit/src/types/index.ts index 32d32dc2..5d4df09d 100644 --- a/packages/kit/src/types/index.ts +++ b/packages/kit/src/types/index.ts @@ -11,31 +11,26 @@ 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 -// `export * from 'devframe/types'` because the rolldown-plugin-dts step -// fails with a `MemberExpression` AST error on namespace re-exports -// from external packages (tsdown 0.21 / rolldown-plugin-dts 0.23). -// Revisit once upstream supports it. export type { ConnectionMeta, - DevToolsCapabilities, - DevToolsDiagnosticsDefinition, - DevToolsDiagnosticsHost, - DevToolsDiagnosticsLogger, - DevToolsHost, - DevToolsNodeRpcSession, - DevToolsRpcClientFunctions, - DevToolsRpcServerFunctions, - DevToolsRpcSharedStates, - DevToolsViewHost, + DevframeCapabilities as DevToolsCapabilities, + DevframeDiagnosticsDefinition as DevToolsDiagnosticsDefinition, + DevframeDiagnosticsHost as DevToolsDiagnosticsHost, + DevframeDiagnosticsLogger as DevToolsDiagnosticsLogger, + DevframeHost as DevToolsHost, + DevframeNodeRpcSession as DevToolsNodeRpcSession, + DevframeRpcClientFunctions as DevToolsRpcClientFunctions, + DevframeRpcServerFunctions as DevToolsRpcServerFunctions, + DevframeRpcSharedStates as DevToolsRpcSharedStates, + DevframeViewHost as DevToolsViewHost, EntriesToObject, EventEmitter, EventsMap, EventUnsubscribe, PartialWithoutId, RpcBroadcastOptions, + RpcDefinitionsFilter, + RpcDefinitionsToFunctions, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost, @@ -43,4 +38,10 @@ export type { RpcStreamingChannelOptions, RpcStreamingHost, Thenable, -} from 'devframe/types' +} from '@devframes/hub/types' + +// `DevframeNodeContext` is the base framework-neutral context — hub does +// not re-export it because hub itself ships `DevframeHubContext` as the +// canonical hub-augmented surface. The kit aliases it for back-compat +// with code that referenced `DevToolsNodeContext` directly. +export type { DevframeNodeContext as DevToolsNodeContext } from 'devframe/types' diff --git a/packages/kit/src/types/json-render.ts b/packages/kit/src/types/json-render.ts index 336af7cc..55005631 100644 --- a/packages/kit/src/types/json-render.ts +++ b/packages/kit/src/types/json-render.ts @@ -1,29 +1,5 @@ -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 type { + JsonRenderElement, + JsonRenderer, + JsonRenderSpec, +} from '@devframes/hub/types' diff --git a/packages/kit/src/types/messages.ts b/packages/kit/src/types/messages.ts index fa273b3e..55a0e09d 100644 --- a/packages/kit/src/types/messages.ts +++ b/packages/kit/src/types/messages.ts @@ -1,146 +1,11 @@ -import type { EventEmitter } from 'devframe/types' - -export type DevToolsMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug' -export type DevToolsMessageEntryFrom = 'server' | 'browser' - -export interface DevToolsMessageElementPosition { - /** CSS selector for the element */ - selector?: string - /** Bounding box of the element */ - boundingBox?: { x: number, y: number, width: number, height: number } - /** Human-readable description of the element */ - description?: string -} - -export interface DevToolsMessageFilePosition { - /** Absolute or relative file path */ - file: string - /** Line number (1-based) */ - line?: number - /** Column number (1-based) */ - column?: number -} - -export interface DevToolsMessageEntry { - /** - * Unique identifier for this message entry (auto-generated if not provided) - */ - id: string - /** - * Short title or summary of the message - */ - message: string - /** - * Optional detailed description or explanation - */ - description?: string - /** - * Severity level, determines color and icon - */ - level: DevToolsMessageLevel - /** - * Optional stack trace string - */ - stacktrace?: string - /** - * Optional DOM element position info (e.g., for a11y issues) - */ - elementPosition?: DevToolsMessageElementPosition - /** - * Optional source file position info (e.g., for lint errors) - */ - filePosition?: DevToolsMessageFilePosition - /** - * Whether this message should also appear as a toast notification - */ - notify?: boolean - /** - * Origin of the message entry, automatically set by the context - */ - from: DevToolsMessageEntryFrom - /** - * Grouping category (e.g., 'a11y', 'lint', 'runtime', 'test') - */ - category?: string - /** - * Optional tags/labels for filtering - */ - labels?: string[] - /** - * Time in ms to auto-dismiss the toast notification (client-side) - */ - autoDismiss?: number - /** - * Time in ms to auto-delete this message entry (server-side) - */ - autoDelete?: number - /** - * Timestamp when the message was created (auto-generated if not provided) - */ - timestamp: number - /** - * Status of the message entry (e.g., 'loading' while an operation is in progress). - * Defaults to 'idle' when not specified. - */ - status?: 'loading' | 'idle' -} - -/** - * Input type for creating a message entry. - * `id`, `timestamp`, and `from` are auto-filled by the host. - */ -export type DevToolsMessageEntryInput = Omit & { - id?: string - timestamp?: number -} - -export interface DevToolsMessageHandle { - /** The underlying message entry data */ - readonly entry: DevToolsMessageEntry - /** Shortcut to entry.id */ - readonly id: string - /** Partial update of this message entry */ - update: (patch: Partial) => Promise - /** Remove this message entry */ - dismiss: () => Promise -} - -export interface DevToolsMessagesClient { - /** - * Add a message entry. Returns a Promise resolving to a handle for subsequent updates/dismissal. - * Can be used without `await` for fire-and-forget usage. - */ - add: (input: DevToolsMessageEntryInput) => Promise - /** Remove a message entry by id */ - remove: (id: string) => Promise - /** Clear all message entries */ - clear: () => Promise -} - -export interface DevToolsMessagesHost { - readonly entries: Map - readonly events: EventEmitter<{ - 'message:added': (entry: DevToolsMessageEntry) => void - 'message:updated': (entry: DevToolsMessageEntry) => void - 'message:removed': (id: string) => void - 'message:cleared': () => void - }> - - /** - * Add a new message entry. If an entry with the same `id` already exists, it will be updated instead. - * Returns a handle for subsequent updates/dismissal. Can be used without `await` for fire-and-forget. - */ - add: (entry: DevToolsMessageEntryInput) => Promise - /** - * Update an existing message entry by id (partial update) - */ - update: (id: string, patch: Partial) => Promise - /** - * Remove a message entry by id - */ - remove: (id: string) => Promise - /** - * Clear all message entries - */ - clear: () => Promise -} +export type { + DevframeMessageElementPosition as DevToolsMessageElementPosition, + DevframeMessageEntry as DevToolsMessageEntry, + DevframeMessageEntryFrom as DevToolsMessageEntryFrom, + DevframeMessageEntryInput as DevToolsMessageEntryInput, + DevframeMessageFilePosition as DevToolsMessageFilePosition, + DevframeMessageHandle as DevToolsMessageHandle, + DevframeMessageLevel as DevToolsMessageLevel, + DevframeMessagesClient as DevToolsMessagesClient, + DevframeMessagesHost as DevToolsMessagesHost, +} from '@devframes/hub/types' diff --git a/packages/kit/src/types/settings.ts b/packages/kit/src/types/settings.ts index e79f1363..165992f9 100644 --- a/packages/kit/src/types/settings.ts +++ b/packages/kit/src/types/settings.ts @@ -1,11 +1 @@ -import type { DevToolsCommandShortcutOverrides } from './commands' - -export interface DevToolsDocksUserSettings { - docksHidden: string[] - docksCategoriesHidden: string[] - docksPinned: string[] - docksCustomOrder: Record - showIframeAddressBar: boolean - closeOnOutsideClick: boolean - commandShortcuts: DevToolsCommandShortcutOverrides -} +export type { DevframeDocksUserSettings as DevToolsDocksUserSettings } from '@devframes/hub/types' diff --git a/packages/kit/src/types/terminals.ts b/packages/kit/src/types/terminals.ts index 396c036c..45561179 100644 --- a/packages/kit/src/types/terminals.ts +++ b/packages/kit/src/types/terminals.ts @@ -1,48 +1,8 @@ -import type { EventEmitter } from 'devframe/types' -import type { ChildProcess } from 'node:child_process' -import type { DevToolsDockEntryIcon } from './docks' - -export interface DevToolsTerminalHost { - readonly sessions: Map - readonly events: EventEmitter<{ - 'terminal:session:updated': (session: DevToolsTerminalSession) => void - }> - - register: (session: DevToolsTerminalSession) => DevToolsTerminalSession - update: (session: DevToolsTerminalSession) => void - - startChildProcess: ( - executeOptions: DevToolsChildProcessExecuteOptions, - terminal: Omit, - ) => Promise -} - -export type DevToolsTerminalStatus = 'running' | 'stopped' | 'error' - -export interface DevToolsTerminalSessionBase { - id: string - title: string - description?: string - status: DevToolsTerminalStatus - icon?: DevToolsDockEntryIcon -} - -export interface DevToolsTerminalSession extends DevToolsTerminalSessionBase { - buffer?: string[] - stream?: ReadableStream -} - -export interface DevToolsChildProcessExecuteOptions { - command: string - args: string[] - cwd?: string - env?: Record -} - -export interface DevToolsChildProcessTerminalSession extends DevToolsTerminalSession { - type: 'child-process' - executeOptions: DevToolsChildProcessExecuteOptions - getChildProcess: () => ChildProcess | undefined - terminate: () => Promise - restart: () => Promise -} +export type { + DevframeChildProcessExecuteOptions as DevToolsChildProcessExecuteOptions, + DevframeChildProcessTerminalSession as DevToolsChildProcessTerminalSession, + DevframeTerminalsHost as DevToolsTerminalHost, + DevframeTerminalSession as DevToolsTerminalSession, + DevframeTerminalSessionBase as DevToolsTerminalSessionBase, + DevframeTerminalStatus as DevToolsTerminalStatus, +} from '@devframes/hub/types' diff --git a/packages/kit/src/types/vite-plugin.ts b/packages/kit/src/types/vite-plugin.ts index 7ebdcb0c..d715a5cc 100644 --- a/packages/kit/src/types/vite-plugin.ts +++ b/packages/kit/src/types/vite-plugin.ts @@ -1,11 +1,11 @@ -import type { DevToolsCapabilities } from 'devframe/types' +import type { DevframeCapabilities } from '@devframes/hub/types' import type { ResolvedConfig, ViteDevServer } from 'vite' import type { KitNodeContext } from '../node/context' export interface DevToolsPluginOptions { capabilities?: { - dev?: DevToolsCapabilities | boolean - build?: DevToolsCapabilities | boolean + dev?: DevframeCapabilities | boolean + build?: DevframeCapabilities | boolean } setup: (context: ViteDevToolsNodeContext) => void | Promise } @@ -16,7 +16,7 @@ export interface DevToolsPluginOptions { * 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`. + * `DevframeNodeContext` from `devframe/types`. */ export interface ViteDevToolsNodeContext extends KitNodeContext { readonly viteConfig: ResolvedConfig diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3bd7009..e2636126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,9 @@ catalogs: specifier: ^7.2.0 version: 7.2.0 deps: + '@devframes/hub': + specifier: ^0.5.2 + version: 0.5.2 actionspack: specifier: ^0.1.4 version: 0.1.4 @@ -44,8 +47,8 @@ catalogs: specifier: ^4.0.0 version: 4.0.0 devframe: - specifier: ^0.4.1 - version: 0.4.1 + specifier: ^0.5.2 + version: 0.5.2 diff: specifier: ^9.0.0 version: 9.0.0 @@ -86,8 +89,8 @@ catalogs: specifier: ^2.0.0 version: 2.0.0 tinyexec: - specifier: ^1.1.2 - version: 1.1.2 + specifier: ^1.2.2 + version: 1.2.2 tinyglobby: specifier: ^0.2.16 version: 0.2.16 @@ -516,7 +519,7 @@ importers: version: 1.1.1 tinyexec: specifier: catalog:deps - version: 1.1.2 + version: 1.2.2 tsdown: specifier: catalog:build version: 0.22.0(@vitejs/devtools@0.1.24(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(@pnpm/logger@1001.0.1)(db0@0.3.4)(idb-keyval@6.2.4)(ioredis@5.10.1)(typescript@6.0.3)(vite@8.0.14))(publint@0.3.21)(tsx@4.22.3)(typescript@6.0.3)(unrun@0.2.37(synckit@0.11.12))(vue-tsc@3.3.1(typescript@6.0.3)) @@ -573,7 +576,7 @@ importers: version: 4.8.4(change-case@5.4.4)(focus-trap@8.0.0)(fuse.js@7.3.0)(idb-keyval@6.2.4)(typescript@6.0.3)(vite@8.0.14(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.3)(yaml@2.9.0))(vitepress@2.0.0-alpha.17(@types/node@25.0.3)(@vitejs/devtools@packages+core)(change-case@5.4.4)(esbuild@0.28.0)(fuse.js@7.3.0)(idb-keyval@6.2.4)(jiti@2.7.0)(oxc-minify@0.131.0)(postcss@8.5.15)(terser@5.44.1)(tsx@4.22.3)(typescript@6.0.3)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)) devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) mermaid: specifier: catalog:docs version: 11.15.0 @@ -606,7 +609,7 @@ importers: version: 1.60.0 tinyexec: specifier: catalog:deps - version: 1.1.2 + version: 1.2.2 vite: specifier: ^8.0.14 version: 8.0.14(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.3)(yaml@2.9.0) @@ -692,7 +695,7 @@ importers: version: link:../../packages/kit tinyexec: specifier: catalog:deps - version: 1.1.2 + version: 1.2.2 devDependencies: serve: specifier: catalog:devtools @@ -706,6 +709,9 @@ importers: packages/core: dependencies: + '@devframes/hub': + specifier: catalog:deps + version: 0.5.2(devframe@0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3)) '@vitejs/devtools-kit': specifier: workspace:* version: link:../kit @@ -720,7 +726,7 @@ importers: version: 7.0.0 devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) h3: specifier: catalog:deps version: 2.0.1-rc.22 @@ -741,7 +747,7 @@ importers: version: 2.1.0 tinyexec: specifier: catalog:deps - version: 1.1.2 + version: 1.2.2 vue: specifier: catalog:deps version: 3.5.34(typescript@6.0.3) @@ -791,12 +797,15 @@ importers: packages/kit: dependencies: + '@devframes/hub': + specifier: catalog:deps + version: 0.5.2(devframe@0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3)) birpc: specifier: catalog:deps version: 4.0.0 devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) mlly: specifier: catalog:deps version: 1.8.2 @@ -811,7 +820,7 @@ importers: version: 2.1.0 tinyexec: specifier: catalog:deps - version: 1.1.2 + version: 1.2.2 devDependencies: human-id: specifier: catalog:inlined @@ -848,7 +857,7 @@ importers: version: 3.2.0 devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) diff: specifier: catalog:deps version: 9.0.0 @@ -966,7 +975,7 @@ importers: version: 4.0.0 devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) pathe: specifier: catalog:deps version: 2.0.3 @@ -1027,7 +1036,7 @@ importers: version: 4.0.0 devframe: specifier: catalog:deps - version: 0.4.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + version: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) envinfo: specifier: catalog:deps version: 7.21.0 @@ -1348,6 +1357,11 @@ packages: '@colordx/core@5.4.3': resolution: {integrity: sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==} + '@devframes/hub@0.5.2': + resolution: {integrity: sha512-qMkBFw1OqhPuNs1tQWkRq0z0Tg49kXNu53bs59tdF4lytKupatWVnL3cpsVPqn+Q5P7A70r99BKTcm+prMtHqw==} + peerDependencies: + devframe: 0.5.2 + '@docsearch/css@4.5.4': resolution: {integrity: sha512-gzO4DJwyM9c4YEPHwaLV1nUCDC2N6yoh0QJj44dce2rcfN71mB+jpu3+F+Y/KMDF1EKV0C3m54leSWsraE94xg==} @@ -5184,6 +5198,14 @@ packages: '@modelcontextprotocol/sdk': optional: true + devframe@0.5.2: + resolution: {integrity: sha512-8dIdlOmuY+6NcCsaI2qS0uRLTZ3SvpejY8OYVbXvdWSQV7pvjdWaYNZhVfOfCSd/a5dSCgSge4vW4DCyJSf7+g==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.0.0 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7968,6 +7990,10 @@ packages: resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} engines: {node: '>=18'} + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -8344,6 +8370,14 @@ packages: typescript: optional: true + valibot@1.4.1: + resolution: {integrity: sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8729,6 +8763,18 @@ packages: utf-8-validate: optional: true + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -8868,13 +8914,13 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 '@antfu/ni@30.1.0': dependencies: fzf: 0.5.2 package-manager-detector: 1.6.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 '@antfu/utils@9.3.0': {} @@ -9105,6 +9151,15 @@ snapshots: '@colordx/core@5.4.3': {} + '@devframes/hub@0.5.2(devframe@0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3))': + dependencies: + birpc: 4.0.0 + devframe: 0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3) + nostics: 0.2.0 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + tinyexec: 1.2.2 + '@docsearch/css@4.5.4': {} '@docsearch/js@4.5.4': {} @@ -9632,7 +9687,7 @@ snapshots: srvx: 0.11.15 std-env: 4.1.0 tinyclip: 0.1.12 - tinyexec: 1.1.2 + tinyexec: 1.2.2 ufo: 1.6.4 youch: 4.1.1 optionalDependencies: @@ -12024,6 +12079,10 @@ snapshots: dependencies: valibot: 1.4.0(typescript@6.0.3) + '@valibot/to-json-schema@1.7.0(valibot@1.4.1(typescript@6.0.3))': + dependencies: + valibot: 1.4.1(typescript@6.0.3) + '@vercel/nft@1.5.0(rollup@4.60.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.3 @@ -12058,7 +12117,7 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 perfect-debounce: 2.1.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 vite: 8.0.14(@types/node@25.0.3)(@vitejs/devtools@0.1.24(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(@pnpm/logger@1001.0.1)(db0@0.3.4)(idb-keyval@6.2.4)(ioredis@5.10.1)(typescript@6.0.3)(vite@8.0.14))(esbuild@0.28.0)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.3)(yaml@2.9.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -13017,7 +13076,7 @@ snapshots: jsonc-parser: 3.3.1 package-manager-detector: 1.6.0 semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 unconfig: 7.5.0 yaml: 2.9.0 @@ -13600,6 +13659,26 @@ snapshots: - crossws - typescript - utf-8-validate + optional: true + + devframe@0.5.2(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(typescript@6.0.3): + dependencies: + '@valibot/to-json-schema': 1.7.0(valibot@1.4.1(typescript@6.0.3)) + birpc: 4.0.0 + cac: 7.0.0 + h3: 2.0.1-rc.22 + mrmime: 2.0.1 + nostics: 0.2.0 + pathe: 2.0.3 + valibot: 1.4.1(typescript@6.0.3) + ws: 8.21.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - crossws + - typescript + - utf-8-validate devlop@1.1.0: dependencies: @@ -15994,7 +16073,7 @@ snapshots: dependencies: citty: 0.2.2 pathe: 2.0.3 - tinyexec: 1.1.2 + tinyexec: 1.2.2 object-assign@4.1.1: optional: true @@ -17262,7 +17341,10 @@ snapshots: tinyclip@0.1.12: {} - tinyexec@1.1.2: {} + tinyexec@1.1.2: + optional: true + + tinyexec@1.2.2: {} tinyglobby@0.2.16: dependencies: @@ -17319,7 +17401,7 @@ snapshots: rolldown: 1.0.2 rolldown-plugin-dts: 0.25.0(rolldown@1.0.2)(typescript@6.0.3)(vue-tsc@3.2.9(typescript@6.0.3)) semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 @@ -17348,7 +17430,7 @@ snapshots: rolldown: 1.0.2 rolldown-plugin-dts: 0.25.0(rolldown@1.0.2)(typescript@6.0.3)(vue-tsc@3.3.1(typescript@6.0.3)) semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 @@ -17377,7 +17459,7 @@ snapshots: rolldown: 1.0.2 rolldown-plugin-dts: 0.25.0(rolldown@1.0.2)(typescript@6.0.3)(vue-tsc@3.2.9(typescript@6.0.3)) semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 @@ -17406,7 +17488,7 @@ snapshots: rolldown: 1.0.2 rolldown-plugin-dts: 0.25.0(rolldown@1.0.2)(typescript@6.0.3)(vue-tsc@3.2.9(typescript@6.0.3)) semver: 7.8.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 @@ -17789,6 +17871,10 @@ snapshots: optionalDependencies: typescript: 6.0.3 + valibot@1.4.1(typescript@6.0.3): + optionalDependencies: + typescript: 6.0.3 + vary@1.1.2: {} vfile-message@4.0.3: @@ -18160,7 +18246,7 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 vite: 8.0.14(@types/node@25.0.3)(@vitejs/devtools@packages+core)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.3)(yaml@2.9.0) @@ -18188,7 +18274,7 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 vite: 8.0.14(@types/node@25.0.3)(@vitejs/devtools@0.1.24(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(@pnpm/logger@1001.0.1)(db0@0.3.4)(idb-keyval@6.2.4)(ioredis@5.10.1)(typescript@6.0.3)(vite@8.0.14))(esbuild@0.28.0)(jiti@2.7.0)(terser@5.44.1)(tsx@4.22.3)(yaml@2.9.0) @@ -18394,6 +18480,8 @@ snapshots: ws@8.20.1: {} + ws@8.21.0: {} + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3414fdf8..07c5bf18 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ strictPeerDependencies: false trustPolicy: no-downgrade trustPolicyExclude: - langium + - tinyexec packages: - packages/* - examples/* @@ -52,11 +53,12 @@ catalogs: unplugin-vue: ^7.2.0 vite: ^8.0.14 deps: + '@devframes/hub': ^0.5.2 '@rolldown/debug': ^1.0.2 actionspack: ^0.1.4 birpc: ^4.0.0 cac: ^7.0.0 - devframe: ^0.4.1 + devframe: ^0.5.2 diff: ^9.0.0 envinfo: ^7.21.0 get-port-please: ^3.2.0 @@ -70,7 +72,7 @@ catalogs: perfect-debounce: ^2.1.0 publint: ^0.3.21 structured-clone-es: ^2.0.0 - tinyexec: ^1.1.2 + tinyexec: ^1.2.2 tinyglobby: ^0.2.16 unconfig: ^7.5.0 unstorage: ^1.17.5 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 234088b2..4e4ee785 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.d.ts @@ -1,96 +1,39 @@ /** * 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; -} -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 { CommandsContext } +export { connectRemoteDevTools } +export { ConnectRemoteDevToolsOptions } +export { DevToolsClientContext } export { DevToolsClientRpcHost } +export { DevToolsRpcClient } +export { DevToolsRpcClientCall } +export { DevToolsRpcClientCallEvent } +export { DevToolsRpcClientCallOptional } +export { DevToolsRpcClientMode } +export { DevToolsRpcClientOptions } +export { DevToolsRpcContext } +export { DockClientScriptContext } +export { DockClientType } +export { DockEntryState } +export { DockEntryStateEvents } +export { DockPanelStorage } +export { DocksContext } +export { DocksEntriesContext } +export { DocksPanelContext } +export { getDevToolsRpcClient } +export { parseRemoteConnection } export { RpcClientEvents } +export { RpcStreamingClientHost } +export { StreamingSubscribeOptions } +export { WhenClauseContext } // #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 bfc1f93c..55fa26d6 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/client.snapshot.js @@ -2,15 +2,15 @@ * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/client` */ // #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"; +// #region Other +export { connectRemoteDevTools } +export { getDevToolsRpcClient } +export { parseRemoteConnection } // #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 02e84179..55e2e220 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts @@ -3,9 +3,18 @@ */ // #region Variables export declare const DEFAULT_CATEGORIES_ORDER: Record; -export declare const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings; +export declare const DEVTOOLS_DIRNAME: string; +export declare const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID: string; +export declare const DEVTOOLS_MOUNT_PATH: string; +export declare const DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH: string; // #endregion -// #region Re-exports -export * from "devframe/constants"; +// #region Other +export { DEFAULT_STATE_USER_SETTINGS } +export { DEVTOOLS_CONNECTION_META_FILENAME } +export { DEVTOOLS_DOCK_IMPORTS_FILENAME } +export { DEVTOOLS_RPC_DUMP_DIRNAME } +export { DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME } +export { DevToolsDocksUserSettings } +export { REMOTE_CONNECTION_KEY } // #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 ecc6f929..cd456653 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.js @@ -3,9 +3,17 @@ */ // #region Variables export var DEFAULT_CATEGORIES_ORDER /* const */ -export var DEFAULT_STATE_USER_SETTINGS /* const */ +export var DEVTOOLS_DIRNAME /* const */ +export var DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID /* const */ +export var DEVTOOLS_MOUNT_PATH /* const */ +export var DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH /* const */ // #endregion -// #region Re-exports -export * from "devframe/constants"; +// #region Other +export { DEFAULT_STATE_USER_SETTINGS } +export { DEVTOOLS_CONNECTION_META_FILENAME } +export { DEVTOOLS_DOCK_IMPORTS_FILENAME } +export { DEVTOOLS_RPC_DUMP_DIRNAME } +export { DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME } +export { REMOTE_CONNECTION_KEY } // #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 27eab3e8..56d0a956 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/index.snapshot.d.ts @@ -1,16 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit` */ -// #region Functions -export declare function defineCommand(_: Omit & { - when?: WhenExpression; -}): DevToolsServerCommandInput; -export declare function defineDockEntry(_: Omit & { - when?: WhenExpression; -}): T; -export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; -// #endregion - // #region Variables export declare const defineRpcFunction: (definition: _$devframe_rpc0.RpcFunctionDefinition) => _$devframe_rpc0.RpcFunctionDefinition; // #endregion @@ -19,6 +9,9 @@ export declare const defineRpcFunction: >; - capabilities?: DevToolsCapabilities | { - dev?: DevToolsCapabilities | boolean; - build?: DevToolsCapabilities | boolean; + dock?: Partial>; + capabilities?: DevframeCapabilities | { + dev?: DevframeCapabilities | boolean; + build?: DevframeCapabilities | boolean; }; setup?: (_: KitNodeContext) => void | Promise; } @@ -19,83 +19,19 @@ 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(_: DevframeDefinition, _?: CreatePluginFromDevframeOptions): PluginWithDevTools; export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; -export declare function createViteDevToolsHost(_: CreateViteDevToolsHostOptions): DevToolsHost; +export declare function createViteDevToolsHost(_: CreateViteDevToolsHostOptions): DevframeHost; // #endregion // #region Other export { createKitContext } export { CreateKitContextOptions } +export { DevToolsCommandsHost } +export { DevToolsDockHost } +export { DevToolsMessagesHost } +export { DevToolsTerminalHost } export { KitNodeContext } +export { mountDevframe } // #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 3fbb88ca..b52bf360 100644 --- a/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js +++ b/test/__snapshots__/tsnapi/@vitejs/devtools-kit/node.snapshot.js @@ -1,69 +1,17 @@ /** * Generated by tsnapi — public API snapshot of `@vitejs/devtools-kit/node` */ -// #region Classes -export class DevToolsCommandsHost { - context - commands - events - constructor(_) {} - register(_) {} - unregister(_) {} - async execute(_, ..._) {} - list() {} - findCommand(_) {} - toSerializable(_) {} -} -export class DevToolsDockHost { - context - views - events - userSettings - remoteDocks - constructor(_) {} - async init() {} - values(_) {} - projectView(_) {} - resolveDevServerOrigin() {} - register(_, _) {} - update(_) {} - prepareRemoteRegistration(_) {} -} -export class DevToolsMessagesHost { - context - entries - events - lastModified - removals - _autoDeleteTimers - _clock - _tick() {} - constructor(_) {} - async add(_) {} - async update(_, _) {} - async remove(_) {} - async clear() {} - _createHandle(_) {} -} -export class DevToolsTerminalHost { - context - 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 + +// #region Other +export { DevToolsCommandsHost } +export { DevToolsDockHost } +export { DevToolsMessagesHost } +export { DevToolsTerminalHost } +export { mountDevframe } // #endregion \ No newline at end of file