From 4b8c4be5b2e075912e8e0be1af1b4935c9f62426 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 22 May 2026 12:05:07 +0900 Subject: [PATCH 1/7] feat(hub): introduce @devframes/hub framework-neutral hub layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lifts the docks/terminals/messages/commands subsystems out of @vitejs/devtools-kit into a framework-neutral package so any framework (Vite, Next.js, etc.) can build a hub kit on the same infrastructure. Adds `mountDevframe` as the framework-neutral mount primitive, `HubHostCapabilities` for optional host capabilities (e.g. `openPath`), and built-in RPCs `hub:open-path` / `hub:commands:execute`. Reserves the `DF8xxx` diagnostic range for hub-side codes with sub-ranges per subsystem. New `examples/minimal-vite-devtools-kit/` acts as a protocol witness — a ~150-line Vite plugin + tiny DOM UI that exercises every hub subsystem end to end. --- AGENTS.md | 12 + alias.ts | 5 + docs/.vitepress/config.ts | 1 + docs/errors/DF8100.md | 22 ++ docs/errors/DF8101.md | 22 ++ docs/errors/DF8102.md | 22 ++ docs/errors/DF8200.md | 22 ++ docs/errors/DF8201.md | 22 ++ docs/errors/DF8400.md | 22 ++ docs/errors/DF8401.md | 22 ++ docs/errors/DF8402.md | 23 ++ docs/errors/DF8500.md | 38 ++++ docs/guide/hub.md | 96 ++++++++ examples/minimal-vite-devtools-kit/README.md | 36 +++ examples/minimal-vite-devtools-kit/index.html | 44 ++++ .../minimal-vite-devtools-kit/package.json | 20 ++ .../src/client/main.ts | 109 +++++++++ .../src/client/style.css | 87 ++++++++ .../minimal-vite-devtools-kit/src/devframe.ts | 35 +++ .../src/minimal-hub-kit.ts | 175 +++++++++++++++ .../minimal-vite-devtools-kit/tsconfig.json | 12 + .../minimal-vite-devtools-kit/vite.config.ts | 13 ++ packages/hub/LICENSE.md | 21 ++ packages/hub/package.json | 54 +++++ packages/hub/src/client/client-script.ts | 16 ++ packages/hub/src/client/context.ts | 12 + packages/hub/src/client/docks.ts | 139 ++++++++++++ packages/hub/src/client/index.ts | 5 + packages/hub/src/client/remote.ts | 122 ++++++++++ packages/hub/src/constants.ts | 24 ++ packages/hub/src/define.ts | 27 +++ packages/hub/src/index.ts | 2 + packages/hub/src/node/context.ts | 152 +++++++++++++ packages/hub/src/node/diagnostics.ts | 48 ++++ packages/hub/src/node/host-commands.ts | 95 ++++++++ packages/hub/src/node/host-docks.ts | 209 ++++++++++++++++++ packages/hub/src/node/host-messages.ts | 134 +++++++++++ packages/hub/src/node/host-terminals.ts | 200 +++++++++++++++++ packages/hub/src/node/hub-builtins.ts | 36 +++ packages/hub/src/node/index.ts | 9 + packages/hub/src/node/mount-devframe.ts | 52 +++++ packages/hub/src/node/rpc-builtins.ts | 30 +++ packages/hub/src/node/utils.ts | 17 ++ packages/hub/src/types/commands.ts | 130 +++++++++++ packages/hub/src/types/docks.ts | 167 ++++++++++++++ packages/hub/src/types/index.ts | 44 ++++ packages/hub/src/types/json-render.ts | 29 +++ packages/hub/src/types/messages.ts | 146 ++++++++++++ packages/hub/src/types/settings.ts | 11 + packages/hub/src/types/terminals.ts | 48 ++++ .../hub/src/utils/diagnostics-reporter.ts | 12 + packages/hub/tsconfig.json | 7 + packages/hub/tsdown.config.ts | 28 +++ pnpm-lock.yaml | 50 +++++ pnpm-workspace.yaml | 1 + .../@devframes/hub/client.snapshot.d.ts | 96 ++++++++ .../tsnapi/@devframes/hub/client.snapshot.js | 16 ++ .../@devframes/hub/constants.snapshot.d.ts | 11 + .../@devframes/hub/constants.snapshot.js | 11 + .../tsnapi/@devframes/hub/index.snapshot.d.ts | 93 ++++++++ .../tsnapi/@devframes/hub/index.snapshot.js | 9 + .../tsnapi/@devframes/hub/node.snapshot.d.ts | 110 +++++++++ .../tsnapi/@devframes/hub/node.snapshot.js | 74 +++++++ .../tsnapi/@devframes/hub/types.snapshot.d.ts | 79 +++++++ .../tsnapi/@devframes/hub/types.snapshot.js | 4 + tsconfig.base.json | 15 ++ turbo.json | 10 + 67 files changed, 3465 insertions(+) create mode 100644 docs/errors/DF8100.md create mode 100644 docs/errors/DF8101.md create mode 100644 docs/errors/DF8102.md create mode 100644 docs/errors/DF8200.md create mode 100644 docs/errors/DF8201.md create mode 100644 docs/errors/DF8400.md create mode 100644 docs/errors/DF8401.md create mode 100644 docs/errors/DF8402.md create mode 100644 docs/errors/DF8500.md create mode 100644 docs/guide/hub.md create mode 100644 examples/minimal-vite-devtools-kit/README.md create mode 100644 examples/minimal-vite-devtools-kit/index.html create mode 100644 examples/minimal-vite-devtools-kit/package.json create mode 100644 examples/minimal-vite-devtools-kit/src/client/main.ts create mode 100644 examples/minimal-vite-devtools-kit/src/client/style.css create mode 100644 examples/minimal-vite-devtools-kit/src/devframe.ts create mode 100644 examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts create mode 100644 examples/minimal-vite-devtools-kit/tsconfig.json create mode 100644 examples/minimal-vite-devtools-kit/vite.config.ts create mode 100644 packages/hub/LICENSE.md create mode 100644 packages/hub/package.json create mode 100644 packages/hub/src/client/client-script.ts create mode 100644 packages/hub/src/client/context.ts create mode 100644 packages/hub/src/client/docks.ts create mode 100644 packages/hub/src/client/index.ts create mode 100644 packages/hub/src/client/remote.ts create mode 100644 packages/hub/src/constants.ts create mode 100644 packages/hub/src/define.ts create mode 100644 packages/hub/src/index.ts create mode 100644 packages/hub/src/node/context.ts create mode 100644 packages/hub/src/node/diagnostics.ts create mode 100644 packages/hub/src/node/host-commands.ts create mode 100644 packages/hub/src/node/host-docks.ts create mode 100644 packages/hub/src/node/host-messages.ts create mode 100644 packages/hub/src/node/host-terminals.ts create mode 100644 packages/hub/src/node/hub-builtins.ts create mode 100644 packages/hub/src/node/index.ts create mode 100644 packages/hub/src/node/mount-devframe.ts create mode 100644 packages/hub/src/node/rpc-builtins.ts create mode 100644 packages/hub/src/node/utils.ts create mode 100644 packages/hub/src/types/commands.ts create mode 100644 packages/hub/src/types/docks.ts create mode 100644 packages/hub/src/types/index.ts create mode 100644 packages/hub/src/types/json-render.ts create mode 100644 packages/hub/src/types/messages.ts create mode 100644 packages/hub/src/types/settings.ts create mode 100644 packages/hub/src/types/terminals.ts create mode 100644 packages/hub/src/utils/diagnostics-reporter.ts create mode 100644 packages/hub/tsconfig.json create mode 100644 packages/hub/tsdown.config.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.js create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts create mode 100644 tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.js diff --git a/AGENTS.md b/AGENTS.md index 3daeef7..5f69e24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,8 @@ **`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. +**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devtools-kit/` for a working ~120-line kit demonstrating the protocol end to end. + ## Stack & Structure ESM TypeScript library. Bundled with `tsdown`. Tested with `vitest`. pnpm workspaces with catalog dependencies (`pnpm-workspace.yaml`); workspace globs reserve `playground`, `docs`, `packages/*`, `examples/*` for future additions. @@ -50,6 +52,16 @@ All node-side warnings and errors use structured diagnostics via [`nostics`](htt Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`). Check the existing diagnostics file to find the next available number. +Range allocation: +- `DF00xx–DF07xx` — `devframe` core (RPC, host, storage, streams, …) +- `DF80xx–DF89xx` — `@devframes/hub`. Sub-ranges: + - `DF80xx` — hub context / lifecycle + - `DF81xx` — docks + - `DF82xx` — terminals + - `DF83xx` — messages + - `DF84xx` — commands + - `DF85xx` — built-in RPC commands + ### Adding a new error 1. **Define the code** in the appropriate `diagnostics.ts`: diff --git a/alias.ts b/alias.ts index d0cb424..6388c5c 100644 --- a/alias.ts +++ b/alias.ts @@ -36,6 +36,11 @@ export const alias = { 'devframe/helpers/vite': r('devframe/src/helpers/vite.ts'), 'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'), 'devframe/adapters/mcp': r('devframe/src/adapters/mcp/index.ts'), + '@devframes/hub/client': r('hub/src/client/index.ts'), + '@devframes/hub/constants': r('hub/src/constants.ts'), + '@devframes/hub/node': r('hub/src/node/index.ts'), + '@devframes/hub/types': r('hub/src/types/index.ts'), + '@devframes/hub': r('hub/src/index.ts'), '@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'), '@devframes/nuxt': r('nuxt/src/index.ts'), 'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'), diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index d129870..a834804 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -28,6 +28,7 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] { { text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` }, { text: 'Client', link: `${prefix}/guide/client` }, { text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` }, + { text: 'Hub (multi-tool)', link: `${prefix}/guide/hub` }, { text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` }, ] } diff --git a/docs/errors/DF8100.md b/docs/errors/DF8100.md new file mode 100644 index 0000000..d8e3945 --- /dev/null +++ b/docs/errors/DF8100.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8100: Dock Already Registered + +## Message + +> Dock with id "`{id}`" is already registered + +## Cause + +`ctx.docks.register(view)` was called with an `id` that another dock already owns. Dock ids are unique per hub context, so the second registration would silently override the first if allowed. + +## Fix + +- Pick a different dock id (best practice: namespace under your tool's id, e.g. `my-tool:overview`). +- If overwriting is intentional (e.g. a hot-reload scenario), pass `force: true`: `ctx.docks.register(view, true)`. + +## Source + +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.register()` throws when `views.has(view.id) && !force`. diff --git a/docs/errors/DF8101.md b/docs/errors/DF8101.md new file mode 100644 index 0000000..8135814 --- /dev/null +++ b/docs/errors/DF8101.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8101: Cannot Change Dock Id + +## Message + +> Cannot change the id of a dock. Use register() to add new docks. + +## Cause + +The `update` handle returned by `ctx.docks.register(view)` received a patch whose `id` differs from the original. Dock ids are immutable post-registration — they key the dock list and any shared-state references. + +## Fix + +- Drop `id` from your patch; only pass the fields you actually want to mutate. +- To replace one dock with a different one: call `ctx.docks.register(newView)` (or `register(newView, true)` to overwrite an existing id). + +## Source + +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.register()` returns an `update` callable that throws this when the patch carries a different `id`. diff --git a/docs/errors/DF8102.md b/docs/errors/DF8102.md new file mode 100644 index 0000000..bc5ba85 --- /dev/null +++ b/docs/errors/DF8102.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8102: 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 no prior registration. `update` is for mutating existing entries; new entries must go through `register`. + +## Fix + +- Use `ctx.docks.register(view)` for new entries. +- Verify the id matches a previously registered dock — typos / case mismatches are the usual cause. + +## Source + +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.update()` throws when `views.has(view.id) === false`. diff --git a/docs/errors/DF8200.md b/docs/errors/DF8200.md new file mode 100644 index 0000000..726b7b0 --- /dev/null +++ b/docs/errors/DF8200.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8200: Terminal Session Already Registered + +## Message + +> Terminal session with id "`{id}`" already registered + +## Cause + +`ctx.terminals.register(session)` or `ctx.terminals.startChildProcess(opts, terminal)` was called with an `id` that another session already owns. Terminal ids are unique per hub context. + +## Fix + +- Pick a different session id (namespace under your tool: `my-tool:build-server`, `my-tool:tests`, …). +- If you want to reuse an existing session's slot, `ctx.terminals.remove(session)` first. + +## Source + +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevToolsTerminalHost.register()` and `startChildProcess()` throw when the id is already taken. diff --git a/docs/errors/DF8201.md b/docs/errors/DF8201.md new file mode 100644 index 0000000..cfabfa4 --- /dev/null +++ b/docs/errors/DF8201.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8201: Terminal Session Not Registered + +## Message + +> Terminal session with id "`{id}`" not registered + +## Cause + +`ctx.terminals.update(patch)` was called with an `id` for which no session has been registered. + +## Fix + +- Use `ctx.terminals.register(session)` to add new sessions. +- Verify the id matches an existing session — common cause is updating after `remove()`. + +## Source + +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevToolsTerminalHost.update()` throws when `sessions.has(patch.id) === false`. diff --git a/docs/errors/DF8400.md b/docs/errors/DF8400.md new file mode 100644 index 0000000..fd093e6 --- /dev/null +++ b/docs/errors/DF8400.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8400: Command Already Registered + +## Message + +> Command "`{id}`" is already registered + +## Cause + +`ctx.commands.register(command)` was called with an `id` that another command already owns. Command ids are unique per hub context. + +## Fix + +- Pick a different command id (namespace under your tool: `my-tool:reload`, `my-tool:open-settings`, …). +- To replace, call `ctx.commands.unregister(id)` first. + +## Source + +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` throws when `commands.has(command.id)`. diff --git a/docs/errors/DF8401.md b/docs/errors/DF8401.md new file mode 100644 index 0000000..8fa88b1 --- /dev/null +++ b/docs/errors/DF8401.md @@ -0,0 +1,22 @@ +--- +outline: deep +--- + +# DF8401: Cannot Change Command Id + +## Message + +> Cannot change the id of a command. Use register() to add new commands. + +## Cause + +The `update` handle returned by `ctx.commands.register(cmd)` received a patch with an `id` key. Command ids are immutable post-registration. + +## Fix + +- Drop `id` from the patch object. +- To register a new command, call `ctx.commands.register(newCmd)`. + +## Source + +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` returns a `update` callable that throws when `'id' in patch`. diff --git a/docs/errors/DF8402.md b/docs/errors/DF8402.md new file mode 100644 index 0000000..1fa3991 --- /dev/null +++ b/docs/errors/DF8402.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# DF8402: Command Not Registered + +## Message + +> Command "`{id}`" is not registered + +## Cause + +`ctx.commands.execute(id, …)` (or the `update()` handle returned by `register()`) was called with an `id` that has no registration. The command was either never registered or has already been `unregister()`'d. + +## Fix + +- Register the command first via `ctx.commands.register({ id, title, handler })`. +- Verify the id — typos and stale references are the usual cause. +- If invoking client-side, register the command on the client via `ctx.commands.register` in the dock client script. + +## Source + +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.execute()` and the `update` handle throw when the id is missing. diff --git a/docs/errors/DF8500.md b/docs/errors/DF8500.md new file mode 100644 index 0000000..e03f9d3 --- /dev/null +++ b/docs/errors/DF8500.md @@ -0,0 +1,38 @@ +--- +outline: deep +--- + +# DF8500: Built-in Command Requires Host Capability + +## Message + +> Built-in command "`{id}`" requires a host capability that this host does not implement. + +## Cause + +A hub built-in command (e.g. `hub:open-path`) was invoked, but the host implementation passed to `createHubContext` did not implement the matching capability. The hub exposes these built-ins uniformly across framework kits — but the underlying capability is host-specific (e.g. `openPath` needs a launch-editor binding the host can call). + +## Fix + +Implement the matching capability on the `DevToolsHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`: + +```ts +import type { HubHostCapabilities } from '@devframes/hub/node' +import type { DevToolsHost } from 'devframe/types' +import { launchEditor } from 'devframe/utils/launch-editor' + +const host: DevToolsHost & HubHostCapabilities = { + // … existing DevToolsHost methods … + async openPath(filepath, line, column) { + const target = line ? `${filepath}:${line}${column ? `:${column}` : ''}` : filepath + launchEditor(target) + return true + }, +} +``` + +See the [Hub guide](/guide/hub#host-capabilities) for the full capability surface. + +## Source + +- [`packages/hub/src/node/hub-builtins.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/hub-builtins.ts) — `registerHubBuiltins()` registers `hub:open-path`, whose handler throws this when `context.host.openPath` is undefined. diff --git a/docs/guide/hub.md b/docs/guide/hub.md new file mode 100644 index 0000000..aee1744 --- /dev/null +++ b/docs/guide/hub.md @@ -0,0 +1,96 @@ +--- +outline: deep +--- + +# Hub (multi-tool) + +`@devframes/hub` extends devframe with the orchestration features that only make sense when many devtools share a UI: a dock registry, terminal aggregation, message/toast queue, and a command palette. It does not ship UI — each framework kit (e.g. `@vitejs/devtools-kit`) provides its own UI on top of the hub's RPC + shared-state protocol. + +> [!WARNING] Experimental +> The hub API surface is still being refined. Names may change before 1.0. + +## What the hub adds + +A hub-aware node context (`HubNodeContext`) extends `DevToolsNodeContext` with four subsystems: + +| Subsystem | Surface | Purpose | +|---|---|---| +| `ctx.docks` | `register / update / values` | Multi-tool dock entries (iframes, launchers, json-render, custom-render). | +| `ctx.terminals` | `register / startChildProcess` | Aggregate terminal sessions, stream output over a well-known channel. | +| `ctx.messages` | `add / update / remove / clear` | Server-side toast/notification queue (FIFO, capped at 1000). | +| `ctx.commands` | `register / execute / list` | Hierarchical command palette with keybindings and `when` clauses. | + +Plus a `createJsonRenderer(spec)` factory for building remote-UI panels via the framework-neutral json-render DSL. + +## Built-in RPC + +Every hub context auto-registers these RPC functions so framework kits don't reimplement them: + +- `hub:commands:execute` — invoke a registered server command by id. `await rpc.call('hub:commands:execute', 'my-tool:do-thing', ...args)`. +- `hub:open-path` — registered as a command, delegates to `host.openPath()` (see [Host capabilities](#host-capabilities)). + +## Host capabilities + +A hub host implements the same `DevToolsHost` interface as devframe, plus optional capabilities the hub knows how to delegate to: + +```ts +interface HubHostCapabilities { + /** Open a file in the user's editor. Backs the built-in `hub:open-path` command. */ + openPath?: (filepath: string, line?: number, column?: number) => boolean | Promise +} +``` + +A framework kit's host implementation looks like this: + +```ts +import type { HubHostCapabilities } from '@devframes/hub/node' +import type { DevToolsHost } from 'devframe/types' +import { launchEditor } from 'devframe/utils/launch-editor' + +const host: DevToolsHost & HubHostCapabilities = { + mountStatic(base, distDir) { /* … */ }, + resolveOrigin() { /* … */ }, + getStorageDir(scope) { /* … */ }, + async openPath(filepath, line, column) { + const target = line ? `${filepath}:${line}${column ? `:${column}` : ''}` : filepath + launchEditor(target) + return true + }, +} +``` + +When a framework kit omits `openPath`, the `hub:open-path` command throws [`DF8500`](/errors/DF8500) instead of silently failing. + +## Mounting a devframe into a hub + +`mountDevframe(ctx, def)` is the framework-neutral primitive that registers any `DevframeDefinition` as a dock and runs its `setup(ctx)`: + +```ts +import { createHubContext, mountDevframe } from '@devframes/hub/node' + +const ctx = await createHubContext({ cwd, host, mode: 'dev' }) +await mountDevframe(ctx, myDevframe) +``` + +Framework kits typically wrap this in a plugin shell. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a Vite `Plugin` whose `devtools.setup` calls into `mountDevframe`. + +## The protocol — what the UI sees + +A hub-aware UI doesn't import any hub classes; it reads three shared-state keys and one RPC method: + +| Channel | Type | What it carries | +|---|---|---| +| `devframe:docks` shared state | `DevToolsDockEntry[]` | The full dock list, including the hub's `~terminals` / `~messages` / `~settings` builtins. | +| `devframe:commands` shared state | `DevToolsServerCommandEntry[]` | Serializable command list (handlers stripped). | +| `devframe:user-settings` shared state | `DevToolsDocksUserSettings` | Persisted per-workspace hub settings. | +| `hub:commands:execute` RPC | `(id, ...args) => unknown` | Server-side command dispatch. | + +Plus broadcast notifications (`devframe:terminals:updated`, `devframe:messages:updated`) that a UI can subscribe to via `rpc.client.register(...)`. + +## Example + +See [`examples/minimal-vite-devtools-kit/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devtools-kit) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub kit follows the same shape: a thin layer that adapts the framework's dev server to the hub. + +## Diagnostics + +Hub-side diagnostic codes live in the `DF8xxx` range. See the [error reference](/errors/) for the full list. diff --git a/examples/minimal-vite-devtools-kit/README.md b/examples/minimal-vite-devtools-kit/README.md new file mode 100644 index 0000000..b29fba9 --- /dev/null +++ b/examples/minimal-vite-devtools-kit/README.md @@ -0,0 +1,36 @@ +# Minimal Vite DevTools Kit + +A protocol-witness example. The `src/minimal-hub-kit.ts` file is the entire "kit" — about 120 lines of Vite plugin code that wires `@devframes/hub` into a Vite dev server. Every framework's hub kit (`@vitejs/devtools-kit`, future `@next/devtools-kit`, etc.) is the same shape. + +## Run it + +```sh +pnpm install +pnpm --filter minimal-vite-devtools-kit-example dev +``` + +Open the printed URL. You should see: + +- A status line showing the RPC backend +- A **Docks** list — one entry per `mountDevframe` call, plus the hub's built-in `~terminals` / `~messages` / `~settings` panels +- A **Commands** list — one entry per `commands.register()` +- A **Messages** list — populated via `messages.add()` on the server +- A **Terminals** list — empty unless a devframe registers one +- A button that exercises `hub:open-path` (opens this README in your editor) + +## What the example proves + +- `createHubContext()` boots a hub without any Vite-specific code path +- A `DevToolsHost & HubHostCapabilities` impl plugs framework specifics (`openPath`, storage paths) into the hub uniformly +- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock +- Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed +- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware + +## Files + +| File | Role | +|---|---| +| `src/minimal-hub-kit.ts` | The Vite plugin — creates hub context, mounts middleware, side-car WS | +| `src/devframe.ts` | A sample `DevframeDefinition` that plugs into the kit | +| `src/client/main.ts` | The browser-side UI that consumes the hub protocol | +| `index.html` | The UI shell | diff --git a/examples/minimal-vite-devtools-kit/index.html b/examples/minimal-vite-devtools-kit/index.html new file mode 100644 index 0000000..b9dfe77 --- /dev/null +++ b/examples/minimal-vite-devtools-kit/index.html @@ -0,0 +1,44 @@ + + + + + + Minimal Vite DevTools Kit + + + +
+

Minimal Vite DevTools Kit

+

Protocol witness — verifies @devframes/hub end to end.

+
+
+
+ Connecting… +
+ +
+

Docks

+
  • Waiting for snapshot…
+
+ +
+

Commands

+
  • Waiting for snapshot…
+

+ +

+
+ +
+

Messages

+
  • No messages yet.
+
+ +
+

Terminals

+
  • No terminal sessions.
+
+
+ + + diff --git a/examples/minimal-vite-devtools-kit/package.json b/examples/minimal-vite-devtools-kit/package.json new file mode 100644 index 0000000..d4023ed --- /dev/null +++ b/examples/minimal-vite-devtools-kit/package.json @@ -0,0 +1,20 @@ +{ + "name": "minimal-vite-devtools-kit-example", + "type": "module", + "version": "0.4.1", + "private": true, + "description": "Protocol-witness example — a tiny Vite plugin built on @devframes/hub that exercises every hub subsystem end-to-end.", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "@devframes/hub": "workspace:*", + "devframe": "workspace:*" + }, + "devDependencies": { + "get-port-please": "catalog:deps", + "pathe": "catalog:deps", + "vite": "catalog:build" + } +} diff --git a/examples/minimal-vite-devtools-kit/src/client/main.ts b/examples/minimal-vite-devtools-kit/src/client/main.ts new file mode 100644 index 0000000..4854970 --- /dev/null +++ b/examples/minimal-vite-devtools-kit/src/client/main.ts @@ -0,0 +1,109 @@ +import type { + DevToolsCommandEntry, + DevToolsDockEntry, + DevToolsMessageEntry, + DevToolsTerminalSession, +} from '@devframes/hub/types' +import { connectDevframe } from '@devframes/hub/client' + +const HUB_BASE = '/__hub/' + +const statusEl = document.querySelector('#status')! +const connEl = document.querySelector('#conn')! +const docksEl = document.querySelector('#docks')! +const commandsEl = document.querySelector('#commands')! +const messagesEl = document.querySelector('#messages')! +const terminalsEl = document.querySelector('#terminals')! +const openPathBtn = document.querySelector('#open-path')! + +function setStatus(text: string, klass?: 'ready' | 'error') { + connEl.textContent = text + statusEl.className = klass ?? '' +} + +function renderList(host: HTMLElement, items: T[], render: (item: T) => string) { + if (!items.length) { + host.innerHTML = '
  • empty
  • ' + return + } + host.innerHTML = items.map(render).join('') +} + +async function main() { + setStatus('Connecting…') + + const rpc = await connectDevframe({ baseURL: HUB_BASE }) + setStatus(`Connected · backend=${rpc.connectionMeta.backend}`, 'ready') + + // 1. Docks — read from `devframe:docks` shared state. + const docks = await rpc.sharedState.get( + 'devframe:docks', + { initialValue: [] }, + ) + const renderDocks = () => renderList(docksEl, docks.value() ?? [], (d) => { + const badge = d.badge ? ` [${d.badge}]` : '' + return `
  • ${d.title} ${d.id}${badge}
  • ` + }) + docks.on('updated', renderDocks) + renderDocks() + + // 2. Commands — read from `devframe:commands` shared state. + const commands = await rpc.sharedState.get( + 'devframe:commands', + { initialValue: [] }, + ) + const renderCommands = () => renderList(commandsEl, commands.value() ?? [], c => + `
  • ${c.title} ${c.id}
  • `) + commands.on('updated', renderCommands) + renderCommands() + + // 3. Messages — pulled via a kit-local RPC. A fuller kit would also + // register a client-side RPC handler for `devframe:messages:updated` + // to refresh on broadcast; this minimal example polls instead. + const refreshMessages = async () => { + const entries = await rpc.call( + 'minimal-hub-kit:messages:list' as any, + ) as DevToolsMessageEntry[] + renderList(messagesEl, entries, m => + `
  • [${m.level}] ${m.message}
  • `) + } + await refreshMessages() + + // 4. Terminals — same pattern as messages. + const refreshTerminals = async () => { + const sessions = await rpc.call( + 'minimal-hub-kit:terminals:list' as any, + ) as Pick[] + renderList(terminalsEl, sessions, t => + `
  • ${t.title} ${t.id} · ${t.status}
  • `) + } + await refreshTerminals() + + setInterval(() => { + void refreshMessages() + void refreshTerminals() + }, 2000) + + // 5. Test the hub:open-path built-in via hub:commands:execute. + openPathBtn.addEventListener('click', async () => { + const target = `${location.origin}/README.md` + .replace(/^https?:\/\/[^/]+/, '') // strip origin, leave path + try { + // Use the project README — server has the actual filesystem path. + const result = await rpc.call( + 'hub:commands:execute' as any, + 'hub:open-path', + target.startsWith('/') ? target.slice(1) : target, + ) + openPathBtn.textContent = `Opened (returned ${JSON.stringify(result)})` + } + catch (err) { + openPathBtn.textContent = `Error: ${(err as Error).message}` + } + }) +} + +main().catch((err) => { + setStatus(`Failed: ${(err as Error).message}`, 'error') + console.error(err) +}) diff --git a/examples/minimal-vite-devtools-kit/src/client/style.css b/examples/minimal-vite-devtools-kit/src/client/style.css new file mode 100644 index 0000000..6127b5e --- /dev/null +++ b/examples/minimal-vite-devtools-kit/src/client/style.css @@ -0,0 +1,87 @@ +:root { + color-scheme: light dark; + font-family: system-ui, sans-serif; + line-height: 1.5; +} + +body { + margin: 0; + padding: 1.5rem 2rem; + max-width: 800px; + margin-inline: auto; +} + +header h1 { + margin-bottom: 0.25rem; +} + +header p { + margin-top: 0; + opacity: 0.7; +} + +section { + margin-block: 1.5rem; +} + +h2 { + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.8; + border-bottom: 1px solid currentcolor; + padding-bottom: 0.25rem; +} + +ul { + list-style: none; + padding-left: 0; +} + +li { + padding: 0.5rem 0.75rem; + border: 1px solid color-mix(in srgb, currentcolor 15%, transparent); + border-radius: 0.5rem; + margin-bottom: 0.5rem; + font-family: ui-monospace, monospace; + font-size: 0.9rem; +} + +li.muted { + opacity: 0.5; + font-style: italic; +} + +code { + font-family: ui-monospace, monospace; + background: color-mix(in srgb, currentcolor 10%, transparent); + padding: 0.1em 0.35em; + border-radius: 0.25em; +} + +button { + font: inherit; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid currentcolor; + background: transparent; + cursor: pointer; +} + +button:hover { + background: color-mix(in srgb, currentcolor 10%, transparent); +} + +#status { + font-family: ui-monospace, monospace; + font-size: 0.85rem; + opacity: 0.7; +} + +#status.error #conn { + color: #c33; +} + +#status.ready #conn { + color: #2a7; +} diff --git a/examples/minimal-vite-devtools-kit/src/devframe.ts b/examples/minimal-vite-devtools-kit/src/devframe.ts new file mode 100644 index 0000000..21fcafa --- /dev/null +++ b/examples/minimal-vite-devtools-kit/src/devframe.ts @@ -0,0 +1,35 @@ +import type { HubNodeContext } from '@devframes/hub/node' +import { defineDevframe } from 'devframe/types' + +/** + * A tiny demo devframe — proves a portable devframe can plug into the + * hub via {@link mountDevframe} and register its own docks / commands / + * messages on top of the host-provided subsystems. + * + * The `ctx` cast is the same one `@vitejs/devtools-kit`'s + * `createPluginFromDevframe` does today; the kit-level mount primitive + * threads a hub-augmented context through `d.setup`. + */ +export default defineDevframe({ + id: 'demo-tool', + name: 'Demo Tool', + icon: 'ph:rocket-duotone', + basePath: '/__demo-tool/', + async setup(rawCtx) { + const ctx = rawCtx as unknown as HubNodeContext + + ctx.commands.register({ + id: 'demo-tool:say-hello', + title: 'Demo · Say Hello', + icon: 'ph:hand-waving-duotone', + category: 'demo', + handler: () => 'Hello from the demo command!', + }) + + await ctx.messages.add({ + level: 'info', + message: 'Demo devframe loaded', + description: 'Registered via mountDevframe(). Proves the devframe ↔ hub plug-in path works.', + }) + }, +}) diff --git a/examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts b/examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts new file mode 100644 index 0000000..faf8108 --- /dev/null +++ b/examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts @@ -0,0 +1,175 @@ +import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' +import type { DevframeDefinition, DevToolsHost } from 'devframe/types' +import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' +import { homedir } from 'node:os' +import { defineRpcFunction } from '@devframes/hub' +import { createHubContext, mountDevframe } from '@devframes/hub/node' +import { DEVTOOLS_CONNECTION_META_FILENAME } from 'devframe/constants' +import { startHttpAndWs } from 'devframe/node' +import { launchEditor } from 'devframe/utils/launch-editor' +import { getPort } from 'get-port-please' +import { join } from 'pathe' + +export interface MinimalHubKitOptions { + /** Mount path for the kit's connection-meta endpoint. Default: `/__hub/`. */ + base?: string + /** Preferred port for the side-car RPC/WS server. Default: a free port near 9777. */ + port?: number + /** Devframes to mount as docks. */ + devframes?: DevframeDefinition[] +} + +// Minimal kit-local RPCs — used by the UI for read-side data. A more +// ambitious kit might hoist these into `@devframes/hub` itself. +const minimalKitMessagesList = defineRpcFunction({ + name: 'minimal-hub-kit:messages:list', + type: 'static', + jsonSerializable: true, + setup: (ctx: HubNodeContext) => ({ + async handler() { + return Array.from(ctx.messages.entries.values()) + }, + }), +}) + +const minimalKitTerminalsList = defineRpcFunction({ + name: 'minimal-hub-kit:terminals:list', + type: 'static', + jsonSerializable: true, + setup: (ctx: HubNodeContext) => ({ + async handler() { + return Array.from(ctx.terminals.sessions.values()).map(s => ({ + id: s.id, + title: s.title, + description: s.description, + status: s.status, + })) + }, + }), +}) + +/** + * A deliberately tiny Vite plugin that wires `@devframes/hub` into a Vite + * dev server: creates a hub context, implements the host capabilities + * (`openPath` via launch-editor), and exposes the side-car WS endpoint + * to the browser via Vite middleware at `__connection.json`. + * + * This file is the entire "kit" — every other framework's hub kit + * (`@vitejs/devtools-kit`, future `@next/devtools-kit`, …) is the same + * shape: a thin layer that adapts a framework's dev server to the hub. + */ +export function minimalHubKit(options: MinimalHubKitOptions = {}): Plugin { + const base = normalizeBase(options.base ?? '/__hub/') + let viteConfig: ResolvedConfig | undefined + let started: { close: () => Promise } | undefined + + return { + name: 'minimal-vite-devtools-kit', + apply: 'serve', + + configResolved(config) { + viteConfig = config + }, + + async configureServer(server: ViteDevServer) { + // Vite re-invokes `configureServer` on each restart. Tear down the + // previous server so we don't leak the WS port. + await started?.close().catch(() => {}) + started = undefined + + const cwd = viteConfig!.root + + const host: DevToolsHost & HubHostCapabilities = { + mountStatic() { + // Static mounting for devframe SPAs would route through Vite's + // middleware in a fuller kit. This minimal example doesn't + // host any per-devframe SPA, so the no-op is honest. + }, + resolveOrigin() { + const resolved = server.resolvedUrls?.local?.[0] + return resolved ? new URL(resolved).origin : 'http://localhost:5173' + }, + getStorageDir(scope) { + return scope === 'workspace' + ? join(cwd, 'node_modules/.minimal-hub-kit') + : join(homedir(), '.minimal-hub-kit') + }, + async openPath(filepath, line, column) { + const absolute = join(cwd, filepath) + const target = line + ? `${absolute}:${line}${column ? `:${column}` : ''}` + : absolute + launchEditor(target) + return true + }, + } + + const port = options.port ?? await getPort({ port: 9777, random: false }) + + const context = await createHubContext({ + cwd, + workspaceRoot: cwd, + mode: 'dev', + host, + builtinRpcDeclarations: [ + // The minimal kit ships its own `messages:list` and `terminals:list` + // RPCs so the UI has something to read. A full hub kit would + // likely standardise these (this is why hub-level RPC built-ins + // exist — see hub:open-path / hub:commands:execute) but for the + // demo we keep them kit-local. + minimalKitMessagesList, + minimalKitTerminalsList, + ], + }) + + // Seed a sample terminal + command directly on the kit so the UI + // shows something even without any plugged-in devframes. + context.commands.register({ + id: 'minimal-hub-kit:ping', + title: 'Kit · Ping', + icon: 'ph:bell-duotone', + category: 'kit', + handler: () => 'pong', + }) + await context.messages.add({ + level: 'success', + message: 'Minimal hub kit started', + description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 0} devframe(s) registered.`, + }) + + for (const def of options.devframes ?? []) { + await mountDevframe(context, def) + } + + started = await startHttpAndWs({ + context, + port, + auth: false, + }) + + // Tell the browser where to find the WS endpoint. `connectDevframe` + // resolves this URL relative to its `baseURL` option. + const metaPath = `${base}${DEVTOOLS_CONNECTION_META_FILENAME}` + server.middlewares.use(metaPath, (_req, res) => { + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ backend: 'websocket', websocket: port })) + }) + + server.httpServer?.once('close', () => { + void started?.close().catch(() => {}) + }) + }, + + async closeBundle() { + await started?.close().catch(() => {}) + started = undefined + }, + } +} + +function normalizeBase(base: string): string { + let out = base.startsWith('/') ? base : `/${base}` + if (!out.endsWith('/')) + out = `${out}/` + return out +} diff --git a/examples/minimal-vite-devtools-kit/tsconfig.json b/examples/minimal-vite-devtools-kit/tsconfig.json new file mode 100644 index 0000000..46dbffd --- /dev/null +++ b/examples/minimal-vite-devtools-kit/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "esModuleInterop": true, + "isolatedDeclarations": false + }, + "include": ["src", "vite.config.ts"] +} diff --git a/examples/minimal-vite-devtools-kit/vite.config.ts b/examples/minimal-vite-devtools-kit/vite.config.ts new file mode 100644 index 0000000..c08e65a --- /dev/null +++ b/examples/minimal-vite-devtools-kit/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import { alias } from '../../alias' +import demoDevframe from './src/devframe' +import { minimalHubKit } from './src/minimal-hub-kit' + +export default defineConfig({ + resolve: { alias }, + plugins: [ + minimalHubKit({ + devframes: [demoDevframe], + }), + ], +}) diff --git a/packages/hub/LICENSE.md b/packages/hub/LICENSE.md new file mode 100644 index 0000000..09e688c --- /dev/null +++ b/packages/hub/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026-PRESENT Anthony Fu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/hub/package.json b/packages/hub/package.json new file mode 100644 index 0000000..beaa427 --- /dev/null +++ b/packages/hub/package.json @@ -0,0 +1,54 @@ +{ + "name": "@devframes/hub", + "type": "module", + "version": "0.4.1", + "description": "Framework-neutral hub layer for devframe — docks, terminals, messages, commands.", + "author": "Anthony Fu ", + "license": "MIT", + "homepage": "https://github.com/devframes/devframe#readme", + "repository": { + "directory": "packages/hub", + "type": "git", + "url": "git+https://github.com/devframes/devframe.git" + }, + "bugs": "https://github.com/devframes/devframe/issues", + "keywords": [ + "devtools", + "devframe", + "hub", + "docks", + "terminals" + ], + "sideEffects": false, + "exports": { + ".": "./dist/index.mjs", + "./client": "./dist/client/index.mjs", + "./constants": "./dist/constants.mjs", + "./node": "./dist/node/index.mjs", + "./types": "./dist/types/index.mjs", + "./package.json": "./package.json" + }, + "types": "./dist/index.d.mts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "watch": "tsdown --watch" + }, + "peerDependencies": { + "devframe": "workspace:*" + }, + "dependencies": { + "birpc": "catalog:deps", + "nostics": "catalog:deps", + "pathe": "catalog:deps", + "perfect-debounce": "catalog:deps", + "tinyexec": "catalog:deps" + }, + "devDependencies": { + "devframe": "workspace:*", + "mlly": "catalog:build", + "tsdown": "catalog:build" + } +} diff --git a/packages/hub/src/client/client-script.ts b/packages/hub/src/client/client-script.ts new file mode 100644 index 0000000..6f90555 --- /dev/null +++ b/packages/hub/src/client/client-script.ts @@ -0,0 +1,16 @@ +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 +} diff --git a/packages/hub/src/client/context.ts b/packages/hub/src/client/context.ts new file mode 100644 index 0000000..88701e0 --- /dev/null +++ b/packages/hub/src/client/context.ts @@ -0,0 +1,12 @@ +import type { DevToolsClientContext } from './docks' + +const CLIENT_CONTEXT_KEY = '__DEVFRAME_HUB_CLIENT_CONTEXT__' + +/** + * Get the global DevTools client context, or `undefined` if not yet initialized. + */ +export function getDevToolsClientContext(): DevToolsClientContext | undefined { + return (globalThis as any)[CLIENT_CONTEXT_KEY] +} + +export { CLIENT_CONTEXT_KEY } diff --git a/packages/hub/src/client/docks.ts b/packages/hub/src/client/docks.ts new file mode 100644 index 0000000..9ae4391 --- /dev/null +++ b/packages/hub/src/client/docks.ts @@ -0,0 +1,139 @@ +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 +} diff --git a/packages/hub/src/client/index.ts b/packages/hub/src/client/index.ts new file mode 100644 index 0000000..475395f --- /dev/null +++ b/packages/hub/src/client/index.ts @@ -0,0 +1,5 @@ +export * from './client-script' +export * from './context' +export * from './docks' +export * from './remote' +export * from 'devframe/client' diff --git a/packages/hub/src/client/remote.ts b/packages/hub/src/client/remote.ts new file mode 100644 index 0000000..fdf1811 --- /dev/null +++ b/packages/hub/src/client/remote.ts @@ -0,0 +1,122 @@ +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('[@devframes/hub] Failed to decode remote connection descriptor.', { cause }) + } + + if (!payload || typeof payload !== 'object') + throw new Error('[@devframes/hub] Remote connection descriptor must be an object.') + + const info = payload as Partial + if (info.v !== 1) + throw new Error(`[@devframes/hub] Unsupported remote connection descriptor version: ${String(info.v)}`) + if (info.backend !== 'websocket' || typeof info.websocket !== 'string' || !info.websocket) + throw new Error('[@devframes/hub] Remote connection descriptor must carry a websocket URL.') + if (typeof info.authToken !== 'string' || !info.authToken) + throw new Error('[@devframes/hub] Remote connection descriptor must carry an auth token.') + if (typeof info.origin !== 'string') + throw new Error('[@devframes/hub] 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 hub 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( + `[@devframes/hub] No remote connection descriptor found in the URL. ` + + `Open this page through a hub-registered dock with \`remote: true\`.`, + ) + } + return getDevToolsRpcClient({ + ...options, + connectionMeta: info, + authToken: info.authToken, + }) +} diff --git a/packages/hub/src/constants.ts b/packages/hub/src/constants.ts new file mode 100644 index 0000000..71f9acc --- /dev/null +++ b/packages/hub/src/constants.ts @@ -0,0 +1,24 @@ +import type { DevToolsDockEntryCategory } from './types/docks' +import type { DevToolsDocksUserSettings } from './types/settings' + +export * from 'devframe/constants' + +export const DEFAULT_CATEGORIES_ORDER: Record = { + '~viteplus': -1000, + 'default': 0, + 'app': 100, + 'framework': 200, + 'web': 300, + 'advanced': 400, + '~builtin': 1000, +} satisfies Record + +export const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings = () => ({ + docksHidden: [], + docksCategoriesHidden: [], + docksPinned: [], + docksCustomOrder: {}, + showIframeAddressBar: false, + closeOnOutsideClick: false, + commandShortcuts: {}, +}) diff --git a/packages/hub/src/define.ts b/packages/hub/src/define.ts new file mode 100644 index 0000000..479fce3 --- /dev/null +++ b/packages/hub/src/define.ts @@ -0,0 +1,27 @@ +import type { WhenContext, WhenExpression } from 'devframe/utils/when' +import type { HubNodeContext } from './node/context' +import type { DevToolsServerCommandInput } from './types/commands' +import type { DevToolsDockUserEntry } from './types/docks' +import type { JsonRenderSpec } from './types/json-render' +import { createDefineWrapperWithContext } from 'devframe/rpc' + +export const defineRpcFunction = createDefineWrapperWithContext() + +export function defineCommand( + command: Omit & { when?: WhenExpression }, +): DevToolsServerCommandInput { + return command as DevToolsServerCommandInput +} + +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 +} diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts new file mode 100644 index 0000000..fca59c2 --- /dev/null +++ b/packages/hub/src/index.ts @@ -0,0 +1,2 @@ +export * from './define' +export type * from './types' diff --git a/packages/hub/src/node/context.ts b/packages/hub/src/node/context.ts new file mode 100644 index 0000000..eddcf62 --- /dev/null +++ b/packages/hub/src/node/context.ts @@ -0,0 +1,152 @@ +import type { CreateHostContextOptions } from 'devframe/node' +import type { DevToolsHost, DevToolsNodeContext } from 'devframe/types' +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 { registerHubBuiltins } from './hub-builtins' +import { builtinHubRpcDeclarations } from './rpc-builtins' + +/** + * Optional capabilities a host can implement to unlock hub built-ins. + * These are not required to construct a {@link HubNodeContext} — the + * built-in RPC commands gate themselves on whether the capability is + * present. + * + * Framework kits (`@vitejs/devtools-kit`, future `@next/devtools-kit`, + * etc.) implement these as part of their host so authors get a uniform + * surface — e.g. `hub:open-path` works the same way regardless of which + * framework hosts the hub. + */ +export interface HubHostCapabilities { + /** + * Open a file in the user's editor. Returns `false` when the host + * has no editor binding for the current environment; throws when the + * launch attempt fails. + * + * Backs the built-in `hub:open-path` RPC command and command-palette + * entry. + */ + openPath?: (filepath: string, line?: number, column?: number) => boolean | Promise +} + +/** + * Hub-augmented node context — extends devframe's framework-neutral + * `DevToolsNodeContext` with the hub-level subsystems (`docks`, + * `terminals`, `messages`, `commands`) and the `createJsonRenderer` + * factory. + * + * Framework kits further extend this with their own slots (e.g. + * `viteConfig`, `viteServer`). + */ +export interface HubNodeContext extends DevToolsNodeContext { + readonly host: DevToolsHost & HubHostCapabilities + docks: DevToolsDockHost + terminals: DevToolsTerminalHost + messages: DevToolsMessagesHost + commands: DevToolsCommandsHost + /** + * Create a JsonRenderer handle for building json-render powered UIs. + */ + createJsonRenderer: (spec: JsonRenderSpec) => JsonRenderer +} + +export interface CreateHubContextOptions extends CreateHostContextOptions {} + +/** + * Create a hub-level node context: wraps devframe's `createHostContext`, + * attaches the hub hosts (`docks`, `terminals`, `messages`, `commands`), + * registers the hub's built-in RPC commands, and wires the shared-state + * synchronization that powers a hub-aware client UI. + */ +export async function createHubContext(options: CreateHubContextOptions): Promise { + const baseContext = await createHostContext({ + ...options, + builtinRpcDeclarations: [ + ...builtinHubRpcDeclarations, + ...(options.builtinRpcDeclarations ?? []), + ], + }) + const context = baseContext as HubNodeContext + + 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 + + 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) + + registerHubBuiltins(context) + + return context +} diff --git a/packages/hub/src/node/diagnostics.ts b/packages/hub/src/node/diagnostics.ts new file mode 100644 index 0000000..c85e7a6 --- /dev/null +++ b/packages/hub/src/node/diagnostics.ts @@ -0,0 +1,48 @@ +import { defineDiagnostics } from 'nostics' +import { hubReporter } from '../utils/diagnostics-reporter' + +// Hub-side diagnostics for docks, terminals, messages, commands, and the +// built-in RPC commands. Shares the `DF` prefix with devframe core; the +// hub reserves the `DF8xxx` range so the unified surface stays +// collision-free. Sub-ranges: +// DF8000-DF8099 — hub context / lifecycle +// DF8100-DF8199 — docks +// DF8200-DF8299 — terminals +// DF8300-DF8399 — messages +// DF8400-DF8499 — commands +// DF8500-DF8599 — built-in RPC commands +export const diagnostics = defineDiagnostics({ + docsBase: 'https://devfra.me/errors', + reporters: [hubReporter], + codes: { + DF8100: { + why: (p: { id: string }) => `Dock with id "${p.id}" is already registered`, + fix: 'Use the `force` parameter to overwrite an existing registration.', + }, + DF8101: { + why: 'Cannot change the id of a dock. Use register() to add new docks.', + }, + DF8102: { + why: (p: { id: string }) => `Dock with id "${p.id}" is not registered. Use register() to add new docks.`, + }, + DF8200: { + why: (p: { id: string }) => `Terminal session with id "${p.id}" already registered`, + }, + DF8201: { + why: (p: { id: string }) => `Terminal session with id "${p.id}" not registered`, + }, + DF8400: { + why: (p: { id: string }) => `Command "${p.id}" is already registered`, + }, + DF8401: { + why: 'Cannot change the id of a command. Use register() to add new commands.', + }, + DF8402: { + why: (p: { id: string }) => `Command "${p.id}" is not registered`, + }, + DF8500: { + why: (p: { id: string }) => `Built-in command "${p.id}" requires a host capability that this host does not implement.`, + fix: 'Implement the matching capability on the `DevToolsHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`.', + }, + }, +}) diff --git a/packages/hub/src/node/host-commands.ts b/packages/hub/src/node/host-commands.ts new file mode 100644 index 0000000..86b7c69 --- /dev/null +++ b/packages/hub/src/node/host-commands.ts @@ -0,0 +1,95 @@ +import type { + DevToolsCommandHandle, + DevToolsCommandsHost as DevToolsCommandsHostType, + DevToolsServerCommandEntry, + DevToolsServerCommandInput, +} from '../types/commands' +import type { HubNodeContext } 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: HubNodeContext, + ) {} + + register(command: DevToolsServerCommandInput): DevToolsCommandHandle { + if (this.commands.has(command.id)) { + throw diagnostics.DF8400({ 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.DF8401() + } + const existing = this.commands.get(command.id) + if (!existing) { + throw diagnostics.DF8402({ 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.DF8402({ 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/hub/src/node/host-docks.ts b/packages/hub/src/node/host-docks.ts new file mode 100644 index 0000000..f18a058 --- /dev/null +++ b/packages/hub/src/node/host-docks.ts @@ -0,0 +1,209 @@ +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 { HubNodeContext } 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: HubNodeContext, + ) { + + } + + 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.DF8100({ 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.DF8101() + } + this.update(Object.assign(this.views.get(view.id)!, patch)) + }, + } + } + + update(view: DevToolsDockUserEntry): void { + if (!this.views.has(view.id)) { + throw diagnostics.DF8102({ 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/hub/src/node/host-messages.ts b/packages/hub/src/node/host-messages.ts new file mode 100644 index 0000000..2b9a1d3 --- /dev/null +++ b/packages/hub/src/node/host-messages.ts @@ -0,0 +1,134 @@ +import type { + DevToolsMessageEntry, + DevToolsMessageEntryInput, + DevToolsMessageHandle, + DevToolsMessagesHost as DevToolsMessagesHostType, +} from '../types/messages' +import type { HubNodeContext } 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: HubNodeContext, + ) {} + + 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/hub/src/node/host-terminals.ts b/packages/hub/src/node/host-terminals.ts new file mode 100644 index 0000000..bb62af4 --- /dev/null +++ b/packages/hub/src/node/host-terminals.ts @@ -0,0 +1,200 @@ +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 { HubNodeContext } 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 + * hub-aware clients 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: HubNodeContext, + ) { + } + + /** + * 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.DF8200({ 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.DF8201({ 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.DF8200({ 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/hub/src/node/hub-builtins.ts b/packages/hub/src/node/hub-builtins.ts new file mode 100644 index 0000000..110b0b7 --- /dev/null +++ b/packages/hub/src/node/hub-builtins.ts @@ -0,0 +1,36 @@ +import type { HubNodeContext } from './context' +import { diagnostics } from './diagnostics' + +/** + * Register the hub's framework-neutral built-in RPC commands. Each + * built-in delegates to an optional host capability; when the host does + * not implement the capability, the command throws `DF8500`. + * + * Today: `hub:open-path` (delegates to `host.openPath`). New built-ins + * land in this file with their own diagnostic code in the `DF85xx` + * sub-range. + */ +export function registerHubBuiltins(context: HubNodeContext): void { + context.commands.register({ + id: 'hub:open-path', + title: 'Open Path in Editor', + icon: 'ph:pencil-duotone', + // Programmatic command — invoked via RPC by tool code, not by the + // user from the command palette directly. Hide from palette search. + showInPalette: false, + handler: async (filepath: unknown, line?: unknown, column?: unknown) => { + const openPath = context.host.openPath + if (!openPath) { + throw diagnostics.DF8500({ id: 'hub:open-path' }) + } + if (typeof filepath !== 'string' || !filepath) { + throw new TypeError('hub:open-path: `filepath` must be a non-empty string') + } + return openPath( + filepath, + typeof line === 'number' ? line : undefined, + typeof column === 'number' ? column : undefined, + ) + }, + }) +} diff --git a/packages/hub/src/node/index.ts b/packages/hub/src/node/index.ts new file mode 100644 index 0000000..d4fb8b3 --- /dev/null +++ b/packages/hub/src/node/index.ts @@ -0,0 +1,9 @@ +export * from './context' +export * from './host-commands' +export * from './host-docks' +export * from './host-messages' +export * from './host-terminals' +export * from './hub-builtins' +export * from './mount-devframe' +export * from './rpc-builtins' +export * from './utils' diff --git a/packages/hub/src/node/mount-devframe.ts b/packages/hub/src/node/mount-devframe.ts new file mode 100644 index 0000000..cbcc3b0 --- /dev/null +++ b/packages/hub/src/node/mount-devframe.ts @@ -0,0 +1,52 @@ +import type { DevframeDefinition } from 'devframe/types' +import type { DevToolsViewIframe } from '../types/docks' +import type { HubNodeContext } from './context' +import { resolveBasePath } from 'devframe/node/internal' +import { resolve } from 'pathe' + +export interface MountDevframeOptions { + /** + * Mount path override. Defaults to `d.basePath` or `/__${d.id}/`. + */ + base?: string + /** + * Overrides for the auto-synthesized iframe dock entry. Use this to + * customize the entry's `category`, override the icon, hide it via + * `when`, etc. Cannot change `id`, `type`, or `url` — those are + * derived from the devframe definition. + */ + dock?: Partial> +} + +/** + * Framework-neutral primitive — mounts a {@link DevframeDefinition} as a + * dock inside a hub-aware context: serves the devframe's SPA at the + * resolved base path, synthesizes an iframe dock entry from the + * definition's metadata, and runs the definition's `setup(ctx)`. + * + * Framework kits wrap this with their own plugin/middleware machinery — + * e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a + * Vite `Plugin` whose `devtools.setup` ultimately delegates here. + */ +export async function mountDevframe( + ctx: HubNodeContext, + d: DevframeDefinition, + options: MountDevframeOptions = {}, +): Promise { + const base = options.base ?? resolveBasePath(d, 'hosted') + + 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) +} diff --git a/packages/hub/src/node/rpc-builtins.ts b/packages/hub/src/node/rpc-builtins.ts new file mode 100644 index 0000000..f93ef47 --- /dev/null +++ b/packages/hub/src/node/rpc-builtins.ts @@ -0,0 +1,30 @@ +import type { RpcFunctionDefinitionAny } from 'devframe/rpc' +import { defineRpcFunction } from '../define' + +/** + * `hub:commands:execute` — Invoke a registered server command by id. The + * arguments after `id` are forwarded to the command's `handler(...)`. + * Returns whatever the handler returns. + * + * Pairs with the `devframe:commands` shared state: clients read the list + * from the shared state and dispatch by id via this RPC. + */ +export const hubCommandsExecute = defineRpcFunction({ + name: 'hub:commands:execute', + type: 'action', + setup: context => ({ + async handler(id: string, ...args: any[]) { + return context.commands.execute(id, ...args) + }, + }), +}) + +/** + * Framework-neutral RPC declarations auto-registered by + * {@link createHubContext}. Provide additional RPCs by passing your own + * array via `CreateHubContextOptions.builtinRpcDeclarations`; the hub's + * list is prepended automatically. + */ +export const builtinHubRpcDeclarations: readonly RpcFunctionDefinitionAny[] = [ + hubCommandsExecute, +] diff --git a/packages/hub/src/node/utils.ts b/packages/hub/src/node/utils.ts new file mode 100644 index 0000000..2c3d0b2 --- /dev/null +++ b/packages/hub/src/node/utils.ts @@ -0,0 +1,17 @@ +import type { ClientScriptEntry } from '../types/docks' +import { toDataURL } from 'mlly' + +/** + * Create a quick `ClientScriptEntry` from an inline function or + * stringified code. Useful for prototyping `action` / `renderer` + * dock entries without setting up a separate importable module. + * + * @experimental Prefer a proper importable module for production use. + */ +export function createSimpleClientScript(fn: string | ((ctx: any) => void)): ClientScriptEntry { + const code = `const fn = ${fn.toString()}; export default fn` + return { + importFrom: toDataURL(code), + importName: 'default', + } +} diff --git a/packages/hub/src/types/commands.ts b/packages/hub/src/types/commands.ts new file mode 100644 index 0000000..5969f08 --- /dev/null +++ b/packages/hub/src/types/commands.ts @@ -0,0 +1,130 @@ +import type { EventEmitter } from 'devframe/types' + +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 +} + +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. + */ + 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 + +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[] +} + +export interface DevToolsCommandShortcutOverrides { + /** + * Command ID → keybinding overrides. Empty array = shortcut disabled. + */ + [commandId: string]: DevToolsCommandKeybinding[] +} diff --git a/packages/hub/src/types/docks.ts b/packages/hub/src/types/docks.ts new file mode 100644 index 0000000..340ee86 --- /dev/null +++ b/packages/hub/src/types/docks.ts @@ -0,0 +1,167 @@ +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 hub injects a connection descriptor + * (WS URL + pre-approved auth token) into the iframe URL so a hosted + * page can connect back via `connectRemoteDevTools()` from + * `@devframes/hub/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. + */ + remote?: boolean | RemoteDockOptions +} + +export interface RemoteDockOptions { + /** + * How to pass the connection descriptor to the hosted page. + * + * - `'fragment'` (default): appended as a URL fragment. + * Not sent in HTTP requests or Referer headers — safest for auth tokens. + * - `'query'`: appended as a URL query parameter. 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[]][] diff --git a/packages/hub/src/types/index.ts b/packages/hub/src/types/index.ts new file mode 100644 index 0000000..2a60f1f --- /dev/null +++ b/packages/hub/src/types/index.ts @@ -0,0 +1,44 @@ +// Re-export the hub-augmented context type so consumers can import it +// from the hub's main `types` barrel. +export type { CreateHubContextOptions, HubNodeContext } from '../node/context' + +export * from './commands' +export * from './docks' +export * from './json-render' +export * from './messages' +export * from './settings' +export * from './terminals' + +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, + EntriesToObject, + EventEmitter, + EventsMap, + EventUnsubscribe, + PartialWithoutId, + RpcBroadcastOptions, + RpcFunctionsHost, + RpcSharedStateGetOptions, + RpcSharedStateHost, + RpcStreamingChannel, + RpcStreamingChannelOptions, + RpcStreamingHost, + Thenable, +} from 'devframe/types' diff --git a/packages/hub/src/types/json-render.ts b/packages/hub/src/types/json-render.ts new file mode 100644 index 0000000..336af7c --- /dev/null +++ b/packages/hub/src/types/json-render.ts @@ -0,0 +1,29 @@ +export interface JsonRenderElement { + type: string + props?: Record + children?: string[] + /** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */ + on?: Record + /** json-render visibility condition */ + visible?: unknown + /** json-render repeat binding */ + repeat?: unknown + /** Allow additional json-render element fields */ + [key: string]: unknown +} + +export interface JsonRenderSpec { + root: string + elements: Record + /** Initial client-side state model for $state/$bindState expressions */ + state?: Record +} + +export interface JsonRenderer { + /** Replace the entire spec */ + updateSpec: (spec: JsonRenderSpec) => void | Promise + /** Update json-render state values (shallow merge into spec.state) */ + updateState: (state: Record) => void | Promise + /** Internal: shared state key used by the client to subscribe */ + readonly _stateKey: string +} diff --git a/packages/hub/src/types/messages.ts b/packages/hub/src/types/messages.ts new file mode 100644 index 0000000..fa273b3 --- /dev/null +++ b/packages/hub/src/types/messages.ts @@ -0,0 +1,146 @@ +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 +} diff --git a/packages/hub/src/types/settings.ts b/packages/hub/src/types/settings.ts new file mode 100644 index 0000000..e79f136 --- /dev/null +++ b/packages/hub/src/types/settings.ts @@ -0,0 +1,11 @@ +import type { DevToolsCommandShortcutOverrides } from './commands' + +export interface DevToolsDocksUserSettings { + docksHidden: string[] + docksCategoriesHidden: string[] + docksPinned: string[] + docksCustomOrder: Record + showIframeAddressBar: boolean + closeOnOutsideClick: boolean + commandShortcuts: DevToolsCommandShortcutOverrides +} diff --git a/packages/hub/src/types/terminals.ts b/packages/hub/src/types/terminals.ts new file mode 100644 index 0000000..396c036 --- /dev/null +++ b/packages/hub/src/types/terminals.ts @@ -0,0 +1,48 @@ +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 +} diff --git a/packages/hub/src/utils/diagnostics-reporter.ts b/packages/hub/src/utils/diagnostics-reporter.ts new file mode 100644 index 0000000..335b773 --- /dev/null +++ b/packages/hub/src/utils/diagnostics-reporter.ts @@ -0,0 +1,12 @@ +import type { Diagnostic } from 'nostics' +import { colors as c } from 'devframe/utils/colors' +import { ansiFormatter } from 'nostics/formatters/ansi' + +const formatAnsi = ansiFormatter(c) + +export interface HubReporterOptions { method?: 'log' | 'warn' | 'error' } + +export function hubReporter(d: Diagnostic, { method = 'warn' }: HubReporterOptions = {}): void { + // eslint-disable-next-line no-console + console[method](formatAnsi(d)) +} diff --git a/packages/hub/tsconfig.json b/packages/hub/tsconfig.json new file mode 100644 index 0000000..8a6d5a7 --- /dev/null +++ b/packages/hub/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "lib": ["esnext", "dom"] + } +} diff --git a/packages/hub/tsdown.config.ts b/packages/hub/tsdown.config.ts new file mode 100644 index 0000000..d8c17bb --- /dev/null +++ b/packages/hub/tsdown.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsdown' + +export default defineConfig({ + entry: { + 'index': 'src/index.ts', + 'constants': 'src/constants.ts', + 'client/index': 'src/client/index.ts', + 'node/index': 'src/node/index.ts', + 'types/index': 'src/types/index.ts', + }, + outExtensions: () => ({ js: '.mjs', dts: '.d.mts' }), + clean: true, + tsconfig: '../../tsconfig.base.json', + dts: true, + platform: 'neutral', + deps: { + neverBundle: [ + 'vite', + 'esbuild', + 'postcss', + 'rolldown', + ], + onlyBundle: [ + 'acorn', + 'mlly', + ], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db93fda..af0db2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,6 +85,9 @@ catalogs: structured-clone-es: specifier: ^2.0.0 version: 2.0.0 + tinyexec: + specifier: ^1.1.2 + version: 1.1.2 tinyglobby: specifier: ^0.2.16 version: 0.2.16 @@ -270,6 +273,25 @@ importers: specifier: catalog:deps version: 8.20.0 + examples/minimal-vite-devtools-kit: + dependencies: + '@devframes/hub': + specifier: workspace:* + version: link:../../packages/hub + devframe: + specifier: workspace:* + version: link:../../packages/devframe + devDependencies: + get-port-please: + specifier: catalog:deps + version: 3.2.0 + pathe: + specifier: catalog:deps + version: 2.0.3 + vite: + specifier: catalog:build + version: 8.0.12(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4) + examples/streaming-chat: dependencies: devframe: @@ -374,6 +396,34 @@ importers: specifier: catalog:deps version: 0.1.2 + packages/hub: + dependencies: + birpc: + specifier: catalog:deps + version: 4.0.0 + nostics: + specifier: catalog:deps + version: 0.2.0 + pathe: + specifier: catalog:deps + version: 2.0.3 + perfect-debounce: + specifier: catalog:deps + version: 2.1.0 + tinyexec: + specifier: catalog:deps + version: 1.1.2 + devDependencies: + devframe: + specifier: workspace:* + version: link:../devframe + mlly: + specifier: catalog:build + version: 1.8.2 + tsdown: + specifier: catalog:build + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3) + packages/nuxt: devDependencies: '@nuxt/kit': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 85c8623..057fbb3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -49,6 +49,7 @@ catalogs: pathe: ^2.0.3 perfect-debounce: ^2.1.0 structured-clone-es: ^2.0.0 + tinyexec: ^1.1.2 tinyglobby: ^0.2.16 valibot: ^1.4.0 whenexpr: ^0.1.2 diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts new file mode 100644 index 0000000..c7b7e0d --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts @@ -0,0 +1,96 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/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 { DevToolsClientRpcHost } +export { RpcClientEvents } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js new file mode 100644 index 0000000..b782ee8 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js @@ -0,0 +1,16 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/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"; +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts new file mode 100644 index 0000000..57f7e43 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts @@ -0,0 +1,11 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/constants` + */ +// #region Variables +export declare const DEFAULT_CATEGORIES_ORDER: Record; +export declare const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings; +// #endregion + +// #region Re-exports +export * from "devframe/constants"; +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.js new file mode 100644 index 0000000..6189f36 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.js @@ -0,0 +1,11 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/constants` + */ +// #region Variables +export var DEFAULT_CATEGORIES_ORDER /* const */ +export var DEFAULT_STATE_USER_SETTINGS /* const */ +// #endregion + +// #region Re-exports +export * from "devframe/constants"; +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts new file mode 100644 index 0000000..6bcb13f --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts @@ -0,0 +1,93 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub` + */ +// #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 + +// #region Other +export { ClientScriptEntry } +export { ConnectionMeta } +export { CreateHubContextOptions } +export { DevToolsCapabilities } +export { DevToolsChildProcessExecuteOptions } +export { DevToolsChildProcessTerminalSession } +export { DevToolsClientCommand } +export { DevToolsCommandBase } +export { DevToolsCommandEntry } +export { DevToolsCommandHandle } +export { DevToolsCommandKeybinding } +export { DevToolsCommandShortcutOverrides } +export { DevToolsCommandsHost } +export { DevToolsCommandsHostEvents } +export { DevToolsDiagnosticsDefinition } +export { DevToolsDiagnosticsHost } +export { DevToolsDiagnosticsLogger } +export { DevToolsDockEntriesGrouped } +export { DevToolsDockEntry } +export { DevToolsDockEntryBase } +export { DevToolsDockEntryCategory } +export { DevToolsDockEntryIcon } +export { DevToolsDockHost } +export { DevToolsDocksUserSettings } +export { DevToolsDockUserEntry } +export { DevToolsHost } +export { DevToolsMessageElementPosition } +export { DevToolsMessageEntry } +export { DevToolsMessageEntryFrom } +export { DevToolsMessageEntryInput } +export { DevToolsMessageFilePosition } +export { DevToolsMessageHandle } +export { DevToolsMessageLevel } +export { DevToolsMessagesClient } +export { DevToolsMessagesHost } +export { DevToolsNodeRpcSession } +export { DevToolsRpcClientFunctions } +export { DevToolsRpcServerFunctions } +export { DevToolsRpcSharedStates } +export { DevToolsServerCommandEntry } +export { DevToolsServerCommandInput } +export { DevToolsTerminalHost } +export { DevToolsTerminalSession } +export { DevToolsTerminalSessionBase } +export { DevToolsTerminalStatus } +export { DevToolsViewAction } +export { DevToolsViewBuiltin } +export { DevToolsViewCustomRender } +export { DevToolsViewHost } +export { DevToolsViewIframe } +export { DevToolsViewJsonRender } +export { DevToolsViewLauncher } +export { DevToolsViewLauncherStatus } +export { EntriesToObject } +export { EventEmitter } +export { EventsMap } +export { EventUnsubscribe } +export { HubNodeContext } +export { JsonRenderElement } +export { JsonRenderer } +export { JsonRenderSpec } +export { PartialWithoutId } +export { RemoteConnectionInfo } +export { RemoteDockOptions } +export { RpcBroadcastOptions } +export { RpcDefinitionsFilter } +export { RpcDefinitionsToFunctions } +export { RpcFunctionsHost } +export { RpcSharedStateGetOptions } +export { RpcSharedStateHost } +export { RpcStreamingChannel } +export { RpcStreamingChannelOptions } +export { RpcStreamingHost } +export { Thenable } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js new file mode 100644 index 0000000..1c23e46 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js @@ -0,0 +1,9 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub` + */ +// #region Other +export { defineCommand } +export { defineDockEntry } +export { defineJsonRenderSpec } +export { defineRpcFunction } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts new file mode 100644 index 0000000..6344e08 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts @@ -0,0 +1,110 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/node` + */ +// #region Interfaces +export interface MountDevframeOptions { + base?: string; + dock?: Partial>; +} +// #endregion + +// #region Classes +export declare class DevToolsCommandsHost implements DevToolsCommandsHost$1 { + readonly context: HubNodeContext; + readonly commands: DevToolsCommandsHost$1['commands']; + readonly events: DevToolsCommandsHost$1['events']; + constructor(_: HubNodeContext); + 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: HubNodeContext; + readonly views: DevToolsDockHost$1['views']; + readonly events: DevToolsDockHost$1['events']; + userSettings: SharedState; + private readonly remoteDocks; + constructor(_: HubNodeContext); + 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: HubNodeContext; + 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(_: HubNodeContext); + add(_: DevToolsMessageEntryInput): Promise; + update(_: string, _: Partial): Promise; + remove(_: string): Promise; + clear(): Promise; + private _createHandle; +} +export declare class DevToolsTerminalHost implements DevToolsTerminalHost$1 { + readonly context: HubNodeContext; + readonly sessions: DevToolsTerminalHost$1['sessions']; + readonly events: DevToolsTerminalHost$1['events']; + private _boundStreams; + private _channel?; + constructor(_: HubNodeContext); + private getStreamingChannel; + register(_: DevToolsTerminalSession): DevToolsTerminalSession; + update(_: PartialWithoutId): void; + remove(_: DevToolsTerminalSession): void; + private bindStream; + startChildProcess(_: DevToolsChildProcessExecuteOptions, _: Omit): Promise; +} +// #endregion + +// #region Functions +export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; +export declare function mountDevframe(_: HubNodeContext, _: DevframeDefinition, _?: MountDevframeOptions): Promise; +export declare function registerHubBuiltins(_: HubNodeContext): void; +// #endregion + +// #region Variables +export declare const builtinHubRpcDeclarations: readonly RpcFunctionDefinitionAny[]; +export declare const hubCommandsExecute: { + name: "hub:commands:execute"; + type?: "action" | undefined; + cacheable?: boolean; + args?: undefined; + returns?: undefined; + jsonSerializable?: boolean; + agent?: _$devframe.RpcFunctionAgentOptions; + setup?: ((context: HubNodeContext) => _$devframe_rpc0.Thenable<_$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise>>) | undefined; + handler?: ((id: string, ...args: any[]) => Promise) | undefined; + dump?: _$devframe_rpc0.RpcDump<[id: string, ...args: any[]], Promise, HubNodeContext> | undefined; + snapshot?: boolean; + __resolved?: _$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise> | undefined; + __promise?: _$devframe_rpc0.Thenable<_$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise>> | undefined; +}; +// #endregion + +// #region Other +export { createHubContext } +export { CreateHubContextOptions } +export { HubHostCapabilities } +export { HubNodeContext } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js new file mode 100644 index 0000000..f4b09e1 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js @@ -0,0 +1,74 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/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 createHubContext(_) {} +export function createSimpleClientScript(_) {} +export async function mountDevframe(_, _, _) {} +export function registerHubBuiltins(_) {} +// #endregion + +// #region Variables +export var builtinHubRpcDeclarations /* const */ +export var hubCommandsExecute /* const */ +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts new file mode 100644 index 0000000..d7d8ba7 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts @@ -0,0 +1,79 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/types` + */ +// #region Other +export { ClientScriptEntry } +export { ConnectionMeta } +export { CreateHubContextOptions } +export { DevToolsCapabilities } +export { DevToolsChildProcessExecuteOptions } +export { DevToolsChildProcessTerminalSession } +export { DevToolsClientCommand } +export { DevToolsCommandBase } +export { DevToolsCommandEntry } +export { DevToolsCommandHandle } +export { DevToolsCommandKeybinding } +export { DevToolsCommandShortcutOverrides } +export { DevToolsCommandsHost } +export { DevToolsCommandsHostEvents } +export { DevToolsDiagnosticsDefinition } +export { DevToolsDiagnosticsHost } +export { DevToolsDiagnosticsLogger } +export { DevToolsDockEntriesGrouped } +export { DevToolsDockEntry } +export { DevToolsDockEntryBase } +export { DevToolsDockEntryCategory } +export { DevToolsDockEntryIcon } +export { DevToolsDockHost } +export { DevToolsDocksUserSettings } +export { DevToolsDockUserEntry } +export { DevToolsHost } +export { DevToolsMessageElementPosition } +export { DevToolsMessageEntry } +export { DevToolsMessageEntryFrom } +export { DevToolsMessageEntryInput } +export { DevToolsMessageFilePosition } +export { DevToolsMessageHandle } +export { DevToolsMessageLevel } +export { DevToolsMessagesClient } +export { DevToolsMessagesHost } +export { DevToolsNodeRpcSession } +export { DevToolsRpcClientFunctions } +export { DevToolsRpcServerFunctions } +export { DevToolsRpcSharedStates } +export { DevToolsServerCommandEntry } +export { DevToolsServerCommandInput } +export { DevToolsTerminalHost } +export { DevToolsTerminalSession } +export { DevToolsTerminalSessionBase } +export { DevToolsTerminalStatus } +export { DevToolsViewAction } +export { DevToolsViewBuiltin } +export { DevToolsViewCustomRender } +export { DevToolsViewHost } +export { DevToolsViewIframe } +export { DevToolsViewJsonRender } +export { DevToolsViewLauncher } +export { DevToolsViewLauncherStatus } +export { EntriesToObject } +export { EventEmitter } +export { EventsMap } +export { EventUnsubscribe } +export { HubNodeContext } +export { JsonRenderElement } +export { JsonRenderer } +export { JsonRenderSpec } +export { PartialWithoutId } +export { RemoteConnectionInfo } +export { RemoteDockOptions } +export { RpcBroadcastOptions } +export { RpcDefinitionsFilter } +export { RpcDefinitionsToFunctions } +export { RpcFunctionsHost } +export { RpcSharedStateGetOptions } +export { RpcSharedStateHost } +export { RpcStreamingChannel } +export { RpcStreamingChannelOptions } +export { RpcStreamingHost } +export { Thenable } +// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.js new file mode 100644 index 0000000..39ad9f9 --- /dev/null +++ b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.js @@ -0,0 +1,4 @@ +/** + * Generated by tsnapi — public API snapshot of `@devframes/hub/types` + */ +/* no exports */ \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index ae47da5..9ab84d6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -99,6 +99,21 @@ "devframe/adapters/mcp": [ "./packages/devframe/src/adapters/mcp/index.ts" ], + "@devframes/hub/client": [ + "./packages/hub/src/client/index.ts" + ], + "@devframes/hub/constants": [ + "./packages/hub/src/constants.ts" + ], + "@devframes/hub/node": [ + "./packages/hub/src/node/index.ts" + ], + "@devframes/hub/types": [ + "./packages/hub/src/types/index.ts" + ], + "@devframes/hub": [ + "./packages/hub/src/index.ts" + ], "@devframes/nuxt/runtime/plugin.client": [ "./packages/nuxt/src/runtime/plugin.client.ts" ], diff --git a/turbo.json b/turbo.json index 676bba6..f14267b 100644 --- a/turbo.json +++ b/turbo.json @@ -6,10 +6,20 @@ "outputLogs": "new-only", "outputs": ["dist/**"] }, + "@devframes/hub#build": { + "outputLogs": "new-only", + "dependsOn": ["devframe#build"], + "outputs": ["dist/**"] + }, "@devframes/nuxt#build": { "outputLogs": "new-only", "outputs": ["dist/**"] }, + "minimal-vite-devtools-kit-example#build": { + "outputLogs": "new-only", + "dependsOn": ["@devframes/hub#build", "devframe#build"], + "outputs": ["dist/**"] + }, "files-inspector-example#build": { "outputLogs": "new-only", "dependsOn": ["devframe#build"], From 34a29e2556b350645e84ca9c76b5e27be09fbf96 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 22 May 2026 15:06:18 +0900 Subject: [PATCH 2/7] feat: add minimal devtools hub examples --- AGENTS.md | 2 +- docs/errors/DF8403.md | 23 ++ docs/guide/hub.md | 2 +- examples/minimal-next-devtools-hub/.gitignore | 6 + examples/minimal-next-devtools-hub/README.md | 36 +++ .../minimal-next-devtools-hub/package.json | 27 +++ .../app/%5F_hub/%5F_connection.json/route.ts | 9 + .../src/client/app/globals.css | 100 ++++++++ .../src/client/app/layout.tsx | 16 ++ .../src/client/app/page.tsx | 220 ++++++++++++++++++ .../src/client/devtools/demo-devframe.ts | 26 +++ .../devtools/minimal-next-devtools-hub.ts | 145 ++++++++++++ .../src/client/next.config.mjs | 8 + .../src/client/tsconfig.json | 13 ++ .../tests/minimal-next-devtools-hub.test.ts | 58 +++++ .../minimal-next-devtools-hub/tsconfig.json | 25 ++ .../vitest.config.ts | 11 + .../README.md | 8 +- .../index.html | 4 +- .../package.json | 4 +- .../src/client/main.ts | 4 +- .../src/client/style.css | 0 .../src/devframe.ts | 0 .../src/minimal-vite-devtools-hub.ts} | 43 ++-- .../tsconfig.json | 0 .../vite.config.ts | 4 +- packages/devframe/src/node/server.ts | 16 +- packages/hub/src/client/remote.ts | 8 + .../hub/src/node/__tests__/context.test.ts | 58 +++++ .../src/node/__tests__/host-commands.test.ts | 63 +++++ .../hub/src/node/__tests__/host-docks.test.ts | 78 +++++++ .../src/node/__tests__/host-messages.test.ts | 23 ++ .../src/node/__tests__/host-terminals.test.ts | 134 +++++++++++ packages/hub/src/node/context.ts | 2 + packages/hub/src/node/diagnostics.ts | 4 + packages/hub/src/node/host-commands.ts | 57 ++++- packages/hub/src/node/host-docks.ts | 24 +- packages/hub/src/node/host-messages.ts | 15 +- packages/hub/src/node/host-terminals.ts | 103 ++++++-- pnpm-lock.yaml | 39 +++- .../tsnapi/devframe/node/internal.snapshot.js | 10 +- turbo.json | 7 +- vitest.config.ts | 6 + 43 files changed, 1363 insertions(+), 78 deletions(-) create mode 100644 docs/errors/DF8403.md create mode 100644 examples/minimal-next-devtools-hub/.gitignore create mode 100644 examples/minimal-next-devtools-hub/README.md create mode 100644 examples/minimal-next-devtools-hub/package.json create mode 100644 examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts create mode 100644 examples/minimal-next-devtools-hub/src/client/app/globals.css create mode 100644 examples/minimal-next-devtools-hub/src/client/app/layout.tsx create mode 100644 examples/minimal-next-devtools-hub/src/client/app/page.tsx create mode 100644 examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts create mode 100644 examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts create mode 100644 examples/minimal-next-devtools-hub/src/client/next.config.mjs create mode 100644 examples/minimal-next-devtools-hub/src/client/tsconfig.json create mode 100644 examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts create mode 100644 examples/minimal-next-devtools-hub/tsconfig.json create mode 100644 examples/minimal-next-devtools-hub/vitest.config.ts rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/README.md (73%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/index.html (93%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/package.json (64%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/src/client/main.ts (97%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/src/client/style.css (100%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/src/devframe.ts (100%) rename examples/{minimal-vite-devtools-kit/src/minimal-hub-kit.ts => minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts} (80%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/tsconfig.json (100%) rename examples/{minimal-vite-devtools-kit => minimal-vite-devtools-hub}/vite.config.ts (69%) create mode 100644 packages/hub/src/node/__tests__/context.test.ts create mode 100644 packages/hub/src/node/__tests__/host-commands.test.ts create mode 100644 packages/hub/src/node/__tests__/host-docks.test.ts create mode 100644 packages/hub/src/node/__tests__/host-messages.test.ts create mode 100644 packages/hub/src/node/__tests__/host-terminals.test.ts diff --git a/AGENTS.md b/AGENTS.md index 5f69e24..3b20b60 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ **`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. -**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devtools-kit/` for a working ~120-line kit demonstrating the protocol end to end. +**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devtools-hub/` for a working ~120-line Vite host demonstrating the protocol end to end. ## Stack & Structure diff --git a/docs/errors/DF8403.md b/docs/errors/DF8403.md new file mode 100644 index 0000000..2685a95 --- /dev/null +++ b/docs/errors/DF8403.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# DF8403: Duplicate Command Id + +## Message + +> Command id "`{id}`" is already used by another command or child command + +## Cause + +`ctx.commands.register(command)` or a command handle `update()` received a command tree where at least one id is already owned by another top-level command or child command. Command ids are globally unique per hub context, including child commands. + +## Fix + +- Namespace every command id under your tool, such as `my-tool:reload` or `my-tool:open-settings`. +- Give each child command its own id instead of reusing the parent id. +- Unregister the previous command before replacing it with a different command tree. + +## Source + +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` and command handle `update()` throw when a command id is duplicated. diff --git a/docs/guide/hub.md b/docs/guide/hub.md index aee1744..d045d03 100644 --- a/docs/guide/hub.md +++ b/docs/guide/hub.md @@ -89,7 +89,7 @@ Plus broadcast notifications (`devframe:terminals:updated`, `devframe:messages:u ## Example -See [`examples/minimal-vite-devtools-kit/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devtools-kit) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub kit follows the same shape: a thin layer that adapts the framework's dev server to the hub. +See [`examples/minimal-vite-devtools-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devtools-hub) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub host follows the same shape: a thin layer that adapts the framework's dev server to the hub. ## Diagnostics diff --git a/examples/minimal-next-devtools-hub/.gitignore b/examples/minimal-next-devtools-hub/.gitignore new file mode 100644 index 0000000..999e272 --- /dev/null +++ b/examples/minimal-next-devtools-hub/.gitignore @@ -0,0 +1,6 @@ +.next +dist +next-env.d.ts +node_modules +out +.turbo diff --git a/examples/minimal-next-devtools-hub/README.md b/examples/minimal-next-devtools-hub/README.md new file mode 100644 index 0000000..d0fd459 --- /dev/null +++ b/examples/minimal-next-devtools-hub/README.md @@ -0,0 +1,36 @@ +# Minimal Next DevTools Hub + +A protocol-witness example. The `src/client/devtools/minimal-next-devtools-hub.ts` file wires `@devframes/hub` into a Next.js App Router app by lazily starting a side-car RPC/WS server from a Node route handler. + +## Run it + +```sh +pnpm install +pnpm --filter minimal-next-devtools-hub dev +``` + +Open the printed URL. You should see: + +- A status line showing the RPC backend +- A **Docks** list with hub built-ins and the mounted demo devframe +- A **Commands** list populated from server-side registrations +- A **Messages** list populated via `messages.add()` on the server +- A **Terminals** list, empty unless a devframe registers one +- Buttons that exercise `hub:open-path` and `hub:commands:execute` + +## What the example proves + +- `createHubContext()` boots a hub without any Vite-specific code path +- A `DevToolsHost & HubHostCapabilities` impl plugs Next host specifics into the hub uniformly +- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock +- Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed +- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json` + +## Files + +| File | Role | +|---|---| +| `src/client/devtools/minimal-next-devtools-hub.ts` | The Next host — creates hub context and side-car WS | +| `src/client/app/%5F_hub/%5F_connection.json/route.ts` | Connection-meta endpoint for `/__hub/__connection.json` that starts the singleton host | +| `src/client/devtools/demo-devframe.ts` | A sample `DevframeDefinition` that plugs into the host | +| `src/client/app/page.tsx` | The browser-side UI that consumes the hub protocol | diff --git a/examples/minimal-next-devtools-hub/package.json b/examples/minimal-next-devtools-hub/package.json new file mode 100644 index 0000000..d5f7997 --- /dev/null +++ b/examples/minimal-next-devtools-hub/package.json @@ -0,0 +1,27 @@ +{ + "name": "minimal-next-devtools-hub", + "type": "module", + "version": "0.4.1", + "private": true, + "description": "Protocol-witness example — a tiny Next.js DevTools Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", + "scripts": { + "dev": "next dev src/client", + "build": "next build src/client", + "test": "vitest run --config vitest.config.ts" + }, + "dependencies": { + "@devframes/hub": "workspace:*", + "devframe": "workspace:*", + "next": "catalog:frontend", + "react": "catalog:frontend", + "react-dom": "catalog:frontend" + }, + "devDependencies": { + "@types/react": "catalog:types", + "@types/react-dom": "catalog:types", + "get-port-please": "catalog:deps", + "pathe": "catalog:deps", + "vitest": "catalog:testing", + "ws": "catalog:deps" + } +} diff --git a/examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts b/examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts new file mode 100644 index 0000000..610ffd5 --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts @@ -0,0 +1,9 @@ +import { ensureMinimalNextDevToolsHub } from '../../../devtools/minimal-next-devtools-hub' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +export async function GET() { + const hub = await ensureMinimalNextDevToolsHub() + return Response.json(hub.connectionMeta) +} diff --git a/examples/minimal-next-devtools-hub/src/client/app/globals.css b/examples/minimal-next-devtools-hub/src/client/app/globals.css new file mode 100644 index 0000000..7bc5dff --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/app/globals.css @@ -0,0 +1,100 @@ +:root { + color-scheme: light dark; + font-family: system-ui, sans-serif; + line-height: 1.5; +} + +body { + margin: 0; + padding: 1.5rem 2rem; +} + +main { + max-width: 800px; + margin-inline: auto; +} + +header h1 { + margin-bottom: 0.25rem; +} + +header p { + margin-top: 0; + opacity: 0.7; +} + +section { + margin-block: 1.5rem; +} + +h2 { + font-size: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.8; + border-bottom: 1px solid currentcolor; + padding-bottom: 0.25rem; +} + +ul { + list-style: none; + padding-left: 0; +} + +li { + padding: 0.5rem 0.75rem; + border: 1px solid color-mix(in srgb, currentcolor 15%, transparent); + border-radius: 0.5rem; + margin-bottom: 0.5rem; + font-family: ui-monospace, monospace; + font-size: 0.9rem; +} + +li.muted { + opacity: 0.5; + font-style: italic; +} + +code { + font-family: ui-monospace, monospace; + background: color-mix(in srgb, currentcolor 10%, transparent); + padding: 0.1em 0.35em; + border-radius: 0.25em; +} + +button { + font: inherit; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: 1px solid currentcolor; + background: transparent; + cursor: pointer; +} + +button:hover { + background: color-mix(in srgb, currentcolor 10%, transparent); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +#status { + font-family: ui-monospace, monospace; + font-size: 0.85rem; + opacity: 0.7; +} + +#status.error span { + color: #c33; +} + +#status.ready span { + color: #2a7; +} + +.badge { + margin-left: 0.35rem; +} diff --git a/examples/minimal-next-devtools-hub/src/client/app/layout.tsx b/examples/minimal-next-devtools-hub/src/client/app/layout.tsx new file mode 100644 index 0000000..37e8bdc --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/app/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next' +import type { ReactNode } from 'react' +import './globals.css' + +export const metadata: Metadata = { + title: 'Minimal Next DevTools Hub', + description: 'A Next.js host for the @devframes/hub protocol.', +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/examples/minimal-next-devtools-hub/src/client/app/page.tsx b/examples/minimal-next-devtools-hub/src/client/app/page.tsx new file mode 100644 index 0000000..6dccb94 --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/app/page.tsx @@ -0,0 +1,220 @@ +'use client' + +import type { DevToolsRpcClient } from '@devframes/hub/client' +import type { + DevToolsCommandEntry, + DevToolsDockEntry, + DevToolsMessageEntry, + DevToolsTerminalSession, +} from '@devframes/hub/types' +import type { ReactNode } from 'react' +import { connectDevframe } from '@devframes/hub/client' +import { useEffect, useRef, useState } from 'react' + +const HUB_BASE = '/__hub/' + +interface Status { + text: string + kind?: 'ready' | 'error' +} + +type TerminalSummary = Pick + +export default function Page() { + const [status, setStatus] = useState({ text: 'Connecting...' }) + const [docks, setDocks] = useState([]) + const [commands, setCommands] = useState([]) + const [messages, setMessages] = useState([]) + const [terminals, setTerminals] = useState([]) + const [openPathResult, setOpenPathResult] = useState('Test hub:open-path on this README') + const [pingResult, setPingResult] = useState('Run ping') + const rpcRef = useRef(null) + + useEffect(() => { + let cancelled = false + let cleanup: (() => void) | undefined + + async function run() { + try { + const rpc = await connectDevframe({ baseURL: HUB_BASE }) + if (cancelled) + return + + rpcRef.current = rpc + setStatus({ text: `Connected: backend=${rpc.connectionMeta.backend}`, kind: 'ready' }) + + const docksState = await rpc.sharedState.get( + 'devframe:docks', + { initialValue: [] }, + ) + const commandsState = await rpc.sharedState.get( + 'devframe:commands', + { initialValue: [] }, + ) + + const renderDocks = () => setDocks(docksState.value() ?? []) + const renderCommands = () => setCommands(commandsState.value() ?? []) + docksState.on('updated', renderDocks) + commandsState.on('updated', renderCommands) + renderDocks() + renderCommands() + + const refreshMessages = async () => { + const entries = await rpc.call( + 'minimal-next-devtools-hub:messages:list' as any, + ) as DevToolsMessageEntry[] + if (!cancelled) + setMessages(entries) + } + + const refreshTerminals = async () => { + const sessions = await rpc.call( + 'minimal-next-devtools-hub:terminals:list' as any, + ) as TerminalSummary[] + if (!cancelled) + setTerminals(sessions) + } + + await refreshMessages() + await refreshTerminals() + + const interval = window.setInterval(() => { + void refreshMessages() + void refreshTerminals() + }, 2000) + + cleanup = () => window.clearInterval(interval) + } + catch (err) { + if (!cancelled) + setStatus({ text: `Failed: ${(err as Error).message}`, kind: 'error' }) + } + } + + void run() + + return () => { + cancelled = true + cleanup?.() + rpcRef.current = null + } + }, []) + + async function openReadme() { + if (!rpcRef.current) + return + try { + const result = await rpcRef.current.call( + 'hub:commands:execute' as any, + 'hub:open-path', + 'README.md', + ) + setOpenPathResult(`Opened: ${JSON.stringify(result)}`) + } + catch (err) { + setOpenPathResult(`Error: ${(err as Error).message}`) + } + } + + async function ping() { + if (!rpcRef.current) + return + try { + const result = await rpcRef.current.call( + 'hub:commands:execute' as any, + 'minimal-next-devtools-hub:ping', + ) + setPingResult(`Ping returned ${JSON.stringify(result)}`) + } + catch (err) { + setPingResult(`Error: ${(err as Error).message}`) + } + } + + return ( +
    +
    +

    Minimal Next DevTools Hub

    +

    + Protocol witness: verifies + {' '} + @devframes/hub + {' '} + end to end from a Next.js host. +

    +
    + +
    + {status.text} +
    + + + {docks.map(dock => ( +
  • + {dock.title} + {' '} + {dock.id} + {'badge' in dock && dock.badge + ? {`[${dock.badge}]`} + : null} +
  • + ))} +
    + + + {commands.map(command => ( +
  • + {command.title} + {' '} + {command.id} +
  • + ))} +
    + +
    + + +
    + + + {messages.map(message => ( +
  • + {`[${message.level}]`} + {' '} + {message.message} +
  • + ))} +
    + + + {terminals.map(terminal => ( +
  • + {terminal.title} + {' '} + {terminal.id} + {' '} + {terminal.status} +
  • + ))} +
    +
    + ) +} + +function Panel({ title, empty, children }: { + title: string + empty: string + children: ReactNode +}) { + const items = Array.isArray(children) ? children : [children] + return ( +
    +

    {title}

    +
      {items.length ? children :
    • {empty}
    • }
    +
    + ) +} diff --git a/examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts b/examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts new file mode 100644 index 0000000..8f9f932 --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts @@ -0,0 +1,26 @@ +import type { HubNodeContext } from '@devframes/hub/node' +import { defineDevframe } from 'devframe/types' + +export default defineDevframe({ + id: 'next-demo-tool', + name: 'Next Demo Tool', + icon: 'ph:rocket-duotone', + basePath: '/__next-demo-tool/', + async setup(rawCtx) { + const ctx = rawCtx as unknown as HubNodeContext + + ctx.commands.register({ + id: 'next-demo-tool:say-hello', + title: 'Next Demo Tool: Say Hello', + icon: 'ph:hand-waving-duotone', + category: 'demo', + handler: () => 'Hello from the Next demo command!', + }) + + await ctx.messages.add({ + level: 'info', + message: 'Next demo devframe loaded', + description: 'Registered via mountDevframe() from the Next host.', + }) + }, +}) diff --git a/examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts b/examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts new file mode 100644 index 0000000..842626b --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts @@ -0,0 +1,145 @@ +import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' +import type { StartedServer } from 'devframe/node' +import type { ConnectionMeta, DevframeDefinition, DevToolsHost } from 'devframe/types' +import { homedir } from 'node:os' +import process from 'node:process' +import { defineRpcFunction } from '@devframes/hub' +import { createHubContext, mountDevframe } from '@devframes/hub/node' +import { startHttpAndWs } from 'devframe/node' +import { launchEditor } from 'devframe/utils/launch-editor' +import { getPort } from 'get-port-please' +import { join } from 'pathe' +import demoDevframe from './demo-devframe' + +export interface MinimalNextDevToolsHubOptions { + /** Preferred port for the side-car RPC/WS server. Default: a free port near 9877. */ + port?: number + /** Hostname for the side-car server. Default: `localhost`. */ + host?: string + /** Workspace root used by hub host capabilities. Default: `process.cwd()`. */ + cwd?: string + /** Devframes to mount as docks. */ + devframes?: DevframeDefinition[] +} + +export interface StartedMinimalNextDevToolsHub extends StartedServer { + context: HubNodeContext + connectionMeta: ConnectionMeta & { backend: 'websocket', websocket: number } +} + +const minimalNextHubMessagesList = defineRpcFunction({ + name: 'minimal-next-devtools-hub:messages:list', + type: 'static', + jsonSerializable: true, + setup: (ctx: HubNodeContext) => ({ + async handler() { + return Array.from(ctx.messages.entries.values()) + }, + }), +}) + +const minimalNextHubTerminalsList = defineRpcFunction({ + name: 'minimal-next-devtools-hub:terminals:list', + type: 'static', + jsonSerializable: true, + setup: (ctx: HubNodeContext) => ({ + async handler() { + return Array.from(ctx.terminals.sessions.values()).map(s => ({ + id: s.id, + title: s.title, + description: s.description, + status: s.status, + })) + }, + }), +}) + +export async function minimalNextDevToolsHub( + options: MinimalNextDevToolsHubOptions = {}, +): Promise { + const cwd = options.cwd ?? process.cwd() + const hostName = options.host ?? 'localhost' + + const host: DevToolsHost & HubHostCapabilities = { + mountStatic() { + // Static mounting for devframe SPAs would route through Next middleware + // in a fuller host. This minimal example keeps mounted devframes headless. + }, + resolveOrigin() { + return `http://${hostName}:3000` + }, + getStorageDir(scope) { + return scope === 'workspace' + ? join(cwd, 'node_modules/.minimal-next-devtools-hub') + : join(homedir(), '.minimal-next-devtools-hub') + }, + async openPath(filepath, line, column) { + const absolute = join(cwd, filepath) + const target = line + ? `${absolute}:${line}${column ? `:${column}` : ''}` + : absolute + launchEditor(target) + return true + }, + } + + const port = options.port ?? await getPort({ host: hostName, port: 9877, random: false }) + + const context = await createHubContext({ + cwd, + workspaceRoot: cwd, + mode: 'dev', + host, + builtinRpcDeclarations: [ + minimalNextHubMessagesList, + minimalNextHubTerminalsList, + ], + }) + + context.commands.register({ + id: 'minimal-next-devtools-hub:ping', + title: 'Next Hub: Ping', + icon: 'ph:bell-duotone', + category: 'hub', + handler: () => 'pong', + }) + + await context.messages.add({ + level: 'success', + message: 'Minimal Next DevTools Hub started', + description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 1} devframe(s) registered.`, + }) + + for (const def of options.devframes ?? [demoDevframe]) { + await mountDevframe(context, def) + } + + const started = await startHttpAndWs({ + context, + host: hostName, + port, + auth: false, + }) + + return Object.assign(started, { + context, + connectionMeta: { + backend: 'websocket' as const, + websocket: started.port, + }, + }) +} + +const GLOBAL_KEY = '__minimalNextDevToolsHub' + +type GlobalWithHub = typeof globalThis & { + [GLOBAL_KEY]?: Promise +} + +export function ensureMinimalNextDevToolsHub( + options: MinimalNextDevToolsHubOptions = {}, +): Promise { + const globalHub = globalThis as GlobalWithHub + globalHub[GLOBAL_KEY] ??= minimalNextDevToolsHub(options) + return globalHub[GLOBAL_KEY] +} diff --git a/examples/minimal-next-devtools-hub/src/client/next.config.mjs b/examples/minimal-next-devtools-hub/src/client/next.config.mjs new file mode 100644 index 0000000..d551674 --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/next.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { unoptimized: true }, + // The workspace typecheck owns source-level project references. + typescript: { ignoreBuildErrors: true }, +} + +export default nextConfig diff --git a/examples/minimal-next-devtools-hub/src/client/tsconfig.json b/examples/minimal-next-devtools-hub/src/client/tsconfig.json new file mode 100644 index 0000000..62aa7e6 --- /dev/null +++ b/examples/minimal-next-devtools-hub/src/client/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx" + ], + "exclude": [ + "node_modules", + ".next", + "out" + ] +} diff --git a/examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts b/examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts new file mode 100644 index 0000000..ca99995 --- /dev/null +++ b/examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts @@ -0,0 +1,58 @@ +import { createRpcClient } from 'devframe/rpc/client' +import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { WebSocket } from 'ws' +import { minimalNextDevToolsHub } from '../src/client/devtools/minimal-next-devtools-hub' + +vi.stubGlobal('WebSocket', WebSocket) + +function bootRpc(port: number) { + const channel = createWsRpcChannel({ url: `ws://127.0.0.1:${port}` }) + return createRpcClient({}, { channel }) +} + +describe('minimal-next-devtools-hub (example)', () => { + let server: Awaited> | undefined + + afterEach(async () => { + await server?.close() + server = undefined + }) + + it('returns connection meta pointing at the WS backend', async () => { + server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + + expect(server.connectionMeta).toEqual({ + backend: 'websocket', + websocket: server.port, + }) + }) + + it('registers hub built-in docks and the mounted demo devframe', async () => { + server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + + const dockIds = server.context.docks.values().map(d => d.id) + expect(dockIds).toContain('next-demo-tool') + expect(dockIds).toContain('~terminals') + expect(dockIds).toContain('~messages') + expect(dockIds).toContain('~settings') + }) + + it('lists startup and demo messages through the kit-local RPC', async () => { + server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + + const rpc = bootRpc(server.port) + const messages = await rpc.$call('minimal-next-devtools-hub:messages:list') as { message: string }[] + expect(messages.map(m => m.message)).toContain('Minimal Next DevTools Hub started') + expect(messages.map(m => m.message)).toContain('Next demo devframe loaded') + }) + + it('executes the ping command through the hub command RPC', async () => { + server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + + const rpc = bootRpc(server.port) + await expect( + rpc.$call('hub:commands:execute', 'minimal-next-devtools-hub:ping'), + ).resolves.toBe('pong') + }) +}) diff --git a/examples/minimal-next-devtools-hub/tsconfig.json b/examples/minimal-next-devtools-hub/tsconfig.json new file mode 100644 index 0000000..77efbe1 --- /dev/null +++ b/examples/minimal-next-devtools-hub/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "incremental": true, + "jsx": "preserve", + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "allowJs": true, + "noEmit": true, + "esModuleInterop": true, + "isolatedDeclarations": false, + "plugins": [{ "name": "next" }] + }, + "include": [ + "src", + "tests", + "vitest.config.ts" + ], + "exclude": [ + "dist", + ".next", + "out" + ] +} diff --git a/examples/minimal-next-devtools-hub/vitest.config.ts b/examples/minimal-next-devtools-hub/vitest.config.ts new file mode 100644 index 0000000..7fd3b94 --- /dev/null +++ b/examples/minimal-next-devtools-hub/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' +import { alias } from '../../alias' + +export default defineConfig({ + resolve: { + alias, + }, + test: { + environment: 'node', + }, +}) diff --git a/examples/minimal-vite-devtools-kit/README.md b/examples/minimal-vite-devtools-hub/README.md similarity index 73% rename from examples/minimal-vite-devtools-kit/README.md rename to examples/minimal-vite-devtools-hub/README.md index b29fba9..1295270 100644 --- a/examples/minimal-vite-devtools-kit/README.md +++ b/examples/minimal-vite-devtools-hub/README.md @@ -1,12 +1,12 @@ -# Minimal Vite DevTools Kit +# Minimal Vite DevTools Hub -A protocol-witness example. The `src/minimal-hub-kit.ts` file is the entire "kit" — about 120 lines of Vite plugin code that wires `@devframes/hub` into a Vite dev server. Every framework's hub kit (`@vitejs/devtools-kit`, future `@next/devtools-kit`, etc.) is the same shape. +A protocol-witness example. The `src/minimal-vite-devtools-hub.ts` file is the entire Vite host — about 120 lines of Vite plugin code that wires `@devframes/hub` into a Vite dev server. Every framework's DevTools Hub host follows the same shape. ## Run it ```sh pnpm install -pnpm --filter minimal-vite-devtools-kit-example dev +pnpm --filter minimal-vite-devtools-hub dev ``` Open the printed URL. You should see: @@ -30,7 +30,7 @@ Open the printed URL. You should see: | File | Role | |---|---| -| `src/minimal-hub-kit.ts` | The Vite plugin — creates hub context, mounts middleware, side-car WS | +| `src/minimal-vite-devtools-hub.ts` | The Vite plugin — creates hub context, mounts middleware, side-car WS | | `src/devframe.ts` | A sample `DevframeDefinition` that plugs into the kit | | `src/client/main.ts` | The browser-side UI that consumes the hub protocol | | `index.html` | The UI shell | diff --git a/examples/minimal-vite-devtools-kit/index.html b/examples/minimal-vite-devtools-hub/index.html similarity index 93% rename from examples/minimal-vite-devtools-kit/index.html rename to examples/minimal-vite-devtools-hub/index.html index b9dfe77..b51934a 100644 --- a/examples/minimal-vite-devtools-kit/index.html +++ b/examples/minimal-vite-devtools-hub/index.html @@ -3,12 +3,12 @@ - Minimal Vite DevTools Kit + Minimal Vite DevTools Hub
    -

    Minimal Vite DevTools Kit

    +

    Minimal Vite DevTools Hub

    Protocol witness — verifies @devframes/hub end to end.

    diff --git a/examples/minimal-vite-devtools-kit/package.json b/examples/minimal-vite-devtools-hub/package.json similarity index 64% rename from examples/minimal-vite-devtools-kit/package.json rename to examples/minimal-vite-devtools-hub/package.json index d4023ed..26b159a 100644 --- a/examples/minimal-vite-devtools-kit/package.json +++ b/examples/minimal-vite-devtools-hub/package.json @@ -1,9 +1,9 @@ { - "name": "minimal-vite-devtools-kit-example", + "name": "minimal-vite-devtools-hub", "type": "module", "version": "0.4.1", "private": true, - "description": "Protocol-witness example — a tiny Vite plugin built on @devframes/hub that exercises every hub subsystem end-to-end.", + "description": "Protocol-witness example — a tiny Vite DevTools Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", "scripts": { "dev": "vite", "build": "vite build" diff --git a/examples/minimal-vite-devtools-kit/src/client/main.ts b/examples/minimal-vite-devtools-hub/src/client/main.ts similarity index 97% rename from examples/minimal-vite-devtools-kit/src/client/main.ts rename to examples/minimal-vite-devtools-hub/src/client/main.ts index 4854970..d25ccc6 100644 --- a/examples/minimal-vite-devtools-kit/src/client/main.ts +++ b/examples/minimal-vite-devtools-hub/src/client/main.ts @@ -62,7 +62,7 @@ async function main() { // to refresh on broadcast; this minimal example polls instead. const refreshMessages = async () => { const entries = await rpc.call( - 'minimal-hub-kit:messages:list' as any, + 'minimal-vite-devtools-hub:messages:list' as any, ) as DevToolsMessageEntry[] renderList(messagesEl, entries, m => `
  • [${m.level}] ${m.message}
  • `) @@ -72,7 +72,7 @@ async function main() { // 4. Terminals — same pattern as messages. const refreshTerminals = async () => { const sessions = await rpc.call( - 'minimal-hub-kit:terminals:list' as any, + 'minimal-vite-devtools-hub:terminals:list' as any, ) as Pick[] renderList(terminalsEl, sessions, t => `
  • ${t.title} ${t.id} · ${t.status}
  • `) diff --git a/examples/minimal-vite-devtools-kit/src/client/style.css b/examples/minimal-vite-devtools-hub/src/client/style.css similarity index 100% rename from examples/minimal-vite-devtools-kit/src/client/style.css rename to examples/minimal-vite-devtools-hub/src/client/style.css diff --git a/examples/minimal-vite-devtools-kit/src/devframe.ts b/examples/minimal-vite-devtools-hub/src/devframe.ts similarity index 100% rename from examples/minimal-vite-devtools-kit/src/devframe.ts rename to examples/minimal-vite-devtools-hub/src/devframe.ts diff --git a/examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts b/examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts similarity index 80% rename from examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts rename to examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts index faf8108..6a720c1 100644 --- a/examples/minimal-vite-devtools-kit/src/minimal-hub-kit.ts +++ b/examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts @@ -10,8 +10,8 @@ import { launchEditor } from 'devframe/utils/launch-editor' import { getPort } from 'get-port-please' import { join } from 'pathe' -export interface MinimalHubKitOptions { - /** Mount path for the kit's connection-meta endpoint. Default: `/__hub/`. */ +export interface MinimalViteDevToolsHubOptions { + /** Mount path for the hub's connection-meta endpoint. Default: `/__hub/`. */ base?: string /** Preferred port for the side-car RPC/WS server. Default: a free port near 9777. */ port?: number @@ -19,10 +19,10 @@ export interface MinimalHubKitOptions { devframes?: DevframeDefinition[] } -// Minimal kit-local RPCs — used by the UI for read-side data. A more -// ambitious kit might hoist these into `@devframes/hub` itself. -const minimalKitMessagesList = defineRpcFunction({ - name: 'minimal-hub-kit:messages:list', +// Minimal hub-local RPCs — used by the UI for read-side data. A more +// ambitious hub host might hoist these into `@devframes/hub` itself. +const minimalViteHubMessagesList = defineRpcFunction({ + name: 'minimal-vite-devtools-hub:messages:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -32,8 +32,8 @@ const minimalKitMessagesList = defineRpcFunction({ }), }) -const minimalKitTerminalsList = defineRpcFunction({ - name: 'minimal-hub-kit:terminals:list', +const minimalViteHubTerminalsList = defineRpcFunction({ + name: 'minimal-vite-devtools-hub:terminals:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -54,17 +54,16 @@ const minimalKitTerminalsList = defineRpcFunction({ * (`openPath` via launch-editor), and exposes the side-car WS endpoint * to the browser via Vite middleware at `__connection.json`. * - * This file is the entire "kit" — every other framework's hub kit - * (`@vitejs/devtools-kit`, future `@next/devtools-kit`, …) is the same - * shape: a thin layer that adapts a framework's dev server to the hub. + * This file is the entire Vite host — every other framework's hub host is + * the same shape: a thin layer that adapts a framework's dev server to the hub. */ -export function minimalHubKit(options: MinimalHubKitOptions = {}): Plugin { +export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = {}): Plugin { const base = normalizeBase(options.base ?? '/__hub/') let viteConfig: ResolvedConfig | undefined let started: { close: () => Promise } | undefined return { - name: 'minimal-vite-devtools-kit', + name: 'minimal-vite-devtools-hub', apply: 'serve', configResolved(config) { @@ -91,8 +90,8 @@ export function minimalHubKit(options: MinimalHubKitOptions = {}): Plugin { }, getStorageDir(scope) { return scope === 'workspace' - ? join(cwd, 'node_modules/.minimal-hub-kit') - : join(homedir(), '.minimal-hub-kit') + ? join(cwd, 'node_modules/.minimal-vite-devtools-hub') + : join(homedir(), '.minimal-vite-devtools-hub') }, async openPath(filepath, line, column) { const absolute = join(cwd, filepath) @@ -112,28 +111,28 @@ export function minimalHubKit(options: MinimalHubKitOptions = {}): Plugin { mode: 'dev', host, builtinRpcDeclarations: [ - // The minimal kit ships its own `messages:list` and `terminals:list` + // The minimal hub ships its own `messages:list` and `terminals:list` // RPCs so the UI has something to read. A full hub kit would // likely standardise these (this is why hub-level RPC built-ins // exist — see hub:open-path / hub:commands:execute) but for the // demo we keep them kit-local. - minimalKitMessagesList, - minimalKitTerminalsList, + minimalViteHubMessagesList, + minimalViteHubTerminalsList, ], }) - // Seed a sample terminal + command directly on the kit so the UI + // Seed a sample command directly on the hub so the UI // shows something even without any plugged-in devframes. context.commands.register({ - id: 'minimal-hub-kit:ping', - title: 'Kit · Ping', + id: 'minimal-vite-devtools-hub:ping', + title: 'Vite Hub · Ping', icon: 'ph:bell-duotone', category: 'kit', handler: () => 'pong', }) await context.messages.add({ level: 'success', - message: 'Minimal hub kit started', + message: 'Minimal Vite DevTools Hub started', description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 0} devframe(s) registered.`, }) diff --git a/examples/minimal-vite-devtools-kit/tsconfig.json b/examples/minimal-vite-devtools-hub/tsconfig.json similarity index 100% rename from examples/minimal-vite-devtools-kit/tsconfig.json rename to examples/minimal-vite-devtools-hub/tsconfig.json diff --git a/examples/minimal-vite-devtools-kit/vite.config.ts b/examples/minimal-vite-devtools-hub/vite.config.ts similarity index 69% rename from examples/minimal-vite-devtools-kit/vite.config.ts rename to examples/minimal-vite-devtools-hub/vite.config.ts index c08e65a..5ab3361 100644 --- a/examples/minimal-vite-devtools-kit/vite.config.ts +++ b/examples/minimal-vite-devtools-hub/vite.config.ts @@ -1,12 +1,12 @@ import { defineConfig } from 'vite' import { alias } from '../../alias' import demoDevframe from './src/devframe' -import { minimalHubKit } from './src/minimal-hub-kit' +import { minimalViteDevToolsHub } from './src/minimal-vite-devtools-hub' export default defineConfig({ resolve: { alias }, plugins: [ - minimalHubKit({ + minimalViteDevToolsHub({ devframes: [demoDevframe], }), ], diff --git a/packages/devframe/src/node/server.ts b/packages/devframe/src/node/server.ts index b32dfa8..4c81177 100644 --- a/packages/devframe/src/node/server.ts +++ b/packages/devframe/src/node/server.ts @@ -8,6 +8,7 @@ import { createRpcServer } from 'devframe/rpc/server' import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server' import { H3, toNodeHandler } from 'h3' import { WebSocketServer as WSServer } from 'ws' +import { getInternalContext } from './internal/context' export interface StartHttpAndWsOptions { context: DevToolsNodeContext @@ -125,14 +126,21 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise resolveListen()) }) - const origin = `http://${bindHost}:${port}` + const address = httpServer.address() + const resolvedPort = typeof address === 'object' && address ? address.port : port + const origin = `http://${bindHost}:${resolvedPort}` + const internal = getInternalContext(context) + const wsUrl = origin.replace(/^http/, 'ws') + internal.wsEndpoint = { + url: wsUrl, + } if (options.onReady) - await options.onReady({ origin, port, app }) + await options.onReady({ origin, port: resolvedPort, app }) return { origin, - port, + port: resolvedPort, app, wss, rpcGroup, @@ -144,6 +152,8 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise(r => wss.close(() => r())) await new Promise(r => httpServer.close(() => r())) + if (getInternalContext(context).wsEndpoint?.url === wsUrl) + getInternalContext(context).wsEndpoint = undefined }, } } diff --git a/packages/hub/src/client/remote.ts b/packages/hub/src/client/remote.ts index fdf1811..5d3d1f2 100644 --- a/packages/hub/src/client/remote.ts +++ b/packages/hub/src/client/remote.ts @@ -19,6 +19,14 @@ function extractKeyFromFragment(hash: string): string | null { if (!hash) return null const raw = hash.startsWith('#') ? hash.slice(1) : hash + const queryIdx = raw.indexOf('?') + if (queryIdx !== -1) { + const params = new URLSearchParams(raw.slice(queryIdx + 1)) + const value = params.get(REMOTE_CONNECTION_KEY) + if (value) + return value + } + for (const part of raw.split('&')) { const [k, v = ''] = part.split('=') if (k === REMOTE_CONNECTION_KEY) diff --git a/packages/hub/src/node/__tests__/context.test.ts b/packages/hub/src/node/__tests__/context.test.ts new file mode 100644 index 0000000..394c623 --- /dev/null +++ b/packages/hub/src/node/__tests__/context.test.ts @@ -0,0 +1,58 @@ +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' +import { createHostContext, startHttpAndWs } from '../../../../devframe/src/node' +import { getInternalContext } from '../../../../devframe/src/node/internal' +import { createHubContext } from '../context' + +function createHost(storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-context-'))) { + return { + mountStatic: () => {}, + resolveOrigin: () => 'http://localhost:5173', + getStorageDir: () => storageDir, + } +} + +describe('createHubContext shared state', () => { + it('seeds built-in docks and commands immediately', async () => { + const context = await createHubContext({ + cwd: process.cwd(), + mode: 'build', + host: createHost(), + }) + + const docks = await context.rpc.sharedState.get('devframe:docks') + expect(docks.value().map(dock => dock.id)).toEqual([ + '~terminals', + '~messages', + '~settings', + ]) + + const commands = await context.rpc.sharedState.get('devframe:commands') + expect(commands.value().map(command => command.id)).toContain('hub:open-path') + }) +}) + +describe('startHttpAndWs remote endpoint metadata', () => { + it('sets and clears the internal websocket endpoint', async () => { + const context = await createHostContext({ + cwd: process.cwd(), + mode: 'dev', + host: createHost(), + }) + + const started = await startHttpAndWs({ + context, + host: '127.0.0.1', + port: 0, + }) + + expect(getInternalContext(context).wsEndpoint).toEqual({ + url: `ws://127.0.0.1:${started.port}`, + }) + + await started.close() + expect(getInternalContext(context).wsEndpoint).toBeUndefined() + }) +}) diff --git a/packages/hub/src/node/__tests__/host-commands.test.ts b/packages/hub/src/node/__tests__/host-commands.test.ts new file mode 100644 index 0000000..ccc4e56 --- /dev/null +++ b/packages/hub/src/node/__tests__/host-commands.test.ts @@ -0,0 +1,63 @@ +import type { HubNodeContext } from '../context' +import { describe, expect, it } from 'vitest' +import { DevToolsCommandsHost } from '../host-commands' + +describe('devToolsCommandsHost command id validation', () => { + it('rejects duplicate ids inside one command tree', () => { + const host = new DevToolsCommandsHost({} as HubNodeContext) + + expect(() => host.register({ + id: 'tool:parent', + title: 'Parent', + children: [ + { id: 'tool:child', title: 'Child' }, + { id: 'tool:child', title: 'Duplicate child' }, + ], + })).toThrow('Command id "tool:child" is already used') + }) + + it('rejects child ids that collide with existing command trees', () => { + const host = new DevToolsCommandsHost({} as HubNodeContext) + host.register({ + id: 'tool:parent', + title: 'Parent', + children: [ + { id: 'tool:child', title: 'Child' }, + ], + }) + + expect(() => host.register({ + id: 'other:parent', + title: 'Other parent', + children: [ + { id: 'tool:child', title: 'Duplicate child' }, + ], + })).toThrow('Command id "tool:child" is already used') + + expect(() => host.register({ + id: 'tool:child', + title: 'Top-level collision', + })).toThrow('Command id "tool:child" is already used') + }) + + it('validates updated children against other command trees', () => { + const host = new DevToolsCommandsHost({} as HubNodeContext) + host.register({ + id: 'other:parent', + title: 'Other parent', + children: [ + { id: 'other:child', title: 'Other child' }, + ], + }) + const handle = host.register({ + id: 'tool:parent', + title: 'Parent', + }) + + expect(() => handle.update({ + children: [ + { id: 'other:child', title: 'Duplicate child' }, + ], + })).toThrow('Command id "other:child" is already used') + }) +}) diff --git a/packages/hub/src/node/__tests__/host-docks.test.ts b/packages/hub/src/node/__tests__/host-docks.test.ts new file mode 100644 index 0000000..c230ac4 --- /dev/null +++ b/packages/hub/src/node/__tests__/host-docks.test.ts @@ -0,0 +1,78 @@ +import type { HubNodeContext } from '../context' +import { mkdtempSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { REMOTE_CONNECTION_KEY } from 'devframe/constants' +import { getInternalContext } from 'devframe/node/internal' +import { describe, expect, it } from 'vitest' +import { parseRemoteConnection } from '../../client/remote' +import { DevToolsDockHost } from '../host-docks' + +function createContext(): HubNodeContext { + const storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-docks-')) + return { + host: { + mountStatic: () => {}, + resolveOrigin: () => 'http://localhost:5173', + getStorageDir: () => storageDir, + }, + } as unknown as HubNodeContext +} + +describe('devToolsDockHost remote URL enrichment', () => { + it('preserves hash routes and replaces existing remote descriptors', () => { + const context = createContext() + getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } + const host = new DevToolsDockHost(context) + + host.register({ + type: 'iframe', + id: 'remote', + title: 'Remote', + url: 'https://remote.test/app#/inspect?tab=state', + remote: true, + }) + + const first = host.values({ includeBuiltin: false })[0] + expect(first.type).toBe('iframe') + const firstUrl = first.type === 'iframe' ? first.url : '' + expect(firstUrl).toContain(`#/inspect?tab=state&${REMOTE_CONNECTION_KEY}=`) + expect(parseRemoteConnection(firstUrl)).toMatchObject({ + backend: 'websocket', + websocket: 'ws://localhost:4173', + origin: 'http://localhost:5173', + }) + + host.update({ + type: 'iframe', + id: 'remote', + title: 'Remote', + url: firstUrl, + remote: true, + }) + + const second = host.values({ includeBuiltin: false })[0] + const secondUrl = second.type === 'iframe' ? second.url : '' + expect(secondUrl.match(new RegExp(REMOTE_CONNECTION_KEY, 'g'))).toHaveLength(1) + expect(secondUrl).toContain('#/inspect?tab=state&') + }) + + it('preserves non-route fragments with the ampersand descriptor form', () => { + const context = createContext() + getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } + const host = new DevToolsDockHost(context) + + host.register({ + type: 'iframe', + id: 'remote', + title: 'Remote', + url: 'https://remote.test/app#section', + remote: true, + }) + + const entry = host.values({ includeBuiltin: false })[0] + const url = entry.type === 'iframe' ? entry.url : '' + expect(url).toContain(`#section&${REMOTE_CONNECTION_KEY}=`) + expect(parseRemoteConnection(url)?.websocket).toBe('ws://localhost:4173') + }) +}) diff --git a/packages/hub/src/node/__tests__/host-messages.test.ts b/packages/hub/src/node/__tests__/host-messages.test.ts new file mode 100644 index 0000000..f4857a1 --- /dev/null +++ b/packages/hub/src/node/__tests__/host-messages.test.ts @@ -0,0 +1,23 @@ +import type { HubNodeContext } from '../context' +import { describe, expect, it } from 'vitest' +import { DevToolsMessagesHost } from '../host-messages' + +describe('devToolsMessagesHost', () => { + it('caps removal history', async () => { + const host = new DevToolsMessagesHost({} as HubNodeContext) + + for (let i = 0; i < 1005; i++) { + const id = `message:${i}` + await host.add({ + id, + level: 'info', + message: id, + }) + await host.remove(id) + } + + expect(host.removals).toHaveLength(1000) + expect(host.removals[0].id).toBe('message:5') + expect(host.removals.at(-1)?.id).toBe('message:1004') + }) +}) diff --git a/packages/hub/src/node/__tests__/host-terminals.test.ts b/packages/hub/src/node/__tests__/host-terminals.test.ts new file mode 100644 index 0000000..fc70198 --- /dev/null +++ b/packages/hub/src/node/__tests__/host-terminals.test.ts @@ -0,0 +1,134 @@ +import type { DevToolsTerminalSession } from '../../types/terminals' +import type { HubNodeContext } from '../context' +import process from 'node:process' +import { describe, expect, it, vi } from 'vitest' +import { DevToolsTerminalHost } from '../host-terminals' + +interface FakeSink { + write: ReturnType + close: ReturnType + error: ReturnType + readonly closed: boolean +} + +function createTerminalHost() { + const sinks = new Map() + const context = { + rpc: { + streaming: { + create: () => ({ + start: ({ id }: { id: string }) => { + let closed = false + const sink: FakeSink = { + write: vi.fn(), + close: vi.fn(() => { + closed = true + }), + error: vi.fn(() => { + closed = true + }), + get closed() { + return closed + }, + } + sinks.set(id, sink) + return sink + }, + }), + }, + }, + } as unknown as HubNodeContext + + return { + host: new DevToolsTerminalHost(context), + sinks, + } +} + +async function waitUntil(assertion: () => void): Promise { + const deadline = Date.now() + 1000 + let lastError: unknown + while (Date.now() < deadline) { + try { + assertion() + return + } + catch (error) { + lastError = error + await new Promise(resolve => setTimeout(resolve, 10)) + } + } + throw lastError +} + +describe('devToolsTerminalHost stream lifecycle', () => { + it('cancels a bound stream when a session is removed', async () => { + const { host, sinks } = createTerminalHost() + let controller: ReadableStreamDefaultController + let cancelled = false + const stream = new ReadableStream({ + start(_controller) { + controller = _controller + }, + cancel() { + cancelled = true + }, + }) + const session: DevToolsTerminalSession = { + id: 'terminal', + title: 'Terminal', + status: 'running', + stream, + } + + host.register(session) + controller!.enqueue('hello') + + await waitUntil(() => { + expect(session.buffer).toEqual(['hello']) + }) + + host.remove(session) + + await waitUntil(() => { + expect(cancelled).toBe(true) + }) + expect(sinks.get('terminal')?.closed).toBe(true) + }) + + it('closes child-process streams after natural process exit', async () => { + const { host, sinks } = createTerminalHost() + + const session = await host.startChildProcess({ + command: process.execPath, + args: ['-e', 'process.stdout.write("done")'], + }, { + id: 'child', + title: 'Child', + }) + + await waitUntil(() => { + expect(sinks.get('child')?.closed).toBe(true) + }) + expect(session.buffer?.join('')).toContain('done') + }) + + it('closes child-process streams on terminate', async () => { + const { host, sinks } = createTerminalHost() + + const session = await host.startChildProcess({ + command: process.execPath, + args: ['-e', 'setInterval(() => {}, 1000)'], + }, { + id: 'child', + title: 'Child', + }) + + await session.terminate() + + await waitUntil(() => { + expect(sinks.get('child')?.closed).toBe(true) + }) + expect(session.getChildProcess()).toBeUndefined() + }) +}) diff --git a/packages/hub/src/node/context.ts b/packages/hub/src/node/context.ts index eddcf62..cd08d05 100644 --- a/packages/hub/src/node/context.ts +++ b/packages/hub/src/node/context.ts @@ -117,6 +117,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis docksSharedState.mutate(() => docks.values()) }, debounceMs) docks.events.on('dock:entry:updated', refreshDocks) + docksSharedState.mutate(() => docks.values()) const broadcastTerminals = debounce(() => { context.rpc.broadcast({ @@ -147,6 +148,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis commands.events.on('command:unregistered', syncCommands) registerHubBuiltins(context) + commandsSharedState.mutate(() => commands.list()) return context } diff --git a/packages/hub/src/node/diagnostics.ts b/packages/hub/src/node/diagnostics.ts index c85e7a6..94a5fdd 100644 --- a/packages/hub/src/node/diagnostics.ts +++ b/packages/hub/src/node/diagnostics.ts @@ -40,6 +40,10 @@ export const diagnostics = defineDiagnostics({ DF8402: { why: (p: { id: string }) => `Command "${p.id}" is not registered`, }, + DF8403: { + why: (p: { id: string }) => `Command id "${p.id}" is already used by another command or child command`, + fix: 'Use globally unique command ids for top-level commands and all child commands.', + }, DF8500: { why: (p: { id: string }) => `Built-in command "${p.id}" requires a host capability that this host does not implement.`, fix: 'Implement the matching capability on the `DevToolsHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`.', diff --git a/packages/hub/src/node/host-commands.ts b/packages/hub/src/node/host-commands.ts index 86b7c69..fb48dd7 100644 --- a/packages/hub/src/node/host-commands.ts +++ b/packages/hub/src/node/host-commands.ts @@ -8,6 +8,48 @@ import type { HubNodeContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { diagnostics } from './diagnostics' +function findChildCommand(command: DevToolsServerCommandInput, id: string): DevToolsServerCommandInput | undefined { + for (const child of command.children ?? []) { + if (child.id === id) + return child + const nested = findChildCommand(child, id) + if (nested) + return nested + } + return undefined +} + +function collectCommandIds(command: DevToolsServerCommandInput, ids: string[] = []): string[] { + ids.push(command.id) + for (const child of command.children ?? []) + collectCommandIds(child, ids) + return ids +} + +function validateCommandIds( + commands: Map, + command: DevToolsServerCommandInput, + ignoreTopLevelId?: string, +): void { + const ids = collectCommandIds(command) + const seen = new Set() + for (const id of ids) { + if (seen.has(id)) + throw diagnostics.DF8403({ id }) + seen.add(id) + } + + for (const [registeredId, registered] of commands) { + if (registeredId === ignoreTopLevelId) + continue + const registeredIds = new Set(collectCommandIds(registered)) + for (const id of ids) { + if (registeredIds.has(id)) + throw diagnostics.DF8403({ id }) + } + } +} + export class DevToolsCommandsHost implements DevToolsCommandsHostType { public readonly commands: DevToolsCommandsHostType['commands'] = new Map() public readonly events: DevToolsCommandsHostType['events'] = createEventEmitter() @@ -20,6 +62,7 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { if (this.commands.has(command.id)) { throw diagnostics.DF8400({ id: command.id }) } + validateCommandIds(this.commands, command) this.commands.set(command.id, command) this.events.emit('command:registered', this.toSerializable(command)) @@ -33,6 +76,12 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { if (!existing) { throw diagnostics.DF8402({ id: command.id }) } + const next = { + ...existing, + ...patch, + id: existing.id, + } + validateCommandIds(this.commands, next, existing.id) Object.assign(existing, patch) this.events.emit('command:registered', this.toSerializable(existing)) }, @@ -71,11 +120,9 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { // 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 - } + const child = findChildCommand(cmd, id) + if (child) + return child } return undefined diff --git a/packages/hub/src/node/host-docks.ts b/packages/hub/src/node/host-docks.ts index f18a058..584c341 100644 --- a/packages/hub/src/node/host-docks.ts +++ b/packages/hub/src/node/host-docks.ts @@ -46,12 +46,32 @@ function buildRemoteUrl(baseUrl: string, payload: RemoteConnectionInfo, transpor const encoded = base64UrlEncode(JSON.stringify(payload)) const param = `${REMOTE_CONNECTION_KEY}=${encoded}` if (transport === 'fragment') { - // Replace any existing fragment bearing our key; otherwise append. + // Replace any existing fragment/query descriptor bearing our key; otherwise append. const hashIdx = baseUrl.indexOf('#') if (hashIdx === -1) return `${baseUrl}#${param}` const before = baseUrl.slice(0, hashIdx) - return `${before}#${param}` + const rawHash = baseUrl.slice(hashIdx + 1) + if (!rawHash) + return `${before}#${param}` + const routeQueryIdx = rawHash.indexOf('?') + if (routeQueryIdx !== -1) { + const beforeQuery = rawHash.slice(0, routeQueryIdx + 1) + const params = new URLSearchParams(rawHash.slice(routeQueryIdx + 1)) + params.set(REMOTE_CONNECTION_KEY, encoded) + return `${before}#${beforeQuery}${params.toString()}` + } + + const parts = rawHash.split('&') + const existingIdx = parts.findIndex((part) => { + const [key] = part.split('=') + return key === REMOTE_CONNECTION_KEY + }) + if (existingIdx >= 0) { + parts[existingIdx] = param + return `${before}#${parts.join('&')}` + } + return `${before}#${rawHash}&${param}` } // query const qIdx = baseUrl.indexOf('?') diff --git a/packages/hub/src/node/host-messages.ts b/packages/hub/src/node/host-messages.ts index 2b9a1d3..4086818 100644 --- a/packages/hub/src/node/host-messages.ts +++ b/packages/hub/src/node/host-messages.ts @@ -9,6 +9,17 @@ import { createEventEmitter } from 'devframe/utils/events' import { nanoid } from 'devframe/utils/nanoid' const MAX_ENTRIES = 1000 +const MAX_REMOVALS = 1000 + +function recordRemoval( + removals: Array<{ id: string, time: number }>, + id: string, + time: number, +): void { + removals.push({ id, time }) + if (removals.length > MAX_REMOVALS) + removals.splice(0, removals.length - MAX_REMOVALS) +} export class DevToolsMessagesHost implements DevToolsMessagesHostType { public readonly entries: DevToolsMessagesHostType['entries'] = new Map() @@ -105,7 +116,7 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { } this.entries.delete(id) this.lastModified.delete(id) - this.removals.push({ id, time: this._tick() }) + recordRemoval(this.removals, id, this._tick()) this.events.emit('message:removed', id) } @@ -115,7 +126,7 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { this._autoDeleteTimers.clear() const tick = this._tick() for (const id of this.entries.keys()) - this.removals.push({ id, time: tick }) + recordRemoval(this.removals, id, tick) this.entries.clear() this.lastModified.clear() this.events.emit('message:cleared') diff --git a/packages/hub/src/node/host-terminals.ts b/packages/hub/src/node/host-terminals.ts index bb62af4..df492c6 100644 --- a/packages/hub/src/node/host-terminals.ts +++ b/packages/hub/src/node/host-terminals.ts @@ -104,25 +104,43 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { // `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. - }) + const reader = session.stream.getReader() + let disposed = false + ;(async () => { + try { + while (true) { + if (disposed) + break + const result = await reader.read() + if (disposed) + break + if (result.done) + break + // Mirror to the legacy session.buffer used by `terminals:read` — + // unbounded history kept for the snapshot endpoint. + sessionBuffer.push(result.value) + sink?.write(result.value) + } + if (!disposed && sink && !sink.closed) + sink.close() + } + catch (error) { + if (!disposed && sink && !sink.closed) + sink.error(error) + } + finally { + try { + reader.releaseLock() + } + catch { + // Already released by the stream implementation. + } + } + })() this._boundStreams.set(session.id, { dispose: () => { + disposed = true + reader.cancel('terminal stream disposed').catch(() => {}) if (sink && !sink.closed) sink.close() }, @@ -140,13 +158,47 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { const { exec } = await import('tinyexec') let controller: ReadableStreamDefaultController | undefined + let cp: TinyExecResult | undefined + let runId = 0 + let streamClosed = false + + const closeStream = () => { + if (streamClosed) + return + streamClosed = true + try { + controller?.close() + } + catch { + // The stream may already be closed by cancellation. + } + } + + const errorStream = (error: unknown) => { + if (streamClosed) + return + streamClosed = true + try { + controller?.error(error) + } + catch { + // The stream may already be closed by cancellation. + } + } + const stream = new ReadableStream({ start(_controller) { controller = _controller }, + cancel() { + cp?.kill() + cp = undefined + closeStream() + }, }) function createChildProcess() { + const currentRun = ++runId const cp = exec( executeOptions.command, executeOptions.args || [], @@ -164,15 +216,25 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { ) ;(async () => { - for await (const chunk of cp) { - controller?.enqueue(chunk) + try { + for await (const chunk of cp) { + if (streamClosed || currentRun !== runId) + return + controller?.enqueue(chunk) + } + if (currentRun === runId) + closeStream() + } + catch (error) { + if (currentRun === runId) + errorStream(error) } })() return cp } - let cp: TinyExecResult | undefined = createChildProcess() + cp = createChildProcess() const restart = async () => { cp?.kill() @@ -181,6 +243,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { const terminate = async () => { cp?.kill() cp = undefined + closeStream() } const session: DevToolsChildProcessTerminalSession = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f1064f..03058b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,7 +288,44 @@ importers: specifier: catalog:deps version: 8.20.0 - examples/minimal-vite-devtools-kit: + examples/minimal-next-devtools-hub: + dependencies: + '@devframes/hub': + specifier: workspace:* + version: link:../../packages/hub + devframe: + specifier: workspace:* + version: link:../../packages/devframe + next: + specifier: catalog:frontend + version: 16.2.6(@playwright/test@1.60.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react: + specifier: catalog:frontend + version: 19.2.6 + react-dom: + specifier: catalog:frontend + version: 19.2.6(react@19.2.6) + devDependencies: + '@types/react': + specifier: catalog:types + version: 19.2.15 + '@types/react-dom': + specifier: catalog:types + version: 19.2.3(@types/react@19.2.15) + get-port-please: + specifier: catalog:deps + version: 3.2.0 + pathe: + specifier: catalog:deps + version: 2.0.3 + vitest: + specifier: catalog:testing + version: 4.1.6(@types/node@25.7.0)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4)) + ws: + specifier: catalog:deps + version: 8.20.0 + + examples/minimal-vite-devtools-hub: dependencies: '@devframes/hub': specifier: workspace:* diff --git a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js b/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js index 235aba6..73cbb22 100644 --- a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js @@ -1,15 +1,9 @@ /** * Generated by tsnapi — public API snapshot of `devframe/node/internal` */ -// #region Functions -export function getInternalContext(_) {} -// #endregion - -// #region Variables -export var internalContextMap /* const */ -// #endregion - // #region Other +export { getInternalContext } +export { internalContextMap } export { normalizeBasePath } export { resolveBasePath } // #endregion \ No newline at end of file diff --git a/turbo.json b/turbo.json index 8f119f0..80cfa16 100644 --- a/turbo.json +++ b/turbo.json @@ -15,7 +15,12 @@ "outputLogs": "new-only", "outputs": ["dist/**"] }, - "minimal-vite-devtools-kit-example#build": { + "minimal-vite-devtools-hub#build": { + "outputLogs": "new-only", + "dependsOn": ["@devframes/hub#build", "devframe#build"], + "outputs": ["dist/**"] + }, + "minimal-next-devtools-hub#build": { "outputLogs": "new-only", "dependsOn": ["@devframes/hub#build", "devframe#build"], "outputs": ["dist/**"] diff --git a/vitest.config.ts b/vitest.config.ts index 391bf30..7143519 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,12 +1,18 @@ import { defineConfig } from 'vitest/config' +import { alias } from './alias' export default defineConfig({ + resolve: { + alias, + }, test: { projects: [ 'packages/devframe', + 'packages/hub', 'examples/files-inspector', 'examples/streaming-chat', 'examples/next-runtime-snapshot', + 'examples/minimal-next-devtools-hub', { test: { name: 'tests', From 4dffab8be713754cdddf11c896804e3caeee6f7a Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 22 May 2026 15:32:51 +0900 Subject: [PATCH 3/7] refactor: rename devtools APIs to devframe --- .github/workflows/ecosystem-ci.yml | 2 +- AGENTS.md | 2 +- README.md | 2 +- docs/.vitepress/config.ts | 2 +- docs/adapters/cli.md | 4 +- docs/adapters/embedded.md | 2 +- docs/errors/DF0006.md | 2 +- docs/errors/DF0007.md | 4 +- docs/errors/DF0008.md | 4 +- docs/errors/DF0012.md | 2 +- docs/errors/DF0013.md | 2 +- docs/errors/DF0014.md | 2 +- docs/errors/DF0015.md | 2 +- docs/errors/DF0016.md | 2 +- docs/errors/DF0017.md | 2 +- docs/errors/DF0019.md | 2 +- docs/errors/DF0020.md | 2 +- docs/errors/DF0021.md | 2 +- docs/errors/DF0022.md | 2 +- docs/errors/DF0023.md | 2 +- docs/errors/DF0024.md | 4 +- docs/errors/DF0025.md | 2 +- docs/errors/DF0026.md | 2 +- docs/errors/DF0027.md | 2 +- docs/errors/DF0028.md | 2 +- docs/errors/DF0029.md | 2 +- docs/errors/DF0030.md | 2 +- docs/errors/DF0031.md | 2 +- docs/errors/DF0032.md | 2 +- docs/errors/DF0033.md | 2 +- docs/errors/DF8100.md | 2 +- docs/errors/DF8101.md | 2 +- docs/errors/DF8102.md | 2 +- docs/errors/DF8200.md | 2 +- docs/errors/DF8201.md | 2 +- docs/errors/DF8400.md | 2 +- docs/errors/DF8401.md | 2 +- docs/errors/DF8402.md | 2 +- docs/errors/DF8403.md | 2 +- docs/errors/DF8500.md | 8 +- docs/guide/agent-native.md | 4 +- docs/guide/built-with.md | 2 +- docs/guide/client.md | 10 +- docs/guide/devframe-definition.md | 12 +- docs/guide/diagnostics.md | 8 +- docs/guide/hub.md | 16 +-- docs/guide/index.md | 2 +- docs/guide/rpc.md | 16 +-- docs/guide/shared-state.md | 4 +- docs/helpers/nuxt.md | 2 +- docs/index.md | 2 +- examples/files-inspector/src/client/app.tsx | 4 +- .../src/client/routes/about.tsx | 4 +- .../src/client/routes/home.tsx | 4 +- examples/files-inspector/tests/_utils.ts | 8 +- .../tests/static-build.test.ts | 14 +-- .../tests/static-serve.test.ts | 10 +- .../.gitignore | 0 .../README.md | 12 +- .../package.json | 4 +- .../app/%5F_hub/%5F_connection.json/route.ts | 4 +- .../src/client/app/globals.css | 0 .../src/client/app/layout.tsx | 2 +- .../src/client/app/page.tsx | 34 +++--- .../src/client/devframe}/demo-devframe.ts | 0 .../devframe/minimal-next-devframe-hub.ts} | 38 +++---- .../src/client/next.config.mjs | 0 .../src/client/tsconfig.json | 0 .../tests/minimal-next-devframe-hub.test.ts} | 20 ++-- .../tsconfig.json | 0 .../vitest.config.ts | 0 .../README.md | 10 +- .../index.html | 4 +- .../package.json | 4 +- .../src/client/main.ts | 20 ++-- .../src/client/style.css | 0 .../src/devframe.ts | 0 .../src/minimal-vite-devframe-hub.ts} | 26 ++--- .../tsconfig.json | 0 .../vite.config.ts | 4 +- .../src/client/app/components/connect.tsx | 4 +- .../next-runtime-snapshot/tests/_utils.ts | 8 +- examples/streaming-chat/README.md | 2 +- examples/streaming-chat/src/client/app.tsx | 4 +- examples/streaming-chat/src/devframe.ts | 2 +- examples/streaming-chat/tests/_utils.ts | 12 +- .../tests/streaming-chat.test.ts | 6 +- package.json | 14 +-- packages/devframe/README.md | 2 +- packages/devframe/package.json | 2 +- packages/devframe/src/adapters/build.ts | 20 ++-- packages/devframe/src/adapters/dev.ts | 8 +- packages/devframe/src/adapters/embedded.ts | 4 +- .../adapters/mcp/__tests__/mcp-server.test.ts | 4 +- .../devframe/src/adapters/mcp/build-server.ts | 24 ++-- packages/devframe/src/client/index.ts | 4 +- .../devframe/src/client/rpc-shared-state.ts | 4 +- packages/devframe/src/client/rpc-static.ts | 8 +- packages/devframe/src/client/rpc-streaming.ts | 4 +- packages/devframe/src/client/rpc-ws.ts | 16 +-- packages/devframe/src/client/rpc.ts | 64 +++++------ .../devframe/src/client/static-rpc.test.ts | 12 +- packages/devframe/src/client/static-rpc.ts | 8 +- packages/devframe/src/constants.ts | 20 ++-- packages/devframe/src/define.ts | 4 +- packages/devframe/src/helpers/vite.ts | 4 +- .../src/node/__tests__/host-agent.test.ts | 12 +- .../src/node/__tests__/host-functions.test.ts | 8 +- .../__tests__/rpc-agent-introspection.test.ts | 4 +- .../src/node/__tests__/rpc-streaming.test.ts | 12 +- .../src/node/__tests__/storage.test.ts | 2 +- packages/devframe/src/node/auth/revoke.ts | 6 +- packages/devframe/src/node/auth/state.ts | 4 +- packages/devframe/src/node/context.ts | 24 ++-- packages/devframe/src/node/diagnostics.ts | 2 +- packages/devframe/src/node/host-agent.ts | 16 +-- .../devframe/src/node/host-diagnostics.ts | 12 +- packages/devframe/src/node/host-functions.ts | 30 ++--- packages/devframe/src/node/host-h3.ts | 14 +-- packages/devframe/src/node/host-views.ts | 6 +- .../devframe/src/node/internal/context.ts | 10 +- packages/devframe/src/node/internal/index.ts | 2 +- .../devframe/src/node/rpc-shared-state.ts | 12 +- packages/devframe/src/node/rpc-streaming.ts | 14 +-- packages/devframe/src/node/rpc/index.ts | 2 +- packages/devframe/src/node/server.ts | 16 +-- .../src/rpc/dump/__tests__/static.test.ts | 18 +-- packages/devframe/src/rpc/dump/static.ts | 8 +- .../devframe/src/rpc/transports/ws-client.ts | 2 +- .../devframe/src/rpc/transports/ws-server.ts | 8 +- .../devframe/src/rpc/transports/ws.test.ts | 2 +- packages/devframe/src/types/agent.ts | 14 +-- packages/devframe/src/types/context.ts | 20 ++-- packages/devframe/src/types/devframe.ts | 4 +- packages/devframe/src/types/diagnostics.ts | 12 +- packages/devframe/src/types/host.ts | 16 +-- packages/devframe/src/types/rpc-augments.ts | 6 +- packages/devframe/src/types/rpc.ts | 34 +++--- packages/devframe/src/types/views.ts | 2 +- packages/devframe/test/dts-dedupe.test.ts | 6 +- packages/hub/src/client/client-script.ts | 4 +- packages/hub/src/client/context.ts | 6 +- packages/hub/src/client/docks.ts | 36 +++--- packages/hub/src/client/remote.ts | 20 ++-- packages/hub/src/constants.ts | 8 +- packages/hub/src/define.ts | 12 +- .../src/node/__tests__/host-commands.test.ts | 8 +- .../hub/src/node/__tests__/host-docks.test.ts | 6 +- .../src/node/__tests__/host-messages.test.ts | 4 +- .../src/node/__tests__/host-terminals.test.ts | 8 +- packages/hub/src/node/context.ts | 32 +++--- packages/hub/src/node/diagnostics.ts | 2 +- packages/hub/src/node/host-commands.ts | 34 +++--- packages/hub/src/node/host-docks.ts | 40 +++---- packages/hub/src/node/host-messages.ts | 24 ++-- packages/hub/src/node/host-terminals.ts | 32 +++--- packages/hub/src/node/mount-devframe.ts | 6 +- packages/hub/src/types/commands.ts | 44 ++++---- packages/hub/src/types/docks.ts | 48 ++++---- packages/hub/src/types/index.ts | 20 ++-- packages/hub/src/types/messages.ts | 42 +++---- packages/hub/src/types/settings.ts | 6 +- packages/hub/src/types/terminals.ts | 34 +++--- packages/nuxt/src/runtime/plugin.client.ts | 2 +- packages/nuxt/src/runtime/types.d.ts | 6 +- packages/nuxt/tsdown.config.ts | 4 +- pnpm-lock.yaml | 64 +++++------ pnpm-workspace.yaml | 16 +-- scripts/ecosystem-ci.ts | 2 +- skills/devframe/SKILL.md | 10 +- .../@devframes/hub/client.snapshot.d.ts | 36 +++--- .../tsnapi/@devframes/hub/client.snapshot.js | 4 +- .../@devframes/hub/constants.snapshot.d.ts | 2 +- .../tsnapi/@devframes/hub/index.snapshot.d.ts | 106 +++++++++--------- .../tsnapi/@devframes/hub/node.snapshot.d.ts | 50 ++++----- .../tsnapi/@devframes/hub/node.snapshot.js | 8 +- .../tsnapi/@devframes/hub/types.snapshot.d.ts | 100 ++++++++--------- .../devframe/adapters/embedded.snapshot.d.ts | 2 +- .../tsnapi/devframe/client.snapshot.d.ts | 46 ++++---- .../tsnapi/devframe/client.snapshot.js | 2 +- .../tsnapi/devframe/constants.snapshot.d.ts | 16 +-- .../tsnapi/devframe/constants.snapshot.js | 16 +-- .../tsnapi/devframe/index.snapshot.d.ts | 32 +++--- .../tsnapi/devframe/node.snapshot.d.ts | 48 ++++---- .../tsnapi/devframe/node.snapshot.js | 8 +- .../tsnapi/devframe/node/auth.snapshot.d.ts | 6 +- .../devframe/node/internal.snapshot.d.ts | 2 +- .../rpc/transports/ws-server.snapshot.d.ts | 2 +- .../tsnapi/devframe/types.snapshot.d.ts | 30 ++--- turbo.json | 4 +- vitest.config.ts | 2 +- 191 files changed, 1062 insertions(+), 1062 deletions(-) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/.gitignore (100%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/README.md (77%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/package.json (87%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/app/%5F_hub/%5F_connection.json/route.ts (51%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/app/globals.css (100%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/app/layout.tsx (90%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/app/page.tsx (86%) rename examples/{minimal-next-devtools-hub/src/client/devtools => minimal-next-devframe-hub/src/client/devframe}/demo-devframe.ts (100%) rename examples/{minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts => minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts} (77%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/next.config.mjs (100%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/src/client/tsconfig.json (100%) rename examples/{minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts => minimal-next-devframe-hub/tests/minimal-next-devframe-hub.test.ts} (71%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/tsconfig.json (100%) rename examples/{minimal-next-devtools-hub => minimal-next-devframe-hub}/vitest.config.ts (100%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/README.md (81%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/index.html (93%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/package.json (68%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/src/client/main.ts (88%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/src/client/style.css (100%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/src/devframe.ts (100%) rename examples/{minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts => minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts} (87%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/tsconfig.json (100%) rename examples/{minimal-vite-devtools-hub => minimal-vite-devframe-hub}/vite.config.ts (69%) diff --git a/.github/workflows/ecosystem-ci.yml b/.github/workflows/ecosystem-ci.yml index 5a6dd64..17df18a 100644 --- a/.github/workflows/ecosystem-ci.yml +++ b/.github/workflows/ecosystem-ci.yml @@ -27,7 +27,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm test:ecosystem env: - ECOSYSTEM_DEVTOOLS_REF: ${{ inputs.ref }} + ECOSYSTEM_DEVFRAME_REF: ${{ inputs.ref }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: failure() uses: actions/upload-artifact@v4 diff --git a/AGENTS.md b/AGENTS.md index 3b20b60..2fb3123 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ **`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. -**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devtools-hub/` for a working ~120-line Vite host demonstrating the protocol end to end. +**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. See `examples/minimal-vite-devframe-hub/` for a working ~120-line Vite host demonstrating the protocol end to end. ## Stack & Structure diff --git a/README.md b/README.md index 1c4cbb9..c505d82 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@

    -Framework-neutral foundation for building generic DevTools. +Framework-neutral foundation for building devframes.

    diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a834804..034b98b 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -99,7 +99,7 @@ export function devframeNav(prefix = ''): DefaultTheme.NavItem[] { export default withMermaid(defineConfig({ title: 'Devframe', - description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.', + description: 'Framework-neutral foundation for building generic devframes — RPC layer, hosts, and adapters.', head: [ ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }], ['link', { rel: 'apple-touch-icon', href: '/logo.svg' }], diff --git a/docs/adapters/cli.md b/docs/adapters/cli.md index 076108c..3e363f8 100644 --- a/docs/adapters/cli.md +++ b/docs/adapters/cli.md @@ -26,11 +26,11 @@ Running the resulting binary: my-devframe # dev server at http://localhost:9999/ my-devframe --port 8080 my-devframe build --out-dir dist-static -my-devframe build --out-dir dist-static --base /devtools/ +my-devframe build --out-dir dist-static --base /devframe/ my-devframe mcp # stdio MCP server (experimental) ``` -Standalone CLI serves the SPA at `/` by default. The `/__devtools/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](./#mount-paths). +Standalone CLI serves the SPA at `/` by default. The `/__devframe/` prefix is for *hosted* adapters where devframe mounts alongside an existing app — see [Mount paths](./#mount-paths). ## Options diff --git a/docs/adapters/embedded.md b/docs/adapters/embedded.md index 5d2ae0b..ef9bf64 100644 --- a/docs/adapters/embedded.md +++ b/docs/adapters/embedded.md @@ -15,6 +15,6 @@ await createEmbedded(devframe, { ctx: existingCtx }) | Option | Required | Description | |--------|----------|-------------| -| `ctx` | ✓ | Target `DevToolsNodeContext` the devframe is registered into. | +| `ctx` | ✓ | Target `DevframeNodeContext` the devframe is registered into. | Useful when a host loads devframes based on runtime conditions (feature flags, user opt-in, dynamic discovery) rather than static config. diff --git a/docs/errors/DF0006.md b/docs/errors/DF0006.md index fa848d0..d4d6076 100644 --- a/docs/errors/DF0006.md +++ b/docs/errors/DF0006.md @@ -18,4 +18,4 @@ Register the function with `ctx.rpc.register(defineRpcFunction({ name }))` befor ## Source -- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `RpcFunctionsHost.invokeLocal()` throws `DF0006` when the requested method has not been registered on this host. +- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `RpcFunctionsHost.invokeLocal()` throws `DF0006` when the requested method has not been registered on this host. diff --git a/docs/errors/DF0007.md b/docs/errors/DF0007.md index 7cc5e40..d95e9cd 100644 --- a/docs/errors/DF0007.md +++ b/docs/errors/DF0007.md @@ -6,7 +6,7 @@ outline: deep ## Message -> AsyncLocalStorage is not set, it likely to be an internal bug of the DevTools foundation +> AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation ## Cause @@ -18,4 +18,4 @@ Only call `getCurrentRpcSession()` from RPC handlers executed by the server. Rep ## Source -- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `getCurrentRpcSession()` throws `DF0007` when called outside the RPC dispatch async context. +- [`packages/devframe/src/node/host-functions.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-functions.ts) — `getCurrentRpcSession()` throws `DF0007` when called outside the RPC dispatch async context. diff --git a/docs/errors/DF0008.md b/docs/errors/DF0008.md index 57db674..b423901 100644 --- a/docs/errors/DF0008.md +++ b/docs/errors/DF0008.md @@ -10,7 +10,7 @@ outline: deep ## Cause -`DevToolsViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk. +`DevframeViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk. ## Fix @@ -18,4 +18,4 @@ Verify the `distDir` path resolves correctly (run your SPA build first, and chec ## Source -- [`packages/devframe/src/node/host-views.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-views.ts) — `DevToolsViewHost.hostStatic()` throws `DF0008` when the resolved `distDir` does not exist on disk. +- [`packages/devframe/src/node/host-views.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-views.ts) — `DevframeViewHost.hostStatic()` throws `DF0008` when the resolved `distDir` does not exist on disk. diff --git a/docs/errors/DF0012.md b/docs/errors/DF0012.md index d4f5ae6..7706726 100644 --- a/docs/errors/DF0012.md +++ b/docs/errors/DF0012.md @@ -18,4 +18,4 @@ Delete the file to reset to defaults, or investigate how it became malformed. ## Source -- [`packages/devframe/src/node/storage.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/storage.ts) — `createStorage()` catches `JSON.parse` errors and logs `DF0012` (with the cause attached) before falling back to `initialValue`. +- [`packages/devframe/src/node/storage.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/storage.ts) — `createStorage()` catches `JSON.parse` errors and logs `DF0012` (with the cause attached) before falling back to `initialValue`. diff --git a/docs/errors/DF0013.md b/docs/errors/DF0013.md index 00a94fd..661b593 100644 --- a/docs/errors/DF0013.md +++ b/docs/errors/DF0013.md @@ -18,4 +18,4 @@ Pass `initialValue` on the first call: `ctx.rpc.sharedState.get(key, { initialVa ## Source -- [`packages/devframe/src/node/rpc-shared-state.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-shared-state.ts) — `RpcSharedStateHost.get()` throws `DF0013` when neither an existing entry nor an `initialValue` is provided for a key. +- [`packages/devframe/src/node/rpc-shared-state.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-shared-state.ts) — `RpcSharedStateHost.get()` throws `DF0013` when neither an existing entry nor an `initialValue` is provided for a key. diff --git a/docs/errors/DF0014.md b/docs/errors/DF0014.md index bef3792..4798c8e 100644 --- a/docs/errors/DF0014.md +++ b/docs/errors/DF0014.md @@ -50,4 +50,4 @@ If you didn't intend for this function to be agent-exposed, remove the `agent` f ## Source -- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — agent registration throws `DF0014` when a tool's `agent.description` is missing or empty. +- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — agent registration throws `DF0014` when a tool's `agent.description` is missing or empty. diff --git a/docs/errors/DF0015.md b/docs/errors/DF0015.md index 35ae3e5..26562df 100644 --- a/docs/errors/DF0015.md +++ b/docs/errors/DF0015.md @@ -34,4 +34,4 @@ ctx.agent.registerTool({ id: 'my-tool', /* new config */ }) ## Source -- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerTool()` throws `DF0015` when the tool id collides with an existing agent tool or an RPC function's `agent` exposure. +- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerTool()` throws `DF0015` when the tool id collides with an existing agent tool or an RPC function's `agent` exposure. diff --git a/docs/errors/DF0016.md b/docs/errors/DF0016.md index 1fce205..de41517 100644 --- a/docs/errors/DF0016.md +++ b/docs/errors/DF0016.md @@ -22,4 +22,4 @@ Pick a distinct id or unregister the existing resource first via the handle retu ## Source -- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerResource()` throws `DF0016` when the resource id is already registered on the host. +- [`packages/devframe/src/node/host-agent.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/host-agent.ts) — `ctx.agent.registerResource()` throws `DF0016` when the resource id is already registered on the host. diff --git a/docs/errors/DF0017.md b/docs/errors/DF0017.md index 38aa747..8d60b7c 100644 --- a/docs/errors/DF0017.md +++ b/docs/errors/DF0017.md @@ -28,4 +28,4 @@ The agent-native surface is experimental and may change without a major version ## Source -- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/adapters/mcp/build-server.ts) — `createMcpServer()` throws `DF0017` when the requested transport is unsupported or when the underlying transport fails to `connect()`. +- [`packages/devframe/src/adapters/mcp/build-server.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/adapters/mcp/build-server.ts) — `createMcpServer()` throws `DF0017` when the requested transport is unsupported or when the underlying transport fails to `connect()`. diff --git a/docs/errors/DF0019.md b/docs/errors/DF0019.md index e084aaf..ec950ab 100644 --- a/docs/errors/DF0019.md +++ b/docs/errors/DF0019.md @@ -49,4 +49,4 @@ defineRpcFunction({ ## Source -- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition has `agent` set but is not declared `jsonSerializable: true`. +- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0019` when a definition has `agent` set but is not declared `jsonSerializable: true`. diff --git a/docs/errors/DF0020.md b/docs/errors/DF0020.md index 36395f2..09a883b 100644 --- a/docs/errors/DF0020.md +++ b/docs/errors/DF0020.md @@ -53,4 +53,4 @@ Or convert the payload to a JSON-safe shape (e.g. an array of entries, an ISO st ## Source -- [`packages/devframe/src/rpc/serialization.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/serialization.ts) — the strict JSON serializer throws `DF0020` (with the offending path and runtime type) when a `jsonSerializable: true` payload contains a non-JSON value. +- [`packages/devframe/src/rpc/serialization.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/serialization.ts) — the strict JSON serializer throws `DF0020` (with the offending path and runtime type) when a `jsonSerializable: true` payload contains a non-JSON value. diff --git a/docs/errors/DF0021.md b/docs/errors/DF0021.md index 47159f9..eac91e2 100644 --- a/docs/errors/DF0021.md +++ b/docs/errors/DF0021.md @@ -22,4 +22,4 @@ ctx.rpc.register(defineRpcFunction({ name: 'my-plugin:fn', handler: () => 1 }), ## Source -- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0021` when an RPC name is already registered and `force` is not set. +- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.register()` throws `DF0021` when an RPC name is already registered and `force` is not set. diff --git a/docs/errors/DF0022.md b/docs/errors/DF0022.md index 5941f87..47cc11a 100644 --- a/docs/errors/DF0022.md +++ b/docs/errors/DF0022.md @@ -18,4 +18,4 @@ Call `ctx.rpc.register()` first, or pass `force: true` to `update()` to register ## Source -- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.update()` throws `DF0022` when the named function was never registered. +- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — `RpcFunctionsCollectorBase.update()` throws `DF0022` when the named function was never registered. diff --git a/docs/errors/DF0023.md b/docs/errors/DF0023.md index 5937e06..975972e 100644 --- a/docs/errors/DF0023.md +++ b/docs/errors/DF0023.md @@ -18,4 +18,4 @@ Confirm the function name matches a registration. RPC names are namespaced — t ## Source -- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — collector `get()`/lookup paths throw `DF0023` when consumers ask for a function that has not been registered. +- [`packages/devframe/src/rpc/collector.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/collector.ts) — collector `get()`/lookup paths throw `DF0023` when consumers ask for a function that has not been registered. diff --git a/docs/errors/DF0024.md b/docs/errors/DF0024.md index 07ca9b7..4ecb3cb 100644 --- a/docs/errors/DF0024.md +++ b/docs/errors/DF0024.md @@ -18,5 +18,5 @@ Add either `handler: ...` directly on the definition, or `setup: ctx => ({ handl ## Source -- [`packages/devframe/src/rpc/handler.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/handler.ts) — invocation throws `DF0024` when neither `handler` nor a `setup` returning `{ handler }` is provided. -- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — dump generation also requires a handler and throws `DF0024` if the definition is incomplete. +- [`packages/devframe/src/rpc/handler.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/handler.ts) — invocation throws `DF0024` when neither `handler` nor a `setup` returning `{ handler }` is provided. +- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — dump generation also requires a handler and throws `DF0024` if the definition is incomplete. diff --git a/docs/errors/DF0025.md b/docs/errors/DF0025.md index fa8285b..e0af4b0 100644 --- a/docs/errors/DF0025.md +++ b/docs/errors/DF0025.md @@ -18,4 +18,4 @@ Re-run `createBuild` to regenerate the dump, or check that the call site uses th ## Source -- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0025` when a client calls a function name that is not present in the baked dump store. +- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0025` when a client calls a function name that is not present in the baked dump store. diff --git a/docs/errors/DF0026.md b/docs/errors/DF0026.md index cccd666..f7f4627 100644 --- a/docs/errors/DF0026.md +++ b/docs/errors/DF0026.md @@ -28,4 +28,4 @@ defineRpcFunction({ ## Source -- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0026` when none of the pre-computed inputs matches the call's args and no `fallback` was configured. +- [`packages/devframe/src/rpc/dump/index.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/dump/index.ts) — the static-mode dump resolver throws `DF0026` when none of the pre-computed inputs matches the call's args and no `fallback` was configured. diff --git a/docs/errors/DF0027.md b/docs/errors/DF0027.md index 099ed35..d9da4c3 100644 --- a/docs/errors/DF0027.md +++ b/docs/errors/DF0027.md @@ -18,4 +18,4 @@ Drop the `dump` field, or change the function `type` to `'static'` / `'query'` i ## Source -- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0027` when a `dump` field is attached to an `'action'` or `'event'` function. +- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0027` when a `dump` field is attached to an `'action'` or `'event'` function. diff --git a/docs/errors/DF0028.md b/docs/errors/DF0028.md index a0f840e..d9758fd 100644 --- a/docs/errors/DF0028.md +++ b/docs/errors/DF0028.md @@ -18,4 +18,4 @@ Remove `snapshot: true`, or change the function `type` to `'query'`. ## Source -- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0028` when `snapshot: true` is set on a function whose type is not `'query'`. +- [`packages/devframe/src/rpc/validation.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/rpc/validation.ts) — definition validation throws `DF0028` when `snapshot: true` is set on a function whose type is not `'query'`. diff --git a/docs/errors/DF0029.md b/docs/errors/DF0029.md index a4111b4..f5c41fc 100644 --- a/docs/errors/DF0029.md +++ b/docs/errors/DF0029.md @@ -22,4 +22,4 @@ This is a soft warning — the stream keeps running and remaining chunks still f ## Source -- [`packages/devframe/src/client/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/client/rpc-streaming.ts) — the client subscription queue logs `DF0029` (with the dropped chunk count) when buffered chunks exceed `highWaterMark`. +- [`packages/devframe/src/client/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/client/rpc-streaming.ts) — the client subscription queue logs `DF0029` (with the dropped chunk count) when buffered chunks exceed `highWaterMark`. diff --git a/docs/errors/DF0030.md b/docs/errors/DF0030.md index b7d0d28..2065fcd 100644 --- a/docs/errors/DF0030.md +++ b/docs/errors/DF0030.md @@ -20,4 +20,4 @@ A client subscribed to a stream id that the server-side channel doesn't know abo ## Source -- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — the streaming subscribe/unsubscribe paths log `DF0030` when a client references an `id` that no producer has started (and no replay buffer covers). +- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — the streaming subscribe/unsubscribe paths log `DF0030` when a client references an `id` that no producer has started (and no replay buffer covers). diff --git a/docs/errors/DF0031.md b/docs/errors/DF0031.md index 7a80000..6e9eb10 100644 --- a/docs/errors/DF0031.md +++ b/docs/errors/DF0031.md @@ -33,4 +33,4 @@ catch (err) { ## Source -- [`packages/devframe/src/utils/streaming-channel.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/utils/streaming-channel.ts) — `stream.write()` throws `DF0031` when called after the stream has been closed, errored, or aborted by the consumer. +- [`packages/devframe/src/utils/streaming-channel.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/utils/streaming-channel.ts) — `stream.write()` throws `DF0031` when called after the stream has been closed, errored, or aborted by the consumer. diff --git a/docs/errors/DF0032.md b/docs/errors/DF0032.md index 109fcca..4171fee 100644 --- a/docs/errors/DF0032.md +++ b/docs/errors/DF0032.md @@ -19,4 +19,4 @@ Two calls to `ctx.rpc.streaming.create(name, ...)` used the same channel name. E ## Source -- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — `ctx.rpc.streaming.create()` throws `DF0032` when the requested channel name is already registered on the context. +- [`packages/devframe/src/node/rpc-streaming.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/node/rpc-streaming.ts) — `ctx.rpc.streaming.create()` throws `DF0032` when the requested channel name is already registered on the context. diff --git a/docs/errors/DF0033.md b/docs/errors/DF0033.md index 8bee827..e2468e4 100644 --- a/docs/errors/DF0033.md +++ b/docs/errors/DF0033.md @@ -26,4 +26,4 @@ This is a soft warning — the surrounding Vite dev server keeps running, but th ## Source -- [`packages/devframe/src/helpers/vite.ts`](https://github.com/vitejs/devtools/blob/main/devframe/packages/devframe/src/helpers/vite.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. +- [`packages/devframe/src/helpers/vite.ts`](https://github.com/vitejs/devframe/blob/main/devframe/packages/devframe/src/helpers/vite.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. diff --git a/docs/errors/DF8100.md b/docs/errors/DF8100.md index d8e3945..5b82136 100644 --- a/docs/errors/DF8100.md +++ b/docs/errors/DF8100.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.register()` throws when `views.has(view.id) && !force`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.register()` throws when `views.has(view.id) && !force`. diff --git a/docs/errors/DF8101.md b/docs/errors/DF8101.md index 8135814..af2fcb9 100644 --- a/docs/errors/DF8101.md +++ b/docs/errors/DF8101.md @@ -19,4 +19,4 @@ The `update` handle returned by `ctx.docks.register(view)` received a patch whos ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.register()` returns an `update` callable that throws this when the patch carries a different `id`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.register()` returns an `update` callable that throws this when the patch carries a different `id`. diff --git a/docs/errors/DF8102.md b/docs/errors/DF8102.md index bc5ba85..e9d0b41 100644 --- a/docs/errors/DF8102.md +++ b/docs/errors/DF8102.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevToolsDockHost.update()` throws when `views.has(view.id) === false`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.update()` throws when `views.has(view.id) === false`. diff --git a/docs/errors/DF8200.md b/docs/errors/DF8200.md index 726b7b0..56d775b 100644 --- a/docs/errors/DF8200.md +++ b/docs/errors/DF8200.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevToolsTerminalHost.register()` and `startChildProcess()` throw when the id is already taken. +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalHost.register()` and `startChildProcess()` throw when the id is already taken. diff --git a/docs/errors/DF8201.md b/docs/errors/DF8201.md index cfabfa4..5d4c1d5 100644 --- a/docs/errors/DF8201.md +++ b/docs/errors/DF8201.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevToolsTerminalHost.update()` throws when `sessions.has(patch.id) === false`. +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalHost.update()` throws when `sessions.has(patch.id) === false`. diff --git a/docs/errors/DF8400.md b/docs/errors/DF8400.md index fd093e6..c6f7257 100644 --- a/docs/errors/DF8400.md +++ b/docs/errors/DF8400.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` throws when `commands.has(command.id)`. +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` throws when `commands.has(command.id)`. diff --git a/docs/errors/DF8401.md b/docs/errors/DF8401.md index 8fa88b1..c6b9a09 100644 --- a/docs/errors/DF8401.md +++ b/docs/errors/DF8401.md @@ -19,4 +19,4 @@ The `update` handle returned by `ctx.commands.register(cmd)` received a patch wi ## Source -- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` returns a `update` callable that throws when `'id' in patch`. +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` returns a `update` callable that throws when `'id' in patch`. diff --git a/docs/errors/DF8402.md b/docs/errors/DF8402.md index 1fa3991..cc1e3e3 100644 --- a/docs/errors/DF8402.md +++ b/docs/errors/DF8402.md @@ -20,4 +20,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.execute()` and the `update` handle throw when the id is missing. +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.execute()` and the `update` handle throw when the id is missing. diff --git a/docs/errors/DF8403.md b/docs/errors/DF8403.md index 2685a95..9310fd1 100644 --- a/docs/errors/DF8403.md +++ b/docs/errors/DF8403.md @@ -20,4 +20,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevToolsCommandsHost.register()` and command handle `update()` throw when a command id is duplicated. +- [`packages/hub/src/node/host-commands.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-commands.ts) — `DevframeCommandsHost.register()` and command handle `update()` throw when a command id is duplicated. diff --git a/docs/errors/DF8500.md b/docs/errors/DF8500.md index e03f9d3..48dec37 100644 --- a/docs/errors/DF8500.md +++ b/docs/errors/DF8500.md @@ -14,15 +14,15 @@ A hub built-in command (e.g. `hub:open-path`) was invoked, but the host implemen ## Fix -Implement the matching capability on the `DevToolsHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`: +Implement the matching capability on the `DevframeHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`: ```ts import type { HubHostCapabilities } from '@devframes/hub/node' -import type { DevToolsHost } from 'devframe/types' +import type { DevframeHost } from 'devframe/types' import { launchEditor } from 'devframe/utils/launch-editor' -const host: DevToolsHost & HubHostCapabilities = { - // … existing DevToolsHost methods … +const host: DevframeHost & HubHostCapabilities = { + // … existing DevframeHost methods … async openPath(filepath, line, column) { const target = line ? `${filepath}:${line}${column ? `:${column}` : ''}` : filepath launchEditor(target) diff --git a/docs/guide/agent-native.md b/docs/guide/agent-native.md index 3049c7f..ade90e6 100644 --- a/docs/guide/agent-native.md +++ b/docs/guide/agent-native.md @@ -5,7 +5,7 @@ outline: deep # Agent-Native Devframe ::: warning Experimental -The agent-native surface (`agent` field on `defineRpcFunction`, `DevToolsAgentHost`, and the `devframe/adapters/mcp` adapter) is experimental and may change without a major version bump until it stabilizes. +The agent-native surface (`agent` field on `defineRpcFunction`, `DevframeAgentHost`, and the `devframe/adapters/mcp` adapter) is experimental and may change without a major version bump until it stabilizes. ::: Devframe can expose the same surface a browser UI consumes — RPC functions, resources, and shared state — to coding agents (Claude Desktop / Cursor / Zed / Claude Code, or any MCP-speaking client). Agent exposure is opt-in per function; functions stay private by default. @@ -15,7 +15,7 @@ Devframe can expose the same surface a browser UI consumes — RPC functions, re Three building blocks: 1. **An `agent` field on `defineRpcFunction`.** Add `agent: { description, ... }` to opt a function in. Functions without the field stay private. -2. **`ctx.agent`** — a host exposed on `DevToolsNodeContext`. Plugins register tools that aren't backed by an RPC, and expose readable resources (e.g. a Markdown build summary). +2. **`ctx.agent`** — a host exposed on `DevframeNodeContext`. Plugins register tools that aren't backed by an RPC, and expose readable resources (e.g. a Markdown build summary). 3. **The MCP adapter** (`devframe/adapters/mcp`) — translates the agent host into a [Model Context Protocol](https://modelcontextprotocol.io) server, currently over `stdio`. ## Exposing an RPC function diff --git a/docs/guide/built-with.md b/docs/guide/built-with.md index 7c8372b..45f7b8d 100644 --- a/docs/guide/built-with.md +++ b/docs/guide/built-with.md @@ -4,7 +4,7 @@ outline: deep # Built with Devframe -Real-world devtools shipping on Devframe: +Real-world devframes: - [**Vite DevTools**](https://devtools.vite.dev/) — the host that bundles multiple devframes into one UI (docks, command palette, terminals). Mount your own definition into it via the [`vite` adapter](/adapters/vite). - [**ESLint Config Inspector**](https://github.com/eslint/config-inspector) — official ESLint tool for inspecting flat configs. diff --git a/docs/guide/client.md b/docs/guide/client.md index 215db22..7d3a286 100644 --- a/docs/guide/client.md +++ b/docs/guide/client.md @@ -8,7 +8,7 @@ The browser-side client is how a dock iframe, remote-hosted page, or standalone ## Connecting -`devframe/client` exports `connectDevframe` (an alias of `getDevToolsRpcClient`) — use either name: +`devframe/client` exports `connectDevframe` (an alias of `getDevframeRpcClient`) — use either name: ```ts import { connectDevframe } from 'devframe/client' @@ -18,7 +18,7 @@ const rpc = await connectDevframe() const modules = await rpc.call('my-devframe:get-modules', { limit: 10 }) ``` -`connectDevframe` auto-detects the backend via `__devtools/__connection.json`, with a sequence of base URLs as fallback. No arguments are needed when the client is hosted from the default mount path. +`connectDevframe` auto-detects the backend via `__devframe/__connection.json`, with a sequence of base URLs as fallback. No arguments are needed when the client is hosted from the default mount path. ### Runtime basePath discovery @@ -46,7 +46,7 @@ await connectDevframe({ | Option | Description | |--------|-------------| -| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devtools/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. | +| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devframe/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. | | `authToken` | Override the auth token. Defaults to a locally-persisted human-readable id. | | `cacheOptions` | `true` to enable caching with defaults, or an options object. | | `wsOptions` | Forwarded to the WebSocket transport (reconnect, heartbeat, etc.). | @@ -55,7 +55,7 @@ await connectDevframe({ ## Modes -The client runs in one of two modes depending on the backend advertised in `__devtools/__connection.json`: +The client runs in one of two modes depending on the backend advertised in `__devframe/__connection.json`: | Backend | When | Capabilities | |---------|------|--------------| @@ -89,7 +89,7 @@ const ok = await rpc.requestTrustWithToken('another-token') ### Broadcast-channel sync -`connectDevframe` listens on a shared `BroadcastChannel` (named `vite-devtools-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When an auth page in another tab announces a new token, every open client requests trust with it automatically — no reload required. +`connectDevframe` listens on a shared `BroadcastChannel` (named `devframe-auth` for cross-tab handshake interop with Vite DevTools' auth page) for `auth-update` messages. When an auth page in another tab announces a new token, every open client requests trust with it automatically — no reload required. ## Calling functions diff --git a/docs/guide/devframe-definition.md b/docs/guide/devframe-definition.md index 27cddc8..8a51903 100644 --- a/docs/guide/devframe-definition.md +++ b/docs/guide/devframe-definition.md @@ -68,19 +68,19 @@ The CLI dev server sets `mode: 'dev'`; `createBuild` sets `mode: 'build'`. ## The setup context -`setup(ctx)` receives a `DevToolsNodeContext`: +`setup(ctx)` receives a `DevframeNodeContext`: ```ts -interface DevToolsNodeContext { +interface DevframeNodeContext { readonly cwd: string readonly workspaceRoot: string readonly mode: 'dev' | 'build' - host: DevToolsHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir) + host: DevframeHost // runtime abstraction (mountStatic / resolveOrigin / getStorageDir) rpc: RpcFunctionsHost // register + broadcast + sharedState - views: DevToolsViewHost // static file hosting (`hostStatic`) - diagnostics: DevToolsDiagnosticsHost - agent: DevToolsAgentHost // experimental + views: DevframeViewHost // static file hosting (`hostStatic`) + diagnostics: DevframeDiagnosticsHost + agent: DevframeAgentHost // experimental } ``` diff --git a/docs/guide/diagnostics.md b/docs/guide/diagnostics.md index bdbed29..5775225 100644 --- a/docs/guide/diagnostics.md +++ b/docs/guide/diagnostics.md @@ -4,7 +4,7 @@ outline: deep # Structured Diagnostics -`ctx.diagnostics` is a thin layer over [`nostics`](https://www.npmjs.com/package/nostics) that lets integrations register coded errors and warnings into a shared lookup without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — with a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the DevTools UI, use [`ctx.messages`](https://devtools.vite.dev/kit/messages). +`ctx.diagnostics` is a thin layer over [`nostics`](https://www.npmjs.com/package/nostics) that lets integrations register coded errors and warnings into a shared lookup without depending on `nostics` directly. Use it for author-defined coded diagnostics — errors, warnings, deprecations — with a stable code, a documentation URL, and a structured payload. For free-form runtime output that should appear in the Devframe UI, use [`ctx.messages`](https://devtools.vite.dev/kit/messages). | Surface | Purpose | Example | |---------|---------|---------| @@ -14,7 +14,7 @@ outline: deep ## Shape ```ts -interface DevToolsDiagnosticsHost { +interface DevframeDiagnosticsHost { /** Proxy-backed lookup over every registered code. */ readonly logger: Record @@ -135,6 +135,6 @@ Each page covers the message, cause, example, and fix — see any [DF code page] ## When to use what - **`ctx.diagnostics`** — coded conditions worth looking up: misconfiguration, deprecations, validation failures, internal invariants. Always docs-backed. Often thrown. -- **`ctx.messages`** — user-facing activity surfaces in the DevTools UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level. +- **`ctx.messages`** — user-facing activity surfaces in the Devframe UI: progress indicators, audit results, "URL copied" toasts. Just a message and a level. -Diagnostics target tool authors and CI; messages target the human in front of the DevTools panel. +Diagnostics target tool authors and CI; messages target the human in front of the Devframe panel. diff --git a/docs/guide/hub.md b/docs/guide/hub.md index d045d03..ccead1a 100644 --- a/docs/guide/hub.md +++ b/docs/guide/hub.md @@ -11,7 +11,7 @@ outline: deep ## What the hub adds -A hub-aware node context (`HubNodeContext`) extends `DevToolsNodeContext` with four subsystems: +A hub-aware node context (`HubNodeContext`) extends `DevframeNodeContext` with four subsystems: | Subsystem | Surface | Purpose | |---|---|---| @@ -31,7 +31,7 @@ Every hub context auto-registers these RPC functions so framework kits don't rei ## Host capabilities -A hub host implements the same `DevToolsHost` interface as devframe, plus optional capabilities the hub knows how to delegate to: +A hub host implements the same `DevframeHost` interface as devframe, plus optional capabilities the hub knows how to delegate to: ```ts interface HubHostCapabilities { @@ -44,10 +44,10 @@ A framework kit's host implementation looks like this: ```ts import type { HubHostCapabilities } from '@devframes/hub/node' -import type { DevToolsHost } from 'devframe/types' +import type { DevframeHost } from 'devframe/types' import { launchEditor } from 'devframe/utils/launch-editor' -const host: DevToolsHost & HubHostCapabilities = { +const host: DevframeHost & HubHostCapabilities = { mountStatic(base, distDir) { /* … */ }, resolveOrigin() { /* … */ }, getStorageDir(scope) { /* … */ }, @@ -80,16 +80,16 @@ A hub-aware UI doesn't import any hub classes; it reads three shared-state keys | Channel | Type | What it carries | |---|---|---| -| `devframe:docks` shared state | `DevToolsDockEntry[]` | The full dock list, including the hub's `~terminals` / `~messages` / `~settings` builtins. | -| `devframe:commands` shared state | `DevToolsServerCommandEntry[]` | Serializable command list (handlers stripped). | -| `devframe:user-settings` shared state | `DevToolsDocksUserSettings` | Persisted per-workspace hub settings. | +| `devframe:docks` shared state | `DevframeDockEntry[]` | The full dock list, including the hub's `~terminals` / `~messages` / `~settings` builtins. | +| `devframe:commands` shared state | `DevframeServerCommandEntry[]` | Serializable command list (handlers stripped). | +| `devframe:user-settings` shared state | `DevframeDocksUserSettings` | Persisted per-workspace hub settings. | | `hub:commands:execute` RPC | `(id, ...args) => unknown` | Server-side command dispatch. | Plus broadcast notifications (`devframe:terminals:updated`, `devframe:messages:updated`) that a UI can subscribe to via `rpc.client.register(...)`. ## Example -See [`examples/minimal-vite-devtools-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devtools-hub) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub host follows the same shape: a thin layer that adapts the framework's dev server to the hub. +See [`examples/minimal-vite-devframe-hub/`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub) for a ~120-line Vite plugin that wires the hub end to end with a vanilla DOM UI. Every framework's hub host follows the same shape: a thin layer that adapts the framework's dev server to the hub. ## Diagnostics diff --git a/docs/guide/index.md b/docs/guide/index.md index 1c830e3..871eea0 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -104,7 +104,7 @@ Devframe has zero dependencies on Vite or any `@vitejs/*` package — the same d ## What's next -- [Devframe Definition](./devframe-definition) — understand `defineDevframe` and the `DevToolsNodeContext` +- [Devframe Definition](./devframe-definition) — understand `defineDevframe` and the `DevframeNodeContext` - [Adapters](/adapters/) — pick the right deployment target for your tool - [RPC](./rpc) — define type-safe server functions your client can call - [Agent-Native](./agent-native) — expose your devframe to Claude Desktop, Cursor, or any MCP client diff --git a/docs/guide/rpc.md b/docs/guide/rpc.md index 9bb164f..100eec3 100644 --- a/docs/guide/rpc.md +++ b/docs/guide/rpc.md @@ -31,7 +31,7 @@ export const getModules = defineRpcFunction({ returns: v.array(v.object({ id: v.string(), size: v.number() })), setup: ctx => ({ handler: async ({ limit }) => { - // `ctx` is the DevToolsNodeContext. + // `ctx` is the DevframeNodeContext. return loadModules().slice(0, limit) }, }), @@ -93,7 +93,7 @@ Prefer a single object argument (`args: [v.object({ ... })]`) over positional ar Two ways to wire a handler: -- **`setup(ctx)`** — receives the `DevToolsNodeContext` and returns `{ handler, dump? }`. Use this when you need the context (shared state, logs, `ctx.mode`, etc.). +- **`setup(ctx)`** — receives the `DevframeNodeContext` and returns `{ handler, dump? }`. Use this when you need the context (shared state, logs, `ctx.mode`, etc.). - **`handler(...)`** — shorthand when the handler is pure and doesn't touch the context. ```ts @@ -165,7 +165,7 @@ const modules = await ctx.rpc.invokeLocal('my-devframe:get-modules', { limit: 10 ## Client-side calls -From the browser, [`connectDevframe`](./client) (or `getDevToolsRpcClient`) returns a client for calling registered functions: +From the browser, [`connectDevframe`](./client) (or `getDevframeRpcClient`) returns a client for calling registered functions: ```ts import { connectDevframe } from 'devframe/client' @@ -179,7 +179,7 @@ Client-side registration (for server→client calls) goes through `rpc.client.re ## Type-safe client registry -Devframe exposes two augmentable interfaces — `DevToolsRpcServerFunctions` (client→server calls) and `DevToolsRpcClientFunctions` (server→client calls) — so each registered RPC name shows up on the typed client. Augment them once per devframe via `declare module 'devframe'`. +Devframe exposes two augmentable interfaces — `DevframeRpcServerFunctions` (client→server calls) and `DevframeRpcClientFunctions` (server→client calls) — so each registered RPC name shows up on the typed client. Augment them once per devframe via `declare module 'devframe'`. The recommended pattern collects every server-side definition into a const array and feeds it through `RpcDefinitionsToFunctions`: @@ -190,7 +190,7 @@ import { getFile, getModules } from './rpc' const serverFunctions = [getModules, getFile] as const declare module 'devframe' { - interface DevToolsRpcServerFunctions + interface DevframeRpcServerFunctions extends RpcDefinitionsToFunctions {} } ``` @@ -211,13 +211,13 @@ For one-off augmentations, declare a single key with `RpcFunctionDefinitionToFun import type { RpcFunctionDefinitionToFunction } from 'devframe/rpc' declare module 'devframe' { - interface DevToolsRpcServerFunctions { + interface DevframeRpcServerFunctions { 'my-devframe:get-modules': RpcFunctionDefinitionToFunction } } ``` -For server→client calls invoked via `ctx.rpc.broadcast`, augment `DevToolsRpcClientFunctions` the same way. +For server→client calls invoked via `ctx.rpc.broadcast`, augment `DevframeRpcClientFunctions` the same way. ## Static dumps @@ -262,7 +262,7 @@ Devframe's WS transport ships payloads using one of two encoders, picked per RPC | `false` (default) | `structured-clone-es` | `s:` | `Map`, `Set`, `Date`, `BigInt`, cycles, class instances | | `true` (opt-in) | strict `JSON.stringify` | _(unprefixed)_ | JSON-only | -The wire stays plain JSON when every participating function is JSON-flagged — debuggable in DevTools, friendly to MCP, and a good default for tools that already speak JSON. +The wire stays plain JSON when every participating function is JSON-flagged — debuggable in Devframe, friendly to MCP, and a good default for tools that already speak JSON. ### Discovering shape errors during dev diff --git a/docs/guide/shared-state.md b/docs/guide/shared-state.md index ed01a75..074d9f2 100644 --- a/docs/guide/shared-state.md +++ b/docs/guide/shared-state.md @@ -139,11 +139,11 @@ Protocol adapters (the [MCP adapter](./agent-native), for example) use this to s ## Type-safe keys -Augment `DevToolsRpcSharedStates` to type each shared-state key once, then both server and client lookups stay typed without per-call generics: +Augment `DevframeRpcSharedStates` to type each shared-state key once, then both server and client lookups stay typed without per-call generics: ```ts declare module 'devframe' { - interface DevToolsRpcSharedStates { + interface DevframeRpcSharedStates { 'my-devframe:state': { count: number items: { id: string, name: string }[] diff --git a/docs/helpers/nuxt.md b/docs/helpers/nuxt.md index fb484d8..e70da89 100644 --- a/docs/helpers/nuxt.md +++ b/docs/helpers/nuxt.md @@ -11,7 +11,7 @@ It handles the four things every Nuxt-powered standalone devtool needs: 1. **Base-agnostic assets.** Sets `app.baseURL: './'` and `vite.base: './'` so the same production build works at `/`, `/tool/`, and any other deployment path without build-time URL rewriting. 2. **Runtime RPC connection.** Adds a client plugin that calls [`connectDevframe()`](/guide/client) once on page load and provides the result as `$rpc` on the Nuxt app. 3. **Dev-time RPC bridge.** When you pass `devframe`, `nuxt dev` spins up a separate WebSocket RPC server and serves `__connection.json` so the SPA can reach it — no hand-rolled Vite plugin required. -4. **TypeScript augmentation.** `useNuxtApp().$rpc` is typed as `DevToolsRpcClient` out of the box. +4. **TypeScript augmentation.** `useNuxtApp().$rpc` is typed as `DevframeRpcClient` out of the box. ## Install diff --git a/docs/index.md b/docs/index.md index cf5494f..c5d9e19 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ layout: home hero: name: Devframe - text: Framework-neutral foundation for DevTools + text: Framework-neutral foundation for Devframe tagline: One devframe definition, adapters to different environments. Managed communication layer, agent-native. image: src: /logo.svg diff --git a/examples/files-inspector/src/client/app.tsx b/examples/files-inspector/src/client/app.tsx index 59cfcd4..e2d8afa 100644 --- a/examples/files-inspector/src/client/app.tsx +++ b/examples/files-inspector/src/client/app.tsx @@ -1,4 +1,4 @@ -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' import { connectDevframe } from 'devframe/client' import { useEffect, useState } from 'preact/hooks' import { About } from './routes/about' @@ -19,7 +19,7 @@ function getRoute(basePath: string): string { export function App() { const basePath = getBasePath() const [route, setRoute] = useState(getRoute(basePath)) - const [rpc, setRpc] = useState(null) + const [rpc, setRpc] = useState(null) useEffect(() => { let cancelled = false diff --git a/examples/files-inspector/src/client/routes/about.tsx b/examples/files-inspector/src/client/routes/about.tsx index f6cfd0c..26ae30b 100644 --- a/examples/files-inspector/src/client/routes/about.tsx +++ b/examples/files-inspector/src/client/routes/about.tsx @@ -1,7 +1,7 @@ -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' import { useEffect, useState } from 'preact/hooks' -export function About({ rpc, basePath }: { rpc: DevToolsRpcClient, basePath: string }) { +export function About({ rpc, basePath }: { rpc: DevframeRpcClient, basePath: string }) { const [cwd, setCwd] = useState('') useEffect(() => { diff --git a/examples/files-inspector/src/client/routes/home.tsx b/examples/files-inspector/src/client/routes/home.tsx index 5f7dd5e..d10ea2b 100644 --- a/examples/files-inspector/src/client/routes/home.tsx +++ b/examples/files-inspector/src/client/routes/home.tsx @@ -1,7 +1,7 @@ -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' import { useEffect, useState } from 'preact/hooks' -export function Home({ rpc }: { rpc: DevToolsRpcClient }) { +export function Home({ rpc }: { rpc: DevframeRpcClient }) { const [files, setFiles] = useState([]) const [loading, setLoading] = useState(true) diff --git a/examples/files-inspector/tests/_utils.ts b/examples/files-inspector/tests/_utils.ts index 4e1f170..269a609 100644 --- a/examples/files-inspector/tests/_utils.ts +++ b/examples/files-inspector/tests/_utils.ts @@ -5,10 +5,10 @@ import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' import { - DEVTOOLS_CONNECTION_META_FILENAME, + DEVFRAME_CONNECTION_META_FILENAME, } from 'devframe/constants' import { - createH3DevToolsHost, + createH3DevframeHost, createHostContext, startHttpAndWs, } from 'devframe/node' @@ -68,7 +68,7 @@ export async function startInspectorServer( const app = new H3() const origin = `http://${host}:${port}` - const h3Host = createH3DevToolsHost({ + const h3Host = createH3DevframeHost({ origin, appName: devframe.id, mount: (base, dir) => { @@ -79,7 +79,7 @@ export async function startInspectorServer( const ctx = await createHostContext({ cwd, mode: 'dev', host: h3Host }) await devframe.setup(ctx) - const metaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}` + const metaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}` app.use(metaPath, () => ({ backend: 'websocket', websocket: port })) mountStaticHandler(app, basePath, resolve(distDir)) diff --git a/examples/files-inspector/tests/static-build.test.ts b/examples/files-inspector/tests/static-build.test.ts index 4179485..97302ca 100644 --- a/examples/files-inspector/tests/static-build.test.ts +++ b/examples/files-inspector/tests/static-build.test.ts @@ -4,8 +4,8 @@ import os from 'node:os' import path from 'node:path' import { createBuild } from 'devframe/adapters/build' import { - DEVTOOLS_CONNECTION_META_FILENAME, - DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME, + DEVFRAME_CONNECTION_META_FILENAME, + DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, } from 'devframe/constants' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import devframe from '../src/devframe' @@ -53,24 +53,24 @@ describe('static build (CLI build surface)', () => { }) it('writes a static-backend connection meta next to index.html', async () => { - // The meta sits at the SPA root (not under `__devtools/`) so any + // The meta sits at the SPA root (not under `__devframe/`) so any // generic static file server (`serve`, `nginx`, `python -m http.server`) // can serve it as a flat tree without nested-dir exclusions. const meta = JSON.parse( await readFile( - path.join(outBuild, DEVTOOLS_CONNECTION_META_FILENAME), + path.join(outBuild, DEVFRAME_CONNECTION_META_FILENAME), 'utf-8', ), ) as { backend: string } expect(meta).toMatchObject({ backend: 'static' }) - // Guard the design: nothing should land under a `__devtools/` subdir. - expect(existsSync(path.join(outBuild, '__devtools'))).toBe(false) + // Guard the design: nothing should land under a `__devframe/` subdir. + expect(existsSync(path.join(outBuild, '__devframe'))).toBe(false) }) it('dumps both RPC functions into the manifest', async () => { const manifest = JSON.parse( await readFile( - path.join(outBuild, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), + path.join(outBuild, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME), 'utf-8', ), ) as DumpManifest diff --git a/examples/files-inspector/tests/static-serve.test.ts b/examples/files-inspector/tests/static-serve.test.ts index 143fb83..c35da41 100644 --- a/examples/files-inspector/tests/static-serve.test.ts +++ b/examples/files-inspector/tests/static-serve.test.ts @@ -88,8 +88,8 @@ describe('static serve (deployed SPA contract)', () => { // This is the path the SPA fetches via relative `./__connection.json` // resolved against `document.baseURI`. The 404 the user originally // reported with `serve dist-static` was caused by the old layout - // putting this file under `/__devtools/__connection.json`, which a - // SPA at any non-`/__devtools/` mount could not discover. + // putting this file under `/__devframe/__connection.json`, which a + // SPA at any non-`/__devframe/` mount could not discover. const res = await fetch(`${server.origin}${mountBase}__connection.json`) expect(res.status).toBe(200) const meta = await res.json() as { backend: string } @@ -120,11 +120,11 @@ describe('static serve (deployed SPA contract)', () => { expect(record.output).toEqual(['README.md', 'package.json', 'sample.txt']) }) - it('does not expose a stray `__devtools/` directory at the SPA root', async () => { + it('does not expose a stray `__devframe/` directory at the SPA root', async () => { // Regression guard: the build output is intentionally flat — - // re-introducing a `__devtools/` subdir would create a nested + // re-introducing a `__devframe/` subdir would create a nested // path the relative-base discovery in the SPA cannot reach. - const res = await fetch(`${server.origin}${mountBase}__devtools/__connection.json`) + const res = await fetch(`${server.origin}${mountBase}__devframe/__connection.json`) expect(res.status).toBe(404) }) }) diff --git a/examples/minimal-next-devtools-hub/.gitignore b/examples/minimal-next-devframe-hub/.gitignore similarity index 100% rename from examples/minimal-next-devtools-hub/.gitignore rename to examples/minimal-next-devframe-hub/.gitignore diff --git a/examples/minimal-next-devtools-hub/README.md b/examples/minimal-next-devframe-hub/README.md similarity index 77% rename from examples/minimal-next-devtools-hub/README.md rename to examples/minimal-next-devframe-hub/README.md index d0fd459..5f6be28 100644 --- a/examples/minimal-next-devtools-hub/README.md +++ b/examples/minimal-next-devframe-hub/README.md @@ -1,12 +1,12 @@ -# Minimal Next DevTools Hub +# Minimal Next Devframe Hub -A protocol-witness example. The `src/client/devtools/minimal-next-devtools-hub.ts` file wires `@devframes/hub` into a Next.js App Router app by lazily starting a side-car RPC/WS server from a Node route handler. +A protocol-witness example. The `src/client/devframe/minimal-next-devframe-hub.ts` file wires `@devframes/hub` into a Next.js App Router app by lazily starting a side-car RPC/WS server from a Node route handler. ## Run it ```sh pnpm install -pnpm --filter minimal-next-devtools-hub dev +pnpm --filter minimal-next-devframe-hub dev ``` Open the printed URL. You should see: @@ -21,7 +21,7 @@ Open the printed URL. You should see: ## What the example proves - `createHubContext()` boots a hub without any Vite-specific code path -- A `DevToolsHost & HubHostCapabilities` impl plugs Next host specifics into the hub uniformly +- A `DevframeHost & HubHostCapabilities` impl plugs Next host specifics into the hub uniformly - `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock - Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed - The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json` @@ -30,7 +30,7 @@ Open the printed URL. You should see: | File | Role | |---|---| -| `src/client/devtools/minimal-next-devtools-hub.ts` | The Next host — creates hub context and side-car WS | +| `src/client/devframe/minimal-next-devframe-hub.ts` | The Next host — creates hub context and side-car WS | | `src/client/app/%5F_hub/%5F_connection.json/route.ts` | Connection-meta endpoint for `/__hub/__connection.json` that starts the singleton host | -| `src/client/devtools/demo-devframe.ts` | A sample `DevframeDefinition` that plugs into the host | +| `src/client/devframe/demo-devframe.ts` | A sample `DevframeDefinition` that plugs into the host | | `src/client/app/page.tsx` | The browser-side UI that consumes the hub protocol | diff --git a/examples/minimal-next-devtools-hub/package.json b/examples/minimal-next-devframe-hub/package.json similarity index 87% rename from examples/minimal-next-devtools-hub/package.json rename to examples/minimal-next-devframe-hub/package.json index d5f7997..f2ef7e8 100644 --- a/examples/minimal-next-devtools-hub/package.json +++ b/examples/minimal-next-devframe-hub/package.json @@ -1,9 +1,9 @@ { - "name": "minimal-next-devtools-hub", + "name": "minimal-next-devframe-hub", "type": "module", "version": "0.4.1", "private": true, - "description": "Protocol-witness example — a tiny Next.js DevTools Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", + "description": "Protocol-witness example — a tiny Next.js Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", "scripts": { "dev": "next dev src/client", "build": "next build src/client", diff --git a/examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts b/examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts similarity index 51% rename from examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts rename to examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts index 610ffd5..96724f7 100644 --- a/examples/minimal-next-devtools-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts +++ b/examples/minimal-next-devframe-hub/src/client/app/%5F_hub/%5F_connection.json/route.ts @@ -1,9 +1,9 @@ -import { ensureMinimalNextDevToolsHub } from '../../../devtools/minimal-next-devtools-hub' +import { ensureMinimalNextDevframeHub } from '../../../devframe/minimal-next-devframe-hub' export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export async function GET() { - const hub = await ensureMinimalNextDevToolsHub() + const hub = await ensureMinimalNextDevframeHub() return Response.json(hub.connectionMeta) } diff --git a/examples/minimal-next-devtools-hub/src/client/app/globals.css b/examples/minimal-next-devframe-hub/src/client/app/globals.css similarity index 100% rename from examples/minimal-next-devtools-hub/src/client/app/globals.css rename to examples/minimal-next-devframe-hub/src/client/app/globals.css diff --git a/examples/minimal-next-devtools-hub/src/client/app/layout.tsx b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx similarity index 90% rename from examples/minimal-next-devtools-hub/src/client/app/layout.tsx rename to examples/minimal-next-devframe-hub/src/client/app/layout.tsx index 37e8bdc..5090a20 100644 --- a/examples/minimal-next-devtools-hub/src/client/app/layout.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/layout.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react' import './globals.css' export const metadata: Metadata = { - title: 'Minimal Next DevTools Hub', + title: 'Minimal Next Devframe Hub', description: 'A Next.js host for the @devframes/hub protocol.', } diff --git a/examples/minimal-next-devtools-hub/src/client/app/page.tsx b/examples/minimal-next-devframe-hub/src/client/app/page.tsx similarity index 86% rename from examples/minimal-next-devtools-hub/src/client/app/page.tsx rename to examples/minimal-next-devframe-hub/src/client/app/page.tsx index 6dccb94..c981000 100644 --- a/examples/minimal-next-devtools-hub/src/client/app/page.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/page.tsx @@ -1,11 +1,11 @@ 'use client' -import type { DevToolsRpcClient } from '@devframes/hub/client' +import type { DevframeRpcClient } from '@devframes/hub/client' import type { - DevToolsCommandEntry, - DevToolsDockEntry, - DevToolsMessageEntry, - DevToolsTerminalSession, + DevframeCommandEntry, + DevframeDockEntry, + DevframeMessageEntry, + DevframeTerminalSession, } from '@devframes/hub/types' import type { ReactNode } from 'react' import { connectDevframe } from '@devframes/hub/client' @@ -18,17 +18,17 @@ interface Status { kind?: 'ready' | 'error' } -type TerminalSummary = Pick +type TerminalSummary = Pick export default function Page() { const [status, setStatus] = useState({ text: 'Connecting...' }) - const [docks, setDocks] = useState([]) - const [commands, setCommands] = useState([]) - const [messages, setMessages] = useState([]) + const [docks, setDocks] = useState([]) + const [commands, setCommands] = useState([]) + const [messages, setMessages] = useState([]) const [terminals, setTerminals] = useState([]) const [openPathResult, setOpenPathResult] = useState('Test hub:open-path on this README') const [pingResult, setPingResult] = useState('Run ping') - const rpcRef = useRef(null) + const rpcRef = useRef(null) useEffect(() => { let cancelled = false @@ -43,11 +43,11 @@ export default function Page() { rpcRef.current = rpc setStatus({ text: `Connected: backend=${rpc.connectionMeta.backend}`, kind: 'ready' }) - const docksState = await rpc.sharedState.get( + const docksState = await rpc.sharedState.get( 'devframe:docks', { initialValue: [] }, ) - const commandsState = await rpc.sharedState.get( + const commandsState = await rpc.sharedState.get( 'devframe:commands', { initialValue: [] }, ) @@ -61,15 +61,15 @@ export default function Page() { const refreshMessages = async () => { const entries = await rpc.call( - 'minimal-next-devtools-hub:messages:list' as any, - ) as DevToolsMessageEntry[] + 'minimal-next-devframe-hub:messages:list' as any, + ) as DevframeMessageEntry[] if (!cancelled) setMessages(entries) } const refreshTerminals = async () => { const sessions = await rpc.call( - 'minimal-next-devtools-hub:terminals:list' as any, + 'minimal-next-devframe-hub:terminals:list' as any, ) as TerminalSummary[] if (!cancelled) setTerminals(sessions) @@ -122,7 +122,7 @@ export default function Page() { try { const result = await rpcRef.current.call( 'hub:commands:execute' as any, - 'minimal-next-devtools-hub:ping', + 'minimal-next-devframe-hub:ping', ) setPingResult(`Ping returned ${JSON.stringify(result)}`) } @@ -134,7 +134,7 @@ export default function Page() { return (

    -

    Minimal Next DevTools Hub

    +

    Minimal Next Devframe Hub

    Protocol witness: verifies {' '} diff --git a/examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts b/examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts similarity index 100% rename from examples/minimal-next-devtools-hub/src/client/devtools/demo-devframe.ts rename to examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts diff --git a/examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts similarity index 77% rename from examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts rename to examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts index 842626b..897c13b 100644 --- a/examples/minimal-next-devtools-hub/src/client/devtools/minimal-next-devtools-hub.ts +++ b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts @@ -1,6 +1,6 @@ import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' import type { StartedServer } from 'devframe/node' -import type { ConnectionMeta, DevframeDefinition, DevToolsHost } from 'devframe/types' +import type { ConnectionMeta, DevframeDefinition, DevframeHost } from 'devframe/types' import { homedir } from 'node:os' import process from 'node:process' import { defineRpcFunction } from '@devframes/hub' @@ -11,7 +11,7 @@ import { getPort } from 'get-port-please' import { join } from 'pathe' import demoDevframe from './demo-devframe' -export interface MinimalNextDevToolsHubOptions { +export interface MinimalNextDevframeHubOptions { /** Preferred port for the side-car RPC/WS server. Default: a free port near 9877. */ port?: number /** Hostname for the side-car server. Default: `localhost`. */ @@ -22,13 +22,13 @@ export interface MinimalNextDevToolsHubOptions { devframes?: DevframeDefinition[] } -export interface StartedMinimalNextDevToolsHub extends StartedServer { +export interface StartedMinimalNextDevframeHub extends StartedServer { context: HubNodeContext connectionMeta: ConnectionMeta & { backend: 'websocket', websocket: number } } const minimalNextHubMessagesList = defineRpcFunction({ - name: 'minimal-next-devtools-hub:messages:list', + name: 'minimal-next-devframe-hub:messages:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -39,7 +39,7 @@ const minimalNextHubMessagesList = defineRpcFunction({ }) const minimalNextHubTerminalsList = defineRpcFunction({ - name: 'minimal-next-devtools-hub:terminals:list', + name: 'minimal-next-devframe-hub:terminals:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -54,13 +54,13 @@ const minimalNextHubTerminalsList = defineRpcFunction({ }), }) -export async function minimalNextDevToolsHub( - options: MinimalNextDevToolsHubOptions = {}, -): Promise { +export async function minimalNextDevframeHub( + options: MinimalNextDevframeHubOptions = {}, +): Promise { const cwd = options.cwd ?? process.cwd() const hostName = options.host ?? 'localhost' - const host: DevToolsHost & HubHostCapabilities = { + const host: DevframeHost & HubHostCapabilities = { mountStatic() { // Static mounting for devframe SPAs would route through Next middleware // in a fuller host. This minimal example keeps mounted devframes headless. @@ -70,8 +70,8 @@ export async function minimalNextDevToolsHub( }, getStorageDir(scope) { return scope === 'workspace' - ? join(cwd, 'node_modules/.minimal-next-devtools-hub') - : join(homedir(), '.minimal-next-devtools-hub') + ? join(cwd, 'node_modules/.minimal-next-devframe-hub') + : join(homedir(), '.minimal-next-devframe-hub') }, async openPath(filepath, line, column) { const absolute = join(cwd, filepath) @@ -97,7 +97,7 @@ export async function minimalNextDevToolsHub( }) context.commands.register({ - id: 'minimal-next-devtools-hub:ping', + id: 'minimal-next-devframe-hub:ping', title: 'Next Hub: Ping', icon: 'ph:bell-duotone', category: 'hub', @@ -106,7 +106,7 @@ export async function minimalNextDevToolsHub( await context.messages.add({ level: 'success', - message: 'Minimal Next DevTools Hub started', + message: 'Minimal Next Devframe Hub started', description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 1} devframe(s) registered.`, }) @@ -130,16 +130,16 @@ export async function minimalNextDevToolsHub( }) } -const GLOBAL_KEY = '__minimalNextDevToolsHub' +const GLOBAL_KEY = '__minimalNextDevframeHub' type GlobalWithHub = typeof globalThis & { - [GLOBAL_KEY]?: Promise + [GLOBAL_KEY]?: Promise } -export function ensureMinimalNextDevToolsHub( - options: MinimalNextDevToolsHubOptions = {}, -): Promise { +export function ensureMinimalNextDevframeHub( + options: MinimalNextDevframeHubOptions = {}, +): Promise { const globalHub = globalThis as GlobalWithHub - globalHub[GLOBAL_KEY] ??= minimalNextDevToolsHub(options) + globalHub[GLOBAL_KEY] ??= minimalNextDevframeHub(options) return globalHub[GLOBAL_KEY] } diff --git a/examples/minimal-next-devtools-hub/src/client/next.config.mjs b/examples/minimal-next-devframe-hub/src/client/next.config.mjs similarity index 100% rename from examples/minimal-next-devtools-hub/src/client/next.config.mjs rename to examples/minimal-next-devframe-hub/src/client/next.config.mjs diff --git a/examples/minimal-next-devtools-hub/src/client/tsconfig.json b/examples/minimal-next-devframe-hub/src/client/tsconfig.json similarity index 100% rename from examples/minimal-next-devtools-hub/src/client/tsconfig.json rename to examples/minimal-next-devframe-hub/src/client/tsconfig.json diff --git a/examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts b/examples/minimal-next-devframe-hub/tests/minimal-next-devframe-hub.test.ts similarity index 71% rename from examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts rename to examples/minimal-next-devframe-hub/tests/minimal-next-devframe-hub.test.ts index ca99995..7bd797a 100644 --- a/examples/minimal-next-devtools-hub/tests/minimal-next-devtools-hub.test.ts +++ b/examples/minimal-next-devframe-hub/tests/minimal-next-devframe-hub.test.ts @@ -2,7 +2,7 @@ import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' import { afterEach, describe, expect, it, vi } from 'vitest' import { WebSocket } from 'ws' -import { minimalNextDevToolsHub } from '../src/client/devtools/minimal-next-devtools-hub' +import { minimalNextDevframeHub } from '../src/client/devframe/minimal-next-devframe-hub' vi.stubGlobal('WebSocket', WebSocket) @@ -11,8 +11,8 @@ function bootRpc(port: number) { return createRpcClient({}, { channel }) } -describe('minimal-next-devtools-hub (example)', () => { - let server: Awaited> | undefined +describe('minimal-next-devframe-hub (example)', () => { + let server: Awaited> | undefined afterEach(async () => { await server?.close() @@ -20,7 +20,7 @@ describe('minimal-next-devtools-hub (example)', () => { }) it('returns connection meta pointing at the WS backend', async () => { - server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + server = await minimalNextDevframeHub({ host: '127.0.0.1' }) expect(server.connectionMeta).toEqual({ backend: 'websocket', @@ -29,7 +29,7 @@ describe('minimal-next-devtools-hub (example)', () => { }) it('registers hub built-in docks and the mounted demo devframe', async () => { - server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + server = await minimalNextDevframeHub({ host: '127.0.0.1' }) const dockIds = server.context.docks.values().map(d => d.id) expect(dockIds).toContain('next-demo-tool') @@ -39,20 +39,20 @@ describe('minimal-next-devtools-hub (example)', () => { }) it('lists startup and demo messages through the kit-local RPC', async () => { - server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + server = await minimalNextDevframeHub({ host: '127.0.0.1' }) const rpc = bootRpc(server.port) - const messages = await rpc.$call('minimal-next-devtools-hub:messages:list') as { message: string }[] - expect(messages.map(m => m.message)).toContain('Minimal Next DevTools Hub started') + const messages = await rpc.$call('minimal-next-devframe-hub:messages:list') as { message: string }[] + expect(messages.map(m => m.message)).toContain('Minimal Next Devframe Hub started') expect(messages.map(m => m.message)).toContain('Next demo devframe loaded') }) it('executes the ping command through the hub command RPC', async () => { - server = await minimalNextDevToolsHub({ host: '127.0.0.1' }) + server = await minimalNextDevframeHub({ host: '127.0.0.1' }) const rpc = bootRpc(server.port) await expect( - rpc.$call('hub:commands:execute', 'minimal-next-devtools-hub:ping'), + rpc.$call('hub:commands:execute', 'minimal-next-devframe-hub:ping'), ).resolves.toBe('pong') }) }) diff --git a/examples/minimal-next-devtools-hub/tsconfig.json b/examples/minimal-next-devframe-hub/tsconfig.json similarity index 100% rename from examples/minimal-next-devtools-hub/tsconfig.json rename to examples/minimal-next-devframe-hub/tsconfig.json diff --git a/examples/minimal-next-devtools-hub/vitest.config.ts b/examples/minimal-next-devframe-hub/vitest.config.ts similarity index 100% rename from examples/minimal-next-devtools-hub/vitest.config.ts rename to examples/minimal-next-devframe-hub/vitest.config.ts diff --git a/examples/minimal-vite-devtools-hub/README.md b/examples/minimal-vite-devframe-hub/README.md similarity index 81% rename from examples/minimal-vite-devtools-hub/README.md rename to examples/minimal-vite-devframe-hub/README.md index 1295270..d03f983 100644 --- a/examples/minimal-vite-devtools-hub/README.md +++ b/examples/minimal-vite-devframe-hub/README.md @@ -1,12 +1,12 @@ -# Minimal Vite DevTools Hub +# Minimal Vite Devframe Hub -A protocol-witness example. The `src/minimal-vite-devtools-hub.ts` file is the entire Vite host — about 120 lines of Vite plugin code that wires `@devframes/hub` into a Vite dev server. Every framework's DevTools Hub host follows the same shape. +A protocol-witness example. The `src/minimal-vite-devframe-hub.ts` file is the entire Vite host — about 120 lines of Vite plugin code that wires `@devframes/hub` into a Vite dev server. Every framework's Devframe Hub host follows the same shape. ## Run it ```sh pnpm install -pnpm --filter minimal-vite-devtools-hub dev +pnpm --filter minimal-vite-devframe-hub dev ``` Open the printed URL. You should see: @@ -21,7 +21,7 @@ Open the printed URL. You should see: ## What the example proves - `createHubContext()` boots a hub without any Vite-specific code path -- A `DevToolsHost & HubHostCapabilities` impl plugs framework specifics (`openPath`, storage paths) into the hub uniformly +- A `DevframeHost & HubHostCapabilities` impl plugs framework specifics (`openPath`, storage paths) into the hub uniformly - `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock - Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed - The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware @@ -30,7 +30,7 @@ Open the printed URL. You should see: | File | Role | |---|---| -| `src/minimal-vite-devtools-hub.ts` | The Vite plugin — creates hub context, mounts middleware, side-car WS | +| `src/minimal-vite-devframe-hub.ts` | The Vite plugin — creates hub context, mounts middleware, side-car WS | | `src/devframe.ts` | A sample `DevframeDefinition` that plugs into the kit | | `src/client/main.ts` | The browser-side UI that consumes the hub protocol | | `index.html` | The UI shell | diff --git a/examples/minimal-vite-devtools-hub/index.html b/examples/minimal-vite-devframe-hub/index.html similarity index 93% rename from examples/minimal-vite-devtools-hub/index.html rename to examples/minimal-vite-devframe-hub/index.html index b51934a..23910a4 100644 --- a/examples/minimal-vite-devtools-hub/index.html +++ b/examples/minimal-vite-devframe-hub/index.html @@ -3,12 +3,12 @@ - Minimal Vite DevTools Hub + Minimal Vite Devframe Hub

    -

    Minimal Vite DevTools Hub

    +

    Minimal Vite Devframe Hub

    Protocol witness — verifies @devframes/hub end to end.

    diff --git a/examples/minimal-vite-devtools-hub/package.json b/examples/minimal-vite-devframe-hub/package.json similarity index 68% rename from examples/minimal-vite-devtools-hub/package.json rename to examples/minimal-vite-devframe-hub/package.json index 26b159a..7afa00a 100644 --- a/examples/minimal-vite-devtools-hub/package.json +++ b/examples/minimal-vite-devframe-hub/package.json @@ -1,9 +1,9 @@ { - "name": "minimal-vite-devtools-hub", + "name": "minimal-vite-devframe-hub", "type": "module", "version": "0.4.1", "private": true, - "description": "Protocol-witness example — a tiny Vite DevTools Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", + "description": "Protocol-witness example — a tiny Vite Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.", "scripts": { "dev": "vite", "build": "vite build" diff --git a/examples/minimal-vite-devtools-hub/src/client/main.ts b/examples/minimal-vite-devframe-hub/src/client/main.ts similarity index 88% rename from examples/minimal-vite-devtools-hub/src/client/main.ts rename to examples/minimal-vite-devframe-hub/src/client/main.ts index d25ccc6..a5afe25 100644 --- a/examples/minimal-vite-devtools-hub/src/client/main.ts +++ b/examples/minimal-vite-devframe-hub/src/client/main.ts @@ -1,8 +1,8 @@ import type { - DevToolsCommandEntry, - DevToolsDockEntry, - DevToolsMessageEntry, - DevToolsTerminalSession, + DevframeCommandEntry, + DevframeDockEntry, + DevframeMessageEntry, + DevframeTerminalSession, } from '@devframes/hub/types' import { connectDevframe } from '@devframes/hub/client' @@ -36,7 +36,7 @@ async function main() { setStatus(`Connected · backend=${rpc.connectionMeta.backend}`, 'ready') // 1. Docks — read from `devframe:docks` shared state. - const docks = await rpc.sharedState.get( + const docks = await rpc.sharedState.get( 'devframe:docks', { initialValue: [] }, ) @@ -48,7 +48,7 @@ async function main() { renderDocks() // 2. Commands — read from `devframe:commands` shared state. - const commands = await rpc.sharedState.get( + const commands = await rpc.sharedState.get( 'devframe:commands', { initialValue: [] }, ) @@ -62,8 +62,8 @@ async function main() { // to refresh on broadcast; this minimal example polls instead. const refreshMessages = async () => { const entries = await rpc.call( - 'minimal-vite-devtools-hub:messages:list' as any, - ) as DevToolsMessageEntry[] + 'minimal-vite-devframe-hub:messages:list' as any, + ) as DevframeMessageEntry[] renderList(messagesEl, entries, m => `
  • [${m.level}] ${m.message}
  • `) } @@ -72,8 +72,8 @@ async function main() { // 4. Terminals — same pattern as messages. const refreshTerminals = async () => { const sessions = await rpc.call( - 'minimal-vite-devtools-hub:terminals:list' as any, - ) as Pick[] + 'minimal-vite-devframe-hub:terminals:list' as any, + ) as Pick[] renderList(terminalsEl, sessions, t => `
  • ${t.title} ${t.id} · ${t.status}
  • `) } diff --git a/examples/minimal-vite-devtools-hub/src/client/style.css b/examples/minimal-vite-devframe-hub/src/client/style.css similarity index 100% rename from examples/minimal-vite-devtools-hub/src/client/style.css rename to examples/minimal-vite-devframe-hub/src/client/style.css diff --git a/examples/minimal-vite-devtools-hub/src/devframe.ts b/examples/minimal-vite-devframe-hub/src/devframe.ts similarity index 100% rename from examples/minimal-vite-devtools-hub/src/devframe.ts rename to examples/minimal-vite-devframe-hub/src/devframe.ts diff --git a/examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts similarity index 87% rename from examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts rename to examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts index 6a720c1..b5a4969 100644 --- a/examples/minimal-vite-devtools-hub/src/minimal-vite-devtools-hub.ts +++ b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts @@ -1,16 +1,16 @@ import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' -import type { DevframeDefinition, DevToolsHost } from 'devframe/types' +import type { DevframeDefinition, DevframeHost } from 'devframe/types' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import { homedir } from 'node:os' import { defineRpcFunction } from '@devframes/hub' import { createHubContext, mountDevframe } from '@devframes/hub/node' -import { DEVTOOLS_CONNECTION_META_FILENAME } from 'devframe/constants' +import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants' import { startHttpAndWs } from 'devframe/node' import { launchEditor } from 'devframe/utils/launch-editor' import { getPort } from 'get-port-please' import { join } from 'pathe' -export interface MinimalViteDevToolsHubOptions { +export interface MinimalViteDevframeHubOptions { /** Mount path for the hub's connection-meta endpoint. Default: `/__hub/`. */ base?: string /** Preferred port for the side-car RPC/WS server. Default: a free port near 9777. */ @@ -22,7 +22,7 @@ export interface MinimalViteDevToolsHubOptions { // Minimal hub-local RPCs — used by the UI for read-side data. A more // ambitious hub host might hoist these into `@devframes/hub` itself. const minimalViteHubMessagesList = defineRpcFunction({ - name: 'minimal-vite-devtools-hub:messages:list', + name: 'minimal-vite-devframe-hub:messages:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -33,7 +33,7 @@ const minimalViteHubMessagesList = defineRpcFunction({ }) const minimalViteHubTerminalsList = defineRpcFunction({ - name: 'minimal-vite-devtools-hub:terminals:list', + name: 'minimal-vite-devframe-hub:terminals:list', type: 'static', jsonSerializable: true, setup: (ctx: HubNodeContext) => ({ @@ -57,13 +57,13 @@ const minimalViteHubTerminalsList = defineRpcFunction({ * This file is the entire Vite host — every other framework's hub host is * the same shape: a thin layer that adapts a framework's dev server to the hub. */ -export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = {}): Plugin { +export function minimalViteDevframeHub(options: MinimalViteDevframeHubOptions = {}): Plugin { const base = normalizeBase(options.base ?? '/__hub/') let viteConfig: ResolvedConfig | undefined let started: { close: () => Promise } | undefined return { - name: 'minimal-vite-devtools-hub', + name: 'minimal-vite-devframe-hub', apply: 'serve', configResolved(config) { @@ -78,7 +78,7 @@ export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = const cwd = viteConfig!.root - const host: DevToolsHost & HubHostCapabilities = { + const host: DevframeHost & HubHostCapabilities = { mountStatic() { // Static mounting for devframe SPAs would route through Vite's // middleware in a fuller kit. This minimal example doesn't @@ -90,8 +90,8 @@ export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = }, getStorageDir(scope) { return scope === 'workspace' - ? join(cwd, 'node_modules/.minimal-vite-devtools-hub') - : join(homedir(), '.minimal-vite-devtools-hub') + ? join(cwd, 'node_modules/.minimal-vite-devframe-hub') + : join(homedir(), '.minimal-vite-devframe-hub') }, async openPath(filepath, line, column) { const absolute = join(cwd, filepath) @@ -124,7 +124,7 @@ export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = // Seed a sample command directly on the hub so the UI // shows something even without any plugged-in devframes. context.commands.register({ - id: 'minimal-vite-devtools-hub:ping', + id: 'minimal-vite-devframe-hub:ping', title: 'Vite Hub · Ping', icon: 'ph:bell-duotone', category: 'kit', @@ -132,7 +132,7 @@ export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = }) await context.messages.add({ level: 'success', - message: 'Minimal Vite DevTools Hub started', + message: 'Minimal Vite Devframe Hub started', description: `Side-car WS on port ${port}. ${options.devframes?.length ?? 0} devframe(s) registered.`, }) @@ -148,7 +148,7 @@ export function minimalViteDevToolsHub(options: MinimalViteDevToolsHubOptions = // Tell the browser where to find the WS endpoint. `connectDevframe` // resolves this URL relative to its `baseURL` option. - const metaPath = `${base}${DEVTOOLS_CONNECTION_META_FILENAME}` + const metaPath = `${base}${DEVFRAME_CONNECTION_META_FILENAME}` server.middlewares.use(metaPath, (_req, res) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ backend: 'websocket', websocket: port })) diff --git a/examples/minimal-vite-devtools-hub/tsconfig.json b/examples/minimal-vite-devframe-hub/tsconfig.json similarity index 100% rename from examples/minimal-vite-devtools-hub/tsconfig.json rename to examples/minimal-vite-devframe-hub/tsconfig.json diff --git a/examples/minimal-vite-devtools-hub/vite.config.ts b/examples/minimal-vite-devframe-hub/vite.config.ts similarity index 69% rename from examples/minimal-vite-devtools-hub/vite.config.ts rename to examples/minimal-vite-devframe-hub/vite.config.ts index 5ab3361..925e42b 100644 --- a/examples/minimal-vite-devtools-hub/vite.config.ts +++ b/examples/minimal-vite-devframe-hub/vite.config.ts @@ -1,12 +1,12 @@ import { defineConfig } from 'vite' import { alias } from '../../alias' import demoDevframe from './src/devframe' -import { minimalViteDevToolsHub } from './src/minimal-vite-devtools-hub' +import { minimalViteDevframeHub } from './src/minimal-vite-devframe-hub' export default defineConfig({ resolve: { alias }, plugins: [ - minimalViteDevToolsHub({ + minimalViteDevframeHub({ devframes: [demoDevframe], }), ], diff --git a/examples/next-runtime-snapshot/src/client/app/components/connect.tsx b/examples/next-runtime-snapshot/src/client/app/components/connect.tsx index e1c6599..87a33af 100644 --- a/examples/next-runtime-snapshot/src/client/app/components/connect.tsx +++ b/examples/next-runtime-snapshot/src/client/app/components/connect.tsx @@ -1,12 +1,12 @@ 'use client' -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' import type { ReactNode } from 'react' import { connectDevframe } from 'devframe/client' import { createContext, useContext, useEffect, useState } from 'react' interface ConnectionState { - rpc: DevToolsRpcClient | null + rpc: DevframeRpcClient | null error: string | null } diff --git a/examples/next-runtime-snapshot/tests/_utils.ts b/examples/next-runtime-snapshot/tests/_utils.ts index adcac27..4dcf3d5 100644 --- a/examples/next-runtime-snapshot/tests/_utils.ts +++ b/examples/next-runtime-snapshot/tests/_utils.ts @@ -1,9 +1,9 @@ import type { StartedServer } from 'devframe/node' import process from 'node:process' import { fileURLToPath } from 'node:url' -import { DEVTOOLS_CONNECTION_META_FILENAME } from 'devframe/constants' +import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants' import { - createH3DevToolsHost, + createH3DevframeHost, createHostContext, startHttpAndWs, } from 'devframe/node' @@ -36,7 +36,7 @@ export async function startSnapshotServer(): Promise { const app = new H3() const origin = `http://${host}:${port}` - const h3Host = createH3DevToolsHost({ + const h3Host = createH3DevframeHost({ origin, appName: devframe.id, mount: (base, dir) => mountStaticHandler(app, base, dir), @@ -45,7 +45,7 @@ export async function startSnapshotServer(): Promise { const ctx = await createHostContext({ cwd: process.cwd(), mode: 'dev', host: h3Host }) await devframe.setup(ctx) - const metaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}` + const metaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}` app.use(metaPath, () => ({ backend: 'websocket', websocket: port })) // Mount the static handler unconditionally — it only stat()s on // request, so a missing dist just produces 404s for HTML routes. diff --git a/examples/streaming-chat/README.md b/examples/streaming-chat/README.md index f0d2cd1..c3f9214 100644 --- a/examples/streaming-chat/README.md +++ b/examples/streaming-chat/README.md @@ -2,7 +2,7 @@ End-to-end demo of devframe's streaming-channel API combined with shared state for persistent chat history. Mirrors the AI-deltas use case from -[vitejs/devtools#306](https://github.com/vitejs/devtools/issues/306): +[vitejs/devframe#306](https://github.com/vitejs/devframe/issues/306): the server emits synthesized "tokens" one at a time over a streaming channel, while the conversation log lives in a devframe `sharedState` so it survives reloads, syncs across panels, and replays cleanly when a diff --git a/examples/streaming-chat/src/client/app.tsx b/examples/streaming-chat/src/client/app.tsx index 327aae5..3fec8e1 100644 --- a/examples/streaming-chat/src/client/app.tsx +++ b/examples/streaming-chat/src/client/app.tsx @@ -1,4 +1,4 @@ -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' import type { StreamReader } from 'devframe/utils/streaming-channel' import type { ChatHistory, ChatMessage } from '../devframe' import { connectDevframe } from 'devframe/client' @@ -8,7 +8,7 @@ const CHANNEL_NAME = 'devframe-streaming-chat:tokens' const HISTORY_KEY = 'devframe-streaming-chat:history' export function App() { - const [rpc, setRpc] = useState(null) + const [rpc, setRpc] = useState(null) const [demoPrompts, setDemoPrompts] = useState([]) const [messages, setMessages] = useState([]) const [liveTokens, setLiveTokens] = useState>({}) diff --git a/examples/streaming-chat/src/devframe.ts b/examples/streaming-chat/src/devframe.ts index 09d245a..5e53cf6 100644 --- a/examples/streaming-chat/src/devframe.ts +++ b/examples/streaming-chat/src/devframe.ts @@ -33,7 +33,7 @@ export interface ChatHistory { } declare module 'devframe/types' { - interface DevToolsRpcSharedStates { + interface DevframeRpcSharedStates { [HISTORY_KEY]: ChatHistory } } diff --git a/examples/streaming-chat/tests/_utils.ts b/examples/streaming-chat/tests/_utils.ts index 54d48d9..755de93 100644 --- a/examples/streaming-chat/tests/_utils.ts +++ b/examples/streaming-chat/tests/_utils.ts @@ -1,13 +1,13 @@ -import type { DevToolsNodeContext, StartedServer } from 'devframe/node' +import type { DevframeNodeContext, StartedServer } from 'devframe/node' import { existsSync } from 'node:fs' import path from 'node:path' import process from 'node:process' import { fileURLToPath } from 'node:url' import { - DEVTOOLS_CONNECTION_META_FILENAME, + DEVFRAME_CONNECTION_META_FILENAME, } from 'devframe/constants' import { - createH3DevToolsHost, + createH3DevframeHost, createHostContext, startHttpAndWs, } from 'devframe/node' @@ -29,7 +29,7 @@ export const CLIENT_DIST = resolve(HERE, '../dist/client') */ export async function startStreamingChatServer(): Promise { // Build the client only if a test exercises the served HTML — RPC-only // tests don't need the dist (we don't call assertClientBuilt unless the @@ -41,7 +41,7 @@ export async function startStreamingChatServer(): Promise mountStaticHandler(app, base, dir), @@ -50,7 +50,7 @@ export async function startStreamingChatServer(): Promise ({ backend: 'websocket', websocket: port })) if (existsSync(path.join(resolve(distDir), 'index.html'))) { mountStaticHandler(app, basePath, resolve(distDir)) diff --git a/examples/streaming-chat/tests/streaming-chat.test.ts b/examples/streaming-chat/tests/streaming-chat.test.ts index bc1aba4..a53ed8e 100644 --- a/examples/streaming-chat/tests/streaming-chat.test.ts +++ b/examples/streaming-chat/tests/streaming-chat.test.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext, StartedServer } from 'devframe/node' +import type { DevframeNodeContext, StartedServer } from 'devframe/node' import type { ChatHistory } from '../src/devframe' import { createRpcStreamingClientHost } from 'devframe/client' import { createRpcClient } from 'devframe/rpc/client' @@ -78,13 +78,13 @@ async function readAll(reader: AsyncIterable): Promise { return out } -async function getHistory(ctx: DevToolsNodeContext): Promise { +async function getHistory(ctx: DevframeNodeContext): Promise { const state = await ctx.rpc.sharedState.get(HISTORY_KEY) return state.value() as ChatHistory } describe('devframe-streaming-chat (example)', () => { - let server: StartedServer & { basePath: string, ctx: DevToolsNodeContext } + let server: StartedServer & { basePath: string, ctx: DevframeNodeContext } beforeEach(async () => { server = await startStreamingChatServer() diff --git a/package.json b/package.json index b28803d..39f5205 100644 --- a/package.json +++ b/package.json @@ -29,22 +29,22 @@ "postinstall": "npx simple-git-hooks && skills-npm" }, "devDependencies": { - "@antfu/eslint-config": "catalog:devtools", + "@antfu/eslint-config": "catalog:tooling", "@antfu/ni": "catalog:build", "@antfu/utils": "catalog:inlined", "@playwright/test": "catalog:testing", "@types/node": "catalog:types", "@types/ws": "catalog:types", - "bumpp": "catalog:devtools", - "eslint": "catalog:devtools", - "nano-staged": "catalog:devtools", - "simple-git-hooks": "catalog:devtools", - "skills-npm": "catalog:devtools", + "bumpp": "catalog:tooling", + "eslint": "catalog:tooling", + "nano-staged": "catalog:tooling", + "simple-git-hooks": "catalog:tooling", + "skills-npm": "catalog:tooling", "tsdown": "catalog:build", "tsnapi": "catalog:testing", "tsx": "catalog:build", "turbo": "catalog:build", - "typescript": "catalog:devtools", + "typescript": "catalog:tooling", "vite": "catalog:build", "vitest": "catalog:testing" }, diff --git a/packages/devframe/README.md b/packages/devframe/README.md index 6665550..6243ce3 100644 --- a/packages/devframe/README.md +++ b/packages/devframe/README.md @@ -6,7 +6,7 @@ [![JSDocs][jsdocs-src]][jsdocs-href] [![License][license-src]][license-href] -Framework-neutral foundation for building generic DevTools. +Framework-neutral foundation for building generic devframes. Documentation: [https://devfra.me/](https://devfra.me/). diff --git a/packages/devframe/package.json b/packages/devframe/package.json index ae80384..f0ddbb3 100644 --- a/packages/devframe/package.json +++ b/packages/devframe/package.json @@ -2,7 +2,7 @@ "name": "devframe", "type": "module", "version": "0.4.1", - "description": "Framework for building generic DevTools", + "description": "Framework for building generic devframes", "author": "Anthony Fu ", "license": "MIT", "homepage": "https://github.com/devframes/devframe#readme", diff --git a/packages/devframe/src/adapters/build.ts b/packages/devframe/src/adapters/build.ts index a82d104..3ea701e 100644 --- a/packages/devframe/src/adapters/build.ts +++ b/packages/devframe/src/adapters/build.ts @@ -7,12 +7,12 @@ import { colors as c } from 'devframe/utils/colors' import { structuredCloneStringify } from 'devframe/utils/structured-clone' import { dirname, resolve } from 'pathe' import { - DEVTOOLS_CONNECTION_META_FILENAME, - DEVTOOLS_RPC_DUMP_DIRNAME, - DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME, + DEVFRAME_CONNECTION_META_FILENAME, + DEVFRAME_RPC_DUMP_DIRNAME, + DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, } from '../constants' import { createHostContext } from '../node/context' -import { createH3DevToolsHost } from '../node/host-h3' +import { createH3DevframeHost } from '../node/host-h3' import { collectStaticRpcDump } from '../rpc/dump/static' import { strictJsonStringify } from '../rpc/serialization' import { resolveBasePath } from './_shared' @@ -47,7 +47,7 @@ export interface CreateBuildOptions { * - When `def.spa` is configured, also write `/spa-loader.json` * describing the SPA's data-loader mode (`'query'` / `'upload'` / * `'none'`). The output is mount-path agnostic — the same bundle - * works at `/`, `/devtools/`, or any base, no rewriting required. + * works at `/`, `/devframe/`, or any base, no rewriting required. */ export async function createBuild(d: DevframeDefinition, options: CreateBuildOptions = {}): Promise { const outDir = resolve(options.outDir ?? 'dist-static') @@ -66,11 +66,11 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt const ctx = await createHostContext({ cwd: process.cwd(), mode: 'build', - host: createH3DevToolsHost({ origin: 'http://localhost', appName: d.id }), + host: createH3DevframeHost({ origin: 'http://localhost', appName: d.id }), }) await d.setup(ctx) - await fs.mkdir(resolve(outDir, DEVTOOLS_RPC_DUMP_DIRNAME), { recursive: true }) + await fs.mkdir(resolve(outDir, DEVFRAME_RPC_DUMP_DIRNAME), { recursive: true }) const jsonSerializableMethods: string[] = [] for (const def of ctx.rpc.definitions.values()) { @@ -78,12 +78,12 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt jsonSerializableMethods.push(def.name) } await fs.writeFile( - resolve(outDir, DEVTOOLS_CONNECTION_META_FILENAME), + resolve(outDir, DEVFRAME_CONNECTION_META_FILENAME), JSON.stringify({ backend: 'static', jsonSerializableMethods }, null, 2), 'utf-8', ) - console.log(c.cyan`[devframe] writing RPC dump to ${resolve(outDir, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME)}`) + console.log(c.cyan`[devframe] writing RPC dump to ${resolve(outDir, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME)}`) const dump = await collectStaticRpcDump(ctx.rpc.definitions.values(), ctx) const indent = options.pretty ? 2 : undefined for (const [filepath, file] of Object.entries(dump.files)) { @@ -102,7 +102,7 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt ) } await fs.writeFile( - resolve(outDir, DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME), + resolve(outDir, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME), JSON.stringify(dump.manifest, null, 2), 'utf-8', ) diff --git a/packages/devframe/src/adapters/dev.ts b/packages/devframe/src/adapters/dev.ts index fc417d5..1c0ec53 100644 --- a/packages/devframe/src/adapters/dev.ts +++ b/packages/devframe/src/adapters/dev.ts @@ -6,9 +6,9 @@ import { mountStaticHandler } from 'devframe/utils/serve-static' import { getPort } from 'get-port-please' import { H3 } from 'h3' import { resolve } from 'pathe' -import { DEVTOOLS_CONNECTION_META_FILENAME } from '../constants' +import { DEVFRAME_CONNECTION_META_FILENAME } from '../constants' import { createHostContext } from '../node/context' -import { createH3DevToolsHost } from '../node/host-h3' +import { createH3DevframeHost } from '../node/host-h3' import { startHttpAndWs } from '../node/server' import { normalizeBasePath, resolveBasePath } from './_shared' @@ -125,7 +125,7 @@ export async function createDevServer( const app = options.app ?? new H3() const origin = `http://${host}:${port}` - const h3Host = createH3DevToolsHost({ + const h3Host = createH3DevframeHost({ origin, appName: def.id, mount: (base, dir) => { @@ -146,7 +146,7 @@ export async function createDevServer( // to know it's a websocket backend bound to that same port. The path // sits at the SPA root (next to index.html) so the deployed SPA can // discover it via a relative `./__connection.json` fetch. - const connectionMetaPath = `${basePath}${DEVTOOLS_CONNECTION_META_FILENAME}` + const connectionMetaPath = `${basePath}${DEVFRAME_CONNECTION_META_FILENAME}` app.use(connectionMetaPath, () => ({ backend: 'websocket', websocket: port })) if (distDir) diff --git a/packages/devframe/src/adapters/embedded.ts b/packages/devframe/src/adapters/embedded.ts index 17d86b6..ae36dc8 100644 --- a/packages/devframe/src/adapters/embedded.ts +++ b/packages/devframe/src/adapters/embedded.ts @@ -1,9 +1,9 @@ -import type { DevToolsNodeContext } from '../types/context' +import type { DevframeNodeContext } from '../types/context' import type { DevframeDefinition } from '../types/devframe' export interface CreateEmbeddedOptions { /** Target context the devframe is registered into. Required. */ - ctx: DevToolsNodeContext + ctx: DevframeNodeContext } /** diff --git a/packages/devframe/src/adapters/mcp/__tests__/mcp-server.test.ts b/packages/devframe/src/adapters/mcp/__tests__/mcp-server.test.ts index f443e4d..ecf705a 100644 --- a/packages/devframe/src/adapters/mcp/__tests__/mcp-server.test.ts +++ b/packages/devframe/src/adapters/mcp/__tests__/mcp-server.test.ts @@ -1,11 +1,11 @@ -import type { DevToolsHost } from '../../../types/host' +import type { DevframeHost } from '../../../types/host' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' import { createHostContext } from 'devframe/node' import { describe, expect, it } from 'vitest' import { buildMcpServerFromContext } from '../build-server' -function nullHost(): DevToolsHost { +function nullHost(): DevframeHost { return { mountStatic: () => { /* no-op */ }, resolveOrigin: () => 'mcp://test', diff --git a/packages/devframe/src/adapters/mcp/build-server.ts b/packages/devframe/src/adapters/mcp/build-server.ts index 365de52..e969d50 100644 --- a/packages/devframe/src/adapters/mcp/build-server.ts +++ b/packages/devframe/src/adapters/mcp/build-server.ts @@ -1,5 +1,5 @@ import type { RpcFunctionDefinitionAnyWithContext } from 'devframe/rpc' -import type { AgentTool, DevframeDefinition, DevToolsHost, DevToolsNodeContext } from 'devframe/types' +import type { AgentTool, DevframeDefinition, DevframeHost, DevframeNodeContext } from 'devframe/types' import type { GenericSchema } from 'valibot' import { homedir } from 'node:os' import process from 'node:process' @@ -50,7 +50,7 @@ export interface McpServerHandle { * @internal */ export function buildMcpServerFromContext( - ctx: DevToolsNodeContext, + ctx: DevframeNodeContext, options: { serverName: string, serverVersion: string, exposeSharedState: boolean | ((k: string) => boolean) }, ): { server: Server, dispose: () => void } { const server = new Server( @@ -104,12 +104,12 @@ export async function createMcpServer( if (transport !== 'stdio') throw diagnostics.DF0017({ transport, reason: 'Only stdio transport is supported in this release.' }) - const host: DevToolsHost = { + const host: DevframeHost = { mountStatic: () => { /* MCP has no static surface */ }, resolveOrigin: () => 'mcp://devframe', getStorageDir: scope => scope === 'workspace' - ? join(process.cwd(), `node_modules/.${definition.id}/devtools`) - : join(homedir(), `.${definition.id}/devtools`), + ? join(process.cwd(), `node_modules/.${definition.id}/devframe`) + : join(homedir(), `.${definition.id}/devframe`), } const ctx = await createHostContext({ @@ -145,7 +145,7 @@ export async function createMcpServer( } } -function registerToolHandlers(server: Server, ctx: DevToolsNodeContext): void { +function registerToolHandlers(server: Server, ctx: DevframeNodeContext): void { server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = ctx.agent.list().tools.map(tool => projectTool(tool, ctx)) return { tools } @@ -180,7 +180,7 @@ function registerToolHandlers(server: Server, ctx: DevToolsNodeContext): void { function registerResourceHandlers( server: Server, - ctx: DevToolsNodeContext, + ctx: DevframeNodeContext, exposeSharedState: boolean | ((key: string) => boolean), ): void { server.setRequestHandler(ListResourcesRequestSchema, async () => { @@ -242,7 +242,7 @@ function registerResourceHandlers( }) } -function projectTool(tool: AgentTool, ctx: DevToolsNodeContext): Record { +function projectTool(tool: AgentTool, ctx: DevframeNodeContext): Record { const inputSchema = tool.inputSchema ?? computeInputSchema(tool, ctx) const outputSchema = tool.outputSchema ?? computeOutputSchema(tool, ctx) return { @@ -259,20 +259,20 @@ function projectTool(tool: AgentTool, ctx: DevToolsNodeContext): Record | undefined + const def = ctx.rpc.definitions.get(tool.rpcName) as RpcFunctionDefinitionAnyWithContext | undefined if (!def) return { type: 'object', properties: {} } const args = def.args as readonly GenericSchema[] | undefined return valibotArgsToJsonSchema(args).schema } -function computeOutputSchema(tool: AgentTool, ctx: DevToolsNodeContext): unknown { +function computeOutputSchema(tool: AgentTool, ctx: DevframeNodeContext): unknown { if (tool.kind !== 'rpc' || !tool.rpcName) return undefined - const def = ctx.rpc.definitions.get(tool.rpcName) as RpcFunctionDefinitionAnyWithContext | undefined + const def = ctx.rpc.definitions.get(tool.rpcName) as RpcFunctionDefinitionAnyWithContext | undefined if (!def) return undefined return valibotReturnToJsonSchema(def.returns as GenericSchema | undefined) diff --git a/packages/devframe/src/client/index.ts b/packages/devframe/src/client/index.ts index 3531de0..d94b4f8 100644 --- a/packages/devframe/src/client/index.ts +++ b/packages/devframe/src/client/index.ts @@ -1,6 +1,6 @@ -import { getDevToolsRpcClient } from './rpc' +import { getDevframeRpcClient } from './rpc' export * from './rpc' export * from './rpc-streaming' -export const connectDevframe = getDevToolsRpcClient +export const connectDevframe = getDevframeRpcClient diff --git a/packages/devframe/src/client/rpc-shared-state.ts b/packages/devframe/src/client/rpc-shared-state.ts index 12e9574..09bb4af 100644 --- a/packages/devframe/src/client/rpc-shared-state.ts +++ b/packages/devframe/src/client/rpc-shared-state.ts @@ -1,9 +1,9 @@ import type { RpcSharedStateGetOptions, RpcSharedStateHost } from 'devframe/types' import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state' -import type { DevToolsRpcClient } from './rpc' +import type { DevframeRpcClient } from './rpc' import { createSharedState } from 'devframe/utils/shared-state' -export function createRpcSharedStateClientHost(rpc: DevToolsRpcClient): RpcSharedStateHost { +export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcSharedStateHost { const sharedState = new Map>() const initialValues = new Map() const keyAddedListeners = new Set<(key: string) => void>() diff --git a/packages/devframe/src/client/rpc-static.ts b/packages/devframe/src/client/rpc-static.ts index 9242c63..ecdff34 100644 --- a/packages/devframe/src/client/rpc-static.ts +++ b/packages/devframe/src/client/rpc-static.ts @@ -1,5 +1,5 @@ -import type { DevToolsRpcClientMode } from './rpc' -import { DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME } from 'devframe/constants' +import type { DevframeRpcClientMode } from './rpc' +import { DEVFRAME_RPC_DUMP_MANIFEST_FILENAME } from 'devframe/constants' import { createStaticRpcCaller } from './static-rpc' export interface CreateStaticRpcClientModeOptions { @@ -8,8 +8,8 @@ export interface CreateStaticRpcClientModeOptions { export async function createStaticRpcClientMode( options: CreateStaticRpcClientModeOptions, -): Promise { - const manifest = await options.fetchJsonFromBases(DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME) +): Promise { + const manifest = await options.fetchJsonFromBases(DEVFRAME_RPC_DUMP_MANIFEST_FILENAME) const staticCaller = createStaticRpcCaller(manifest, options.fetchJsonFromBases) return { diff --git a/packages/devframe/src/client/rpc-streaming.ts b/packages/devframe/src/client/rpc-streaming.ts index ebb6c55..7264658 100644 --- a/packages/devframe/src/client/rpc-streaming.ts +++ b/packages/devframe/src/client/rpc-streaming.ts @@ -1,5 +1,5 @@ import type { StreamErrorPayload, StreamReader, StreamSink } from 'devframe/utils/streaming-channel' -import type { DevToolsRpcClient } from './rpc' +import type { DevframeRpcClient } from './rpc' import { createStreamReader, createStreamSink } from 'devframe/utils/streaming-channel' const STREAM_KEY_SEPARATOR = '\x1F' @@ -41,7 +41,7 @@ export interface RpcStreamingClientHost { * registers the two `:chunk` / `:end` event handlers once, then per-stream * state lives in a `Map`. */ -export function createRpcStreamingClientHost(rpc: DevToolsRpcClient): RpcStreamingClientHost { +export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreamingClientHost { const readers = new Map>() const uploads = new Map>() diff --git a/packages/devframe/src/client/rpc-ws.ts b/packages/devframe/src/client/rpc-ws.ts index b0b99d2..54d8ccb 100644 --- a/packages/devframe/src/client/rpc-ws.ts +++ b/packages/devframe/src/client/rpc-ws.ts @@ -1,5 +1,5 @@ -import type { ConnectionMeta, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, EventEmitter } from 'devframe/types' -import type { DevToolsClientRpcHost, DevToolsRpcClientMode, DevToolsRpcClientOptions, RpcClientEvents } from './rpc' +import type { ConnectionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions, EventEmitter } from 'devframe/types' +import type { DevframeClientRpcHost, DevframeRpcClientMode, DevframeRpcClientOptions, RpcClientEvents } from './rpc' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' import { promiseWithResolver } from 'devframe/utils/promise' @@ -9,9 +9,9 @@ export interface CreateWsRpcClientModeOptions { authToken: string connectionMeta: ConnectionMeta events: EventEmitter - clientRpc: DevToolsClientRpcHost - rpcOptions?: DevToolsRpcClientOptions['rpcOptions'] - wsOptions?: DevToolsRpcClientOptions['wsOptions'] + clientRpc: DevframeClientRpcHost + rpcOptions?: DevframeRpcClientOptions['rpcOptions'] + wsOptions?: DevframeRpcClientOptions['wsOptions'] } function isNumeric(str: string | number | undefined) { @@ -22,7 +22,7 @@ function isNumeric(str: string | number | undefined) { export function createWsRpcClientMode( options: CreateWsRpcClientModeOptions, -): DevToolsRpcClientMode { +): DevframeRpcClientMode { const { authToken, connectionMeta, @@ -46,7 +46,7 @@ export function createWsRpcClientMode( for (const name of connectionMeta.jsonSerializableMethods ?? []) definitions.set(name, { jsonSerializable: true }) - const serverRpc = createRpcClient( + const serverRpc = createRpcClient( clientRpc.functions, { channel: createWsRpcChannel({ @@ -84,7 +84,7 @@ export function createWsRpcClientMode( info.device.type, ].filter(i => i).join(' ') - const result = await serverRpc.$call('vite:anonymous:auth', { + const result = await serverRpc.$call('devframe:anonymous:auth', { authToken: token, ua, origin: location.origin, diff --git a/packages/devframe/src/client/rpc.ts b/packages/devframe/src/client/rpc.ts index d090986..d5c3bf5 100644 --- a/packages/devframe/src/client/rpc.ts +++ b/packages/devframe/src/client/rpc.ts @@ -1,10 +1,10 @@ import type { BirpcOptions, BirpcReturn } from 'birpc' import type { RpcCacheOptions, RpcFunctionsCollector } from 'devframe/rpc' import type { WsRpcChannelOptions } from 'devframe/rpc/transports/ws-client' -import type { ConnectionMeta, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, EventEmitter, RpcSharedStateHost } from 'devframe/types' +import type { ConnectionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions, EventEmitter, RpcSharedStateHost } from 'devframe/types' import type { RpcStreamingClientHost } from './rpc-streaming' import { - DEVTOOLS_CONNECTION_META_FILENAME, + DEVFRAME_CONNECTION_META_FILENAME, } from 'devframe/constants' import { RpcCacheManager, RpcFunctionsCollectorBase } from 'devframe/rpc' import { createEventEmitter } from 'devframe/utils/events' @@ -14,23 +14,23 @@ import { createStaticRpcClientMode } from './rpc-static' import { createRpcStreamingClientHost } from './rpc-streaming' import { createWsRpcClientMode } from './rpc-ws' -export interface DevToolsRpcContext { +export interface DevframeRpcContext { /** * The RPC client to interact with the server */ - readonly rpc: DevToolsRpcClient + readonly rpc: DevframeRpcClient } -export type DevToolsClientRpcHost = RpcFunctionsCollector +export type DevframeClientRpcHost = RpcFunctionsCollector export interface RpcClientEvents { 'rpc:is-trusted:updated': (isTrusted: boolean) => void } -const CONNECTION_META_KEY = '__VITE_DEVTOOLS_CONNECTION_META__' -const CONNECTION_AUTH_TOKEN_KEY = '__VITE_DEVTOOLS_CONNECTION_AUTH_TOKEN__' +const CONNECTION_META_KEY = '__DEVFRAME_CONNECTION_META__' +const CONNECTION_AUTH_TOKEN_KEY = '__DEVFRAME_CONNECTION_AUTH_TOKEN__' -export interface DevToolsRpcClientOptions { +export interface DevframeRpcClientOptions { connectionMeta?: ConnectionMeta baseURL?: string | string[] /** @@ -38,15 +38,15 @@ export interface DevToolsRpcClientOptions { */ authToken?: string wsOptions?: Partial - rpcOptions?: Partial> + rpcOptions?: Partial> cacheOptions?: boolean | Partial } -export type DevToolsRpcClientCall = BirpcReturn['$call'] -export type DevToolsRpcClientCallEvent = BirpcReturn['$callEvent'] -export type DevToolsRpcClientCallOptional = BirpcReturn['$callOptional'] +export type DevframeRpcClientCall = BirpcReturn['$call'] +export type DevframeRpcClientCallEvent = BirpcReturn['$callEvent'] +export type DevframeRpcClientCallOptional = BirpcReturn['$callOptional'] -export interface DevToolsRpcClient { +export interface DevframeRpcClient { /** * The events of the client */ @@ -83,19 +83,19 @@ export interface DevToolsRpcClient { /** * Call a RPC function on the server */ - call: DevToolsRpcClientCall + call: DevframeRpcClientCall /** * Call a RPC event on the server, and does not expect a response */ - callEvent: DevToolsRpcClientCallEvent + callEvent: DevframeRpcClientCallEvent /** * Call a RPC optional function on the server */ - callOptional: DevToolsRpcClientCallOptional + callOptional: DevframeRpcClientCallOptional /** * The client RPC host */ - client: DevToolsClientRpcHost + client: DevframeClientRpcHost /** * The shared state host @@ -113,14 +113,14 @@ export interface DevToolsRpcClient { cacheManager: RpcCacheManager } -export interface DevToolsRpcClientMode { +export interface DevframeRpcClientMode { readonly isTrusted: boolean - ensureTrusted: DevToolsRpcClient['ensureTrusted'] - requestTrust: DevToolsRpcClient['requestTrust'] - requestTrustWithToken: DevToolsRpcClient['requestTrustWithToken'] - call: DevToolsRpcClient['call'] - callEvent: DevToolsRpcClient['callEvent'] - callOptional: DevToolsRpcClient['callOptional'] + ensureTrusted: DevframeRpcClient['ensureTrusted'] + requestTrust: DevframeRpcClient['requestTrust'] + requestTrustWithToken: DevframeRpcClient['requestTrustWithToken'] + call: DevframeRpcClient['call'] + callEvent: DevframeRpcClient['callEvent'] + callOptional: DevframeRpcClient['callOptional'] } function getConnectionAuthTokenFromWindows(userAuthToken?: string): string { @@ -168,9 +168,9 @@ function findConnectionMetaFromWindows(): ConnectionMeta | undefined { } } -export async function getDevToolsRpcClient( - options: DevToolsRpcClientOptions = {}, -): Promise { +export async function getDevframeRpcClient( + options: DevframeRpcClientOptions = {}, +): Promise { // Default to a relative base — the SPA owns its mount path at runtime, // so the connection meta and dump shards live alongside `index.html`. // Embedded surfaces that run inside a host page (e.g. a webcomponent @@ -202,7 +202,7 @@ export async function getDevToolsRpcClient( const errors: Error[] = [] for (const base of bases) { try { - connectionMeta = await fetch(resolveBasePath(base, DEVTOOLS_CONNECTION_META_FILENAME)) + connectionMeta = await fetch(resolveBasePath(base, DEVFRAME_CONNECTION_META_FILENAME)) .then(r => r.json()) as ConnectionMeta resolvedBaseURL = base ;(globalThis as any)[CONNECTION_META_KEY] = connectionMeta @@ -220,11 +220,11 @@ export async function getDevToolsRpcClient( } const cacheManager = new RpcCacheManager({ functions: [], ...(typeof options.cacheOptions === 'object' ? options.cacheOptions : {}) }) - const context: DevToolsRpcContext = { + const context: DevframeRpcContext = { rpc: undefined!, } const authToken = getConnectionAuthTokenFromWindows(options.authToken) - const clientRpc: DevToolsClientRpcHost = new RpcFunctionsCollectorBase(context) + const clientRpc: DevframeClientRpcHost = new RpcFunctionsCollectorBase(context) async function fetchJsonFromBases(path: string): Promise { const candidates = [ @@ -283,7 +283,7 @@ export async function getDevToolsRpcClient( wsOptions: options.wsOptions, }) - const rpc: DevToolsRpcClient = { + const rpc: DevframeRpcClient = { events, get isTrusted() { return mode.isTrusted @@ -316,7 +316,7 @@ export async function getDevToolsRpcClient( // Listen for auth updates from other tabs (e.g., auth URL page). // Channel name kept for cross-tab interop with the Vite DevTools auth page. try { - const bc = new BroadcastChannel('vite-devtools-auth') + const bc = new BroadcastChannel('devframe-auth') bc.onmessage = (event) => { if (event.data?.type === 'auth-update' && event.data.authToken) { rpc.requestTrustWithToken(event.data.authToken) diff --git a/packages/devframe/src/client/static-rpc.test.ts b/packages/devframe/src/client/static-rpc.test.ts index 7008268..e91fe3a 100644 --- a/packages/devframe/src/client/static-rpc.test.ts +++ b/packages/devframe/src/client/static-rpc.test.ts @@ -1,11 +1,11 @@ -import { DEVTOOLS_RPC_DUMP_DIRNAME } from 'devframe/constants' +import { DEVFRAME_RPC_DUMP_DIRNAME } from 'devframe/constants' import { hash } from 'devframe/utils/hash' import { structuredCloneStringify } from 'devframe/utils/structured-clone' import { describe, expect, it } from 'vitest' import { createStaticRpcCaller } from './static-rpc' -const DEMO_STATIC_VERSION_PATH = `${DEVTOOLS_RPC_DUMP_DIRNAME}/demo~version.static.json` -const DEMO_QUERY_BASE_PATH = `${DEVTOOLS_RPC_DUMP_DIRNAME}/demo~get-item` +const DEMO_STATIC_VERSION_PATH = `${DEVFRAME_RPC_DUMP_DIRNAME}/demo~version.static.json` +const DEMO_QUERY_BASE_PATH = `${DEVFRAME_RPC_DUMP_DIRNAME}/demo~get-item` const DEMO_QUERY_RECORDS_PATH = `${DEMO_QUERY_BASE_PATH}.record` const DEMO_QUERY_FALLBACK_PATH = `${DEMO_QUERY_BASE_PATH}.fallback.json` @@ -80,7 +80,7 @@ describe('createStaticRpcCaller', () => { await expect(caller.callOptional('demo:missing', [])).resolves.toBeUndefined() await expect(caller.callEvent('demo:missing', [])).resolves.toBeUndefined() - await expect(caller.call('demo:missing', [])).rejects.toThrow('[devtools-rpc] Function "demo:missing" not found in dump store') + await expect(caller.call('demo:missing', [])).rejects.toThrow('[devframe-rpc] Function "demo:missing" not found in dump store') }) it('treats callEvent as no-op in static mode even for known methods', async () => { @@ -117,7 +117,7 @@ describe('createStaticRpcCaller', () => { { 'demo:graph': { type: 'static', - path: `${DEVTOOLS_RPC_DUMP_DIRNAME}/demo~graph.static.json`, + path: `${DEVFRAME_RPC_DUMP_DIRNAME}/demo~graph.static.json`, serialization: 'structured-clone', }, }, @@ -161,7 +161,7 @@ describe('createStaticRpcCaller', () => { { 'demo:legacy-static': { type: 'static', - path: `${DEVTOOLS_RPC_DUMP_DIRNAME}/demo~legacy.static.json`, + path: `${DEVFRAME_RPC_DUMP_DIRNAME}/demo~legacy.static.json`, // no `serialization` field — must default to JSON parsing }, }, diff --git a/packages/devframe/src/client/static-rpc.ts b/packages/devframe/src/client/static-rpc.ts index 796d366..06ba74d 100644 --- a/packages/devframe/src/client/static-rpc.ts +++ b/packages/devframe/src/client/static-rpc.ts @@ -102,14 +102,14 @@ export function createStaticRpcCaller( async function call(functionName: string, args: any[]) { if (!(functionName in manifest)) { - throw new Error(`[devtools-rpc] Function "${functionName}" not found in dump store`) + throw new Error(`[devframe-rpc] Function "${functionName}" not found in dump store`) } const entry = manifest[functionName] if (isStaticEntry(entry)) { if (args.length > 0) { throw new Error( - `[devtools-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, + `[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, ) } return await loadStatic(entry) @@ -130,7 +130,7 @@ export function createStaticRpcCaller( } throw new Error( - `[devtools-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, + `[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, ) } @@ -139,7 +139,7 @@ export function createStaticRpcCaller( } throw new Error( - `[devtools-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, + `[devframe-rpc] No dump match for "${functionName}" with args: ${JSON.stringify(args)}`, ) } diff --git a/packages/devframe/src/constants.ts b/packages/devframe/src/constants.ts index c04f0b3..106fd8e 100644 --- a/packages/devframe/src/constants.ts +++ b/packages/devframe/src/constants.ts @@ -1,17 +1,17 @@ -// DevTools runtime routes and static output conventions. -export const DEVTOOLS_MOUNT_PATH = '/__devtools/' -export const DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH = '/__devtools' -export const DEVTOOLS_DIRNAME = '__devtools' +// Devframe runtime routes and static output conventions. +export const DEVFRAME_MOUNT_PATH = '/__devframe/' +export const DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH = '/__devframe' +export const DEVFRAME_DIRNAME = '__devframe' -export const DEVTOOLS_CONNECTION_META_FILENAME = '__connection.json' -export const DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME = '__rpc-dump/index.json' -export const DEVTOOLS_DOCK_IMPORTS_FILENAME = '__client-imports.js' -export const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID = '/__devtools-client-imports.js' -export const DEVTOOLS_RPC_DUMP_DIRNAME = '__rpc-dump' +export const DEVFRAME_CONNECTION_META_FILENAME = '__connection.json' +export const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = '__rpc-dump/index.json' +export const DEVFRAME_DOCK_IMPORTS_FILENAME = '__client-imports.js' +export const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = '/__devframe-client-imports.js' +export const DEVFRAME_RPC_DUMP_DIRNAME = '__rpc-dump' /** * URL fragment / query parameter name carrying the remote dock * connection descriptor (defined as `RemoteConnectionInfo` in * `@vitejs/devtools-kit`) injected into remote-UI iframe dock URLs. */ -export const REMOTE_CONNECTION_KEY = 'vite-devtools-kit-connection' +export const REMOTE_CONNECTION_KEY = 'devframe-remote-connection' diff --git a/packages/devframe/src/define.ts b/packages/devframe/src/define.ts index 64af131..3de788f 100644 --- a/packages/devframe/src/define.ts +++ b/packages/devframe/src/define.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { DevframeNodeContext } from 'devframe/types' import { createDefineWrapperWithContext } from 'devframe/rpc' -export const defineRpcFunction = createDefineWrapperWithContext() +export const defineRpcFunction = createDefineWrapperWithContext() diff --git a/packages/devframe/src/helpers/vite.ts b/packages/devframe/src/helpers/vite.ts index 45d1fd2..647213c 100644 --- a/packages/devframe/src/helpers/vite.ts +++ b/packages/devframe/src/helpers/vite.ts @@ -3,7 +3,7 @@ import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static' import { resolve } from 'pathe' import { resolveBasePath } from '../adapters/_shared' import { createDevServer, resolveDevServerPort } from '../adapters/dev' -import { DEVTOOLS_CONNECTION_META_FILENAME } from '../constants' +import { DEVFRAME_CONNECTION_META_FILENAME } from '../constants' import { diagnostics } from '../node/diagnostics' export interface ViteDevBridgeOptions { @@ -110,7 +110,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio return } - const metaPath = `${base}${DEVTOOLS_CONNECTION_META_FILENAME}` + const metaPath = `${base}${DEVFRAME_CONNECTION_META_FILENAME}` server.middlewares.use(metaPath, (_req: unknown, res: any) => { res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ backend: 'websocket', websocket: port })) diff --git a/packages/devframe/src/node/__tests__/host-agent.test.ts b/packages/devframe/src/node/__tests__/host-agent.test.ts index d0dac66..ab8e152 100644 --- a/packages/devframe/src/node/__tests__/host-agent.test.ts +++ b/packages/devframe/src/node/__tests__/host-agent.test.ts @@ -1,17 +1,17 @@ import type { RpcFunctionDefinitionAnyWithContext } from '../../rpc/types' -import type { DevToolsNodeContext } from '../../types/context' +import type { DevframeNodeContext } from '../../types/context' import { describe, expect, it, vi } from 'vitest' -import { DevToolsAgentHost } from '../host-agent' +import { DevframeAgentHost } from '../host-agent' import { RpcFunctionsHost } from '../host-functions' -function createContext(): DevToolsNodeContext { - const ctx = {} as DevToolsNodeContext +function createContext(): DevframeNodeContext { + const ctx = {} as DevframeNodeContext ctx.rpc = new RpcFunctionsHost(ctx) - ctx.agent = new DevToolsAgentHost(ctx) + ctx.agent = new DevframeAgentHost(ctx) return ctx } -function rpcDef(def: RpcFunctionDefinitionAnyWithContext): RpcFunctionDefinitionAnyWithContext { +function rpcDef(def: RpcFunctionDefinitionAnyWithContext): RpcFunctionDefinitionAnyWithContext { return def } diff --git a/packages/devframe/src/node/__tests__/host-functions.test.ts b/packages/devframe/src/node/__tests__/host-functions.test.ts index 028aad9..e21f145 100644 --- a/packages/devframe/src/node/__tests__/host-functions.test.ts +++ b/packages/devframe/src/node/__tests__/host-functions.test.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { DevframeNodeContext } from 'devframe/types' import { defineRpcFunction } from 'devframe' import { describe, expect, it } from 'vitest' import { RpcFunctionsHost } from '../host-functions' @@ -11,7 +11,7 @@ const returnV2 = async () => 'v2' const setupWith = (handler: () => Promise) => async () => ({ handler }) describe('rpcFunctionsHost', () => { - const mockContext = {} as DevToolsNodeContext + const mockContext = {} as DevframeNodeContext describe('register() collision detection', () => { it('should register a new RPC function successfully', () => { @@ -131,7 +131,7 @@ describe('rpcFunctionsHost', () => { describe('broadcast() without rpc group', () => { it('should not throw in build mode', async () => { - const host = new RpcFunctionsHost({ mode: 'build' } as DevToolsNodeContext) + const host = new RpcFunctionsHost({ mode: 'build' } as DevframeNodeContext) await expect(host.broadcast({ method: 'devframe:terminals:updated', args: [], @@ -139,7 +139,7 @@ describe('rpcFunctionsHost', () => { }) it('should not throw in dev mode when rpc group is not yet set', async () => { - const host = new RpcFunctionsHost({ mode: 'dev' } as DevToolsNodeContext) + const host = new RpcFunctionsHost({ mode: 'dev' } as DevframeNodeContext) await expect(host.broadcast({ method: 'devframe:terminals:updated', args: [], diff --git a/packages/devframe/src/node/__tests__/rpc-agent-introspection.test.ts b/packages/devframe/src/node/__tests__/rpc-agent-introspection.test.ts index d125765..d698213 100644 --- a/packages/devframe/src/node/__tests__/rpc-agent-introspection.test.ts +++ b/packages/devframe/src/node/__tests__/rpc-agent-introspection.test.ts @@ -1,8 +1,8 @@ -import type { DevToolsHost } from '../../types/host' +import type { DevframeHost } from '../../types/host' import { describe, expect, it } from 'vitest' import { createHostContext } from '../context' -function nullHost(): DevToolsHost { +function nullHost(): DevframeHost { return { mountStatic: () => { /* no-op */ }, resolveOrigin: () => 'http://localhost:0', diff --git a/packages/devframe/src/node/__tests__/rpc-streaming.test.ts b/packages/devframe/src/node/__tests__/rpc-streaming.test.ts index e5e95db..48f7727 100644 --- a/packages/devframe/src/node/__tests__/rpc-streaming.test.ts +++ b/packages/devframe/src/node/__tests__/rpc-streaming.test.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions } from 'devframe/types' +import type { DevframeNodeContext, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types' import { AsyncLocalStorage } from 'node:async_hooks' import { createRpcStreamingClientHost } from 'devframe/client' import { createRpcClient } from 'devframe/rpc/client' @@ -24,12 +24,12 @@ interface Harness { async function bootHost(): Promise { const port = allocatePort() - const mockContext = {} as DevToolsNodeContext + const mockContext = {} as DevframeNodeContext const rpcHost = new RpcFunctionsHost(mockContext) const asyncStorage = new AsyncLocalStorage() - const rpcGroup = createRpcServer( + const rpcGroup = createRpcServer( rpcHost.functions, { rpcOptions: { @@ -70,13 +70,13 @@ async function bootHost(): Promise { } interface FakeClient { - rpc: ReturnType> + rpc: ReturnType> streaming: ReturnType close: () => void } function bootClient(port: number): FakeClient { - // Mimic the minimal `DevToolsRpcClient` surface that + // Mimic the minimal `DevframeRpcClient` surface that // `createRpcStreamingClientHost` uses (events, isTrusted, callEvent, // client.register). const listeners = new Set<(trusted: boolean) => void>() @@ -97,7 +97,7 @@ function bootClient(port: number): FakeClient { }, } - const rpc = createRpcClient( + const rpc = createRpcClient( clientFns, { channel: createWsRpcChannel({ url: `ws://127.0.0.1:${port}` }), diff --git a/packages/devframe/src/node/__tests__/storage.test.ts b/packages/devframe/src/node/__tests__/storage.test.ts index 8fbf93f..49f511b 100644 --- a/packages/devframe/src/node/__tests__/storage.test.ts +++ b/packages/devframe/src/node/__tests__/storage.test.ts @@ -10,7 +10,7 @@ function wait(ms: number) { describe('createStorage', () => { it('falls back to initial value when persisted JSON is invalid', async () => { - const dir = fs.mkdtempSync(join(os.tmpdir(), 'vite-devtools-storage-')) + const dir = fs.mkdtempSync(join(os.tmpdir(), 'devframe-storage-')) const filepath = join(dir, 'state.json') fs.writeFileSync(filepath, '{invalid json', 'utf-8') diff --git a/packages/devframe/src/node/auth/revoke.ts b/packages/devframe/src/node/auth/revoke.ts index 23208b1..bd4b309 100644 --- a/packages/devframe/src/node/auth/revoke.ts +++ b/packages/devframe/src/node/auth/revoke.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { DevframeNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { RpcFunctionsHost } from '../host-functions' import type { InternalAnonymousAuthStorage } from '../internal/context' @@ -10,7 +10,7 @@ import type { InternalAnonymousAuthStorage } from '../internal/context' * Shared between persisted-auth revocation and remote-dock token revocation. */ export async function revokeActiveConnectionsForToken( - context: DevToolsNodeContext, + context: DevframeNodeContext, token: string, ): Promise { const rpcHost = context.rpc as unknown as RpcFunctionsHost | undefined @@ -41,7 +41,7 @@ export async function revokeActiveConnectionsForToken( * using this token that they are no longer trusted. */ export async function revokeAuthToken( - context: DevToolsNodeContext, + context: DevframeNodeContext, storage: SharedState, token: string, ): Promise { diff --git a/packages/devframe/src/node/auth/state.ts b/packages/devframe/src/node/auth/state.ts index f6cd43c..39015ec 100644 --- a/packages/devframe/src/node/auth/state.ts +++ b/packages/devframe/src/node/auth/state.ts @@ -1,11 +1,11 @@ -import type { DevToolsNodeRpcSession } from 'devframe/types' +import type { DevframeNodeRpcSession } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { InternalAnonymousAuthStorage } from '../internal/context' import { humanId } from 'devframe/utils/human-id' export interface PendingAuthRequest { clientAuthToken: string - session: DevToolsNodeRpcSession + session: DevframeNodeRpcSession ua: string origin: string resolve: (result: { isTrusted: boolean }) => void diff --git a/packages/devframe/src/node/context.ts b/packages/devframe/src/node/context.ts index e780f92..00dca4e 100644 --- a/packages/devframe/src/node/context.ts +++ b/packages/devframe/src/node/context.ts @@ -1,18 +1,18 @@ import type { RpcFunctionDefinitionAny } from 'devframe/rpc' -import type { DevToolsHost, DevToolsNodeContext } from 'devframe/types' +import type { DevframeHost, DevframeNodeContext } from 'devframe/types' import { diagnostics as rpcDiagnostics } from '../rpc/diagnostics' import { diagnostics as devframeDiagnostics } from './diagnostics' -import { DevToolsAgentHost } from './host-agent' -import { DevToolsDiagnosticsHost } from './host-diagnostics' +import { DevframeAgentHost } from './host-agent' +import { DevframeDiagnosticsHost } from './host-diagnostics' import { RpcFunctionsHost } from './host-functions' -import { DevToolsViewHost } from './host-views' +import { DevframeViewHost } from './host-views' import { BUILTIN_AGENT_RPC } from './rpc' export interface CreateHostContextOptions { cwd: string workspaceRoot?: string mode: 'dev' | 'build' - host: DevToolsHost + host: DevframeHost /** * Built-in RPC declarations to register on the host. Framework * adapters (vite, rolldown, cli) can pass the ones they need; the @@ -22,17 +22,17 @@ export interface CreateHostContextOptions { } /** - * Framework- and build-tool-agnostic core of the DevTools node context. + * Framework- and build-tool-agnostic core of the Devframe node context. * Wires the RPC host, view (HTTP file-serving) host, diagnostics, and * agent subsystems. Host adapters can wrap this to augment `ctx` with * extra surfaces — for example, `@vitejs/devtools-kit`'s * `createKitContext` attaches `docks`, `terminals`, `messages`, * `commands`, and `createJsonRenderer` when mounted into Vite DevTools. */ -export async function createHostContext(options: CreateHostContextOptions): Promise { +export async function createHostContext(options: CreateHostContextOptions): Promise { const { cwd, workspaceRoot = cwd, mode, host, builtinRpcDeclarations = [] } = options - const context: DevToolsNodeContext = { + const context: DevframeNodeContext = { cwd, workspaceRoot, mode, @@ -41,11 +41,11 @@ export async function createHostContext(options: CreateHostContextOptions): Prom views: undefined!, diagnostics: undefined!, agent: undefined!, - } as unknown as DevToolsNodeContext + } as unknown as DevframeNodeContext const rpcHost = new RpcFunctionsHost(context) - const viewsHost = new DevToolsViewHost(context) - const diagnosticsHost = new DevToolsDiagnosticsHost(context, [devframeDiagnostics, rpcDiagnostics]) + const viewsHost = new DevframeViewHost(context) + const diagnosticsHost = new DevframeDiagnosticsHost(context, [devframeDiagnostics, rpcDiagnostics]) context.rpc = rpcHost context.views = viewsHost context.diagnostics = diagnosticsHost @@ -53,7 +53,7 @@ export async function createHostContext(options: CreateHostContextOptions): Prom // Agent host must be constructed after `rpcHost` so it can subscribe // to `onChanged` — it auto-discovers RPC functions flagged with // the `agent` field. - const agentHost = new DevToolsAgentHost(context) + const agentHost = new DevframeAgentHost(context) context.agent = agentHost // Auto-register devframe's own agent introspection RPCs. These power diff --git a/packages/devframe/src/node/diagnostics.ts b/packages/devframe/src/node/diagnostics.ts index 0feadf2..ac1cbce 100644 --- a/packages/devframe/src/node/diagnostics.ts +++ b/packages/devframe/src/node/diagnostics.ts @@ -9,7 +9,7 @@ export const diagnostics = defineDiagnostics({ why: (p: { name: string }) => `RPC function "${p.name}" is not registered`, }, DF0007: { - why: 'AsyncLocalStorage is not set, it likely to be an internal bug of the DevTools foundation', + why: 'AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation', }, DF0008: { why: (p: { distDir: string }) => `distDir ${p.distDir} does not exist`, diff --git a/packages/devframe/src/node/host-agent.ts b/packages/devframe/src/node/host-agent.ts index da7d074..5ccb644 100644 --- a/packages/devframe/src/node/host-agent.ts +++ b/packages/devframe/src/node/host-agent.ts @@ -7,9 +7,9 @@ import type { AgentResourceInput, AgentTool, AgentToolInput, - DevToolsAgentHostEvents, - DevToolsAgentHost as DevToolsAgentHostType, - DevToolsNodeContext, + DevframeAgentHostEvents, + DevframeAgentHost as DevframeAgentHostType, + DevframeNodeContext, EventEmitter, RpcFunctionAgentOptions, } from 'devframe/types' @@ -34,15 +34,15 @@ interface RegisteredResource { * * @experimental */ -export class DevToolsAgentHost implements DevToolsAgentHostType { - public readonly events: EventEmitter = createEventEmitter() +export class DevframeAgentHost implements DevframeAgentHostType { + public readonly events: EventEmitter = createEventEmitter() private readonly tools = new Map() private readonly resources = new Map() private _rpcUnsubscribe: (() => void) | undefined constructor( - public readonly context: DevToolsNodeContext, + public readonly context: DevframeNodeContext, ) { // Watch the RPC host for new `agent`-flagged definitions. this._rpcUnsubscribe = context.rpc.onChanged(() => { @@ -205,7 +205,7 @@ export class DevToolsAgentHost implements DevToolsAgentHostType { return out } - private _findRpcDefinition(id: string): RpcFunctionDefinitionAnyWithContext | undefined { + private _findRpcDefinition(id: string): RpcFunctionDefinitionAnyWithContext | undefined { const def = this.context.rpc.definitions.get(id) if (def?.agent) return def @@ -214,7 +214,7 @@ export class DevToolsAgentHost implements DevToolsAgentHostType { private _coercePositionalArgs( args: unknown, - def: RpcFunctionDefinitionAnyWithContext, + def: RpcFunctionDefinitionAnyWithContext, ): unknown[] { if (Array.isArray(args)) return args diff --git a/packages/devframe/src/node/host-diagnostics.ts b/packages/devframe/src/node/host-diagnostics.ts index 4485317..7b5c69a 100644 --- a/packages/devframe/src/node/host-diagnostics.ts +++ b/packages/devframe/src/node/host-diagnostics.ts @@ -1,24 +1,24 @@ -import type { DevToolsDiagnosticsHost as DevToolsDiagnosticsHostType, DevToolsDiagnosticsLogger, DevToolsNodeContext } from 'devframe/types' +import type { DevframeDiagnosticsHost as DevframeDiagnosticsHostType, DevframeDiagnosticsLogger, DevframeNodeContext } from 'devframe/types' import { defineDiagnostics } from 'nostics' import { devframeReporter } from '../utils/diagnostics-reporter' -export class DevToolsDiagnosticsHost implements DevToolsDiagnosticsHostType { +export class DevframeDiagnosticsHost implements DevframeDiagnosticsHostType { private _registry: Record = {} - readonly logger: DevToolsDiagnosticsLogger = new Proxy({} as DevToolsDiagnosticsLogger, { + readonly logger: DevframeDiagnosticsLogger = new Proxy({} as DevframeDiagnosticsLogger, { get: (_, code: string) => this._registry[code], }) - readonly defineDiagnostics: DevToolsDiagnosticsHostType['defineDiagnostics'] = (opts) => { + readonly defineDiagnostics: DevframeDiagnosticsHostType['defineDiagnostics'] = (opts) => { const merged = { ...opts, reporters: [devframeReporter, ...(opts.reporters ?? [])], } as Parameters[0] - return defineDiagnostics(merged) as ReturnType + return defineDiagnostics(merged) as ReturnType } constructor( - public readonly context: DevToolsNodeContext, + public readonly context: DevframeNodeContext, initialDefinitions: Array> = [], ) { for (const d of initialDefinitions) diff --git a/packages/devframe/src/node/host-functions.ts b/packages/devframe/src/node/host-functions.ts index e0a3ee6..6c5b8e3 100644 --- a/packages/devframe/src/node/host-functions.ts +++ b/packages/devframe/src/node/host-functions.ts @@ -1,5 +1,5 @@ import type { BirpcGroup } from 'birpc' -import type { DevToolsNodeContext, DevToolsNodeRpcSession, DevToolsNodeRpcSessionMeta, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, RpcBroadcastOptions, RpcFunctionsHost as RpcFunctionsHostType, RpcSharedStateHost, RpcStreamingHost } from 'devframe/types' +import type { DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions, RpcBroadcastOptions, RpcFunctionsHost as RpcFunctionsHostType, RpcSharedStateHost, RpcStreamingHost } from 'devframe/types' import type { AsyncLocalStorage } from 'node:async_hooks' import { RpcFunctionsCollectorBase } from 'devframe/rpc' import { createDebug } from 'obug' @@ -7,16 +7,16 @@ import { diagnostics } from './diagnostics' import { createRpcSharedStateServerHost } from './rpc-shared-state' import { createRpcStreamingServerHost } from './rpc-streaming' -const debugBroadcast = createDebug('vite:devtools:rpc:broadcast') +const debugBroadcast = createDebug('devframe:rpc:broadcast') -export class RpcFunctionsHost extends RpcFunctionsCollectorBase implements RpcFunctionsHostType { +export class RpcFunctionsHost extends RpcFunctionsCollectorBase implements RpcFunctionsHostType { /** * @internal */ - _rpcGroup: BirpcGroup = undefined! - _asyncStorage: AsyncLocalStorage = undefined! + _rpcGroup: BirpcGroup = undefined! + _asyncStorage: AsyncLocalStorage = undefined! - constructor(context: DevToolsNodeContext) { + constructor(context: DevframeNodeContext) { super(context) this.sharedState = createRpcSharedStateServerHost(this) @@ -33,30 +33,30 @@ export class RpcFunctionsHost extends RpcFunctionsCollectorBase, + T extends keyof DevframeRpcServerFunctions, + Args extends Parameters, >( method: T, ...args: Args - ): Promise>> { + ): Promise>> { if (!this.definitions.has(method as string)) { throw diagnostics.DF0006({ name: String(method) }) } const handler = await this.getHandler(method) return await Promise.resolve( - (handler as (...args: Args) => ReturnType)(...args), - ) as Awaited> + (handler as (...args: Args) => ReturnType)(...args), + ) as Awaited> } async broadcast< - T extends keyof DevToolsRpcClientFunctions, - Args extends Parameters, + T extends keyof DevframeRpcClientFunctions, + Args extends Parameters, >( options: RpcBroadcastOptions, ): Promise { @@ -78,7 +78,7 @@ export class RpcFunctionsHost extends RpcFunctionsCollectorBase void | Promise /** * Namespace for storage paths returned by `getStorageDir`. Workspace - * state lives under `${workspaceRoot}/node_modules/./devtools/` - * and global state under `${homedir()}/./devtools/`. Pick the + * state lives under `${workspaceRoot}/node_modules/./devframe/` + * and global state under `${homedir()}/./devframe/`. Pick the * devtool's id (or another stable, filesystem-safe identifier) so the * standalone host doesn't collide with other tools' storage. */ @@ -33,9 +33,9 @@ export interface CreateH3DevToolsHostOptions { } /** - * h3-backed {@link DevToolsHost} — used by the standalone CLI adapter. + * h3-backed {@link DevframeHost} — used by the standalone CLI adapter. */ -export function createH3DevToolsHost(options: CreateH3DevToolsHostOptions): DevToolsHost { +export function createH3DevframeHost(options: CreateH3DevframeHostOptions): DevframeHost { const workspaceRoot = options.workspaceRoot ?? process.cwd() return { mountStatic(base, distDir) { @@ -45,7 +45,7 @@ export function createH3DevToolsHost(options: CreateH3DevToolsHostOptions): DevT return options.origin }, getStorageDir(scope) { - const namespace = `.${options.appName}/devtools` + const namespace = `.${options.appName}/devframe` return scope === 'workspace' ? join(workspaceRoot, 'node_modules', namespace) : join(homedir(), namespace) diff --git a/packages/devframe/src/node/host-views.ts b/packages/devframe/src/node/host-views.ts index 7226b62..dcf704e 100644 --- a/packages/devframe/src/node/host-views.ts +++ b/packages/devframe/src/node/host-views.ts @@ -1,15 +1,15 @@ -import type { DevToolsNodeContext, DevToolsViewHost as DevToolsViewHostType } from 'devframe/types' +import type { DevframeNodeContext, DevframeViewHost as DevframeViewHostType } from 'devframe/types' import { existsSync } from 'node:fs' import { diagnostics } from './diagnostics' -export class DevToolsViewHost implements DevToolsViewHostType { +export class DevframeViewHost implements DevframeViewHostType { /** * @internal */ public buildStaticDirs: { baseUrl: string, distDir: string }[] = [] constructor( - public readonly context: DevToolsNodeContext, + public readonly context: DevframeNodeContext, ) { } diff --git a/packages/devframe/src/node/internal/context.ts b/packages/devframe/src/node/internal/context.ts index 20e0081..43506b8 100644 --- a/packages/devframe/src/node/internal/context.ts +++ b/packages/devframe/src/node/internal/context.ts @@ -1,4 +1,4 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { DevframeNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import { humanId } from 'devframe/utils/human-id' import { join } from 'pathe' @@ -21,7 +21,7 @@ export interface RemoteTokenRecord { originLock: boolean } -export interface DevToolsInternalContext { +export interface DevframeInternalContext { storage: { auth: SharedState } @@ -55,9 +55,9 @@ export interface DevToolsInternalContext { } } -export const internalContextMap = new WeakMap() +export const internalContextMap = new WeakMap() -export function getInternalContext(context: DevToolsNodeContext): DevToolsInternalContext { +export function getInternalContext(context: DevframeNodeContext): DevframeInternalContext { if (!internalContextMap.has(context)) { const storage = createStorage({ filepath: join(context.host.getStorageDir('global'), 'auth.json'), @@ -73,7 +73,7 @@ export function getInternalContext(context: DevToolsNodeContext): DevToolsIntern void revokeActiveConnectionsForToken(context, token) } - const internalContext: DevToolsInternalContext = { + const internalContext: DevframeInternalContext = { storage: { auth: storage, }, diff --git a/packages/devframe/src/node/internal/index.ts b/packages/devframe/src/node/internal/index.ts index e3c0234..3b119ec 100644 --- a/packages/devframe/src/node/internal/index.ts +++ b/packages/devframe/src/node/internal/index.ts @@ -20,7 +20,7 @@ export { } from './context' export type { - DevToolsInternalContext, + DevframeInternalContext, InternalAnonymousAuthStorage, RemoteTokenRecord, } from './context' diff --git a/packages/devframe/src/node/rpc-shared-state.ts b/packages/devframe/src/node/rpc-shared-state.ts index 329862b..ef4b2da 100644 --- a/packages/devframe/src/node/rpc-shared-state.ts +++ b/packages/devframe/src/node/rpc-shared-state.ts @@ -1,11 +1,11 @@ -import type { DevToolsRpcSharedStates, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost } from 'devframe/types' +import type { DevframeRpcSharedStates, RpcFunctionsHost, RpcSharedStateGetOptions, RpcSharedStateHost } from 'devframe/types' import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state' import { createSharedState } from 'devframe/utils/shared-state' import { createDebug } from 'obug' import { diagnostics } from './diagnostics' -const debug = createDebug('vite:devtools:rpc:state:changed') -const debugSubscribe = createDebug('vite:devtools:rpc:state:subscribe') +const debug = createDebug('devframe:rpc:state:changed') +const debugSubscribe = createDebug('devframe:rpc:state:subscribe') export function createRpcSharedStateServerHost( rpc: RpcFunctionsHost, @@ -97,7 +97,7 @@ export function createRpcSharedStateServerHost( handler: async (key: string) => { if (!sharedState.has(key)) return undefined - const state = await host.get(key as keyof DevToolsRpcSharedStates) + const state = await host.get(key as keyof DevframeRpcSharedStates) return state.value() }, // Pre-compute snapshots for the build-mode static dump so the SPA @@ -111,7 +111,7 @@ export function createRpcSharedStateServerHost( name: 'devframe:rpc:server-state:set', type: 'query', handler: async (key: string, value: any, syncId: string) => { - const state = await host.get(key as keyof DevToolsRpcSharedStates, { + const state = await host.get(key as keyof DevframeRpcSharedStates, { initialValue: value, }) state.mutate(() => value, syncId) @@ -124,7 +124,7 @@ export function createRpcSharedStateServerHost( handler: async (key: string, patches: SharedStatePatch[], syncId: string) => { if (!sharedState.has(key)) return - const state = await host.get(key as keyof DevToolsRpcSharedStates) + const state = await host.get(key as keyof DevframeRpcSharedStates) state.patch(patches, syncId) }, }) diff --git a/packages/devframe/src/node/rpc-streaming.ts b/packages/devframe/src/node/rpc-streaming.ts index 023775e..3e1d2d8 100644 --- a/packages/devframe/src/node/rpc-streaming.ts +++ b/packages/devframe/src/node/rpc-streaming.ts @@ -1,5 +1,5 @@ import type { - DevToolsNodeRpcSessionMeta, + DevframeNodeRpcSessionMeta, RpcFunctionsHost, RpcStreamingChannel, RpcStreamingChannelOptions, @@ -10,7 +10,7 @@ import { createStreamReader, createStreamSink } from 'devframe/utils/streaming-c import { createDebug } from 'obug' import { diagnostics } from './diagnostics' -const debug = createDebug('vite:devtools:rpc:streaming') +const debug = createDebug('devframe:rpc:streaming') const STREAM_KEY_SEPARATOR = '\x1F' @@ -20,7 +20,7 @@ function streamKey(channel: string, id: string): string { interface ServerStreamRecord { sink: StreamSink - subscribers: Set + subscribers: Set unbinders: (() => void)[] /** Timer scheduled when stream closes with no subscribers; cleared on resubscribe. */ retentionTimer?: ReturnType @@ -29,7 +29,7 @@ interface ServerStreamRecord { interface ServerInboundRecord { reader: StreamReader /** First session that wrote to this inbound — locks ownership for cleanup. */ - uploaderMeta?: DevToolsNodeRpcSessionMeta + uploaderMeta?: DevframeNodeRpcSessionMeta } interface ChannelState { @@ -254,7 +254,7 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin args: [name, sink.id, seq, chunk], event: true, optional: true, - filter: client => record.subscribers.has(client.$meta as DevToolsNodeRpcSessionMeta), + filter: client => record.subscribers.has(client.$meta as DevframeNodeRpcSessionMeta), }) }), ) @@ -265,7 +265,7 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin args: [name, sink.id, error], event: true, optional: true, - filter: client => record.subscribers.has(client.$meta as DevToolsNodeRpcSessionMeta), + filter: client => record.subscribers.has(client.$meta as DevframeNodeRpcSessionMeta), }) maybeFreeStream(state, sink.id) }), @@ -332,7 +332,7 @@ export function createRpcStreamingServerHost(rpc: RpcFunctionsHost): RpcStreamin return { create: createChannel, - _onSessionDisconnected(meta: DevToolsNodeRpcSessionMeta) { + _onSessionDisconnected(meta: DevframeNodeRpcSessionMeta) { // Outbound: drop subscriber, abort if last one drops. if (meta.subscribedStreams) { for (const key of meta.subscribedStreams) { diff --git a/packages/devframe/src/node/rpc/index.ts b/packages/devframe/src/node/rpc/index.ts index 7fd3f00..413ed6c 100644 --- a/packages/devframe/src/node/rpc/index.ts +++ b/packages/devframe/src/node/rpc/index.ts @@ -20,7 +20,7 @@ export const BUILTIN_AGENT_RPC = [ ] as const declare module 'devframe/types' { - interface DevToolsRpcServerFunctions { + interface DevframeRpcServerFunctions { 'devframe:agent:list-tools': () => Promise 'devframe:agent:invoke-tool': (id: string, args: unknown) => Promise 'devframe:agent:list-resources': () => Promise diff --git a/packages/devframe/src/node/server.ts b/packages/devframe/src/node/server.ts index 4c81177..d4410cb 100644 --- a/packages/devframe/src/node/server.ts +++ b/packages/devframe/src/node/server.ts @@ -1,5 +1,5 @@ import type { BirpcGroup } from 'birpc' -import type { DevToolsNodeContext, DevToolsNodeRpcSession, DevToolsRpcClientFunctions, DevToolsRpcServerFunctions } from 'devframe/types' +import type { DevframeNodeContext, DevframeNodeRpcSession, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types' import type { WebSocketServer } from 'ws' import type { RpcFunctionsHost } from './host-functions' import { AsyncLocalStorage } from 'node:async_hooks' @@ -11,7 +11,7 @@ import { WebSocketServer as WSServer } from 'ws' import { getInternalContext } from './internal/context' export interface StartHttpAndWsOptions { - context: DevToolsNodeContext + context: DevframeNodeContext host?: string port: number /** @@ -45,7 +45,7 @@ export interface StartedServer { port: number app: H3 wss: WebSocketServer - rpcGroup: BirpcGroup + rpcGroup: BirpcGroup close: () => Promise } @@ -62,9 +62,9 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise() + const asyncStorage = new AsyncLocalStorage() - const rpcGroup = createRpcServer( + const rpcGroup = createRpcServer( rpcHost.functions, { rpcOptions: { @@ -103,15 +103,15 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise { const session = rpcHost.getCurrentRpcSession() diff --git a/packages/devframe/src/rpc/dump/__tests__/static.test.ts b/packages/devframe/src/rpc/dump/__tests__/static.test.ts index 1b2369a..1b1194a 100644 --- a/packages/devframe/src/rpc/dump/__tests__/static.test.ts +++ b/packages/devframe/src/rpc/dump/__tests__/static.test.ts @@ -1,5 +1,5 @@ import { defineRpcFunction } from 'devframe' -import { DEVTOOLS_RPC_DUMP_DIRNAME } from 'devframe/constants' +import { DEVFRAME_RPC_DUMP_DIRNAME } from 'devframe/constants' import { strictJsonStringify } from 'devframe/rpc' import { structuredCloneDeserialize, structuredCloneStringify } from 'devframe/utils/structured-clone' import { describe, expect, it } from 'vitest' @@ -15,7 +15,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getVersion], {}) - const expectedPath = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~json-version.static.json` + const expectedPath = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~json-version.static.json` expect(result.manifest['test:json-version']).toEqual({ type: 'static', @@ -33,7 +33,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getVersion], {}) - const expectedPath = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~get-version.static.json` + const expectedPath = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~get-version.static.json` expect(result.manifest['test:get-version']).toEqual({ type: 'static', @@ -63,7 +63,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getItem], {}) - const basePath = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~get-item` + const basePath = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~get-item` const manifest = result.manifest['test:get-item'] as { type: 'query' records: Record @@ -114,7 +114,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getGraph], {}) - const path = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~graph.static.json` + const path = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~graph.static.json` const file = result.files[path]! expect(file.serialization).toBe('structured-clone') @@ -136,7 +136,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getMap], {}) - const path = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~roundtrip-map.static.json` + const path = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~roundtrip-map.static.json` const file = result.files[path]! // Server side: write to disk as sc-encoded text. @@ -160,7 +160,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getEntries], {}) - const fallbackPath = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~entries.fallback.json` + const fallbackPath = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~entries.fallback.json` const fallback = result.files[fallbackPath]! expect(fallback.serialization).toBe('structured-clone') @@ -188,7 +188,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getList], {}) - const path = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~json-list.static.json` + const path = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~json-list.static.json` const file = result.files[path]! expect(file.serialization).toBe('json') @@ -207,7 +207,7 @@ describe('collectStaticRpcDump', () => { }) const result = await collectStaticRpcDump([getMapJson], {}) - const path = `${DEVTOOLS_RPC_DUMP_DIRNAME}/test~bad-json.static.json` + const path = `${DEVFRAME_RPC_DUMP_DIRNAME}/test~bad-json.static.json` const file = result.files[path]! // collectStaticRpcDump records the value as-is; the strict diff --git a/packages/devframe/src/rpc/dump/static.ts b/packages/devframe/src/rpc/dump/static.ts index 2120d05..01b4723 100644 --- a/packages/devframe/src/rpc/dump/static.ts +++ b/packages/devframe/src/rpc/dump/static.ts @@ -1,6 +1,6 @@ import type { RpcDumpRecord, RpcFunctionDefinitionAny } from '../types' import { - DEVTOOLS_RPC_DUMP_DIRNAME, + DEVFRAME_RPC_DUMP_DIRNAME, } from 'devframe/constants' import { getRpcHandler } from '../handler' import { dumpFunctions } from './collect' @@ -48,15 +48,15 @@ function makeDumpKey(name: string): string { } function makeStaticPath(name: string): string { - return `${DEVTOOLS_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.static.json` + return `${DEVFRAME_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.static.json` } function makeQueryRecordPath(name: string, hash: string): string { - return `${DEVTOOLS_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.record.${hash}.json` + return `${DEVFRAME_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.record.${hash}.json` } function makeQueryFallbackPath(name: string): string { - return `${DEVTOOLS_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.fallback.json` + return `${DEVFRAME_RPC_DUMP_DIRNAME}/${makeDumpKey(name)}.fallback.json` } async function resolveRecord(record: RpcDumpRecord | (() => Promise)): Promise { diff --git a/packages/devframe/src/rpc/transports/ws-client.ts b/packages/devframe/src/rpc/transports/ws-client.ts index e246309..230ff94 100644 --- a/packages/devframe/src/rpc/transports/ws-client.ts +++ b/packages/devframe/src/rpc/transports/ws-client.ts @@ -30,7 +30,7 @@ const EMPTY_DEFS: ReadonlyMap> - onConnected?: (ws: WebSocket, req: IncomingMessage, meta: DevToolsNodeRpcSessionMeta) => void - onDisconnected?: (ws: WebSocket, meta: DevToolsNodeRpcSessionMeta) => void + onConnected?: (ws: WebSocket, req: IncomingMessage, meta: DevframeNodeRpcSessionMeta) => void + onDisconnected?: (ws: WebSocket, meta: DevframeNodeRpcSessionMeta) => void /** Override the default per-call serializer. Most callers should leave this unset. */ serialize?: ChannelOptions['serialize'] /** Override the default per-call deserializer. Most callers should leave this unset. */ @@ -98,7 +98,7 @@ export function attachWsRpcTransport< } wss.on('connection', (ws, req) => { - const meta: DevToolsNodeRpcSessionMeta = { + const meta: DevframeNodeRpcSessionMeta = { id: sessionId++, ws, subscribedStates: new Set(), diff --git a/packages/devframe/src/rpc/transports/ws.test.ts b/packages/devframe/src/rpc/transports/ws.test.ts index 0d8370c..d84c52e 100644 --- a/packages/devframe/src/rpc/transports/ws.test.ts +++ b/packages/devframe/src/rpc/transports/ws.test.ts @@ -8,7 +8,7 @@ import { attachWsRpcTransport } from './ws-server' vi.stubGlobal('WebSocket', WebSocket) -describe('devtools rpc', () => { +describe('devframe rpc', () => { it('should work w/ ws transport', async () => { const PORT = 3333 // Use 127.0.0.1 on both client and server so they agree on the diff --git a/packages/devframe/src/types/agent.ts b/packages/devframe/src/types/agent.ts index a966a3a..bb559bb 100644 --- a/packages/devframe/src/types/agent.ts +++ b/packages/devframe/src/types/agent.ts @@ -33,7 +33,7 @@ export interface AgentTool { } /** - * Input accepted by `DevToolsAgentHost.registerTool()`. Handler is + * Input accepted by `DevframeAgentHost.registerTool()`. Handler is * stripped from the serializable `AgentTool` projection. * * @experimental @@ -53,7 +53,7 @@ export interface AgentToolInput { /** * Serializable description of an agent-readable resource. Resources - * surface structured or textual snapshots of devtools state. + * surface structured or textual snapshots of devframe state. * * @experimental */ @@ -68,7 +68,7 @@ export interface AgentResource { } /** - * Input accepted by `DevToolsAgentHost.registerResource()`. + * Input accepted by `DevframeAgentHost.registerResource()`. * * @experimental */ @@ -115,11 +115,11 @@ export interface AgentHandle { } /** - * Events emitted by `DevToolsAgentHost`. + * Events emitted by `DevframeAgentHost`. * * @experimental */ -export interface DevToolsAgentHostEvents { +export interface DevframeAgentHostEvents { 'agent:tool:registered': (tool: AgentTool) => void 'agent:tool:unregistered': (id: string) => void 'agent:resource:registered': (resource: AgentResource) => void @@ -140,8 +140,8 @@ export interface DevToolsAgentHostEvents { * @experimental The agent-native surface is experimental and may change * without a major version bump until it stabilizes. */ -export interface DevToolsAgentHost { - readonly events: EventEmitter +export interface DevframeAgentHost { + readonly events: EventEmitter /** * Register a tool not backed by an RPC function. Use this when you diff --git a/packages/devframe/src/types/context.ts b/packages/devframe/src/types/context.ts index a5c4b71..2fa2761 100644 --- a/packages/devframe/src/types/context.ts +++ b/packages/devframe/src/types/context.ts @@ -1,9 +1,9 @@ -import type { DevToolsAgentHost } from './agent' -import type { DevToolsDiagnosticsHost } from './diagnostics' -import type { DevToolsHost } from './host' -import type { DevToolsViewHost } from './views' +import type { DevframeAgentHost } from './agent' +import type { DevframeDiagnosticsHost } from './diagnostics' +import type { DevframeHost } from './host' +import type { DevframeViewHost } from './views' -export interface DevToolsCapabilities { +export interface DevframeCapabilities { rpc?: boolean views?: boolean } @@ -15,7 +15,7 @@ export interface DevToolsCapabilities { * `createKitContext` adds `docks`, `terminals`, `messages`, and * `commands` when mounted into Vite DevTools. */ -export interface DevToolsNodeContext { +export interface DevframeNodeContext { readonly workspaceRoot: string readonly cwd: string /** @@ -37,20 +37,20 @@ export interface DevToolsNodeContext { * Host runtime abstraction — exposes `mountStatic` / `resolveOrigin` / * `getStorageDir`. */ - host: DevToolsHost + host: DevframeHost rpc: import('./rpc').RpcFunctionsHost - views: DevToolsViewHost + views: DevframeViewHost /** * Structured diagnostics host — wraps `nostics` and lets integrations * register their own coded errors/warnings into the shared lookup. */ - diagnostics: DevToolsDiagnosticsHost + diagnostics: DevframeDiagnosticsHost /** * Agent host — aggregates the agent-exposed surface of this devtool. * * @experimental */ - agent: DevToolsAgentHost + agent: DevframeAgentHost } export interface ConnectionMeta { diff --git a/packages/devframe/src/types/devframe.ts b/packages/devframe/src/types/devframe.ts index a1e8a28..a95906d 100644 --- a/packages/devframe/src/types/devframe.ts +++ b/packages/devframe/src/types/devframe.ts @@ -1,6 +1,6 @@ import type { CAC } from 'cac' import type { CliFlagsSchema } from '../adapters/flags' -import type { DevToolsNodeContext } from './context' +import type { DevframeNodeContext } from './context' export type DevframeRuntime = 'cli' | 'build' | 'spa' | 'vite' | 'embedded' @@ -120,7 +120,7 @@ export interface DevframeDefinition { spa?: boolean | Record } /** Server-side setup — the primary entrypoint. Runs in every runtime. */ - setup: (ctx: DevToolsNodeContext, info?: DevframeSetupInfo) => void | Promise + setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise /** Browser-only setup for the SPA adapter (bundled into the client). */ setupBrowser?: (ctx: DevframeBrowserContext) => void | Promise cli?: DevframeCliOptions diff --git a/packages/devframe/src/types/diagnostics.ts b/packages/devframe/src/types/diagnostics.ts index 7ad491b..03d4815 100644 --- a/packages/devframe/src/types/diagnostics.ts +++ b/packages/devframe/src/types/diagnostics.ts @@ -7,7 +7,7 @@ import type { defineDiagnostics, Diagnostic, DiagnosticDefinition } from 'nostic * types don't allow assigning a narrow-keyed result to a generically-keyed * one. The host stores them in a heterogeneous registry. */ -export type DevToolsDiagnosticsDefinition = ReturnType> +export type DevframeDiagnosticsDefinition = ReturnType> /** * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves @@ -16,14 +16,14 @@ export type DevToolsDiagnosticsDefinition = ReturnType +export type DevframeDiagnosticsLogger = Record /** * Options accepted by the host's `defineDiagnostics()` factory — mirrors * `nostics`'s shape but the host pre-wires its ANSI console reporter, so * plugins typically omit `reporters`. */ -export interface DevToolsDefineDiagnosticsOptions> { +export interface DevframeDefineDiagnosticsOptions> { docsBase?: string | ((code: keyof Codes) => string | undefined) codes: Codes reporters?: ReadonlyArray<(d: Diagnostic, o?: any) => void> @@ -52,7 +52,7 @@ export interface DevToolsDefineDiagnosticsOptions>( - options: DevToolsDefineDiagnosticsOptions, + options: DevframeDefineDiagnosticsOptions, ) => ReturnType> } diff --git a/packages/devframe/src/types/host.ts b/packages/devframe/src/types/host.ts index 70e5cd8..686f311 100644 --- a/packages/devframe/src/types/host.ts +++ b/packages/devframe/src/types/host.ts @@ -1,4 +1,4 @@ -// DevToolsHost — abstraction over the runtime that serves the DevTools +// DevframeHost — abstraction over the runtime that serves the Devframe // UI and RPC endpoints (Vite dev server, standalone h3 CLI server, static // snapshot, embedded, etc.). // @@ -8,10 +8,10 @@ // - packages/devframe/src/node/host-h3.ts — h3 CLI server // - (build/spa/embedded) — added as the respective adapters land -export interface DevToolsHost { +export interface DevframeHost { /** * Serve a static directory at the given URL base. Called by - * `DevToolsViewHost.hostStatic`. Implementations map this to whatever + * `DevframeViewHost.hostStatic`. Implementations map this to whatever * the underlying runtime expects (Vite middleware, h3 handler, no-op * for build snapshots). */ @@ -27,16 +27,16 @@ export interface DevToolsHost { resolveOrigin: () => string /** - * Resolve a directory the host owns for persisted devtools state. + * Resolve a directory the host owns for persisted devframe state. * Each host picks its own app-name namespace so storage doesn't - * collide between, say, the Vite host (`.vite/devtools`) and a - * standalone CLI host (`./devtools`). + * collide between, say, the Vite host (`.vite/devframe`) and a + * standalone CLI host (`./devframe`). * * - `workspace` — per-project state (settings, caches). Typically - * under `${workspaceRoot}/node_modules/./devtools/`. + * under `${workspaceRoot}/node_modules/./devframe/`. * - `global` — per-user state (auth tokens, machine-wide * preferences). Typically under - * `${homedir()}/./devtools/`. + * `${homedir()}/./devframe/`. * * Implementations should ensure the directory exists or be safe to * pass to a downstream `createStorage(...)` call that creates it diff --git a/packages/devframe/src/types/rpc-augments.ts b/packages/devframe/src/types/rpc-augments.ts index 8d4d5e5..9403d08 100644 --- a/packages/devframe/src/types/rpc-augments.ts +++ b/packages/devframe/src/types/rpc-augments.ts @@ -1,7 +1,7 @@ /** * To be extended */ -export interface DevToolsRpcClientFunctions { +export interface DevframeRpcClientFunctions { /** * Streaming chunk pushed from server to subscribed clients. Wired by * `RpcStreamingHost`; do not register manually. @@ -28,7 +28,7 @@ export interface DevToolsRpcClientFunctions { /** * To be extended */ -export interface DevToolsRpcServerFunctions { +export interface DevframeRpcServerFunctions { /** * Subscribe a client to a shared-state key. Wired by * `RpcSharedStateHost`; do not register manually. @@ -97,4 +97,4 @@ export interface DevToolsRpcServerFunctions { /** * To be extended */ -export interface DevToolsRpcSharedStates {} +export interface DevframeRpcSharedStates {} diff --git a/packages/devframe/src/types/rpc.ts b/packages/devframe/src/types/rpc.ts index caed6a1..643eb9d 100644 --- a/packages/devframe/src/types/rpc.ts +++ b/packages/devframe/src/types/rpc.ts @@ -1,16 +1,16 @@ import type { BirpcReturn } from 'birpc' import type { RpcFunctionsCollectorBase } from 'devframe/rpc' -import type { DevToolsNodeRpcSessionMeta } from 'devframe/rpc/transports/ws-server' +import type { DevframeNodeRpcSessionMeta } from 'devframe/rpc/transports/ws-server' import type { SharedState } from 'devframe/utils/shared-state' import type { StreamReader, StreamSink } from 'devframe/utils/streaming-channel' -import type { DevToolsNodeContext } from './context' -import type { DevToolsRpcClientFunctions, DevToolsRpcServerFunctions, DevToolsRpcSharedStates } from './rpc-augments' +import type { DevframeNodeContext } from './context' +import type { DevframeRpcClientFunctions, DevframeRpcServerFunctions, DevframeRpcSharedStates } from './rpc-augments' -export type { DevToolsNodeRpcSessionMeta } +export type { DevframeNodeRpcSessionMeta } -export interface DevToolsNodeRpcSession { - meta: DevToolsNodeRpcSessionMeta - rpc: BirpcReturn +export interface DevframeNodeRpcSession { + meta: DevframeNodeRpcSessionMeta + rpc: BirpcReturn } export interface RpcBroadcastOptions { @@ -18,29 +18,29 @@ export interface RpcBroadcastOptions { args: Args optional?: boolean event?: boolean - filter?: (client: BirpcReturn) => boolean | void + filter?: (client: BirpcReturn) => boolean | void } -export type RpcFunctionsHost = RpcFunctionsCollectorBase & { +export type RpcFunctionsHost = RpcFunctionsCollectorBase & { /** * Invoke a locally registered server RPC function directly. * * This bypasses transport and is useful for server-side cross-function calls. */ invokeLocal: < - T extends keyof DevToolsRpcServerFunctions, - Args extends Parameters, + T extends keyof DevframeRpcServerFunctions, + Args extends Parameters, >( method: T, ...args: Args - ) => Promise>> + ) => Promise>> /** * Broadcast a message to all connected clients */ broadcast: < - T extends keyof DevToolsRpcClientFunctions, - Args extends Parameters, + T extends keyof DevframeRpcClientFunctions, + Args extends Parameters, >( options: RpcBroadcastOptions, ) => Promise @@ -50,7 +50,7 @@ export type RpcFunctionsHost = RpcFunctionsCollectorBase DevToolsNodeRpcSession | undefined + getCurrentRpcSession: () => DevframeNodeRpcSession | undefined /** * The shared state host @@ -72,7 +72,7 @@ export interface RpcSharedStateGetOptions { } export interface RpcSharedStateHost { - get: (key: T, options?: RpcSharedStateGetOptions) => Promise> + get: (key: T, options?: RpcSharedStateGetOptions) => Promise> keys: () => string[] /** * Subscribe to new shared-state keys becoming available. Fires when @@ -180,5 +180,5 @@ export interface RpcStreamingHost { * * @internal */ - _onSessionDisconnected: (meta: DevToolsNodeRpcSessionMeta) => void + _onSessionDisconnected: (meta: DevframeNodeRpcSessionMeta) => void } diff --git a/packages/devframe/src/types/views.ts b/packages/devframe/src/types/views.ts index 42ae967..111a90f 100644 --- a/packages/devframe/src/types/views.ts +++ b/packages/devframe/src/types/views.ts @@ -1,4 +1,4 @@ -export interface DevToolsViewHost { +export interface DevframeViewHost { /** * @internal */ diff --git a/packages/devframe/test/dts-dedupe.test.ts b/packages/devframe/test/dts-dedupe.test.ts index c96cbbc..0df727e 100644 --- a/packages/devframe/test/dts-dedupe.test.ts +++ b/packages/devframe/test/dts-dedupe.test.ts @@ -6,9 +6,9 @@ import { describe, expect, it } from 'vitest' const distRoot = fileURLToPath(new URL('../dist/', import.meta.url)) const AUGMENTABLE_INTERFACES = [ - 'DevToolsRpcClientFunctions', - 'DevToolsRpcServerFunctions', - 'DevToolsRpcSharedStates', + 'DevframeRpcClientFunctions', + 'DevframeRpcServerFunctions', + 'DevframeRpcSharedStates', ] as const describe('rpc-augments dedupe', () => { diff --git a/packages/hub/src/client/client-script.ts b/packages/hub/src/client/client-script.ts index 6f90555..86d2f8f 100644 --- a/packages/hub/src/client/client-script.ts +++ b/packages/hub/src/client/client-script.ts @@ -1,4 +1,4 @@ -import type { DevToolsMessagesClient } from '../types/messages' +import type { DevframeMessagesClient } from '../types/messages' import type { DockEntryState, DocksContext } from './docks' /** @@ -12,5 +12,5 @@ export interface DockClientScriptContext extends DocksContext { /** * Messages client scoped to this dock entry's source */ - messages: DevToolsMessagesClient + messages: DevframeMessagesClient } diff --git a/packages/hub/src/client/context.ts b/packages/hub/src/client/context.ts index 88701e0..df79766 100644 --- a/packages/hub/src/client/context.ts +++ b/packages/hub/src/client/context.ts @@ -1,11 +1,11 @@ -import type { DevToolsClientContext } from './docks' +import type { DevframeClientContext } from './docks' const CLIENT_CONTEXT_KEY = '__DEVFRAME_HUB_CLIENT_CONTEXT__' /** - * Get the global DevTools client context, or `undefined` if not yet initialized. + * Get the global Devframe client context, or `undefined` if not yet initialized. */ -export function getDevToolsClientContext(): DevToolsClientContext | undefined { +export function getDevframeClientContext(): DevframeClientContext | undefined { return (globalThis as any)[CLIENT_CONTEXT_KEY] } diff --git a/packages/hub/src/client/docks.ts b/packages/hub/src/client/docks.ts index 9ae4391..a5d217c 100644 --- a/packages/hub/src/client/docks.ts +++ b/packages/hub/src/client/docks.ts @@ -1,12 +1,12 @@ -import type { DevToolsRpcContext } from 'devframe/client' +import type { DevframeRpcContext } 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' +import type { DevframeClientCommand, DevframeCommandEntry, DevframeCommandKeybinding } from '../types/commands' +import type { DevframeDockEntriesGrouped, DevframeDockEntry, DevframeDockUserEntry } from '../types/docks' +import type { DevframeDocksUserSettings } from '../types/settings' -export type { DevToolsClientRpcHost, RpcClientEvents } from 'devframe/client' +export type { DevframeClientRpcHost, RpcClientEvents } from 'devframe/client' export interface DockPanelStorage { mode: 'float' | 'edge' @@ -21,7 +21,7 @@ export interface DockPanelStorage { export type DockClientType = 'embedded' | 'standalone' -export interface DocksContext extends DevToolsRpcContext { +export interface DocksContext extends DevframeRpcContext { /** * Type of the client environment * @@ -55,7 +55,7 @@ export interface WhenClauseContext { readonly context: WhenContext } -export type DevToolsClientContext = DocksContext +export type DevframeClientContext = DocksContext export interface DocksPanelContext { store: DockPanelStorage @@ -66,11 +66,11 @@ export interface DocksPanelContext { export interface DocksEntriesContext { selectedId: string | null - readonly selected: DevToolsDockEntry | null - entries: DevToolsDockEntry[] + readonly selected: DevframeDockEntry | null + entries: DevframeDockEntry[] entryToStateMap: Map - groupedEntries: DevToolsDockEntriesGrouped - settings: SharedState + groupedEntries: DevframeDockEntriesGrouped + settings: SharedState /** * Get the state of a dock entry by its ID */ @@ -90,7 +90,7 @@ export interface DocksEntriesContext { } export interface DockEntryState { - entryMeta: DevToolsDockEntry + entryMeta: DevframeDockEntry readonly isActive: boolean domElements: { iframe?: HTMLIFrameElement | null @@ -102,7 +102,7 @@ export interface DockEntryState { export interface DockEntryStateEvents { 'entry:activated': () => void 'entry:deactivated': () => void - 'entry:updated': (newMeta: DevToolsDockUserEntry) => void + 'entry:updated': (newMeta: DevframeDockUserEntry) => void 'dom:panel:mounted': (panel: HTMLDivElement) => void 'dom:iframe:mounted': (iframe: HTMLIFrameElement) => void } @@ -111,15 +111,15 @@ export interface CommandsContext { /** * All commands (server + client) */ - readonly commands: DevToolsCommandEntry[] + readonly commands: DevframeCommandEntry[] /** * Palette-visible commands only (filtered by `showInPalette !== false`) */ - readonly paletteCommands: DevToolsCommandEntry[] + readonly paletteCommands: DevframeCommandEntry[] /** * Register client-side command(s). Returns cleanup function. */ - register: (cmd: DevToolsClientCommand | DevToolsClientCommand[]) => () => void + register: (cmd: DevframeClientCommand | DevframeClientCommand[]) => () => void /** * Execute a command by ID. Delegates to RPC for server commands. */ @@ -127,11 +127,11 @@ export interface CommandsContext { /** * Get effective keybindings for a command (defaults merged with overrides) */ - getKeybindings: (id: string) => DevToolsCommandKeybinding[] + getKeybindings: (id: string) => DevframeCommandKeybinding[] /** * User settings store (persisted, includes command shortcuts) */ - settings: SharedState + settings: SharedState /** * Whether the command palette is open */ diff --git a/packages/hub/src/client/remote.ts b/packages/hub/src/client/remote.ts index 5d3d1f2..6897104 100644 --- a/packages/hub/src/client/remote.ts +++ b/packages/hub/src/client/remote.ts @@ -1,9 +1,9 @@ -import type { DevToolsRpcClient, DevToolsRpcClientOptions } from 'devframe/client' +import type { DevframeRpcClient, DevframeRpcClientOptions } from 'devframe/client' import type { RemoteConnectionInfo } from '../types' -import { getDevToolsRpcClient } from 'devframe/client' +import { getDevframeRpcClient } from 'devframe/client' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' -export type ConnectRemoteDevToolsOptions = Omit +export type ConnectRemoteDevframeOptions = Omit function base64UrlDecode(value: string): string { const padLen = (4 - value.length % 4) % 4 @@ -104,17 +104,17 @@ export function parseRemoteConnection(input?: string): RemoteConnectionInfo | nu } /** - * One-liner for a hosted DevTools page: reads the connection descriptor from - * the current URL and returns a connected {@link DevToolsRpcClient}. + * One-liner for a hosted Devframe page: reads the connection descriptor from + * the current URL and returns a connected {@link DevframeRpcClient}. * - * Pairs with `remote: true` on a `DevToolsViewIframe` registered on the node + * Pairs with `remote: true` on a `DevframeViewIframe` registered on the node * side — the hub injects the descriptor into the iframe URL. * * @throws if no descriptor is present in the URL. */ -export async function connectRemoteDevTools( - options: ConnectRemoteDevToolsOptions = {}, -): Promise { +export async function connectRemoteDevframe( + options: ConnectRemoteDevframeOptions = {}, +): Promise { const info = parseRemoteConnection() if (!info) { throw new Error( @@ -122,7 +122,7 @@ export async function connectRemoteDevTools( + `Open this page through a hub-registered dock with \`remote: true\`.`, ) } - return getDevToolsRpcClient({ + return getDevframeRpcClient({ ...options, connectionMeta: info, authToken: info.authToken, diff --git a/packages/hub/src/constants.ts b/packages/hub/src/constants.ts index 71f9acc..f0712da 100644 --- a/packages/hub/src/constants.ts +++ b/packages/hub/src/constants.ts @@ -1,5 +1,5 @@ -import type { DevToolsDockEntryCategory } from './types/docks' -import type { DevToolsDocksUserSettings } from './types/settings' +import type { DevframeDockEntryCategory } from './types/docks' +import type { DevframeDocksUserSettings } from './types/settings' export * from 'devframe/constants' @@ -11,9 +11,9 @@ export const DEFAULT_CATEGORIES_ORDER: Record = { 'web': 300, 'advanced': 400, '~builtin': 1000, -} satisfies Record +} satisfies Record -export const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings = () => ({ +export const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings = () => ({ docksHidden: [], docksCategoriesHidden: [], docksPinned: [], diff --git a/packages/hub/src/define.ts b/packages/hub/src/define.ts index 479fce3..bc09f47 100644 --- a/packages/hub/src/define.ts +++ b/packages/hub/src/define.ts @@ -1,20 +1,20 @@ import type { WhenContext, WhenExpression } from 'devframe/utils/when' import type { HubNodeContext } from './node/context' -import type { DevToolsServerCommandInput } from './types/commands' -import type { DevToolsDockUserEntry } from './types/docks' +import type { DevframeServerCommandInput } from './types/commands' +import type { DevframeDockUserEntry } from './types/docks' import type { JsonRenderSpec } from './types/json-render' import { createDefineWrapperWithContext } from 'devframe/rpc' export const defineRpcFunction = createDefineWrapperWithContext() export function defineCommand( - command: Omit & { when?: WhenExpression }, -): DevToolsServerCommandInput { - return command as DevToolsServerCommandInput + command: Omit & { when?: WhenExpression }, +): DevframeServerCommandInput { + return command as DevframeServerCommandInput } export function defineDockEntry< - const T extends DevToolsDockUserEntry, + const T extends DevframeDockUserEntry, const W extends string = '', >( entry: Omit & { when?: WhenExpression }, diff --git a/packages/hub/src/node/__tests__/host-commands.test.ts b/packages/hub/src/node/__tests__/host-commands.test.ts index ccc4e56..795e346 100644 --- a/packages/hub/src/node/__tests__/host-commands.test.ts +++ b/packages/hub/src/node/__tests__/host-commands.test.ts @@ -1,10 +1,10 @@ import type { HubNodeContext } from '../context' import { describe, expect, it } from 'vitest' -import { DevToolsCommandsHost } from '../host-commands' +import { DevframeCommandsHost } from '../host-commands' describe('devToolsCommandsHost command id validation', () => { it('rejects duplicate ids inside one command tree', () => { - const host = new DevToolsCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as HubNodeContext) expect(() => host.register({ id: 'tool:parent', @@ -17,7 +17,7 @@ describe('devToolsCommandsHost command id validation', () => { }) it('rejects child ids that collide with existing command trees', () => { - const host = new DevToolsCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as HubNodeContext) host.register({ id: 'tool:parent', title: 'Parent', @@ -41,7 +41,7 @@ describe('devToolsCommandsHost command id validation', () => { }) it('validates updated children against other command trees', () => { - const host = new DevToolsCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as HubNodeContext) host.register({ id: 'other:parent', title: 'Other parent', diff --git a/packages/hub/src/node/__tests__/host-docks.test.ts b/packages/hub/src/node/__tests__/host-docks.test.ts index c230ac4..23cb164 100644 --- a/packages/hub/src/node/__tests__/host-docks.test.ts +++ b/packages/hub/src/node/__tests__/host-docks.test.ts @@ -6,7 +6,7 @@ import { REMOTE_CONNECTION_KEY } from 'devframe/constants' import { getInternalContext } from 'devframe/node/internal' import { describe, expect, it } from 'vitest' import { parseRemoteConnection } from '../../client/remote' -import { DevToolsDockHost } from '../host-docks' +import { DevframeDockHost } from '../host-docks' function createContext(): HubNodeContext { const storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-docks-')) @@ -23,7 +23,7 @@ describe('devToolsDockHost remote URL enrichment', () => { it('preserves hash routes and replaces existing remote descriptors', () => { const context = createContext() getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } - const host = new DevToolsDockHost(context) + const host = new DevframeDockHost(context) host.register({ type: 'iframe', @@ -60,7 +60,7 @@ describe('devToolsDockHost remote URL enrichment', () => { it('preserves non-route fragments with the ampersand descriptor form', () => { const context = createContext() getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } - const host = new DevToolsDockHost(context) + const host = new DevframeDockHost(context) host.register({ type: 'iframe', diff --git a/packages/hub/src/node/__tests__/host-messages.test.ts b/packages/hub/src/node/__tests__/host-messages.test.ts index f4857a1..34e791e 100644 --- a/packages/hub/src/node/__tests__/host-messages.test.ts +++ b/packages/hub/src/node/__tests__/host-messages.test.ts @@ -1,10 +1,10 @@ import type { HubNodeContext } from '../context' import { describe, expect, it } from 'vitest' -import { DevToolsMessagesHost } from '../host-messages' +import { DevframeMessagesHost } from '../host-messages' describe('devToolsMessagesHost', () => { it('caps removal history', async () => { - const host = new DevToolsMessagesHost({} as HubNodeContext) + const host = new DevframeMessagesHost({} as HubNodeContext) for (let i = 0; i < 1005; i++) { const id = `message:${i}` diff --git a/packages/hub/src/node/__tests__/host-terminals.test.ts b/packages/hub/src/node/__tests__/host-terminals.test.ts index fc70198..7f5057d 100644 --- a/packages/hub/src/node/__tests__/host-terminals.test.ts +++ b/packages/hub/src/node/__tests__/host-terminals.test.ts @@ -1,8 +1,8 @@ -import type { DevToolsTerminalSession } from '../../types/terminals' +import type { DevframeTerminalSession } from '../../types/terminals' import type { HubNodeContext } from '../context' import process from 'node:process' import { describe, expect, it, vi } from 'vitest' -import { DevToolsTerminalHost } from '../host-terminals' +import { DevframeTerminalHost } from '../host-terminals' interface FakeSink { write: ReturnType @@ -40,7 +40,7 @@ function createTerminalHost() { } as unknown as HubNodeContext return { - host: new DevToolsTerminalHost(context), + host: new DevframeTerminalHost(context), sinks, } } @@ -74,7 +74,7 @@ describe('devToolsTerminalHost stream lifecycle', () => { cancelled = true }, }) - const session: DevToolsTerminalSession = { + const session: DevframeTerminalSession = { id: 'terminal', title: 'Terminal', status: 'running', diff --git a/packages/hub/src/node/context.ts b/packages/hub/src/node/context.ts index cd08d05..d7bbe8a 100644 --- a/packages/hub/src/node/context.ts +++ b/packages/hub/src/node/context.ts @@ -1,16 +1,16 @@ import type { CreateHostContextOptions } from 'devframe/node' -import type { DevToolsHost, DevToolsNodeContext } from 'devframe/types' -import type { DevToolsCommandsHost } from '../types/commands' -import type { DevToolsDockHost } from '../types/docks' +import type { DevframeHost, DevframeNodeContext } from 'devframe/types' +import type { DevframeCommandsHost } from '../types/commands' +import type { DevframeDockHost } from '../types/docks' import type { JsonRenderer, JsonRenderSpec } from '../types/json-render' -import type { DevToolsMessagesHost } from '../types/messages' -import type { DevToolsTerminalHost } from '../types/terminals' +import type { DevframeMessagesHost } from '../types/messages' +import type { DevframeTerminalHost } 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 { DevframeCommandsHost as CommandsHostImpl } from './host-commands' +import { DevframeDockHost as DocksHostImpl } from './host-docks' +import { DevframeMessagesHost as MessagesHostImpl } from './host-messages' +import { DevframeTerminalHost as TerminalsHostImpl } from './host-terminals' import { registerHubBuiltins } from './hub-builtins' import { builtinHubRpcDeclarations } from './rpc-builtins' @@ -39,19 +39,19 @@ export interface HubHostCapabilities { /** * Hub-augmented node context — extends devframe's framework-neutral - * `DevToolsNodeContext` with the hub-level subsystems (`docks`, + * `DevframeNodeContext` with the hub-level subsystems (`docks`, * `terminals`, `messages`, `commands`) and the `createJsonRenderer` * factory. * * Framework kits further extend this with their own slots (e.g. * `viteConfig`, `viteServer`). */ -export interface HubNodeContext extends DevToolsNodeContext { - readonly host: DevToolsHost & HubHostCapabilities - docks: DevToolsDockHost - terminals: DevToolsTerminalHost - messages: DevToolsMessagesHost - commands: DevToolsCommandsHost +export interface HubNodeContext extends DevframeNodeContext { + readonly host: DevframeHost & HubHostCapabilities + docks: DevframeDockHost + terminals: DevframeTerminalHost + messages: DevframeMessagesHost + commands: DevframeCommandsHost /** * Create a JsonRenderer handle for building json-render powered UIs. */ diff --git a/packages/hub/src/node/diagnostics.ts b/packages/hub/src/node/diagnostics.ts index 94a5fdd..e10eb37 100644 --- a/packages/hub/src/node/diagnostics.ts +++ b/packages/hub/src/node/diagnostics.ts @@ -46,7 +46,7 @@ export const diagnostics = defineDiagnostics({ }, DF8500: { why: (p: { id: string }) => `Built-in command "${p.id}" requires a host capability that this host does not implement.`, - fix: 'Implement the matching capability on the `DevToolsHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`.', + fix: 'Implement the matching capability on the `DevframeHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`.', }, }, }) diff --git a/packages/hub/src/node/host-commands.ts b/packages/hub/src/node/host-commands.ts index fb48dd7..0fc7a1f 100644 --- a/packages/hub/src/node/host-commands.ts +++ b/packages/hub/src/node/host-commands.ts @@ -1,14 +1,14 @@ import type { - DevToolsCommandHandle, - DevToolsCommandsHost as DevToolsCommandsHostType, - DevToolsServerCommandEntry, - DevToolsServerCommandInput, + DevframeCommandHandle, + DevframeCommandsHost as DevframeCommandsHostType, + DevframeServerCommandEntry, + DevframeServerCommandInput, } from '../types/commands' import type { HubNodeContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { diagnostics } from './diagnostics' -function findChildCommand(command: DevToolsServerCommandInput, id: string): DevToolsServerCommandInput | undefined { +function findChildCommand(command: DevframeServerCommandInput, id: string): DevframeServerCommandInput | undefined { for (const child of command.children ?? []) { if (child.id === id) return child @@ -19,7 +19,7 @@ function findChildCommand(command: DevToolsServerCommandInput, id: string): DevT return undefined } -function collectCommandIds(command: DevToolsServerCommandInput, ids: string[] = []): string[] { +function collectCommandIds(command: DevframeServerCommandInput, ids: string[] = []): string[] { ids.push(command.id) for (const child of command.children ?? []) collectCommandIds(child, ids) @@ -27,8 +27,8 @@ function collectCommandIds(command: DevToolsServerCommandInput, ids: string[] = } function validateCommandIds( - commands: Map, - command: DevToolsServerCommandInput, + commands: Map, + command: DevframeServerCommandInput, ignoreTopLevelId?: string, ): void { const ids = collectCommandIds(command) @@ -50,15 +50,15 @@ function validateCommandIds( } } -export class DevToolsCommandsHost implements DevToolsCommandsHostType { - public readonly commands: DevToolsCommandsHostType['commands'] = new Map() - public readonly events: DevToolsCommandsHostType['events'] = createEventEmitter() +export class DevframeCommandsHost implements DevframeCommandsHostType { + public readonly commands: DevframeCommandsHostType['commands'] = new Map() + public readonly events: DevframeCommandsHostType['events'] = createEventEmitter() constructor( public readonly context: HubNodeContext, ) {} - register(command: DevToolsServerCommandInput): DevToolsCommandHandle { + register(command: DevframeServerCommandInput): DevframeCommandHandle { if (this.commands.has(command.id)) { throw diagnostics.DF8400({ id: command.id }) } @@ -68,7 +68,7 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { return { id: command.id, - update: (patch: Partial>) => { + update: (patch: Partial>) => { if ('id' in patch) { throw diagnostics.DF8401() } @@ -108,11 +108,11 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { return found.handler(...args) } - list(): DevToolsServerCommandEntry[] { + list(): DevframeServerCommandEntry[] { return Array.from(this.commands.values()).map(cmd => this.toSerializable(cmd)) } - private findCommand(id: string): DevToolsServerCommandInput | undefined { + private findCommand(id: string): DevframeServerCommandInput | undefined { // Check top-level const topLevel = this.commands.get(id) if (topLevel) @@ -128,13 +128,13 @@ export class DevToolsCommandsHost implements DevToolsCommandsHostType { return undefined } - private toSerializable(cmd: DevToolsServerCommandInput): DevToolsServerCommandEntry { + private toSerializable(cmd: DevframeServerCommandInput): DevframeServerCommandEntry { const { handler: _, children, ...rest } = cmd return { ...rest, source: 'server', ...(children - ? { children: children.map((c: DevToolsServerCommandInput) => this.toSerializable(c)) } + ? { children: children.map((c: DevframeServerCommandInput) => this.toSerializable(c)) } : {} ), } diff --git a/packages/hub/src/node/host-docks.ts b/packages/hub/src/node/host-docks.ts index 584c341..9be2abf 100644 --- a/packages/hub/src/node/host-docks.ts +++ b/packages/hub/src/node/host-docks.ts @@ -1,15 +1,15 @@ -import type { DevToolsNodeContext } from 'devframe/types' +import type { DevframeNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { - DevToolsDockEntry, - DevToolsDockHost as DevToolsDockHostType, - DevToolsDockUserEntry, - DevToolsViewBuiltin, - DevToolsViewIframe, + DevframeDockEntry, + DevframeDockHost as DevframeDockHostType, + DevframeDockUserEntry, + DevframeViewBuiltin, + DevframeViewIframe, RemoteConnectionInfo, RemoteDockOptions, } from '../types/docks' -import type { DevToolsDocksUserSettings } from '../types/settings' +import type { DevframeDocksUserSettings } from '../types/settings' import type { HubNodeContext } from './context' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' import { createStorage } from 'devframe/node' @@ -82,10 +82,10 @@ function buildRemoteUrl(baseUrl: string, payload: RemoteConnectionInfo, transpor 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! +export class DevframeDockHost implements DevframeDockHostType { + public readonly views: DevframeDockHostType['views'] = new Map() + public readonly events: DevframeDockHostType['events'] = createEventEmitter() + public userSettings: SharedState = undefined! /** Dock-id → allocated remote token + resolved options. */ private readonly remoteDocks = new Map() @@ -109,9 +109,9 @@ export class DevToolsDockHost implements DevToolsDockHostType { includeBuiltin = true, }: { includeBuiltin?: boolean - } = {}): DevToolsDockEntry[] { + } = {}): DevframeDockEntry[] { const context = this.context - const builtinDocksEntries: DevToolsViewBuiltin[] = [ + const builtinDocksEntries: DevframeViewBuiltin[] = [ { type: '~builtin', id: '~terminals', @@ -148,11 +148,11 @@ export class DevToolsDockHost implements DevToolsDockHostType { ] } - private projectView(view: DevToolsDockUserEntry): DevToolsDockUserEntry { + private projectView(view: DevframeDockUserEntry): DevframeDockUserEntry { if (view.type !== 'iframe' || !view.remote) return view const record = this.remoteDocks.get(view.id) - const endpoint = getInternalContext(this.context as DevToolsNodeContext).wsEndpoint + const endpoint = getInternalContext(this.context as DevframeNodeContext).wsEndpoint if (!record || !endpoint) return view @@ -166,14 +166,14 @@ export class DevToolsDockHost implements DevToolsDockHostType { return { ...view, url: buildRemoteUrl(view.url, payload, record.options.transport), - } satisfies DevToolsViewIframe + } satisfies DevframeViewIframe } private resolveDevServerOrigin(): string { return this.context.host.resolveOrigin() } - register(view: T, force?: boolean): { + register(view: T, force?: boolean): { update: (patch: Partial) => void } { if (this.views.has(view.id) && !force) { @@ -193,7 +193,7 @@ export class DevToolsDockHost implements DevToolsDockHostType { } } - update(view: DevToolsDockUserEntry): void { + update(view: DevframeDockUserEntry): void { if (!this.views.has(view.id)) { throw diagnostics.DF8102({ id: view.id }) } @@ -202,8 +202,8 @@ export class DevToolsDockHost implements DevToolsDockHostType { this.events.emit('dock:entry:updated', view) } - private prepareRemoteRegistration(view: DevToolsDockUserEntry): void { - const internal = getInternalContext(this.context as DevToolsNodeContext) + private prepareRemoteRegistration(view: DevframeDockUserEntry): void { + const internal = getInternalContext(this.context as DevframeNodeContext) // Always revoke any previously allocated token for this dock id — covers // force re-registration and update() paths. internal.revokeRemoteTokensForDock(view.id) diff --git a/packages/hub/src/node/host-messages.ts b/packages/hub/src/node/host-messages.ts index 4086818..cb98180 100644 --- a/packages/hub/src/node/host-messages.ts +++ b/packages/hub/src/node/host-messages.ts @@ -1,8 +1,8 @@ import type { - DevToolsMessageEntry, - DevToolsMessageEntryInput, - DevToolsMessageHandle, - DevToolsMessagesHost as DevToolsMessagesHostType, + DevframeMessageEntry, + DevframeMessageEntryInput, + DevframeMessageHandle, + DevframeMessagesHost as DevframeMessagesHostType, } from '../types/messages' import type { HubNodeContext } from './context' import { createEventEmitter } from 'devframe/utils/events' @@ -21,9 +21,9 @@ function recordRemoval( removals.splice(0, removals.length - MAX_REMOVALS) } -export class DevToolsMessagesHost implements DevToolsMessagesHostType { - public readonly entries: DevToolsMessagesHostType['entries'] = new Map() - public readonly events: DevToolsMessagesHostType['events'] = createEventEmitter() +export class DevframeMessagesHost implements DevframeMessagesHostType { + public readonly entries: DevframeMessagesHostType['entries'] = new Map() + public readonly events: DevframeMessagesHostType['events'] = createEventEmitter() /** Tracks when each entry was last added or updated (monotonic) */ readonly lastModified = new Map() @@ -41,14 +41,14 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { public readonly context: HubNodeContext, ) {} - async add(input: DevToolsMessageEntryInput): Promise { + async add(input: DevframeMessageEntryInput): 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 = { + const entry: DevframeMessageEntry = { ...input, id: input.id ?? nanoid(), timestamp: input.timestamp ?? Date.now(), @@ -74,12 +74,12 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { return this._createHandle(entry.id) } - async update(id: string, patch: Partial): Promise { + async update(id: string, patch: Partial): Promise { const existing = this.entries.get(id) if (!existing) return undefined - const updated: DevToolsMessageEntry = { + const updated: DevframeMessageEntry = { ...existing, ...patch, id: existing.id, @@ -132,7 +132,7 @@ export class DevToolsMessagesHost implements DevToolsMessagesHostType { this.events.emit('message:cleared') } - private _createHandle(id: string): DevToolsMessageHandle { + private _createHandle(id: string): DevframeMessageHandle { // eslint-disable-next-line ts/no-this-alias const host = this return { diff --git a/packages/hub/src/node/host-terminals.ts b/packages/hub/src/node/host-terminals.ts index df492c6..86e174b 100644 --- a/packages/hub/src/node/host-terminals.ts +++ b/packages/hub/src/node/host-terminals.ts @@ -1,11 +1,11 @@ import type { RpcStreamingChannel } from 'devframe/types' import type { Result as TinyExecResult } from 'tinyexec' import type { - DevToolsChildProcessExecuteOptions, - DevToolsChildProcessTerminalSession, - DevToolsTerminalHost as DevToolsTerminalHostType, - DevToolsTerminalSession, - DevToolsTerminalSessionBase, + DevframeChildProcessExecuteOptions, + DevframeChildProcessTerminalSession, + DevframeTerminalHost as DevframeTerminalHostType, + DevframeTerminalSession, + DevframeTerminalSessionBase, } from '../types/terminals' import type { HubNodeContext } from './context' import process from 'node:process' @@ -21,9 +21,9 @@ type PartialWithoutId = Partial & { id: string } 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() +export class DevframeTerminalHost implements DevframeTerminalHostType { + public readonly sessions: DevframeTerminalHostType['sessions'] = new Map() + public readonly events: DevframeTerminalHostType['events'] = createEventEmitter() private _boundStreams = new Map void @@ -54,7 +54,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { return this._channel } - register(session: DevToolsTerminalSession): DevToolsTerminalSession { + register(session: DevframeTerminalSession): DevframeTerminalSession { if (this.sessions.has(session.id)) { throw diagnostics.DF8200({ id: session.id }) } @@ -64,7 +64,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { return session } - update(patch: PartialWithoutId): void { + update(patch: PartialWithoutId): void { if (!this.sessions.has(patch.id)) { throw diagnostics.DF8201({ id: patch.id }) } @@ -75,14 +75,14 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { this.events.emit('terminal:session:updated', session) } - remove(session: DevToolsTerminalSession): void { + remove(session: DevframeTerminalSession): 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) { + private bindStream(session: DevframeTerminalSession) { // Skip when the same stream is already bound if (this._boundStreams.has(session.id) && this._boundStreams.get(session.id)?.stream === session.stream) return @@ -149,9 +149,9 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { } async startChildProcess( - executeOptions: DevToolsChildProcessExecuteOptions, - terminal: Omit, - ): Promise { + executeOptions: DevframeChildProcessExecuteOptions, + terminal: Omit, + ): Promise { if (this.sessions.has(terminal.id)) { throw diagnostics.DF8200({ id: terminal.id }) } @@ -246,7 +246,7 @@ export class DevToolsTerminalHost implements DevToolsTerminalHostType { closeStream() } - const session: DevToolsChildProcessTerminalSession = { + const session: DevframeChildProcessTerminalSession = { ...terminal, status: 'running', stream, diff --git a/packages/hub/src/node/mount-devframe.ts b/packages/hub/src/node/mount-devframe.ts index cbcc3b0..74e363a 100644 --- a/packages/hub/src/node/mount-devframe.ts +++ b/packages/hub/src/node/mount-devframe.ts @@ -1,5 +1,5 @@ import type { DevframeDefinition } from 'devframe/types' -import type { DevToolsViewIframe } from '../types/docks' +import type { DevframeViewIframe } from '../types/docks' import type { HubNodeContext } from './context' import { resolveBasePath } from 'devframe/node/internal' import { resolve } from 'pathe' @@ -15,7 +15,7 @@ export interface MountDevframeOptions { * `when`, etc. Cannot change `id`, `type`, or `url` — those are * derived from the devframe definition. */ - dock?: Partial> + dock?: Partial> } /** @@ -46,7 +46,7 @@ export async function mountDevframe( ...options.dock, type: 'iframe', url: base, - } as DevToolsViewIframe) + } as DevframeViewIframe) await d.setup(ctx) } diff --git a/packages/hub/src/types/commands.ts b/packages/hub/src/types/commands.ts index 5969f08..699cf87 100644 --- a/packages/hub/src/types/commands.ts +++ b/packages/hub/src/types/commands.ts @@ -1,6 +1,6 @@ import type { EventEmitter } from 'devframe/types' -export interface DevToolsCommandKeybinding { +export interface DevframeCommandKeybinding { /** * Keyboard shortcut string. * Use "Mod" for platform-aware modifier (Cmd on macOS, Ctrl elsewhere). @@ -9,7 +9,7 @@ export interface DevToolsCommandKeybinding { key: string } -export interface DevToolsCommandBase { +export interface DevframeCommandBase { /** * Unique namespaced ID, e.g. "vite:open-in-editor" */ @@ -38,13 +38,13 @@ export interface DevToolsCommandBase { /** * Default keyboard shortcut(s) for this command */ - keybindings?: DevToolsCommandKeybinding[] + keybindings?: DevframeCommandKeybinding[] } /** * Server command input — what plugins pass to `ctx.commands.register()`. */ -export interface DevToolsServerCommandInput extends DevToolsCommandBase { +export interface DevframeServerCommandInput extends DevframeCommandBase { /** * Handler for this command. Optional if the command only serves as a group for children. */ @@ -53,57 +53,57 @@ export interface DevToolsServerCommandInput extends DevToolsCommandBase { * Static sub-commands. Two levels max (parent → children). * Each child must have a globally unique `id`. */ - children?: DevToolsServerCommandInput[] + children?: DevframeServerCommandInput[] } /** * Serializable server command entry — sent over RPC (no handler). */ -export interface DevToolsServerCommandEntry extends DevToolsCommandBase { +export interface DevframeServerCommandEntry extends DevframeCommandBase { source: 'server' - children?: DevToolsServerCommandEntry[] + children?: DevframeServerCommandEntry[] } /** * Client command — registered in the webcomponent context. */ -export interface DevToolsClientCommand extends DevToolsCommandBase { +export interface DevframeClientCommand extends DevframeCommandBase { 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 + action?: (...args: any[]) => void | DevframeClientCommand[] | Promise /** * Static sub-commands. Two levels max (parent → children). */ - children?: DevToolsClientCommand[] + children?: DevframeClientCommand[] } /** * Union of command entries visible in the palette. */ -export type DevToolsCommandEntry = DevToolsServerCommandEntry | DevToolsClientCommand +export type DevframeCommandEntry = DevframeServerCommandEntry | DevframeClientCommand -export interface DevToolsCommandHandle { +export interface DevframeCommandHandle { readonly id: string - update: (patch: Partial>) => void + update: (patch: Partial>) => void unregister: () => void } -export interface DevToolsCommandsHostEvents { - 'command:registered': (command: DevToolsServerCommandEntry) => void +export interface DevframeCommandsHostEvents { + 'command:registered': (command: DevframeServerCommandEntry) => void 'command:unregistered': (id: string) => void } -export interface DevToolsCommandsHost { - readonly commands: Map - readonly events: EventEmitter +export interface DevframeCommandsHost { + readonly commands: Map + readonly events: EventEmitter /** * Register a command (with optional children). */ - register: (command: DevToolsServerCommandInput) => DevToolsCommandHandle + register: (command: DevframeServerCommandInput) => DevframeCommandHandle /** * Unregister a command by ID (removes parent and all children). @@ -119,12 +119,12 @@ export interface DevToolsCommandsHost { /** * Returns serializable list (no handlers), preserving tree structure. */ - list: () => DevToolsServerCommandEntry[] + list: () => DevframeServerCommandEntry[] } -export interface DevToolsCommandShortcutOverrides { +export interface DevframeCommandShortcutOverrides { /** * Command ID → keybinding overrides. Empty array = shortcut disabled. */ - [commandId: string]: DevToolsCommandKeybinding[] + [commandId: string]: DevframeCommandKeybinding[] } diff --git a/packages/hub/src/types/docks.ts b/packages/hub/src/types/docks.ts index 340ee86..f3f0621 100644 --- a/packages/hub/src/types/docks.ts +++ b/packages/hub/src/types/docks.ts @@ -1,28 +1,28 @@ import type { ConnectionMeta, EventEmitter } from 'devframe/types' import type { JsonRenderer } from './json-render' -export interface DevToolsDockHost { - readonly views: Map +export interface DevframeDockHost { + readonly views: Map readonly events: EventEmitter<{ - 'dock:entry:updated': (entry: DevToolsDockUserEntry) => void + 'dock:entry:updated': (entry: DevframeDockUserEntry) => void }> - register: (entry: T, force?: boolean) => { + register: (entry: T, force?: boolean) => { update: (patch: Partial) => void } - update: (entry: DevToolsDockUserEntry) => void - values: (options?: { includeBuiltin?: boolean }) => DevToolsDockEntry[] + update: (entry: DevframeDockUserEntry) => void + values: (options?: { includeBuiltin?: boolean }) => DevframeDockEntry[] } // TODO: refine categories more clearly -export type DevToolsDockEntryCategory = 'app' | 'framework' | 'web' | 'advanced' | 'default' | '~viteplus' | '~builtin' +export type DevframeDockEntryCategory = 'app' | 'framework' | 'web' | 'advanced' | 'default' | '~viteplus' | '~builtin' -export type DevToolsDockEntryIcon = string | { light: string, dark: string } +export type DevframeDockEntryIcon = string | { light: string, dark: string } -export interface DevToolsDockEntryBase { +export interface DevframeDockEntryBase { id: string title: string - icon: DevToolsDockEntryIcon + icon: DevframeDockEntryIcon /** * The default order of the entry in the dock. * The higher the number the earlier it appears. @@ -33,7 +33,7 @@ export interface DevToolsDockEntryBase { * The category of the entry * @default 'default' */ - category?: DevToolsDockEntryCategory + category?: DevframeDockEntryCategory /** * Conditional visibility expression. * When set, the dock entry is only visible when the expression evaluates to true. @@ -64,7 +64,7 @@ export interface ClientScriptEntry { importName?: string } -export interface DevToolsViewIframe extends DevToolsDockEntryBase { +export interface DevframeViewIframe extends DevframeDockEntryBase { type: 'iframe' url: string /** @@ -80,7 +80,7 @@ export interface DevToolsViewIframe extends DevToolsDockEntryBase { /** * Enable remote-UI mode: the hub injects a connection descriptor * (WS URL + pre-approved auth token) into the iframe URL so a hosted - * page can connect back via `connectRemoteDevTools()` from + * page can connect back via `connectRemoteDevframe()` from * `@devframes/hub/client` — without needing to ship a dist with the * plugin. * @@ -123,14 +123,14 @@ export interface RemoteConnectionInfo extends ConnectionMeta { origin: string } -export type DevToolsViewLauncherStatus = 'idle' | 'loading' | 'success' | 'error' +export type DevframeViewLauncherStatus = 'idle' | 'loading' | 'success' | 'error' -export interface DevToolsViewLauncher extends DevToolsDockEntryBase { +export interface DevframeViewLauncher extends DevframeDockEntryBase { type: 'launcher' launcher: { - icon?: DevToolsDockEntryIcon + icon?: DevframeDockEntryIcon title: string - status?: DevToolsViewLauncherStatus + status?: DevframeViewLauncherStatus error?: string description?: string buttonStart?: string @@ -139,29 +139,29 @@ export interface DevToolsViewLauncher extends DevToolsDockEntryBase { } } -export interface DevToolsViewAction extends DevToolsDockEntryBase { +export interface DevframeViewAction extends DevframeDockEntryBase { type: 'action' action: ClientScriptEntry } -export interface DevToolsViewCustomRender extends DevToolsDockEntryBase { +export interface DevframeViewCustomRender extends DevframeDockEntryBase { type: 'custom-render' renderer: ClientScriptEntry } -export interface DevToolsViewBuiltin extends DevToolsDockEntryBase { +export interface DevframeViewBuiltin extends DevframeDockEntryBase { type: '~builtin' id: '~terminals' | '~messages' | '~client-auth-notice' | '~settings' | '~popup' } -export interface DevToolsViewJsonRender extends DevToolsDockEntryBase { +export interface DevframeViewJsonRender extends DevframeDockEntryBase { type: 'json-render' /** JsonRenderer handle created by ctx.createJsonRenderer() */ ui: JsonRenderer } -export type DevToolsDockUserEntry = DevToolsViewIframe | DevToolsViewAction | DevToolsViewCustomRender | DevToolsViewLauncher | DevToolsViewJsonRender +export type DevframeDockUserEntry = DevframeViewIframe | DevframeViewAction | DevframeViewCustomRender | DevframeViewLauncher | DevframeViewJsonRender -export type DevToolsDockEntry = DevToolsDockUserEntry | DevToolsViewBuiltin +export type DevframeDockEntry = DevframeDockUserEntry | DevframeViewBuiltin -export type DevToolsDockEntriesGrouped = [category: string, entries: DevToolsDockEntry[]][] +export type DevframeDockEntriesGrouped = [category: string, entries: DevframeDockEntry[]][] diff --git a/packages/hub/src/types/index.ts b/packages/hub/src/types/index.ts index 2a60f1f..ef955c1 100644 --- a/packages/hub/src/types/index.ts +++ b/packages/hub/src/types/index.ts @@ -18,16 +18,16 @@ export type { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from 'devframe/r // Revisit once upstream supports it. export type { ConnectionMeta, - DevToolsCapabilities, - DevToolsDiagnosticsDefinition, - DevToolsDiagnosticsHost, - DevToolsDiagnosticsLogger, - DevToolsHost, - DevToolsNodeRpcSession, - DevToolsRpcClientFunctions, - DevToolsRpcServerFunctions, - DevToolsRpcSharedStates, - DevToolsViewHost, + DevframeCapabilities, + DevframeDiagnosticsDefinition, + DevframeDiagnosticsHost, + DevframeDiagnosticsLogger, + DevframeHost, + DevframeNodeRpcSession, + DevframeRpcClientFunctions, + DevframeRpcServerFunctions, + DevframeRpcSharedStates, + DevframeViewHost, EntriesToObject, EventEmitter, EventsMap, diff --git a/packages/hub/src/types/messages.ts b/packages/hub/src/types/messages.ts index fa273b3..43ba3c9 100644 --- a/packages/hub/src/types/messages.ts +++ b/packages/hub/src/types/messages.ts @@ -1,9 +1,9 @@ import type { EventEmitter } from 'devframe/types' -export type DevToolsMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug' -export type DevToolsMessageEntryFrom = 'server' | 'browser' +export type DevframeMessageLevel = 'info' | 'warn' | 'error' | 'success' | 'debug' +export type DevframeMessageEntryFrom = 'server' | 'browser' -export interface DevToolsMessageElementPosition { +export interface DevframeMessageElementPosition { /** CSS selector for the element */ selector?: string /** Bounding box of the element */ @@ -12,7 +12,7 @@ export interface DevToolsMessageElementPosition { description?: string } -export interface DevToolsMessageFilePosition { +export interface DevframeMessageFilePosition { /** Absolute or relative file path */ file: string /** Line number (1-based) */ @@ -21,7 +21,7 @@ export interface DevToolsMessageFilePosition { column?: number } -export interface DevToolsMessageEntry { +export interface DevframeMessageEntry { /** * Unique identifier for this message entry (auto-generated if not provided) */ @@ -37,7 +37,7 @@ export interface DevToolsMessageEntry { /** * Severity level, determines color and icon */ - level: DevToolsMessageLevel + level: DevframeMessageLevel /** * Optional stack trace string */ @@ -45,11 +45,11 @@ export interface DevToolsMessageEntry { /** * Optional DOM element position info (e.g., for a11y issues) */ - elementPosition?: DevToolsMessageElementPosition + elementPosition?: DevframeMessageElementPosition /** * Optional source file position info (e.g., for lint errors) */ - filePosition?: DevToolsMessageFilePosition + filePosition?: DevframeMessageFilePosition /** * Whether this message should also appear as a toast notification */ @@ -57,7 +57,7 @@ export interface DevToolsMessageEntry { /** * Origin of the message entry, automatically set by the context */ - from: DevToolsMessageEntryFrom + from: DevframeMessageEntryFrom /** * Grouping category (e.g., 'a11y', 'lint', 'runtime', 'test') */ @@ -89,39 +89,39 @@ export interface DevToolsMessageEntry { * Input type for creating a message entry. * `id`, `timestamp`, and `from` are auto-filled by the host. */ -export type DevToolsMessageEntryInput = Omit & { +export type DevframeMessageEntryInput = Omit & { id?: string timestamp?: number } -export interface DevToolsMessageHandle { +export interface DevframeMessageHandle { /** The underlying message entry data */ - readonly entry: DevToolsMessageEntry + readonly entry: DevframeMessageEntry /** Shortcut to entry.id */ readonly id: string /** Partial update of this message entry */ - update: (patch: Partial) => Promise + update: (patch: Partial) => Promise /** Remove this message entry */ dismiss: () => Promise } -export interface DevToolsMessagesClient { +export interface DevframeMessagesClient { /** * 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 + add: (input: DevframeMessageEntryInput) => Promise /** Remove a message entry by id */ remove: (id: string) => Promise /** Clear all message entries */ clear: () => Promise } -export interface DevToolsMessagesHost { - readonly entries: Map +export interface DevframeMessagesHost { + readonly entries: Map readonly events: EventEmitter<{ - 'message:added': (entry: DevToolsMessageEntry) => void - 'message:updated': (entry: DevToolsMessageEntry) => void + 'message:added': (entry: DevframeMessageEntry) => void + 'message:updated': (entry: DevframeMessageEntry) => void 'message:removed': (id: string) => void 'message:cleared': () => void }> @@ -130,11 +130,11 @@ export interface DevToolsMessagesHost { * 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 + add: (entry: DevframeMessageEntryInput) => Promise /** * Update an existing message entry by id (partial update) */ - update: (id: string, patch: Partial) => Promise + update: (id: string, patch: Partial) => Promise /** * Remove a message entry by id */ diff --git a/packages/hub/src/types/settings.ts b/packages/hub/src/types/settings.ts index e79f136..02eb633 100644 --- a/packages/hub/src/types/settings.ts +++ b/packages/hub/src/types/settings.ts @@ -1,11 +1,11 @@ -import type { DevToolsCommandShortcutOverrides } from './commands' +import type { DevframeCommandShortcutOverrides } from './commands' -export interface DevToolsDocksUserSettings { +export interface DevframeDocksUserSettings { docksHidden: string[] docksCategoriesHidden: string[] docksPinned: string[] docksCustomOrder: Record showIframeAddressBar: boolean closeOnOutsideClick: boolean - commandShortcuts: DevToolsCommandShortcutOverrides + commandShortcuts: DevframeCommandShortcutOverrides } diff --git a/packages/hub/src/types/terminals.ts b/packages/hub/src/types/terminals.ts index 396c036..f59e8ac 100644 --- a/packages/hub/src/types/terminals.ts +++ b/packages/hub/src/types/terminals.ts @@ -1,47 +1,47 @@ import type { EventEmitter } from 'devframe/types' import type { ChildProcess } from 'node:child_process' -import type { DevToolsDockEntryIcon } from './docks' +import type { DevframeDockEntryIcon } from './docks' -export interface DevToolsTerminalHost { - readonly sessions: Map +export interface DevframeTerminalHost { + readonly sessions: Map readonly events: EventEmitter<{ - 'terminal:session:updated': (session: DevToolsTerminalSession) => void + 'terminal:session:updated': (session: DevframeTerminalSession) => void }> - register: (session: DevToolsTerminalSession) => DevToolsTerminalSession - update: (session: DevToolsTerminalSession) => void + register: (session: DevframeTerminalSession) => DevframeTerminalSession + update: (session: DevframeTerminalSession) => void startChildProcess: ( - executeOptions: DevToolsChildProcessExecuteOptions, - terminal: Omit, - ) => Promise + executeOptions: DevframeChildProcessExecuteOptions, + terminal: Omit, + ) => Promise } -export type DevToolsTerminalStatus = 'running' | 'stopped' | 'error' +export type DevframeTerminalStatus = 'running' | 'stopped' | 'error' -export interface DevToolsTerminalSessionBase { +export interface DevframeTerminalSessionBase { id: string title: string description?: string - status: DevToolsTerminalStatus - icon?: DevToolsDockEntryIcon + status: DevframeTerminalStatus + icon?: DevframeDockEntryIcon } -export interface DevToolsTerminalSession extends DevToolsTerminalSessionBase { +export interface DevframeTerminalSession extends DevframeTerminalSessionBase { buffer?: string[] stream?: ReadableStream } -export interface DevToolsChildProcessExecuteOptions { +export interface DevframeChildProcessExecuteOptions { command: string args: string[] cwd?: string env?: Record } -export interface DevToolsChildProcessTerminalSession extends DevToolsTerminalSession { +export interface DevframeChildProcessTerminalSession extends DevframeTerminalSession { type: 'child-process' - executeOptions: DevToolsChildProcessExecuteOptions + executeOptions: DevframeChildProcessExecuteOptions getChildProcess: () => ChildProcess | undefined terminate: () => Promise restart: () => Promise diff --git a/packages/nuxt/src/runtime/plugin.client.ts b/packages/nuxt/src/runtime/plugin.client.ts index 43061f1..af2bbfc 100644 --- a/packages/nuxt/src/runtime/plugin.client.ts +++ b/packages/nuxt/src/runtime/plugin.client.ts @@ -11,7 +11,7 @@ export default defineNuxtPlugin({ const rpc = await connectDevframe({ baseURL }) return { provide: { - rpc: rpc as import('devframe/client').DevToolsRpcClient, + rpc: rpc as import('devframe/client').DevframeRpcClient, }, } }, diff --git a/packages/nuxt/src/runtime/types.d.ts b/packages/nuxt/src/runtime/types.d.ts index 40a5898..c3f3d5a 100644 --- a/packages/nuxt/src/runtime/types.d.ts +++ b/packages/nuxt/src/runtime/types.d.ts @@ -1,4 +1,4 @@ -import type { DevToolsRpcClient } from 'devframe/client' +import type { DevframeRpcClient } from 'devframe/client' declare module '#app' { interface NuxtApp { @@ -6,14 +6,14 @@ declare module '#app' { * Devframe RPC client, provided by the `@devframes/nuxt` module's * client plugin. */ - $rpc: DevToolsRpcClient + $rpc: DevframeRpcClient } } declare module 'vue' { interface ComponentCustomProperties { /** Devframe RPC client (see `NuxtApp['$rpc']`). */ - $rpc: DevToolsRpcClient + $rpc: DevframeRpcClient } } diff --git a/packages/nuxt/tsdown.config.ts b/packages/nuxt/tsdown.config.ts index 9dd368b..d8ac6b8 100644 --- a/packages/nuxt/tsdown.config.ts +++ b/packages/nuxt/tsdown.config.ts @@ -47,9 +47,9 @@ export default defineConfig([{ await Promise.all([ fs.cp('src/runtime/types.d.ts', 'dist/runtime/types.d.ts'), fs.writeFile('dist/runtime/plugin.client.d.ts', `import type { Plugin } from '#app'; -import type { DevToolsRpcClient } from 'devframe/client'; +import type { DevframeRpcClient } from 'devframe/client'; declare const plugin: Plugin<{ - rpc: DevToolsRpcClient; + rpc: DevframeRpcClient; }>; export default plugin; `, 'utf-8'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03058b2..0070e1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,28 +100,6 @@ catalogs: ws: specifier: ^8.20.0 version: 8.20.0 - devtools: - '@antfu/eslint-config': - specifier: ^9.0.0 - version: 9.0.0 - bumpp: - specifier: ^11.1.0 - version: 11.1.0 - eslint: - specifier: ^10.3.0 - version: 10.3.0 - nano-staged: - specifier: ^1.0.2 - version: 1.0.2 - simple-git-hooks: - specifier: ^2.13.1 - version: 2.13.1 - skills-npm: - specifier: ^1.1.1 - version: 1.1.1 - typescript: - specifier: ^6.0.3 - version: 6.0.3 docs: mermaid: specifier: ^11.15.0 @@ -162,6 +140,28 @@ catalogs: vitest: specifier: ^4.1.6 version: 4.1.6 + tooling: + '@antfu/eslint-config': + specifier: ^9.0.0 + version: 9.0.0 + bumpp: + specifier: ^11.1.0 + version: 11.1.0 + eslint: + specifier: ^10.3.0 + version: 10.3.0 + nano-staged: + specifier: ^1.0.2 + version: 1.0.2 + simple-git-hooks: + specifier: ^2.13.1 + version: 2.13.1 + skills-npm: + specifier: ^1.1.1 + version: 1.1.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 types: '@types/node': specifier: ^25.7.0 @@ -185,7 +185,7 @@ importers: .: devDependencies: '@antfu/eslint-config': - specifier: catalog:devtools + specifier: catalog:tooling version: 9.0.0(@typescript-eslint/rule-tester@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3))(@typescript-eslint/utils@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(@vue/compiler-sfc@3.5.34)(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.7.0)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4))) '@antfu/ni': specifier: catalog:build @@ -203,19 +203,19 @@ importers: specifier: catalog:types version: 8.18.1 bumpp: - specifier: catalog:devtools + specifier: catalog:tooling version: 11.1.0 eslint: - specifier: catalog:devtools + specifier: catalog:tooling version: 10.3.0(jiti@2.7.0) nano-staged: - specifier: catalog:devtools + specifier: catalog:tooling version: 1.0.2 simple-git-hooks: - specifier: catalog:devtools + specifier: catalog:tooling version: 2.13.1 skills-npm: - specifier: catalog:devtools + specifier: catalog:tooling version: 1.1.1 tsdown: specifier: catalog:build @@ -230,7 +230,7 @@ importers: specifier: catalog:build version: 2.9.12 typescript: - specifier: catalog:devtools + specifier: catalog:tooling version: 6.0.3 vite: specifier: catalog:build @@ -288,7 +288,7 @@ importers: specifier: catalog:deps version: 8.20.0 - examples/minimal-next-devtools-hub: + examples/minimal-next-devframe-hub: dependencies: '@devframes/hub': specifier: workspace:* @@ -325,7 +325,7 @@ importers: specifier: catalog:deps version: 8.20.0 - examples/minimal-vite-devtools-hub: + examples/minimal-vite-devframe-hub: dependencies: '@devframes/hub': specifier: workspace:* @@ -7049,7 +7049,7 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/parser': 8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3) '@vitest/eslint-plugin': 1.6.17(@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.3.0(jiti@2.7.0))(typescript@6.0.3)(vitest@4.1.6(@types/node@25.7.0)(vite@8.0.12(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.4))) - ansis: 4.2.0 + ansis: 4.3.0 cac: 7.0.0 eslint: 10.3.0(jiti@2.7.0) eslint-config-flat-gitignore: 2.3.0(eslint@10.3.0(jiti@2.7.0)) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49588b6..4b54674 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -55,14 +55,6 @@ catalogs: valibot: ^1.4.0 whenexpr: ^0.1.2 ws: ^8.20.0 - devtools: - '@antfu/eslint-config': ^9.0.0 - bumpp: ^11.1.0 - eslint: ^10.3.0 - nano-staged: ^1.0.2 - simple-git-hooks: ^2.13.1 - skills-npm: ^1.1.1 - typescript: ^6.0.3 docs: mermaid: ^11.15.0 vitepress: ^2.0.0-alpha.17 @@ -79,6 +71,14 @@ catalogs: '@playwright/test': ^1.50.0 tsnapi: ^0.3.3 vitest: ^4.1.6 + tooling: + '@antfu/eslint-config': ^9.0.0 + bumpp: ^11.1.0 + eslint: ^10.3.0 + nano-staged: ^1.0.2 + simple-git-hooks: ^2.13.1 + skills-npm: ^1.1.1 + typescript: ^6.0.3 types: '@types/node': ^25.7.0 '@types/react': ^19.2.15 diff --git a/scripts/ecosystem-ci.ts b/scripts/ecosystem-ci.ts index 6858650..02b3f0c 100644 --- a/scripts/ecosystem-ci.ts +++ b/scripts/ecosystem-ci.ts @@ -40,7 +40,7 @@ async function main(): Promise { } async function resolveRef(): Promise { - const override = process.env.ECOSYSTEM_DEVTOOLS_REF + const override = process.env.ECOSYSTEM_DEVFRAME_REF if (override) return override diff --git a/skills/devframe/SKILL.md b/skills/devframe/SKILL.md index 0e4b871..e8fc7cf 100644 --- a/skills/devframe/SKILL.md +++ b/skills/devframe/SKILL.md @@ -76,7 +76,7 @@ See `templates/counter-devframe.ts` for a runnable counter example, `templates/s 'get-modules' // ✗ — may collide with other devframes sharing the host ``` -## DevToolsNodeContext at a glance +## DevframeNodeContext at a glance `setup(ctx)` receives the framework-neutral server-side surface. Each host corresponds to a [docs](https://devfra.me/) page: @@ -340,9 +340,9 @@ const rpc = await connectDevframe() const data = await rpc.call('my-inspector:get-stats', { limit: 10 }) ``` -`connectDevframe` auto-detects the backend via `/.devtools/.connection.json`: +`connectDevframe` auto-detects the backend via `/.devframe/.connection.json`: -- **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `vite-devtools-auth` BroadcastChannel. +- **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `devframe-auth` BroadcastChannel. - **static** (build / spa output) — read-only, resolves calls from the baked RPC dump. Use `rpc.sharedState.get(key)` for observable state, `rpc.client.register(defineRpcFunction(...))` to receive server broadcasts, and `rpc.callOptional(...)` when a missing handler should resolve to `undefined` instead of throwing. @@ -373,7 +373,7 @@ At runtime, static clients look up the argument hash in the dump; misses resolve | Subcommand | Action | |------------|--------| -| *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/.devtools/` | +| *(default)* | Dev server on port 9999 (or `--port`) — WebSocket RPC, `cli.distDir` served at `/.devframe/` | | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) | | `spa` | Deployable SPA → `./dist-spa/` | | `mcp` | stdio MCP server (experimental) | @@ -405,7 +405,7 @@ For "open file in editor" + "reveal in finder", prefer the prebuilt `openHelpers - Unit-test host classes with fake contexts. - Run `templates/counter-devframe.ts` under each adapter for integration coverage. -- Snapshot the build-static RPC dump (`/.devtools/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs. +- Snapshot the build-static RPC dump (`/.devframe/.rpc-dump/index.json`) to catch accidental drift in `static` function outputs. ## Further reading diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts index c7b7e0d..7f61b9f 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts @@ -3,20 +3,20 @@ */ // #region Interfaces export interface CommandsContext { - readonly commands: DevToolsCommandEntry[]; - readonly paletteCommands: DevToolsCommandEntry[]; - register: (_: DevToolsClientCommand | DevToolsClientCommand[]) => () => void; + readonly commands: DevframeCommandEntry[]; + readonly paletteCommands: DevframeCommandEntry[]; + register: (_: DevframeClientCommand | DevframeClientCommand[]) => () => void; execute: (_: string, ..._: any[]) => Promise; - getKeybindings: (_: string) => DevToolsCommandKeybinding[]; - settings: SharedState; + getKeybindings: (_: string) => DevframeCommandKeybinding[]; + settings: SharedState; paletteOpen: boolean; } export interface DockClientScriptContext extends DocksContext { current: DockEntryState; - messages: DevToolsMessagesClient; + messages: DevframeMessagesClient; } export interface DockEntryState { - entryMeta: DevToolsDockEntry; + entryMeta: DevframeDockEntry; readonly isActive: boolean; domElements: { iframe?: HTMLIFrameElement | null; @@ -27,7 +27,7 @@ export interface DockEntryState { export interface DockEntryStateEvents { 'entry:activated': () => void; 'entry:deactivated': () => void; - 'entry:updated': (_: DevToolsDockUserEntry) => void; + 'entry:updated': (_: DevframeDockUserEntry) => void; 'dom:panel:mounted': (_: HTMLDivElement) => void; 'dom:iframe:mounted': (_: HTMLIFrameElement) => void; } @@ -41,7 +41,7 @@ export interface DockPanelStorage { open: boolean; inactiveTimeout: number; } -export interface DocksContext extends DevToolsRpcContext { +export interface DocksContext extends DevframeRpcContext { readonly clientType: 'embedded' | 'standalone'; readonly panel: DocksPanelContext; readonly docks: DocksEntriesContext; @@ -50,11 +50,11 @@ export interface DocksContext extends DevToolsRpcContext { } export interface DocksEntriesContext { selectedId: string | null; - readonly selected: DevToolsDockEntry | null; - entries: DevToolsDockEntry[]; + readonly selected: DevframeDockEntry | null; + entries: DevframeDockEntry[]; entryToStateMap: Map; - groupedEntries: DevToolsDockEntriesGrouped; - settings: SharedState; + groupedEntries: DevframeDockEntriesGrouped; + settings: SharedState; getStateById: (_: string) => DockEntryState | undefined; switchEntry: (_?: string | null) => Promise; toggleEntry: (_: string) => Promise; @@ -71,14 +71,14 @@ export interface WhenClauseContext { // #endregion // #region Types -export type ConnectRemoteDevToolsOptions = Omit; -export type DevToolsClientContext = DocksContext; +export type ConnectRemoteDevframeOptions = Omit; +export type DevframeClientContext = DocksContext; export type DockClientType = 'embedded' | 'standalone'; // #endregion // #region Functions -export declare function connectRemoteDevTools(_?: ConnectRemoteDevToolsOptions): Promise; -export declare function getDevToolsClientContext(): DevToolsClientContext | undefined; +export declare function connectRemoteDevframe(_?: ConnectRemoteDevframeOptions): Promise; +export declare function getDevframeClientContext(): DevframeClientContext | undefined; export declare function parseRemoteConnection(_?: string): RemoteConnectionInfo | null; // #endregion @@ -91,6 +91,6 @@ export * from "devframe/client"; // #endregion // #region Other -export { DevToolsClientRpcHost } +export { DevframeClientRpcHost } export { RpcClientEvents } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js index b782ee8..4efacf2 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js @@ -2,8 +2,8 @@ * Generated by tsnapi — public API snapshot of `@devframes/hub/client` */ // #region Functions -export async function connectRemoteDevTools(_) {} -export function getDevToolsClientContext() {} +export async function connectRemoteDevframe(_) {} +export function getDevframeClientContext() {} export function parseRemoteConnection(_) {} // #endregion diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts index 57f7e43..cb54f03 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/constants.snapshot.d.ts @@ -3,7 +3,7 @@ */ // #region Variables export declare const DEFAULT_CATEGORIES_ORDER: Record; -export declare const DEFAULT_STATE_USER_SETTINGS: () => DevToolsDocksUserSettings; +export declare const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings; // #endregion // #region Re-exports diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts index 6bcb13f..f11286c 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts @@ -2,10 +2,10 @@ * Generated by tsnapi — public API snapshot of `@devframes/hub` */ // #region Functions -export declare function defineCommand(_: Omit & { +export declare function defineCommand(_: Omit & { when?: WhenExpression; -}): DevToolsServerCommandInput; -export declare function defineDockEntry(_: Omit & { +}): DevframeServerCommandInput; +export declare function defineDockEntry(_: Omit & { when?: WhenExpression; }): T; export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; @@ -19,56 +19,56 @@ export declare const defineRpcFunction: >; + dock?: Partial>; } // #endregion // #region Classes -export declare class DevToolsCommandsHost implements DevToolsCommandsHost$1 { +export declare class DevframeCommandsHost implements DevframeCommandsHost$1 { readonly context: HubNodeContext; - readonly commands: DevToolsCommandsHost$1['commands']; - readonly events: DevToolsCommandsHost$1['events']; + readonly commands: DevframeCommandsHost$1['commands']; + readonly events: DevframeCommandsHost$1['events']; constructor(_: HubNodeContext); - register(_: DevToolsServerCommandInput): DevToolsCommandHandle; + register(_: DevframeServerCommandInput): DevframeCommandHandle; unregister(_: string): boolean; execute(_: string, ..._: any[]): Promise; - list(): DevToolsServerCommandEntry[]; + list(): DevframeServerCommandEntry[]; private findCommand; private toSerializable; } -export declare class DevToolsDockHost implements DevToolsDockHost$1 { +export declare class DevframeDockHost implements DevframeDockHost$1 { readonly context: HubNodeContext; - readonly views: DevToolsDockHost$1['views']; - readonly events: DevToolsDockHost$1['events']; - userSettings: SharedState; + readonly views: DevframeDockHost$1['views']; + readonly events: DevframeDockHost$1['events']; + userSettings: SharedState; private readonly remoteDocks; constructor(_: HubNodeContext); init(): Promise; @@ -33,19 +33,19 @@ export declare class DevToolsDockHost implements DevToolsDockHost$1 { includeBuiltin }?: { includeBuiltin?: boolean; - }): DevToolsDockEntry[]; + }): DevframeDockEntry[]; private projectView; private resolveDevServerOrigin; - register(_: T, _?: boolean): { + register(_: T, _?: boolean): { update: (_: Partial) => void; }; - update(_: DevToolsDockUserEntry): void; + update(_: DevframeDockUserEntry): void; private prepareRemoteRegistration; } -export declare class DevToolsMessagesHost implements DevToolsMessagesHost$1 { +export declare class DevframeMessagesHost implements DevframeMessagesHost$1 { readonly context: HubNodeContext; - readonly entries: DevToolsMessagesHost$1['entries']; - readonly events: DevToolsMessagesHost$1['events']; + readonly entries: DevframeMessagesHost$1['entries']; + readonly events: DevframeMessagesHost$1['events']; readonly lastModified: Map; readonly removals: Array<{ id: string; @@ -55,25 +55,25 @@ export declare class DevToolsMessagesHost implements DevToolsMessagesHost$1 { private _clock; private _tick; constructor(_: HubNodeContext); - add(_: DevToolsMessageEntryInput): Promise; - update(_: string, _: Partial): Promise; + add(_: DevframeMessageEntryInput): Promise; + update(_: string, _: Partial): Promise; remove(_: string): Promise; clear(): Promise; private _createHandle; } -export declare class DevToolsTerminalHost implements DevToolsTerminalHost$1 { +export declare class DevframeTerminalHost implements DevframeTerminalHost$1 { readonly context: HubNodeContext; - readonly sessions: DevToolsTerminalHost$1['sessions']; - readonly events: DevToolsTerminalHost$1['events']; + readonly sessions: DevframeTerminalHost$1['sessions']; + readonly events: DevframeTerminalHost$1['events']; private _boundStreams; private _channel?; constructor(_: HubNodeContext); private getStreamingChannel; - register(_: DevToolsTerminalSession): DevToolsTerminalSession; - update(_: PartialWithoutId): void; - remove(_: DevToolsTerminalSession): void; + register(_: DevframeTerminalSession): DevframeTerminalSession; + update(_: PartialWithoutId): void; + remove(_: DevframeTerminalSession): void; private bindStream; - startChildProcess(_: DevToolsChildProcessExecuteOptions, _: Omit): Promise; + startChildProcess(_: DevframeChildProcessExecuteOptions, _: Omit): Promise; } // #endregion diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js index f4b09e1..4f49684 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js @@ -2,7 +2,7 @@ * Generated by tsnapi — public API snapshot of `@devframes/hub/node` */ // #region Classes -export class DevToolsCommandsHost { +export class DevframeCommandsHost { context commands events @@ -14,7 +14,7 @@ export class DevToolsCommandsHost { findCommand(_) {} toSerializable(_) {} } -export class DevToolsDockHost { +export class DevframeDockHost { context views events @@ -29,7 +29,7 @@ export class DevToolsDockHost { update(_) {} prepareRemoteRegistration(_) {} } -export class DevToolsMessagesHost { +export class DevframeMessagesHost { context entries events @@ -45,7 +45,7 @@ export class DevToolsMessagesHost { async clear() {} _createHandle(_) {} } -export class DevToolsTerminalHost { +export class DevframeTerminalHost { context sessions events diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts index d7d8ba7..3d3465a 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts @@ -5,56 +5,56 @@ export { ClientScriptEntry } export { ConnectionMeta } export { CreateHubContextOptions } -export { DevToolsCapabilities } -export { DevToolsChildProcessExecuteOptions } -export { DevToolsChildProcessTerminalSession } -export { DevToolsClientCommand } -export { DevToolsCommandBase } -export { DevToolsCommandEntry } -export { DevToolsCommandHandle } -export { DevToolsCommandKeybinding } -export { DevToolsCommandShortcutOverrides } -export { DevToolsCommandsHost } -export { DevToolsCommandsHostEvents } -export { DevToolsDiagnosticsDefinition } -export { DevToolsDiagnosticsHost } -export { DevToolsDiagnosticsLogger } -export { DevToolsDockEntriesGrouped } -export { DevToolsDockEntry } -export { DevToolsDockEntryBase } -export { DevToolsDockEntryCategory } -export { DevToolsDockEntryIcon } -export { DevToolsDockHost } -export { DevToolsDocksUserSettings } -export { DevToolsDockUserEntry } -export { DevToolsHost } -export { DevToolsMessageElementPosition } -export { DevToolsMessageEntry } -export { DevToolsMessageEntryFrom } -export { DevToolsMessageEntryInput } -export { DevToolsMessageFilePosition } -export { DevToolsMessageHandle } -export { DevToolsMessageLevel } -export { DevToolsMessagesClient } -export { DevToolsMessagesHost } -export { DevToolsNodeRpcSession } -export { DevToolsRpcClientFunctions } -export { DevToolsRpcServerFunctions } -export { DevToolsRpcSharedStates } -export { DevToolsServerCommandEntry } -export { DevToolsServerCommandInput } -export { DevToolsTerminalHost } -export { DevToolsTerminalSession } -export { DevToolsTerminalSessionBase } -export { DevToolsTerminalStatus } -export { DevToolsViewAction } -export { DevToolsViewBuiltin } -export { DevToolsViewCustomRender } -export { DevToolsViewHost } -export { DevToolsViewIframe } -export { DevToolsViewJsonRender } -export { DevToolsViewLauncher } -export { DevToolsViewLauncherStatus } +export { DevframeCapabilities } +export { DevframeChildProcessExecuteOptions } +export { DevframeChildProcessTerminalSession } +export { DevframeClientCommand } +export { DevframeCommandBase } +export { DevframeCommandEntry } +export { DevframeCommandHandle } +export { DevframeCommandKeybinding } +export { DevframeCommandShortcutOverrides } +export { DevframeCommandsHost } +export { DevframeCommandsHostEvents } +export { DevframeDiagnosticsDefinition } +export { DevframeDiagnosticsHost } +export { DevframeDiagnosticsLogger } +export { DevframeDockEntriesGrouped } +export { DevframeDockEntry } +export { DevframeDockEntryBase } +export { DevframeDockEntryCategory } +export { DevframeDockEntryIcon } +export { DevframeDockHost } +export { DevframeDocksUserSettings } +export { DevframeDockUserEntry } +export { DevframeHost } +export { DevframeMessageElementPosition } +export { DevframeMessageEntry } +export { DevframeMessageEntryFrom } +export { DevframeMessageEntryInput } +export { DevframeMessageFilePosition } +export { DevframeMessageHandle } +export { DevframeMessageLevel } +export { DevframeMessagesClient } +export { DevframeMessagesHost } +export { DevframeNodeRpcSession } +export { DevframeRpcClientFunctions } +export { DevframeRpcServerFunctions } +export { DevframeRpcSharedStates } +export { DevframeServerCommandEntry } +export { DevframeServerCommandInput } +export { DevframeTerminalHost } +export { DevframeTerminalSession } +export { DevframeTerminalSessionBase } +export { DevframeTerminalStatus } +export { DevframeViewAction } +export { DevframeViewBuiltin } +export { DevframeViewCustomRender } +export { DevframeViewHost } +export { DevframeViewIframe } +export { DevframeViewJsonRender } +export { DevframeViewLauncher } +export { DevframeViewLauncherStatus } export { EntriesToObject } export { EventEmitter } export { EventsMap } diff --git a/tests/__snapshots__/tsnapi/devframe/adapters/embedded.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/adapters/embedded.snapshot.d.ts index 256156c..c63ad99 100644 --- a/tests/__snapshots__/tsnapi/devframe/adapters/embedded.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/adapters/embedded.snapshot.d.ts @@ -3,7 +3,7 @@ */ // #region Interfaces export interface CreateEmbeddedOptions { - ctx: DevToolsNodeContext; + ctx: DevframeNodeContext; } // #endregion diff --git a/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts index c0acee9..651268c 100644 --- a/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts @@ -2,40 +2,40 @@ * Generated by tsnapi — public API snapshot of `devframe/client` */ // #region Interfaces -export interface DevToolsRpcClient { +export interface DevframeRpcClient { events: EventEmitter; readonly isTrusted: boolean | null; readonly connectionMeta: ConnectionMeta; ensureTrusted: (_?: number) => Promise; requestTrust: () => Promise; requestTrustWithToken: (_: string) => Promise; - call: DevToolsRpcClientCall; - callEvent: DevToolsRpcClientCallEvent; - callOptional: DevToolsRpcClientCallOptional; - client: DevToolsClientRpcHost; + call: DevframeRpcClientCall; + callEvent: DevframeRpcClientCallEvent; + callOptional: DevframeRpcClientCallOptional; + client: DevframeClientRpcHost; sharedState: RpcSharedStateHost; streaming: RpcStreamingClientHost; cacheManager: RpcCacheManager; } -export interface DevToolsRpcClientMode { +export interface DevframeRpcClientMode { readonly isTrusted: boolean; - ensureTrusted: DevToolsRpcClient['ensureTrusted']; - requestTrust: DevToolsRpcClient['requestTrust']; - requestTrustWithToken: DevToolsRpcClient['requestTrustWithToken']; - call: DevToolsRpcClient['call']; - callEvent: DevToolsRpcClient['callEvent']; - callOptional: DevToolsRpcClient['callOptional']; + ensureTrusted: DevframeRpcClient['ensureTrusted']; + requestTrust: DevframeRpcClient['requestTrust']; + requestTrustWithToken: DevframeRpcClient['requestTrustWithToken']; + call: DevframeRpcClient['call']; + callEvent: DevframeRpcClient['callEvent']; + callOptional: DevframeRpcClient['callOptional']; } -export interface DevToolsRpcClientOptions { +export interface DevframeRpcClientOptions { connectionMeta?: ConnectionMeta; baseURL?: string | string[]; authToken?: string; wsOptions?: Partial; - rpcOptions?: Partial>; + rpcOptions?: Partial>; cacheOptions?: boolean | Partial; } -export interface DevToolsRpcContext { - readonly rpc: DevToolsRpcClient; +export interface DevframeRpcContext { + readonly rpc: DevframeRpcClient; } export interface RpcClientEvents { 'rpc:is-trusted:updated': (_: boolean) => void; @@ -50,17 +50,17 @@ export interface StreamingSubscribeOptions { // #endregion // #region Types -export type DevToolsClientRpcHost = RpcFunctionsCollector; -export type DevToolsRpcClientCall = BirpcReturn['$call']; -export type DevToolsRpcClientCallEvent = BirpcReturn['$callEvent']; -export type DevToolsRpcClientCallOptional = BirpcReturn['$callOptional']; +export type DevframeClientRpcHost = RpcFunctionsCollector; +export type DevframeRpcClientCall = BirpcReturn['$call']; +export type DevframeRpcClientCallEvent = BirpcReturn['$callEvent']; +export type DevframeRpcClientCallOptional = BirpcReturn['$callOptional']; // #endregion // #region Functions -export declare function createRpcStreamingClientHost(_: DevToolsRpcClient): RpcStreamingClientHost; -export declare function getDevToolsRpcClient(_?: DevToolsRpcClientOptions): Promise; +export declare function createRpcStreamingClientHost(_: DevframeRpcClient): RpcStreamingClientHost; +export declare function getDevframeRpcClient(_?: DevframeRpcClientOptions): Promise; // #endregion // #region Variables -export declare const connectDevframe: typeof getDevToolsRpcClient; +export declare const connectDevframe: typeof getDevframeRpcClient; // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/client.snapshot.js b/tests/__snapshots__/tsnapi/devframe/client.snapshot.js index 55665f5..948fd17 100644 --- a/tests/__snapshots__/tsnapi/devframe/client.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/client.snapshot.js @@ -3,7 +3,7 @@ */ // #region Functions export function createRpcStreamingClientHost(_) {} -export async function getDevToolsRpcClient(_) {} +export async function getDevframeRpcClient(_) {} // #endregion // #region Variables diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts index 089b973..de0b4fc 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts @@ -2,13 +2,13 @@ * Generated by tsnapi — public API snapshot of `devframe/constants` */ // #region Variables -export declare const DEVTOOLS_CONNECTION_META_FILENAME: string; -export declare const DEVTOOLS_DIRNAME: string; -export declare const DEVTOOLS_DOCK_IMPORTS_FILENAME: 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; -export declare const DEVTOOLS_RPC_DUMP_DIRNAME: string; -export declare const DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME: string; +export declare const DEVFRAME_CONNECTION_META_FILENAME: string; +export declare const DEVFRAME_DIRNAME: string; +export declare const DEVFRAME_DOCK_IMPORTS_FILENAME: string; +export declare const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID: string; +export declare const DEVFRAME_MOUNT_PATH: string; +export declare const DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH: string; +export declare const DEVFRAME_RPC_DUMP_DIRNAME: string; +export declare const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME: string; export declare const REMOTE_CONNECTION_KEY: string; // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js index bc40e7f..2146d20 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js @@ -2,13 +2,13 @@ * Generated by tsnapi — public API snapshot of `devframe/constants` */ // #region Variables -export var DEVTOOLS_CONNECTION_META_FILENAME /* const */ -export var DEVTOOLS_DIRNAME /* const */ -export var DEVTOOLS_DOCK_IMPORTS_FILENAME /* const */ -export var DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID /* const */ -export var DEVTOOLS_MOUNT_PATH /* const */ -export var DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH /* const */ -export var DEVTOOLS_RPC_DUMP_DIRNAME /* const */ -export var DEVTOOLS_RPC_DUMP_MANIFEST_FILENAME /* const */ +export var DEVFRAME_CONNECTION_META_FILENAME /* const */ +export var DEVFRAME_DIRNAME /* const */ +export var DEVFRAME_DOCK_IMPORTS_FILENAME /* const */ +export var DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID /* const */ +export var DEVFRAME_MOUNT_PATH /* const */ +export var DEVFRAME_MOUNT_PATH_NO_TRAILING_SLASH /* const */ +export var DEVFRAME_RPC_DUMP_DIRNAME /* const */ +export var DEVFRAME_RPC_DUMP_MANIFEST_FILENAME /* const */ export var REMOTE_CONNECTION_KEY /* const */ // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 2bb3743..6797571 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -2,7 +2,7 @@ * Generated by tsnapi — public API snapshot of `devframe` */ // #region Variables -export declare const defineRpcFunction: (definition: RpcFunctionDefinition) => RpcFunctionDefinition; +export declare const defineRpcFunction: (definition: RpcFunctionDefinition) => RpcFunctionDefinition; // #endregion // #region Other @@ -15,28 +15,28 @@ export { AgentTool } export { AgentToolInput } export { ConnectionMeta } export { defineDevframe } +export { DevframeAgentHost } +export { DevframeAgentHostEvents } export { DevframeBrowserContext } +export { DevframeCapabilities } export { DevframeCliOptions } +export { DevframeDefineDiagnosticsOptions } export { DevframeDefinition } export { DevframeDeploymentKind } +export { DevframeDiagnosticsDefinition } +export { DevframeDiagnosticsHost } +export { DevframeDiagnosticsLogger } +export { DevframeHost } +export { DevframeNodeContext } +export { DevframeNodeRpcSession } +export { DevframeNodeRpcSessionMeta } +export { DevframeRpcClientFunctions } +export { DevframeRpcServerFunctions } +export { DevframeRpcSharedStates } export { DevframeRuntime } export { DevframeSetupInfo } export { DevframeSpaOptions } -export { DevToolsAgentHost } -export { DevToolsAgentHostEvents } -export { DevToolsCapabilities } -export { DevToolsDefineDiagnosticsOptions } -export { DevToolsDiagnosticsDefinition } -export { DevToolsDiagnosticsHost } -export { DevToolsDiagnosticsLogger } -export { DevToolsHost } -export { DevToolsNodeContext } -export { DevToolsNodeRpcSession } -export { DevToolsNodeRpcSessionMeta } -export { DevToolsRpcClientFunctions } -export { DevToolsRpcServerFunctions } -export { DevToolsRpcSharedStates } -export { DevToolsViewHost } +export { DevframeViewHost } export { EntriesToObject } export { EventEmitter } export { EventsMap } diff --git a/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts index 7f11684..97087d9 100644 --- a/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts @@ -2,7 +2,7 @@ * Generated by tsnapi — public API snapshot of `devframe/node` */ // #region Interfaces -export interface CreateH3DevToolsHostOptions { +export interface CreateH3DevframeHostOptions { app?: unknown; origin: string; mount?: (_: string, _: string) => void | Promise; @@ -13,7 +13,7 @@ export interface CreateHostContextOptions { cwd: string; workspaceRoot?: string; mode: 'dev' | 'build'; - host: DevToolsHost; + host: DevframeHost; builtinRpcDeclarations?: readonly RpcFunctionDefinitionAny[]; } export interface CreateStorageOptions { @@ -25,13 +25,13 @@ export interface CreateStorageOptions { // #endregion // #region Classes -export declare class DevToolsAgentHost implements DevToolsAgentHost$1 { - readonly context: DevToolsNodeContext; - readonly events: EventEmitter; +export declare class DevframeAgentHost implements DevframeAgentHost$1 { + readonly context: DevframeNodeContext; + readonly events: EventEmitter; private readonly tools; private readonly resources; private _rpcUnsubscribe; - constructor(_: DevToolsNodeContext); + constructor(_: DevframeNodeContext); registerTool(_: AgentToolInput): AgentHandle; unregisterTool(_: string): boolean; registerResource(_: AgentResourceInput): AgentHandle; @@ -48,39 +48,39 @@ export declare class DevToolsAgentHost implements DevToolsAgentHost$1 { private _findRpcDefinition; private _coercePositionalArgs; } -export declare class DevToolsDiagnosticsHost implements DevToolsDiagnosticsHost$1 { - readonly context: DevToolsNodeContext; +export declare class DevframeDiagnosticsHost implements DevframeDiagnosticsHost$1 { + readonly context: DevframeNodeContext; private _registry; - readonly logger: DevToolsDiagnosticsLogger; - readonly defineDiagnostics: DevToolsDiagnosticsHost$1['defineDiagnostics']; - constructor(_: DevToolsNodeContext, _?: Array>); + readonly logger: DevframeDiagnosticsLogger; + readonly defineDiagnostics: DevframeDiagnosticsHost$1['defineDiagnostics']; + constructor(_: DevframeNodeContext, _?: Array>); register(_: Record): void; } -export declare class DevToolsViewHost implements DevToolsViewHost$1 { - readonly context: DevToolsNodeContext; +export declare class DevframeViewHost implements DevframeViewHost$1 { + readonly context: DevframeNodeContext; buildStaticDirs: { baseUrl: string; distDir: string; }[]; - constructor(_: DevToolsNodeContext); + constructor(_: DevframeNodeContext); hostStatic(_: string, _: string): void; } -export declare class RpcFunctionsHost extends RpcFunctionsCollectorBase implements RpcFunctionsHost$1 { - _rpcGroup: BirpcGroup; - _asyncStorage: AsyncLocalStorage; - constructor(_: DevToolsNodeContext); +export declare class RpcFunctionsHost extends RpcFunctionsCollectorBase implements RpcFunctionsHost$1 { + _rpcGroup: BirpcGroup; + _asyncStorage: AsyncLocalStorage; + constructor(_: DevframeNodeContext); sharedState: RpcSharedStateHost; streaming: RpcStreamingHost; - _emitSessionDisconnected(_: DevToolsNodeRpcSessionMeta): void; - invokeLocal>(_: T, ..._: Args): Promise>>; - broadcast>(_: RpcBroadcastOptions): Promise; - getCurrentRpcSession(): DevToolsNodeRpcSession | undefined; + _emitSessionDisconnected(_: DevframeNodeRpcSessionMeta): void; + invokeLocal>(_: T, ..._: Args): Promise>>; + broadcast>(_: RpcBroadcastOptions): Promise; + getCurrentRpcSession(): DevframeNodeRpcSession | undefined; } // #endregion // #region Functions -export declare function createH3DevToolsHost(_: CreateH3DevToolsHostOptions): DevToolsHost; -export declare function createHostContext(_: CreateHostContextOptions): Promise; +export declare function createH3DevframeHost(_: CreateH3DevframeHostOptions): DevframeHost; +export declare function createHostContext(_: CreateHostContextOptions): Promise; export declare function createRpcSharedStateServerHost(_: RpcFunctionsHost$1): RpcSharedStateHost; export declare function createRpcStreamingServerHost(_: RpcFunctionsHost$1): RpcStreamingHost; export declare function createStorage(_: CreateStorageOptions): SharedState; diff --git a/tests/__snapshots__/tsnapi/devframe/node.snapshot.js b/tests/__snapshots__/tsnapi/devframe/node.snapshot.js index 09aade8..0cdf553 100644 --- a/tests/__snapshots__/tsnapi/devframe/node.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/node.snapshot.js @@ -7,14 +7,14 @@ export function normalizeHttpServerUrl(_, _) {} // #endregion // #region Other -export { createH3DevToolsHost } +export { createH3DevframeHost } export { createHostContext } export { createRpcSharedStateServerHost } export { createRpcStreamingServerHost } export { createStorage } -export { DevToolsAgentHost } -export { DevToolsDiagnosticsHost } -export { DevToolsViewHost } +export { DevframeAgentHost } +export { DevframeDiagnosticsHost } +export { DevframeViewHost } export { RpcFunctionsHost } export { startHttpAndWs } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/node/auth.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/node/auth.snapshot.d.ts index b618952..86bb867 100644 --- a/tests/__snapshots__/tsnapi/devframe/node/auth.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/node/auth.snapshot.d.ts @@ -4,7 +4,7 @@ // #region Interfaces export interface PendingAuthRequest { clientAuthToken: string; - session: DevToolsNodeRpcSession; + session: DevframeNodeRpcSession; ua: string; origin: string; resolve: (_: { @@ -21,7 +21,7 @@ export declare function consumeTempAuthToken(_: string, _: SharedState; -export declare function revokeAuthToken(_: DevToolsNodeContext, _: SharedState, _: string): Promise; +export declare function revokeActiveConnectionsForToken(_: DevframeNodeContext, _: string): Promise; +export declare function revokeAuthToken(_: DevframeNodeContext, _: SharedState, _: string): Promise; export declare function setPendingAuth(_: PendingAuthRequest | null): void; // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts index 574165c..678e10b 100644 --- a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts @@ -7,7 +7,7 @@ export declare function resolveBasePath(_: DevframeDefinition, _: DevframeDeploy // #endregion // #region Other -export { DevToolsInternalContext } +export { DevframeInternalContext } export { getInternalContext } export { InternalAnonymousAuthStorage } export { internalContextMap } diff --git a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts index fa54ae2..a98666c 100644 --- a/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/rpc/transports/ws-server.snapshot.d.ts @@ -3,6 +3,6 @@ */ // #region Other export { attachWsRpcTransport } -export { DevToolsNodeRpcSessionMeta } +export { DevframeNodeRpcSessionMeta } export { WsRpcTransportOptions } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index f6e3258..cd62e31 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -11,28 +11,28 @@ export { AgentTool } export { AgentToolInput } export { ConnectionMeta } export { defineDevframe } +export { DevframeAgentHost } +export { DevframeAgentHostEvents } export { DevframeBrowserContext } +export { DevframeCapabilities } export { DevframeCliOptions } +export { DevframeDefineDiagnosticsOptions } export { DevframeDefinition } export { DevframeDeploymentKind } +export { DevframeDiagnosticsDefinition } +export { DevframeDiagnosticsHost } +export { DevframeDiagnosticsLogger } +export { DevframeHost } +export { DevframeNodeContext } +export { DevframeNodeRpcSession } +export { DevframeNodeRpcSessionMeta } +export { DevframeRpcClientFunctions } +export { DevframeRpcServerFunctions } +export { DevframeRpcSharedStates } export { DevframeRuntime } export { DevframeSetupInfo } export { DevframeSpaOptions } -export { DevToolsAgentHost } -export { DevToolsAgentHostEvents } -export { DevToolsCapabilities } -export { DevToolsDefineDiagnosticsOptions } -export { DevToolsDiagnosticsDefinition } -export { DevToolsDiagnosticsHost } -export { DevToolsDiagnosticsLogger } -export { DevToolsHost } -export { DevToolsNodeContext } -export { DevToolsNodeRpcSession } -export { DevToolsNodeRpcSessionMeta } -export { DevToolsRpcClientFunctions } -export { DevToolsRpcServerFunctions } -export { DevToolsRpcSharedStates } -export { DevToolsViewHost } +export { DevframeViewHost } export { EntriesToObject } export { EventEmitter } export { EventsMap } diff --git a/turbo.json b/turbo.json index 80cfa16..5eb83cf 100644 --- a/turbo.json +++ b/turbo.json @@ -15,12 +15,12 @@ "outputLogs": "new-only", "outputs": ["dist/**"] }, - "minimal-vite-devtools-hub#build": { + "minimal-vite-devframe-hub#build": { "outputLogs": "new-only", "dependsOn": ["@devframes/hub#build", "devframe#build"], "outputs": ["dist/**"] }, - "minimal-next-devtools-hub#build": { + "minimal-next-devframe-hub#build": { "outputLogs": "new-only", "dependsOn": ["@devframes/hub#build", "devframe#build"], "outputs": ["dist/**"] diff --git a/vitest.config.ts b/vitest.config.ts index 7143519..79b7cdc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ 'examples/files-inspector', 'examples/streaming-chat', 'examples/next-runtime-snapshot', - 'examples/minimal-next-devtools-hub', + 'examples/minimal-next-devframe-hub', { test: { name: 'tests', From 96137ab19b36643632ce89930778682fc1336164 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 22 May 2026 16:59:02 +0900 Subject: [PATCH 4/7] test(hub): align test labels and imports with devframe rename --- packages/hub/src/node/__tests__/context.test.ts | 4 ++-- packages/hub/src/node/__tests__/host-commands.test.ts | 2 +- packages/hub/src/node/__tests__/host-docks.test.ts | 2 +- packages/hub/src/node/__tests__/host-messages.test.ts | 2 +- packages/hub/src/node/__tests__/host-terminals.test.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/hub/src/node/__tests__/context.test.ts b/packages/hub/src/node/__tests__/context.test.ts index 394c623..f72da8c 100644 --- a/packages/hub/src/node/__tests__/context.test.ts +++ b/packages/hub/src/node/__tests__/context.test.ts @@ -1,9 +1,9 @@ import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { createHostContext, startHttpAndWs } from 'devframe/node' +import { getInternalContext } from 'devframe/node/internal' import { describe, expect, it } from 'vitest' -import { createHostContext, startHttpAndWs } from '../../../../devframe/src/node' -import { getInternalContext } from '../../../../devframe/src/node/internal' import { createHubContext } from '../context' function createHost(storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-context-'))) { diff --git a/packages/hub/src/node/__tests__/host-commands.test.ts b/packages/hub/src/node/__tests__/host-commands.test.ts index 795e346..8bae0c4 100644 --- a/packages/hub/src/node/__tests__/host-commands.test.ts +++ b/packages/hub/src/node/__tests__/host-commands.test.ts @@ -2,7 +2,7 @@ import type { HubNodeContext } from '../context' import { describe, expect, it } from 'vitest' import { DevframeCommandsHost } from '../host-commands' -describe('devToolsCommandsHost command id validation', () => { +describe('devframeCommandsHost command id validation', () => { it('rejects duplicate ids inside one command tree', () => { const host = new DevframeCommandsHost({} as HubNodeContext) diff --git a/packages/hub/src/node/__tests__/host-docks.test.ts b/packages/hub/src/node/__tests__/host-docks.test.ts index 23cb164..554a2a2 100644 --- a/packages/hub/src/node/__tests__/host-docks.test.ts +++ b/packages/hub/src/node/__tests__/host-docks.test.ts @@ -19,7 +19,7 @@ function createContext(): HubNodeContext { } as unknown as HubNodeContext } -describe('devToolsDockHost remote URL enrichment', () => { +describe('devframeDockHost remote URL enrichment', () => { it('preserves hash routes and replaces existing remote descriptors', () => { const context = createContext() getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } diff --git a/packages/hub/src/node/__tests__/host-messages.test.ts b/packages/hub/src/node/__tests__/host-messages.test.ts index 34e791e..a28282a 100644 --- a/packages/hub/src/node/__tests__/host-messages.test.ts +++ b/packages/hub/src/node/__tests__/host-messages.test.ts @@ -2,7 +2,7 @@ import type { HubNodeContext } from '../context' import { describe, expect, it } from 'vitest' import { DevframeMessagesHost } from '../host-messages' -describe('devToolsMessagesHost', () => { +describe('devframeMessagesHost', () => { it('caps removal history', async () => { const host = new DevframeMessagesHost({} as HubNodeContext) diff --git a/packages/hub/src/node/__tests__/host-terminals.test.ts b/packages/hub/src/node/__tests__/host-terminals.test.ts index 7f5057d..b53e148 100644 --- a/packages/hub/src/node/__tests__/host-terminals.test.ts +++ b/packages/hub/src/node/__tests__/host-terminals.test.ts @@ -61,7 +61,7 @@ async function waitUntil(assertion: () => void): Promise { throw lastError } -describe('devToolsTerminalHost stream lifecycle', () => { +describe('devframeTerminalHost stream lifecycle', () => { it('cancels a bound stream when a session is removed', async () => { const { host, sinks } = createTerminalHost() let controller: ReadableStreamDefaultController From 4d2054d6f659e44c50864462240a625803e51f42 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 23 May 2026 02:35:20 +0900 Subject: [PATCH 5/7] refactor(hub): remove built-in openPath capability Host-specific behavior (editor open, finder reveal, ...) belongs in kit-registered RPC functions, not in the framework-neutral hub surface. Drop HubHostCapabilities, hub:open-path, hub-builtins.ts, and the DF8500 diagnostic; trim the docs and examples to match. --- docs/errors/DF8500.md | 38 ------------------- docs/guide/hub.md | 35 +---------------- examples/minimal-next-devframe-hub/README.md | 6 +-- .../src/client/app/page.tsx | 20 ---------- .../devframe/minimal-next-devframe-hub.ts | 13 +------ examples/minimal-vite-devframe-hub/README.md | 6 +-- examples/minimal-vite-devframe-hub/index.html | 2 +- .../src/client/main.ts | 17 ++++----- .../src/minimal-vite-devframe-hub.ts | 24 ++++-------- .../hub/src/node/__tests__/context.test.ts | 5 +-- packages/hub/src/node/context.ts | 31 ++------------- packages/hub/src/node/diagnostics.ts | 13 ++----- packages/hub/src/node/hub-builtins.ts | 36 ------------------ packages/hub/src/node/index.ts | 1 - .../tsnapi/@devframes/hub/node.snapshot.d.ts | 2 - .../tsnapi/@devframes/hub/node.snapshot.js | 1 - 16 files changed, 34 insertions(+), 216 deletions(-) delete mode 100644 docs/errors/DF8500.md delete mode 100644 packages/hub/src/node/hub-builtins.ts diff --git a/docs/errors/DF8500.md b/docs/errors/DF8500.md deleted file mode 100644 index 48dec37..0000000 --- a/docs/errors/DF8500.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -outline: deep ---- - -# DF8500: Built-in Command Requires Host Capability - -## Message - -> Built-in command "`{id}`" requires a host capability that this host does not implement. - -## Cause - -A hub built-in command (e.g. `hub:open-path`) was invoked, but the host implementation passed to `createHubContext` did not implement the matching capability. The hub exposes these built-ins uniformly across framework kits — but the underlying capability is host-specific (e.g. `openPath` needs a launch-editor binding the host can call). - -## Fix - -Implement the matching capability on the `DevframeHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`: - -```ts -import type { HubHostCapabilities } from '@devframes/hub/node' -import type { DevframeHost } from 'devframe/types' -import { launchEditor } from 'devframe/utils/launch-editor' - -const host: DevframeHost & HubHostCapabilities = { - // … existing DevframeHost methods … - async openPath(filepath, line, column) { - const target = line ? `${filepath}:${line}${column ? `:${column}` : ''}` : filepath - launchEditor(target) - return true - }, -} -``` - -See the [Hub guide](/guide/hub#host-capabilities) for the full capability surface. - -## Source - -- [`packages/hub/src/node/hub-builtins.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/hub-builtins.ts) — `registerHubBuiltins()` registers `hub:open-path`, whose handler throws this when `context.host.openPath` is undefined. diff --git a/docs/guide/hub.md b/docs/guide/hub.md index ccead1a..8fbd877 100644 --- a/docs/guide/hub.md +++ b/docs/guide/hub.md @@ -24,42 +24,11 @@ Plus a `createJsonRenderer(spec)` factory for building remote-UI panels via the ## Built-in RPC -Every hub context auto-registers these RPC functions so framework kits don't reimplement them: +Every hub context auto-registers this RPC function so framework kits don't reimplement it: - `hub:commands:execute` — invoke a registered server command by id. `await rpc.call('hub:commands:execute', 'my-tool:do-thing', ...args)`. -- `hub:open-path` — registered as a command, delegates to `host.openPath()` (see [Host capabilities](#host-capabilities)). -## Host capabilities - -A hub host implements the same `DevframeHost` interface as devframe, plus optional capabilities the hub knows how to delegate to: - -```ts -interface HubHostCapabilities { - /** Open a file in the user's editor. Backs the built-in `hub:open-path` command. */ - openPath?: (filepath: string, line?: number, column?: number) => boolean | Promise -} -``` - -A framework kit's host implementation looks like this: - -```ts -import type { HubHostCapabilities } from '@devframes/hub/node' -import type { DevframeHost } from 'devframe/types' -import { launchEditor } from 'devframe/utils/launch-editor' - -const host: DevframeHost & HubHostCapabilities = { - mountStatic(base, distDir) { /* … */ }, - resolveOrigin() { /* … */ }, - getStorageDir(scope) { /* … */ }, - async openPath(filepath, line, column) { - const target = line ? `${filepath}:${line}${column ? `:${column}` : ''}` : filepath - launchEditor(target) - return true - }, -} -``` - -When a framework kit omits `openPath`, the `hub:open-path` command throws [`DF8500`](/errors/DF8500) instead of silently failing. +Host-specific capabilities (open in editor, reveal in finder, …) ship as kit-registered RPC functions rather than as part of the hub surface. ## Mounting a devframe into a hub diff --git a/examples/minimal-next-devframe-hub/README.md b/examples/minimal-next-devframe-hub/README.md index 5f6be28..bd89a0e 100644 --- a/examples/minimal-next-devframe-hub/README.md +++ b/examples/minimal-next-devframe-hub/README.md @@ -16,14 +16,14 @@ Open the printed URL. You should see: - A **Commands** list populated from server-side registrations - A **Messages** list populated via `messages.add()` on the server - A **Terminals** list, empty unless a devframe registers one -- Buttons that exercise `hub:open-path` and `hub:commands:execute` +- A button that exercises `hub:commands:execute` by dispatching the sample ping command ## What the example proves - `createHubContext()` boots a hub without any Vite-specific code path -- A `DevframeHost & HubHostCapabilities` impl plugs Next host specifics into the hub uniformly +- A `DevframeHost` impl plugs Next host specifics into the hub uniformly - `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock -- Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed +- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed - The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the Next route handler at `/__hub/__connection.json` ## Files diff --git a/examples/minimal-next-devframe-hub/src/client/app/page.tsx b/examples/minimal-next-devframe-hub/src/client/app/page.tsx index c981000..65e3282 100644 --- a/examples/minimal-next-devframe-hub/src/client/app/page.tsx +++ b/examples/minimal-next-devframe-hub/src/client/app/page.tsx @@ -26,7 +26,6 @@ export default function Page() { const [commands, setCommands] = useState([]) const [messages, setMessages] = useState([]) const [terminals, setTerminals] = useState([]) - const [openPathResult, setOpenPathResult] = useState('Test hub:open-path on this README') const [pingResult, setPingResult] = useState('Run ping') const rpcRef = useRef(null) @@ -100,22 +99,6 @@ export default function Page() { } }, []) - async function openReadme() { - if (!rpcRef.current) - return - try { - const result = await rpcRef.current.call( - 'hub:commands:execute' as any, - 'hub:open-path', - 'README.md', - ) - setOpenPathResult(`Opened: ${JSON.stringify(result)}`) - } - catch (err) { - setOpenPathResult(`Error: ${(err as Error).message}`) - } - } - async function ping() { if (!rpcRef.current) return @@ -172,9 +155,6 @@ export default function Page() {
    - diff --git a/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts index 897c13b..866ca90 100644 --- a/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts +++ b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts @@ -1,4 +1,4 @@ -import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' +import type { HubNodeContext } from '@devframes/hub/node' import type { StartedServer } from 'devframe/node' import type { ConnectionMeta, DevframeDefinition, DevframeHost } from 'devframe/types' import { homedir } from 'node:os' @@ -6,7 +6,6 @@ import process from 'node:process' import { defineRpcFunction } from '@devframes/hub' import { createHubContext, mountDevframe } from '@devframes/hub/node' import { startHttpAndWs } from 'devframe/node' -import { launchEditor } from 'devframe/utils/launch-editor' import { getPort } from 'get-port-please' import { join } from 'pathe' import demoDevframe from './demo-devframe' @@ -60,7 +59,7 @@ export async function minimalNextDevframeHub( const cwd = options.cwd ?? process.cwd() const hostName = options.host ?? 'localhost' - const host: DevframeHost & HubHostCapabilities = { + const host: DevframeHost = { mountStatic() { // Static mounting for devframe SPAs would route through Next middleware // in a fuller host. This minimal example keeps mounted devframes headless. @@ -73,14 +72,6 @@ export async function minimalNextDevframeHub( ? join(cwd, 'node_modules/.minimal-next-devframe-hub') : join(homedir(), '.minimal-next-devframe-hub') }, - async openPath(filepath, line, column) { - const absolute = join(cwd, filepath) - const target = line - ? `${absolute}:${line}${column ? `:${column}` : ''}` - : absolute - launchEditor(target) - return true - }, } const port = options.port ?? await getPort({ host: hostName, port: 9877, random: false }) diff --git a/examples/minimal-vite-devframe-hub/README.md b/examples/minimal-vite-devframe-hub/README.md index d03f983..6159215 100644 --- a/examples/minimal-vite-devframe-hub/README.md +++ b/examples/minimal-vite-devframe-hub/README.md @@ -16,14 +16,14 @@ Open the printed URL. You should see: - A **Commands** list — one entry per `commands.register()` - A **Messages** list — populated via `messages.add()` on the server - A **Terminals** list — empty unless a devframe registers one -- A button that exercises `hub:open-path` (opens this README in your editor) +- A button that exercises `hub:commands:execute` by dispatching the sample ping command ## What the example proves - `createHubContext()` boots a hub without any Vite-specific code path -- A `DevframeHost & HubHostCapabilities` impl plugs framework specifics (`openPath`, storage paths) into the hub uniformly +- A `DevframeHost` impl plugs framework specifics (storage paths, origin resolution) into the hub uniformly - `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock -- Hub built-in RPCs (`hub:open-path`, `hub:commands:execute`) work regardless of how the host was constructed +- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed - The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the kit's `__connection.json` middleware ## Files diff --git a/examples/minimal-vite-devframe-hub/index.html b/examples/minimal-vite-devframe-hub/index.html index 23910a4..daf3fb5 100644 --- a/examples/minimal-vite-devframe-hub/index.html +++ b/examples/minimal-vite-devframe-hub/index.html @@ -25,7 +25,7 @@

    Docks

    Commands

    • Waiting for snapshot…

    - +

    diff --git a/examples/minimal-vite-devframe-hub/src/client/main.ts b/examples/minimal-vite-devframe-hub/src/client/main.ts index a5afe25..c98db05 100644 --- a/examples/minimal-vite-devframe-hub/src/client/main.ts +++ b/examples/minimal-vite-devframe-hub/src/client/main.ts @@ -14,7 +14,7 @@ const docksEl = document.querySelector('#docks')! const commandsEl = document.querySelector('#commands')! const messagesEl = document.querySelector('#messages')! const terminalsEl = document.querySelector('#terminals')! -const openPathBtn = document.querySelector('#open-path')! +const pingBtn = document.querySelector('#ping')! function setStatus(text: string, klass?: 'ready' | 'error') { connEl.textContent = text @@ -84,21 +84,18 @@ async function main() { void refreshTerminals() }, 2000) - // 5. Test the hub:open-path built-in via hub:commands:execute. - openPathBtn.addEventListener('click', async () => { - const target = `${location.origin}/README.md` - .replace(/^https?:\/\/[^/]+/, '') // strip origin, leave path + // 5. Exercise the hub:commands:execute built-in by dispatching the + // sample ping command registered server-side. + pingBtn.addEventListener('click', async () => { try { - // Use the project README — server has the actual filesystem path. const result = await rpc.call( 'hub:commands:execute' as any, - 'hub:open-path', - target.startsWith('/') ? target.slice(1) : target, + 'minimal-vite-devframe-hub:ping', ) - openPathBtn.textContent = `Opened (returned ${JSON.stringify(result)})` + pingBtn.textContent = `Ping returned ${JSON.stringify(result)}` } catch (err) { - openPathBtn.textContent = `Error: ${(err as Error).message}` + pingBtn.textContent = `Error: ${(err as Error).message}` } }) } diff --git a/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts index b5a4969..16402a0 100644 --- a/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts +++ b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts @@ -1,4 +1,4 @@ -import type { HubHostCapabilities, HubNodeContext } from '@devframes/hub/node' +import type { HubNodeContext } from '@devframes/hub/node' import type { DevframeDefinition, DevframeHost } from 'devframe/types' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import { homedir } from 'node:os' @@ -6,7 +6,6 @@ import { defineRpcFunction } from '@devframes/hub' import { createHubContext, mountDevframe } from '@devframes/hub/node' import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants' import { startHttpAndWs } from 'devframe/node' -import { launchEditor } from 'devframe/utils/launch-editor' import { getPort } from 'get-port-please' import { join } from 'pathe' @@ -50,9 +49,9 @@ const minimalViteHubTerminalsList = defineRpcFunction({ /** * A deliberately tiny Vite plugin that wires `@devframes/hub` into a Vite - * dev server: creates a hub context, implements the host capabilities - * (`openPath` via launch-editor), and exposes the side-car WS endpoint - * to the browser via Vite middleware at `__connection.json`. + * dev server: creates a hub context, implements the framework-neutral + * `DevframeHost` surface, and exposes the side-car WS endpoint to the + * browser via Vite middleware at `__connection.json`. * * This file is the entire Vite host — every other framework's hub host is * the same shape: a thin layer that adapts a framework's dev server to the hub. @@ -78,7 +77,7 @@ export function minimalViteDevframeHub(options: MinimalViteDevframeHubOptions = const cwd = viteConfig!.root - const host: DevframeHost & HubHostCapabilities = { + const host: DevframeHost = { mountStatic() { // Static mounting for devframe SPAs would route through Vite's // middleware in a fuller kit. This minimal example doesn't @@ -93,14 +92,6 @@ export function minimalViteDevframeHub(options: MinimalViteDevframeHubOptions = ? join(cwd, 'node_modules/.minimal-vite-devframe-hub') : join(homedir(), '.minimal-vite-devframe-hub') }, - async openPath(filepath, line, column) { - const absolute = join(cwd, filepath) - const target = line - ? `${absolute}:${line}${column ? `:${column}` : ''}` - : absolute - launchEditor(target) - return true - }, } const port = options.port ?? await getPort({ port: 9777, random: false }) @@ -113,9 +104,8 @@ export function minimalViteDevframeHub(options: MinimalViteDevframeHubOptions = builtinRpcDeclarations: [ // The minimal hub ships its own `messages:list` and `terminals:list` // RPCs so the UI has something to read. A full hub kit would - // likely standardise these (this is why hub-level RPC built-ins - // exist — see hub:open-path / hub:commands:execute) but for the - // demo we keep them kit-local. + // likely standardise these (alongside the built-in + // `hub:commands:execute`) but for the demo we keep them kit-local. minimalViteHubMessagesList, minimalViteHubTerminalsList, ], diff --git a/packages/hub/src/node/__tests__/context.test.ts b/packages/hub/src/node/__tests__/context.test.ts index f72da8c..29451a4 100644 --- a/packages/hub/src/node/__tests__/context.test.ts +++ b/packages/hub/src/node/__tests__/context.test.ts @@ -15,7 +15,7 @@ function createHost(storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-contex } describe('createHubContext shared state', () => { - it('seeds built-in docks and commands immediately', async () => { + it('seeds built-in docks immediately', async () => { const context = await createHubContext({ cwd: process.cwd(), mode: 'build', @@ -28,9 +28,6 @@ describe('createHubContext shared state', () => { '~messages', '~settings', ]) - - const commands = await context.rpc.sharedState.get('devframe:commands') - expect(commands.value().map(command => command.id)).toContain('hub:open-path') }) }) diff --git a/packages/hub/src/node/context.ts b/packages/hub/src/node/context.ts index d7bbe8a..6684d9c 100644 --- a/packages/hub/src/node/context.ts +++ b/packages/hub/src/node/context.ts @@ -11,32 +11,8 @@ import { DevframeCommandsHost as CommandsHostImpl } from './host-commands' import { DevframeDockHost as DocksHostImpl } from './host-docks' import { DevframeMessagesHost as MessagesHostImpl } from './host-messages' import { DevframeTerminalHost as TerminalsHostImpl } from './host-terminals' -import { registerHubBuiltins } from './hub-builtins' import { builtinHubRpcDeclarations } from './rpc-builtins' -/** - * Optional capabilities a host can implement to unlock hub built-ins. - * These are not required to construct a {@link HubNodeContext} — the - * built-in RPC commands gate themselves on whether the capability is - * present. - * - * Framework kits (`@vitejs/devtools-kit`, future `@next/devtools-kit`, - * etc.) implement these as part of their host so authors get a uniform - * surface — e.g. `hub:open-path` works the same way regardless of which - * framework hosts the hub. - */ -export interface HubHostCapabilities { - /** - * Open a file in the user's editor. Returns `false` when the host - * has no editor binding for the current environment; throws when the - * launch attempt fails. - * - * Backs the built-in `hub:open-path` RPC command and command-palette - * entry. - */ - openPath?: (filepath: string, line?: number, column?: number) => boolean | Promise -} - /** * Hub-augmented node context — extends devframe's framework-neutral * `DevframeNodeContext` with the hub-level subsystems (`docks`, @@ -44,10 +20,12 @@ export interface HubHostCapabilities { * factory. * * Framework kits further extend this with their own slots (e.g. - * `viteConfig`, `viteServer`). + * `viteConfig`, `viteServer`). Host-specific capabilities (editor open, + * filesystem reveal, etc.) ship as kit-registered RPC functions rather + * than as part of this surface. */ export interface HubNodeContext extends DevframeNodeContext { - readonly host: DevframeHost & HubHostCapabilities + readonly host: DevframeHost docks: DevframeDockHost terminals: DevframeTerminalHost messages: DevframeMessagesHost @@ -147,7 +125,6 @@ export async function createHubContext(options: CreateHubContextOptions): Promis commands.events.on('command:registered', syncCommands) commands.events.on('command:unregistered', syncCommands) - registerHubBuiltins(context) commandsSharedState.mutate(() => commands.list()) return context diff --git a/packages/hub/src/node/diagnostics.ts b/packages/hub/src/node/diagnostics.ts index e10eb37..7bb704a 100644 --- a/packages/hub/src/node/diagnostics.ts +++ b/packages/hub/src/node/diagnostics.ts @@ -1,16 +1,15 @@ import { defineDiagnostics } from 'nostics' import { hubReporter } from '../utils/diagnostics-reporter' -// Hub-side diagnostics for docks, terminals, messages, commands, and the -// built-in RPC commands. Shares the `DF` prefix with devframe core; the -// hub reserves the `DF8xxx` range so the unified surface stays -// collision-free. Sub-ranges: +// Hub-side diagnostics for docks, terminals, messages, and commands. +// Shares the `DF` prefix with devframe core; the hub reserves the +// `DF8xxx` range so the unified surface stays collision-free. +// Sub-ranges: // DF8000-DF8099 — hub context / lifecycle // DF8100-DF8199 — docks // DF8200-DF8299 — terminals // DF8300-DF8399 — messages // DF8400-DF8499 — commands -// DF8500-DF8599 — built-in RPC commands export const diagnostics = defineDiagnostics({ docsBase: 'https://devfra.me/errors', reporters: [hubReporter], @@ -44,9 +43,5 @@ export const diagnostics = defineDiagnostics({ why: (p: { id: string }) => `Command id "${p.id}" is already used by another command or child command`, fix: 'Use globally unique command ids for top-level commands and all child commands.', }, - DF8500: { - why: (p: { id: string }) => `Built-in command "${p.id}" requires a host capability that this host does not implement.`, - fix: 'Implement the matching capability on the `DevframeHost` returned to `createHubContext`. For `hub:open-path`, implement `host.openPath(filepath, line?, column?)`.', - }, }, }) diff --git a/packages/hub/src/node/hub-builtins.ts b/packages/hub/src/node/hub-builtins.ts deleted file mode 100644 index 110b0b7..0000000 --- a/packages/hub/src/node/hub-builtins.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { HubNodeContext } from './context' -import { diagnostics } from './diagnostics' - -/** - * Register the hub's framework-neutral built-in RPC commands. Each - * built-in delegates to an optional host capability; when the host does - * not implement the capability, the command throws `DF8500`. - * - * Today: `hub:open-path` (delegates to `host.openPath`). New built-ins - * land in this file with their own diagnostic code in the `DF85xx` - * sub-range. - */ -export function registerHubBuiltins(context: HubNodeContext): void { - context.commands.register({ - id: 'hub:open-path', - title: 'Open Path in Editor', - icon: 'ph:pencil-duotone', - // Programmatic command — invoked via RPC by tool code, not by the - // user from the command palette directly. Hide from palette search. - showInPalette: false, - handler: async (filepath: unknown, line?: unknown, column?: unknown) => { - const openPath = context.host.openPath - if (!openPath) { - throw diagnostics.DF8500({ id: 'hub:open-path' }) - } - if (typeof filepath !== 'string' || !filepath) { - throw new TypeError('hub:open-path: `filepath` must be a non-empty string') - } - return openPath( - filepath, - typeof line === 'number' ? line : undefined, - typeof column === 'number' ? column : undefined, - ) - }, - }) -} diff --git a/packages/hub/src/node/index.ts b/packages/hub/src/node/index.ts index d4fb8b3..c5160de 100644 --- a/packages/hub/src/node/index.ts +++ b/packages/hub/src/node/index.ts @@ -3,7 +3,6 @@ export * from './host-commands' export * from './host-docks' export * from './host-messages' export * from './host-terminals' -export * from './hub-builtins' export * from './mount-devframe' export * from './rpc-builtins' export * from './utils' diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts index 994ad39..631ca46 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts @@ -80,7 +80,6 @@ export declare class DevframeTerminalHost implements DevframeTerminalHost$1 { // #region Functions export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; export declare function mountDevframe(_: HubNodeContext, _: DevframeDefinition, _?: MountDevframeOptions): Promise; -export declare function registerHubBuiltins(_: HubNodeContext): void; // #endregion // #region Variables @@ -105,6 +104,5 @@ export declare const hubCommandsExecute: { // #region Other export { createHubContext } export { CreateHubContextOptions } -export { HubHostCapabilities } export { HubNodeContext } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js index 4f49684..c051584 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js @@ -65,7 +65,6 @@ export class DevframeTerminalHost { export async function createHubContext(_) {} export function createSimpleClientScript(_) {} export async function mountDevframe(_, _, _) {} -export function registerHubBuiltins(_) {} // #endregion // #region Variables From 16dd84aebfb2a87bf0ea930b6bc4fa5547a10792 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 23 May 2026 02:40:08 +0900 Subject: [PATCH 6/7] refactor(hub): tighten public surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address downstream feedback from @vitejs/devtools-kit: - Drop the Vite-specific `~viteplus` category from the hub's framework-neutral surface; widen `DevframeDockEntryCategory` to `(string & {})` so kits register their own categories ad-hoc. - Rename `DevframeDockHost` → `DevframeDocksHost` and `DevframeTerminalHost` → `DevframeTerminalsHost` so host class names match their `ctx.docks` / `ctx.terminals` property names. - Rename `HubNodeContext` → `DevframeHubContext` to follow the `Devframe*` prefix pattern the rest of the codebase uses. - Rename hub's `defineRpcFunction` → `defineHubRpcFunction` so the hub-context-typed factory can't be confused with devframe's or a kit's identically-named export. --- docs/errors/DF8100.md | 2 +- docs/errors/DF8101.md | 2 +- docs/errors/DF8102.md | 2 +- docs/errors/DF8200.md | 2 +- docs/errors/DF8201.md | 2 +- docs/guide/hub.md | 2 +- .../src/client/devframe/demo-devframe.ts | 4 +-- .../devframe/minimal-next-devframe-hub.ts | 14 ++++---- .../minimal-vite-devframe-hub/src/devframe.ts | 4 +-- .../src/minimal-vite-devframe-hub.ts | 12 +++---- packages/hub/src/constants.ts | 4 +-- packages/hub/src/define.ts | 4 +-- .../src/node/__tests__/host-commands.test.ts | 8 ++--- .../hub/src/node/__tests__/host-docks.test.ts | 12 +++---- .../src/node/__tests__/host-messages.test.ts | 4 +-- .../src/node/__tests__/host-terminals.test.ts | 8 ++--- packages/hub/src/node/context.ts | 18 +++++----- packages/hub/src/node/host-commands.ts | 4 +-- packages/hub/src/node/host-docks.ts | 12 +++---- packages/hub/src/node/host-messages.ts | 4 +-- packages/hub/src/node/host-terminals.ts | 12 +++---- packages/hub/src/node/mount-devframe.ts | 4 +-- packages/hub/src/node/rpc-builtins.ts | 4 +-- packages/hub/src/types/docks.ts | 15 ++++++-- packages/hub/src/types/index.ts | 2 +- packages/hub/src/types/terminals.ts | 2 +- .../tsnapi/@devframes/hub/index.snapshot.d.ts | 8 ++--- .../tsnapi/@devframes/hub/index.snapshot.js | 2 +- .../tsnapi/@devframes/hub/node.snapshot.d.ts | 36 +++++++++---------- .../tsnapi/@devframes/hub/node.snapshot.js | 4 +-- .../tsnapi/@devframes/hub/types.snapshot.d.ts | 6 ++-- 31 files changed, 113 insertions(+), 106 deletions(-) diff --git a/docs/errors/DF8100.md b/docs/errors/DF8100.md index 5b82136..b9c46ca 100644 --- a/docs/errors/DF8100.md +++ b/docs/errors/DF8100.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.register()` throws when `views.has(view.id) && !force`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.register()` throws when `views.has(view.id) && !force`. diff --git a/docs/errors/DF8101.md b/docs/errors/DF8101.md index af2fcb9..b04270c 100644 --- a/docs/errors/DF8101.md +++ b/docs/errors/DF8101.md @@ -19,4 +19,4 @@ The `update` handle returned by `ctx.docks.register(view)` received a patch whos ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.register()` returns an `update` callable that throws this when the patch carries a different `id`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.register()` returns an `update` callable that throws this when the patch carries a different `id`. diff --git a/docs/errors/DF8102.md b/docs/errors/DF8102.md index e9d0b41..8049b45 100644 --- a/docs/errors/DF8102.md +++ b/docs/errors/DF8102.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDockHost.update()` throws when `views.has(view.id) === false`. +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.update()` throws when `views.has(view.id) === false`. diff --git a/docs/errors/DF8200.md b/docs/errors/DF8200.md index 56d775b..dc73c70 100644 --- a/docs/errors/DF8200.md +++ b/docs/errors/DF8200.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalHost.register()` and `startChildProcess()` throw when the id is already taken. +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalsHost.register()` and `startChildProcess()` throw when the id is already taken. diff --git a/docs/errors/DF8201.md b/docs/errors/DF8201.md index 5d4c1d5..d828f6a 100644 --- a/docs/errors/DF8201.md +++ b/docs/errors/DF8201.md @@ -19,4 +19,4 @@ outline: deep ## Source -- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalHost.update()` throws when `sessions.has(patch.id) === false`. +- [`packages/hub/src/node/host-terminals.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-terminals.ts) — `DevframeTerminalsHost.update()` throws when `sessions.has(patch.id) === false`. diff --git a/docs/guide/hub.md b/docs/guide/hub.md index 8fbd877..12e315c 100644 --- a/docs/guide/hub.md +++ b/docs/guide/hub.md @@ -11,7 +11,7 @@ outline: deep ## What the hub adds -A hub-aware node context (`HubNodeContext`) extends `DevframeNodeContext` with four subsystems: +A hub-aware node context (`DevframeHubContext`) extends `DevframeNodeContext` with four subsystems: | Subsystem | Surface | Purpose | |---|---|---| diff --git a/examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts b/examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts index 8f9f932..470fe52 100644 --- a/examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts +++ b/examples/minimal-next-devframe-hub/src/client/devframe/demo-devframe.ts @@ -1,4 +1,4 @@ -import type { HubNodeContext } from '@devframes/hub/node' +import type { DevframeHubContext } from '@devframes/hub/node' import { defineDevframe } from 'devframe/types' export default defineDevframe({ @@ -7,7 +7,7 @@ export default defineDevframe({ icon: 'ph:rocket-duotone', basePath: '/__next-demo-tool/', async setup(rawCtx) { - const ctx = rawCtx as unknown as HubNodeContext + const ctx = rawCtx as unknown as DevframeHubContext ctx.commands.register({ id: 'next-demo-tool:say-hello', diff --git a/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts index 866ca90..a34009a 100644 --- a/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts +++ b/examples/minimal-next-devframe-hub/src/client/devframe/minimal-next-devframe-hub.ts @@ -1,9 +1,9 @@ -import type { HubNodeContext } from '@devframes/hub/node' +import type { DevframeHubContext } from '@devframes/hub/node' import type { StartedServer } from 'devframe/node' import type { ConnectionMeta, DevframeDefinition, DevframeHost } from 'devframe/types' import { homedir } from 'node:os' import process from 'node:process' -import { defineRpcFunction } from '@devframes/hub' +import { defineHubRpcFunction } from '@devframes/hub' import { createHubContext, mountDevframe } from '@devframes/hub/node' import { startHttpAndWs } from 'devframe/node' import { getPort } from 'get-port-please' @@ -22,26 +22,26 @@ export interface MinimalNextDevframeHubOptions { } export interface StartedMinimalNextDevframeHub extends StartedServer { - context: HubNodeContext + context: DevframeHubContext connectionMeta: ConnectionMeta & { backend: 'websocket', websocket: number } } -const minimalNextHubMessagesList = defineRpcFunction({ +const minimalNextHubMessagesList = defineHubRpcFunction({ name: 'minimal-next-devframe-hub:messages:list', type: 'static', jsonSerializable: true, - setup: (ctx: HubNodeContext) => ({ + setup: (ctx: DevframeHubContext) => ({ async handler() { return Array.from(ctx.messages.entries.values()) }, }), }) -const minimalNextHubTerminalsList = defineRpcFunction({ +const minimalNextHubTerminalsList = defineHubRpcFunction({ name: 'minimal-next-devframe-hub:terminals:list', type: 'static', jsonSerializable: true, - setup: (ctx: HubNodeContext) => ({ + setup: (ctx: DevframeHubContext) => ({ async handler() { return Array.from(ctx.terminals.sessions.values()).map(s => ({ id: s.id, diff --git a/examples/minimal-vite-devframe-hub/src/devframe.ts b/examples/minimal-vite-devframe-hub/src/devframe.ts index 21fcafa..b8062b3 100644 --- a/examples/minimal-vite-devframe-hub/src/devframe.ts +++ b/examples/minimal-vite-devframe-hub/src/devframe.ts @@ -1,4 +1,4 @@ -import type { HubNodeContext } from '@devframes/hub/node' +import type { DevframeHubContext } from '@devframes/hub/node' import { defineDevframe } from 'devframe/types' /** @@ -16,7 +16,7 @@ export default defineDevframe({ icon: 'ph:rocket-duotone', basePath: '/__demo-tool/', async setup(rawCtx) { - const ctx = rawCtx as unknown as HubNodeContext + const ctx = rawCtx as unknown as DevframeHubContext ctx.commands.register({ id: 'demo-tool:say-hello', diff --git a/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts index 16402a0..46a79d9 100644 --- a/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts +++ b/examples/minimal-vite-devframe-hub/src/minimal-vite-devframe-hub.ts @@ -1,8 +1,8 @@ -import type { HubNodeContext } from '@devframes/hub/node' +import type { DevframeHubContext } from '@devframes/hub/node' import type { DevframeDefinition, DevframeHost } from 'devframe/types' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import { homedir } from 'node:os' -import { defineRpcFunction } from '@devframes/hub' +import { defineHubRpcFunction } from '@devframes/hub' import { createHubContext, mountDevframe } from '@devframes/hub/node' import { DEVFRAME_CONNECTION_META_FILENAME } from 'devframe/constants' import { startHttpAndWs } from 'devframe/node' @@ -20,22 +20,22 @@ export interface MinimalViteDevframeHubOptions { // Minimal hub-local RPCs — used by the UI for read-side data. A more // ambitious hub host might hoist these into `@devframes/hub` itself. -const minimalViteHubMessagesList = defineRpcFunction({ +const minimalViteHubMessagesList = defineHubRpcFunction({ name: 'minimal-vite-devframe-hub:messages:list', type: 'static', jsonSerializable: true, - setup: (ctx: HubNodeContext) => ({ + setup: (ctx: DevframeHubContext) => ({ async handler() { return Array.from(ctx.messages.entries.values()) }, }), }) -const minimalViteHubTerminalsList = defineRpcFunction({ +const minimalViteHubTerminalsList = defineHubRpcFunction({ name: 'minimal-vite-devframe-hub:terminals:list', type: 'static', jsonSerializable: true, - setup: (ctx: HubNodeContext) => ({ + setup: (ctx: DevframeHubContext) => ({ async handler() { return Array.from(ctx.terminals.sessions.values()).map(s => ({ id: s.id, diff --git a/packages/hub/src/constants.ts b/packages/hub/src/constants.ts index f0712da..474884b 100644 --- a/packages/hub/src/constants.ts +++ b/packages/hub/src/constants.ts @@ -1,17 +1,15 @@ -import type { DevframeDockEntryCategory } from './types/docks' import type { DevframeDocksUserSettings } from './types/settings' export * from 'devframe/constants' export const DEFAULT_CATEGORIES_ORDER: Record = { - '~viteplus': -1000, 'default': 0, 'app': 100, 'framework': 200, 'web': 300, 'advanced': 400, '~builtin': 1000, -} satisfies Record +} export const DEFAULT_STATE_USER_SETTINGS: () => DevframeDocksUserSettings = () => ({ docksHidden: [], diff --git a/packages/hub/src/define.ts b/packages/hub/src/define.ts index bc09f47..e69b87b 100644 --- a/packages/hub/src/define.ts +++ b/packages/hub/src/define.ts @@ -1,11 +1,11 @@ import type { WhenContext, WhenExpression } from 'devframe/utils/when' -import type { HubNodeContext } from './node/context' +import type { DevframeHubContext } from './node/context' import type { DevframeServerCommandInput } from './types/commands' import type { DevframeDockUserEntry } from './types/docks' import type { JsonRenderSpec } from './types/json-render' import { createDefineWrapperWithContext } from 'devframe/rpc' -export const defineRpcFunction = createDefineWrapperWithContext() +export const defineHubRpcFunction = createDefineWrapperWithContext() export function defineCommand( command: Omit & { when?: WhenExpression }, diff --git a/packages/hub/src/node/__tests__/host-commands.test.ts b/packages/hub/src/node/__tests__/host-commands.test.ts index 8bae0c4..7f9f5da 100644 --- a/packages/hub/src/node/__tests__/host-commands.test.ts +++ b/packages/hub/src/node/__tests__/host-commands.test.ts @@ -1,10 +1,10 @@ -import type { HubNodeContext } from '../context' +import type { DevframeHubContext } from '../context' import { describe, expect, it } from 'vitest' import { DevframeCommandsHost } from '../host-commands' describe('devframeCommandsHost command id validation', () => { it('rejects duplicate ids inside one command tree', () => { - const host = new DevframeCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as DevframeHubContext) expect(() => host.register({ id: 'tool:parent', @@ -17,7 +17,7 @@ describe('devframeCommandsHost command id validation', () => { }) it('rejects child ids that collide with existing command trees', () => { - const host = new DevframeCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as DevframeHubContext) host.register({ id: 'tool:parent', title: 'Parent', @@ -41,7 +41,7 @@ describe('devframeCommandsHost command id validation', () => { }) it('validates updated children against other command trees', () => { - const host = new DevframeCommandsHost({} as HubNodeContext) + const host = new DevframeCommandsHost({} as DevframeHubContext) host.register({ id: 'other:parent', title: 'Other parent', diff --git a/packages/hub/src/node/__tests__/host-docks.test.ts b/packages/hub/src/node/__tests__/host-docks.test.ts index 554a2a2..bc2314f 100644 --- a/packages/hub/src/node/__tests__/host-docks.test.ts +++ b/packages/hub/src/node/__tests__/host-docks.test.ts @@ -1,4 +1,4 @@ -import type { HubNodeContext } from '../context' +import type { DevframeHubContext } from '../context' import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -6,9 +6,9 @@ import { REMOTE_CONNECTION_KEY } from 'devframe/constants' import { getInternalContext } from 'devframe/node/internal' import { describe, expect, it } from 'vitest' import { parseRemoteConnection } from '../../client/remote' -import { DevframeDockHost } from '../host-docks' +import { DevframeDocksHost } from '../host-docks' -function createContext(): HubNodeContext { +function createContext(): DevframeHubContext { const storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-docks-')) return { host: { @@ -16,14 +16,14 @@ function createContext(): HubNodeContext { resolveOrigin: () => 'http://localhost:5173', getStorageDir: () => storageDir, }, - } as unknown as HubNodeContext + } as unknown as DevframeHubContext } describe('devframeDockHost remote URL enrichment', () => { it('preserves hash routes and replaces existing remote descriptors', () => { const context = createContext() getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } - const host = new DevframeDockHost(context) + const host = new DevframeDocksHost(context) host.register({ type: 'iframe', @@ -60,7 +60,7 @@ describe('devframeDockHost remote URL enrichment', () => { it('preserves non-route fragments with the ampersand descriptor form', () => { const context = createContext() getInternalContext(context).wsEndpoint = { url: 'ws://localhost:4173' } - const host = new DevframeDockHost(context) + const host = new DevframeDocksHost(context) host.register({ type: 'iframe', diff --git a/packages/hub/src/node/__tests__/host-messages.test.ts b/packages/hub/src/node/__tests__/host-messages.test.ts index a28282a..903de4b 100644 --- a/packages/hub/src/node/__tests__/host-messages.test.ts +++ b/packages/hub/src/node/__tests__/host-messages.test.ts @@ -1,10 +1,10 @@ -import type { HubNodeContext } from '../context' +import type { DevframeHubContext } from '../context' import { describe, expect, it } from 'vitest' import { DevframeMessagesHost } from '../host-messages' describe('devframeMessagesHost', () => { it('caps removal history', async () => { - const host = new DevframeMessagesHost({} as HubNodeContext) + const host = new DevframeMessagesHost({} as DevframeHubContext) for (let i = 0; i < 1005; i++) { const id = `message:${i}` diff --git a/packages/hub/src/node/__tests__/host-terminals.test.ts b/packages/hub/src/node/__tests__/host-terminals.test.ts index b53e148..c32a3f9 100644 --- a/packages/hub/src/node/__tests__/host-terminals.test.ts +++ b/packages/hub/src/node/__tests__/host-terminals.test.ts @@ -1,8 +1,8 @@ import type { DevframeTerminalSession } from '../../types/terminals' -import type { HubNodeContext } from '../context' +import type { DevframeHubContext } from '../context' import process from 'node:process' import { describe, expect, it, vi } from 'vitest' -import { DevframeTerminalHost } from '../host-terminals' +import { DevframeTerminalsHost } from '../host-terminals' interface FakeSink { write: ReturnType @@ -37,10 +37,10 @@ function createTerminalHost() { }), }, }, - } as unknown as HubNodeContext + } as unknown as DevframeHubContext return { - host: new DevframeTerminalHost(context), + host: new DevframeTerminalsHost(context), sinks, } } diff --git a/packages/hub/src/node/context.ts b/packages/hub/src/node/context.ts index 6684d9c..ec9bf5d 100644 --- a/packages/hub/src/node/context.ts +++ b/packages/hub/src/node/context.ts @@ -1,16 +1,16 @@ import type { CreateHostContextOptions } from 'devframe/node' import type { DevframeHost, DevframeNodeContext } from 'devframe/types' import type { DevframeCommandsHost } from '../types/commands' -import type { DevframeDockHost } from '../types/docks' +import type { DevframeDocksHost } from '../types/docks' import type { JsonRenderer, JsonRenderSpec } from '../types/json-render' import type { DevframeMessagesHost } from '../types/messages' -import type { DevframeTerminalHost } from '../types/terminals' +import type { DevframeTerminalsHost } from '../types/terminals' import { createHostContext } from 'devframe/node' import { debounce } from 'perfect-debounce' import { DevframeCommandsHost as CommandsHostImpl } from './host-commands' -import { DevframeDockHost as DocksHostImpl } from './host-docks' +import { DevframeDocksHost as DocksHostImpl } from './host-docks' import { DevframeMessagesHost as MessagesHostImpl } from './host-messages' -import { DevframeTerminalHost as TerminalsHostImpl } from './host-terminals' +import { DevframeTerminalsHost as TerminalsHostImpl } from './host-terminals' import { builtinHubRpcDeclarations } from './rpc-builtins' /** @@ -24,10 +24,10 @@ import { builtinHubRpcDeclarations } from './rpc-builtins' * filesystem reveal, etc.) ship as kit-registered RPC functions rather * than as part of this surface. */ -export interface HubNodeContext extends DevframeNodeContext { +export interface DevframeHubContext extends DevframeNodeContext { readonly host: DevframeHost - docks: DevframeDockHost - terminals: DevframeTerminalHost + docks: DevframeDocksHost + terminals: DevframeTerminalsHost messages: DevframeMessagesHost commands: DevframeCommandsHost /** @@ -44,7 +44,7 @@ export interface CreateHubContextOptions extends CreateHostContextOptions {} * registers the hub's built-in RPC commands, and wires the shared-state * synchronization that powers a hub-aware client UI. */ -export async function createHubContext(options: CreateHubContextOptions): Promise { +export async function createHubContext(options: CreateHubContextOptions): Promise { const baseContext = await createHostContext({ ...options, builtinRpcDeclarations: [ @@ -52,7 +52,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis ...(options.builtinRpcDeclarations ?? []), ], }) - const context = baseContext as HubNodeContext + const context = baseContext as DevframeHubContext const docks = new DocksHostImpl(context) const terminals = new TerminalsHostImpl(context) diff --git a/packages/hub/src/node/host-commands.ts b/packages/hub/src/node/host-commands.ts index 0fc7a1f..2668f39 100644 --- a/packages/hub/src/node/host-commands.ts +++ b/packages/hub/src/node/host-commands.ts @@ -4,7 +4,7 @@ import type { DevframeServerCommandEntry, DevframeServerCommandInput, } from '../types/commands' -import type { HubNodeContext } from './context' +import type { DevframeHubContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { diagnostics } from './diagnostics' @@ -55,7 +55,7 @@ export class DevframeCommandsHost implements DevframeCommandsHostType { public readonly events: DevframeCommandsHostType['events'] = createEventEmitter() constructor( - public readonly context: HubNodeContext, + public readonly context: DevframeHubContext, ) {} register(command: DevframeServerCommandInput): DevframeCommandHandle { diff --git a/packages/hub/src/node/host-docks.ts b/packages/hub/src/node/host-docks.ts index 9be2abf..0733206 100644 --- a/packages/hub/src/node/host-docks.ts +++ b/packages/hub/src/node/host-docks.ts @@ -2,7 +2,7 @@ import type { DevframeNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { DevframeDockEntry, - DevframeDockHost as DevframeDockHostType, + DevframeDocksHost as DevframeDocksHostType, DevframeDockUserEntry, DevframeViewBuiltin, DevframeViewIframe, @@ -10,7 +10,7 @@ import type { RemoteDockOptions, } from '../types/docks' import type { DevframeDocksUserSettings } from '../types/settings' -import type { HubNodeContext } from './context' +import type { DevframeHubContext } from './context' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' import { createStorage } from 'devframe/node' import { getInternalContext } from 'devframe/node/internal' @@ -82,16 +82,16 @@ function buildRemoteUrl(baseUrl: string, payload: RemoteConnectionInfo, transpor return `${beforeHash}${sep}${param}${hash}` } -export class DevframeDockHost implements DevframeDockHostType { - public readonly views: DevframeDockHostType['views'] = new Map() - public readonly events: DevframeDockHostType['events'] = createEventEmitter() +export class DevframeDocksHost implements DevframeDocksHostType { + public readonly views: DevframeDocksHostType['views'] = new Map() + public readonly events: DevframeDocksHostType['events'] = createEventEmitter() public userSettings: SharedState = undefined! /** Dock-id → allocated remote token + resolved options. */ private readonly remoteDocks = new Map() constructor( - public readonly context: HubNodeContext, + public readonly context: DevframeHubContext, ) { } diff --git a/packages/hub/src/node/host-messages.ts b/packages/hub/src/node/host-messages.ts index cb98180..592db29 100644 --- a/packages/hub/src/node/host-messages.ts +++ b/packages/hub/src/node/host-messages.ts @@ -4,7 +4,7 @@ import type { DevframeMessageHandle, DevframeMessagesHost as DevframeMessagesHostType, } from '../types/messages' -import type { HubNodeContext } from './context' +import type { DevframeHubContext } from './context' import { createEventEmitter } from 'devframe/utils/events' import { nanoid } from 'devframe/utils/nanoid' @@ -38,7 +38,7 @@ export class DevframeMessagesHost implements DevframeMessagesHostType { } constructor( - public readonly context: HubNodeContext, + public readonly context: DevframeHubContext, ) {} async add(input: DevframeMessageEntryInput): Promise { diff --git a/packages/hub/src/node/host-terminals.ts b/packages/hub/src/node/host-terminals.ts index 86e174b..1ac8ceb 100644 --- a/packages/hub/src/node/host-terminals.ts +++ b/packages/hub/src/node/host-terminals.ts @@ -3,11 +3,11 @@ import type { Result as TinyExecResult } from 'tinyexec' import type { DevframeChildProcessExecuteOptions, DevframeChildProcessTerminalSession, - DevframeTerminalHost as DevframeTerminalHostType, DevframeTerminalSession, DevframeTerminalSessionBase, + DevframeTerminalsHost as DevframeTerminalsHostType, } from '../types/terminals' -import type { HubNodeContext } from './context' +import type { DevframeHubContext } from './context' import process from 'node:process' import { createEventEmitter } from 'devframe/utils/events' import { diagnostics } from './diagnostics' @@ -21,9 +21,9 @@ type PartialWithoutId = Partial & { id: string } const TERMINAL_STREAM_CHANNEL = 'devframe:terminals' as const const TERMINAL_REPLAY_WINDOW = 1000 -export class DevframeTerminalHost implements DevframeTerminalHostType { - public readonly sessions: DevframeTerminalHostType['sessions'] = new Map() - public readonly events: DevframeTerminalHostType['events'] = createEventEmitter() +export class DevframeTerminalsHost implements DevframeTerminalsHostType { + public readonly sessions: DevframeTerminalsHostType['sessions'] = new Map() + public readonly events: DevframeTerminalsHostType['events'] = createEventEmitter() private _boundStreams = new Map void @@ -33,7 +33,7 @@ export class DevframeTerminalHost implements DevframeTerminalHostType { private _channel?: RpcStreamingChannel constructor( - public readonly context: HubNodeContext, + public readonly context: DevframeHubContext, ) { } diff --git a/packages/hub/src/node/mount-devframe.ts b/packages/hub/src/node/mount-devframe.ts index 74e363a..2827e3d 100644 --- a/packages/hub/src/node/mount-devframe.ts +++ b/packages/hub/src/node/mount-devframe.ts @@ -1,6 +1,6 @@ import type { DevframeDefinition } from 'devframe/types' import type { DevframeViewIframe } from '../types/docks' -import type { HubNodeContext } from './context' +import type { DevframeHubContext } from './context' import { resolveBasePath } from 'devframe/node/internal' import { resolve } from 'pathe' @@ -29,7 +29,7 @@ export interface MountDevframeOptions { * Vite `Plugin` whose `devtools.setup` ultimately delegates here. */ export async function mountDevframe( - ctx: HubNodeContext, + ctx: DevframeHubContext, d: DevframeDefinition, options: MountDevframeOptions = {}, ): Promise { diff --git a/packages/hub/src/node/rpc-builtins.ts b/packages/hub/src/node/rpc-builtins.ts index f93ef47..f250936 100644 --- a/packages/hub/src/node/rpc-builtins.ts +++ b/packages/hub/src/node/rpc-builtins.ts @@ -1,5 +1,5 @@ import type { RpcFunctionDefinitionAny } from 'devframe/rpc' -import { defineRpcFunction } from '../define' +import { defineHubRpcFunction } from '../define' /** * `hub:commands:execute` — Invoke a registered server command by id. The @@ -9,7 +9,7 @@ import { defineRpcFunction } from '../define' * Pairs with the `devframe:commands` shared state: clients read the list * from the shared state and dispatch by id via this RPC. */ -export const hubCommandsExecute = defineRpcFunction({ +export const hubCommandsExecute = defineHubRpcFunction({ name: 'hub:commands:execute', type: 'action', setup: context => ({ diff --git a/packages/hub/src/types/docks.ts b/packages/hub/src/types/docks.ts index f3f0621..9062ea1 100644 --- a/packages/hub/src/types/docks.ts +++ b/packages/hub/src/types/docks.ts @@ -1,7 +1,7 @@ import type { ConnectionMeta, EventEmitter } from 'devframe/types' import type { JsonRenderer } from './json-render' -export interface DevframeDockHost { +export interface DevframeDocksHost { readonly views: Map readonly events: EventEmitter<{ 'dock:entry:updated': (entry: DevframeDockUserEntry) => void @@ -14,8 +14,17 @@ export interface DevframeDockHost { values: (options?: { includeBuiltin?: boolean }) => DevframeDockEntry[] } -// TODO: refine categories more clearly -export type DevframeDockEntryCategory = 'app' | 'framework' | 'web' | 'advanced' | 'default' | '~viteplus' | '~builtin' +// Known categories the hub orders by default. Kits may pass their own +// category ids; `(string & {})` keeps autocomplete on the known set while +// allowing arbitrary string values. +export type DevframeDockEntryCategory + = | 'app' + | 'framework' + | 'web' + | 'advanced' + | 'default' + | '~builtin' + | (string & {}) export type DevframeDockEntryIcon = string | { light: string, dark: string } diff --git a/packages/hub/src/types/index.ts b/packages/hub/src/types/index.ts index ef955c1..e97130e 100644 --- a/packages/hub/src/types/index.ts +++ b/packages/hub/src/types/index.ts @@ -1,6 +1,6 @@ // Re-export the hub-augmented context type so consumers can import it // from the hub's main `types` barrel. -export type { CreateHubContextOptions, HubNodeContext } from '../node/context' +export type { CreateHubContextOptions, DevframeHubContext } from '../node/context' export * from './commands' export * from './docks' diff --git a/packages/hub/src/types/terminals.ts b/packages/hub/src/types/terminals.ts index f59e8ac..c34251e 100644 --- a/packages/hub/src/types/terminals.ts +++ b/packages/hub/src/types/terminals.ts @@ -2,7 +2,7 @@ import type { EventEmitter } from 'devframe/types' import type { ChildProcess } from 'node:child_process' import type { DevframeDockEntryIcon } from './docks' -export interface DevframeTerminalHost { +export interface DevframeTerminalsHost { readonly sessions: Map readonly events: EventEmitter<{ 'terminal:session:updated': (session: DevframeTerminalSession) => void diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts index f11286c..5805bf1 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts @@ -12,7 +12,7 @@ export declare function defineJsonRenderSpec(_: JsonRenderSpec): JsonRenderSpec; // #endregion // #region Variables -export declare const defineRpcFunction: (definition: _$devframe_rpc0.RpcFunctionDefinition) => _$devframe_rpc0.RpcFunctionDefinition; +export declare const defineHubRpcFunction: (definition: _$devframe_rpc0.RpcFunctionDefinition) => _$devframe_rpc0.RpcFunctionDefinition; // #endregion // #region Other @@ -38,10 +38,11 @@ export { DevframeDockEntry } export { DevframeDockEntryBase } export { DevframeDockEntryCategory } export { DevframeDockEntryIcon } -export { DevframeDockHost } +export { DevframeDocksHost } export { DevframeDocksUserSettings } export { DevframeDockUserEntry } export { DevframeHost } +export { DevframeHubContext } export { DevframeMessageElementPosition } export { DevframeMessageEntry } export { DevframeMessageEntryFrom } @@ -57,9 +58,9 @@ export { DevframeRpcServerFunctions } export { DevframeRpcSharedStates } export { DevframeServerCommandEntry } export { DevframeServerCommandInput } -export { DevframeTerminalHost } export { DevframeTerminalSession } export { DevframeTerminalSessionBase } +export { DevframeTerminalsHost } export { DevframeTerminalStatus } export { DevframeViewAction } export { DevframeViewBuiltin } @@ -73,7 +74,6 @@ export { EntriesToObject } export { EventEmitter } export { EventsMap } export { EventUnsubscribe } -export { HubNodeContext } export { JsonRenderElement } export { JsonRenderer } export { JsonRenderSpec } diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js index 1c23e46..b50f2b5 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.js @@ -4,6 +4,6 @@ // #region Other export { defineCommand } export { defineDockEntry } +export { defineHubRpcFunction } export { defineJsonRenderSpec } -export { defineRpcFunction } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts index 631ca46..bc64b3f 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.d.ts @@ -10,10 +10,10 @@ export interface MountDevframeOptions { // #region Classes export declare class DevframeCommandsHost implements DevframeCommandsHost$1 { - readonly context: HubNodeContext; + readonly context: DevframeHubContext; readonly commands: DevframeCommandsHost$1['commands']; readonly events: DevframeCommandsHost$1['events']; - constructor(_: HubNodeContext); + constructor(_: DevframeHubContext); register(_: DevframeServerCommandInput): DevframeCommandHandle; unregister(_: string): boolean; execute(_: string, ..._: any[]): Promise; @@ -21,13 +21,13 @@ export declare class DevframeCommandsHost implements DevframeCommandsHost$1 { private findCommand; private toSerializable; } -export declare class DevframeDockHost implements DevframeDockHost$1 { - readonly context: HubNodeContext; - readonly views: DevframeDockHost$1['views']; - readonly events: DevframeDockHost$1['events']; +export declare class DevframeDocksHost implements DevframeDocksHost$1 { + readonly context: DevframeHubContext; + readonly views: DevframeDocksHost$1['views']; + readonly events: DevframeDocksHost$1['events']; userSettings: SharedState; private readonly remoteDocks; - constructor(_: HubNodeContext); + constructor(_: DevframeHubContext); init(): Promise; values({ includeBuiltin @@ -43,7 +43,7 @@ export declare class DevframeDockHost implements DevframeDockHost$1 { private prepareRemoteRegistration; } export declare class DevframeMessagesHost implements DevframeMessagesHost$1 { - readonly context: HubNodeContext; + readonly context: DevframeHubContext; readonly entries: DevframeMessagesHost$1['entries']; readonly events: DevframeMessagesHost$1['events']; readonly lastModified: Map; @@ -54,20 +54,20 @@ export declare class DevframeMessagesHost implements DevframeMessagesHost$1 { private _autoDeleteTimers; private _clock; private _tick; - constructor(_: HubNodeContext); + constructor(_: DevframeHubContext); add(_: DevframeMessageEntryInput): Promise; update(_: string, _: Partial): Promise; remove(_: string): Promise; clear(): Promise; private _createHandle; } -export declare class DevframeTerminalHost implements DevframeTerminalHost$1 { - readonly context: HubNodeContext; - readonly sessions: DevframeTerminalHost$1['sessions']; - readonly events: DevframeTerminalHost$1['events']; +export declare class DevframeTerminalsHost implements DevframeTerminalsHost$1 { + readonly context: DevframeHubContext; + readonly sessions: DevframeTerminalsHost$1['sessions']; + readonly events: DevframeTerminalsHost$1['events']; private _boundStreams; private _channel?; - constructor(_: HubNodeContext); + constructor(_: DevframeHubContext); private getStreamingChannel; register(_: DevframeTerminalSession): DevframeTerminalSession; update(_: PartialWithoutId): void; @@ -79,7 +79,7 @@ export declare class DevframeTerminalHost implements DevframeTerminalHost$1 { // #region Functions export declare function createSimpleClientScript(_: string | ((_: any) => void)): ClientScriptEntry; -export declare function mountDevframe(_: HubNodeContext, _: DevframeDefinition, _?: MountDevframeOptions): Promise; +export declare function mountDevframe(_: DevframeHubContext, _: DevframeDefinition, _?: MountDevframeOptions): Promise; // #endregion // #region Variables @@ -92,9 +92,9 @@ export declare const hubCommandsExecute: { returns?: undefined; jsonSerializable?: boolean; agent?: _$devframe.RpcFunctionAgentOptions; - setup?: ((context: HubNodeContext) => _$devframe_rpc0.Thenable<_$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise>>) | undefined; + setup?: ((context: DevframeHubContext) => _$devframe_rpc0.Thenable<_$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise>>) | undefined; handler?: ((id: string, ...args: any[]) => Promise) | undefined; - dump?: _$devframe_rpc0.RpcDump<[id: string, ...args: any[]], Promise, HubNodeContext> | undefined; + dump?: _$devframe_rpc0.RpcDump<[id: string, ...args: any[]], Promise, DevframeHubContext> | undefined; snapshot?: boolean; __resolved?: _$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise> | undefined; __promise?: _$devframe_rpc0.Thenable<_$devframe_rpc0.RpcFunctionSetupResult<[id: string, ...args: any[]], Promise>> | undefined; @@ -104,5 +104,5 @@ export declare const hubCommandsExecute: { // #region Other export { createHubContext } export { CreateHubContextOptions } -export { HubNodeContext } +export { DevframeHubContext } // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js index c051584..576a32c 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/hub/node.snapshot.js @@ -14,7 +14,7 @@ export class DevframeCommandsHost { findCommand(_) {} toSerializable(_) {} } -export class DevframeDockHost { +export class DevframeDocksHost { context views events @@ -45,7 +45,7 @@ export class DevframeMessagesHost { async clear() {} _createHandle(_) {} } -export class DevframeTerminalHost { +export class DevframeTerminalsHost { context sessions events diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts index 3d3465a..b3b6446 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/types.snapshot.d.ts @@ -24,10 +24,11 @@ export { DevframeDockEntry } export { DevframeDockEntryBase } export { DevframeDockEntryCategory } export { DevframeDockEntryIcon } -export { DevframeDockHost } +export { DevframeDocksHost } export { DevframeDocksUserSettings } export { DevframeDockUserEntry } export { DevframeHost } +export { DevframeHubContext } export { DevframeMessageElementPosition } export { DevframeMessageEntry } export { DevframeMessageEntryFrom } @@ -43,9 +44,9 @@ export { DevframeRpcServerFunctions } export { DevframeRpcSharedStates } export { DevframeServerCommandEntry } export { DevframeServerCommandInput } -export { DevframeTerminalHost } export { DevframeTerminalSession } export { DevframeTerminalSessionBase } +export { DevframeTerminalsHost } export { DevframeTerminalStatus } export { DevframeViewAction } export { DevframeViewBuiltin } @@ -59,7 +60,6 @@ export { EntriesToObject } export { EventEmitter } export { EventsMap } export { EventUnsubscribe } -export { HubNodeContext } export { JsonRenderElement } export { JsonRenderer } export { JsonRenderSpec } From 4238046a1b202aa689b64971d901be259d11c9ee Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Sat, 23 May 2026 02:42:31 +0900 Subject: [PATCH 7/7] refactor(devframe): rename node/internal subpath to node/hub-internals This subpath is the bridge `@devframes/hub` (and any future first-party hub adapter) uses to reach into devframe's remote-dock token machinery and basePath resolver. Calling it "internal" while a published package depends on it understates the contract: rename to `hub-internals` and document it as the stable surface for first-party hub adapters. --- alias.ts | 2 +- packages/devframe/package.json | 2 +- packages/devframe/src/node/auth/revoke.ts | 2 +- packages/devframe/src/node/auth/state.ts | 2 +- .../{internal => hub-internals}/context.ts | 0 .../devframe/src/node/hub-internals/index.ts | 25 ++++++++++++++++++ packages/devframe/src/node/internal/index.ts | 26 ------------------- packages/devframe/src/node/server.ts | 2 +- packages/devframe/tsdown.config.ts | 2 +- .../hub/src/node/__tests__/context.test.ts | 2 +- .../hub/src/node/__tests__/host-docks.test.ts | 2 +- packages/hub/src/node/host-docks.ts | 2 +- packages/hub/src/node/mount-devframe.ts | 2 +- ...pshot.d.ts => hub-internals.snapshot.d.ts} | 2 +- ....snapshot.js => hub-internals.snapshot.js} | 2 +- tsconfig.base.json | 4 +-- 16 files changed, 39 insertions(+), 40 deletions(-) rename packages/devframe/src/node/{internal => hub-internals}/context.ts (100%) create mode 100644 packages/devframe/src/node/hub-internals/index.ts delete mode 100644 packages/devframe/src/node/internal/index.ts rename tests/__snapshots__/tsnapi/devframe/node/{internal.snapshot.d.ts => hub-internals.snapshot.d.ts} (96%) rename tests/__snapshots__/tsnapi/devframe/node/{internal.snapshot.js => hub-internals.snapshot.js} (93%) diff --git a/alias.ts b/alias.ts index 6388c5c..e1f0e42 100644 --- a/alias.ts +++ b/alias.ts @@ -14,7 +14,7 @@ export const alias = { 'devframe/rpc': r('devframe/src/rpc'), 'devframe/types': r('devframe/src/types/index.ts'), 'devframe/node/auth': r('devframe/src/node/auth/index.ts'), - 'devframe/node/internal': r('devframe/src/node/internal/index.ts'), + 'devframe/node/hub-internals': r('devframe/src/node/hub-internals/index.ts'), 'devframe/node': r('devframe/src/node/index.ts'), 'devframe/constants': r('devframe/src/constants.ts'), 'devframe/utils/colors': r('devframe/src/utils/colors.ts'), diff --git a/packages/devframe/package.json b/packages/devframe/package.json index f0ddbb3..c8cbbbb 100644 --- a/packages/devframe/package.json +++ b/packages/devframe/package.json @@ -30,7 +30,7 @@ "./helpers/vite": "./dist/helpers/vite.mjs", "./node": "./dist/node/index.mjs", "./node/auth": "./dist/node/auth.mjs", - "./node/internal": "./dist/node/internal.mjs", + "./node/hub-internals": "./dist/node/hub-internals.mjs", "./recipes/open-helpers": "./dist/recipes/open-helpers.mjs", "./rpc": "./dist/rpc/index.mjs", "./rpc/client": "./dist/rpc/client.mjs", diff --git a/packages/devframe/src/node/auth/revoke.ts b/packages/devframe/src/node/auth/revoke.ts index bd4b309..541505f 100644 --- a/packages/devframe/src/node/auth/revoke.ts +++ b/packages/devframe/src/node/auth/revoke.ts @@ -1,7 +1,7 @@ import type { DevframeNodeContext } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' import type { RpcFunctionsHost } from '../host-functions' -import type { InternalAnonymousAuthStorage } from '../internal/context' +import type { InternalAnonymousAuthStorage } from '../hub-internals/context' /** * Flip `isTrusted` to false on any live WS clients connected with `token` diff --git a/packages/devframe/src/node/auth/state.ts b/packages/devframe/src/node/auth/state.ts index 39015ec..346a219 100644 --- a/packages/devframe/src/node/auth/state.ts +++ b/packages/devframe/src/node/auth/state.ts @@ -1,6 +1,6 @@ import type { DevframeNodeRpcSession } from 'devframe/types' import type { SharedState } from 'devframe/utils/shared-state' -import type { InternalAnonymousAuthStorage } from '../internal/context' +import type { InternalAnonymousAuthStorage } from '../hub-internals/context' import { humanId } from 'devframe/utils/human-id' export interface PendingAuthRequest { diff --git a/packages/devframe/src/node/internal/context.ts b/packages/devframe/src/node/hub-internals/context.ts similarity index 100% rename from packages/devframe/src/node/internal/context.ts rename to packages/devframe/src/node/hub-internals/context.ts diff --git a/packages/devframe/src/node/hub-internals/index.ts b/packages/devframe/src/node/hub-internals/index.ts new file mode 100644 index 0000000..5e3feb6 --- /dev/null +++ b/packages/devframe/src/node/hub-internals/index.ts @@ -0,0 +1,25 @@ +/** + * Public surface for first-party hub adapters (`@devframes/hub` and + * similar). Carries the primitives a hub-kit author needs to bridge a + * framework dev server into a hub context — base-path resolution and + * the remote-dock token bridge used by the hub's docks host. + * + * Stable across minor versions; treat additions or removals as breaking + * only across major versions. + */ + +export { + normalizeBasePath, + resolveBasePath, +} from '../../adapters/_shared' + +export { + getInternalContext, + internalContextMap, +} from './context' + +export type { + DevframeInternalContext, + InternalAnonymousAuthStorage, + RemoteTokenRecord, +} from './context' diff --git a/packages/devframe/src/node/internal/index.ts b/packages/devframe/src/node/internal/index.ts deleted file mode 100644 index 3b119ec..0000000 --- a/packages/devframe/src/node/internal/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Reserved for `@vitejs/devtools-kit` and other first-party adapters - * that reach into devframe's private machinery (currently the - * remote-dock token bridge required by the relocated `DocksHost`). - * - * End users should not import from this subpath. The surface is - * unstable and may change without a major bump. - * - * @internal - */ - -export { - normalizeBasePath, - resolveBasePath, -} from '../../adapters/_shared' - -export { - getInternalContext, - internalContextMap, -} from './context' - -export type { - DevframeInternalContext, - InternalAnonymousAuthStorage, - RemoteTokenRecord, -} from './context' diff --git a/packages/devframe/src/node/server.ts b/packages/devframe/src/node/server.ts index d4410cb..5ac1729 100644 --- a/packages/devframe/src/node/server.ts +++ b/packages/devframe/src/node/server.ts @@ -8,7 +8,7 @@ import { createRpcServer } from 'devframe/rpc/server' import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server' import { H3, toNodeHandler } from 'h3' import { WebSocketServer as WSServer } from 'ws' -import { getInternalContext } from './internal/context' +import { getInternalContext } from './hub-internals/context' export interface StartHttpAndWsOptions { context: DevframeNodeContext diff --git a/packages/devframe/tsdown.config.ts b/packages/devframe/tsdown.config.ts index 96f7a6f..7a455d5 100644 --- a/packages/devframe/tsdown.config.ts +++ b/packages/devframe/tsdown.config.ts @@ -83,7 +83,7 @@ const serverEntries = { 'rpc/transports/ws-server': 'src/rpc/transports/ws-server.ts', 'node/index': 'src/node/index.ts', 'node/auth': 'src/node/auth/index.ts', - 'node/internal': 'src/node/internal/index.ts', + 'node/hub-internals': 'src/node/hub-internals/index.ts', 'utils/launch-editor': 'src/utils/launch-editor.ts', 'utils/open': 'src/utils/open.ts', 'utils/serve-static': 'src/utils/serve-static.ts', diff --git a/packages/hub/src/node/__tests__/context.test.ts b/packages/hub/src/node/__tests__/context.test.ts index 29451a4..f8d2c32 100644 --- a/packages/hub/src/node/__tests__/context.test.ts +++ b/packages/hub/src/node/__tests__/context.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { createHostContext, startHttpAndWs } from 'devframe/node' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { describe, expect, it } from 'vitest' import { createHubContext } from '../context' diff --git a/packages/hub/src/node/__tests__/host-docks.test.ts b/packages/hub/src/node/__tests__/host-docks.test.ts index bc2314f..f825e1a 100644 --- a/packages/hub/src/node/__tests__/host-docks.test.ts +++ b/packages/hub/src/node/__tests__/host-docks.test.ts @@ -3,7 +3,7 @@ import { mkdtempSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { describe, expect, it } from 'vitest' import { parseRemoteConnection } from '../../client/remote' import { DevframeDocksHost } from '../host-docks' diff --git a/packages/hub/src/node/host-docks.ts b/packages/hub/src/node/host-docks.ts index 0733206..2a6924c 100644 --- a/packages/hub/src/node/host-docks.ts +++ b/packages/hub/src/node/host-docks.ts @@ -13,7 +13,7 @@ import type { DevframeDocksUserSettings } from '../types/settings' import type { DevframeHubContext } from './context' import { REMOTE_CONNECTION_KEY } from 'devframe/constants' import { createStorage } from 'devframe/node' -import { getInternalContext } from 'devframe/node/internal' +import { getInternalContext } from 'devframe/node/hub-internals' import { createEventEmitter } from 'devframe/utils/events' import { join } from 'pathe' import { DEFAULT_STATE_USER_SETTINGS } from '../constants' diff --git a/packages/hub/src/node/mount-devframe.ts b/packages/hub/src/node/mount-devframe.ts index 2827e3d..9b0333b 100644 --- a/packages/hub/src/node/mount-devframe.ts +++ b/packages/hub/src/node/mount-devframe.ts @@ -1,7 +1,7 @@ import type { DevframeDefinition } from 'devframe/types' import type { DevframeViewIframe } from '../types/docks' import type { DevframeHubContext } from './context' -import { resolveBasePath } from 'devframe/node/internal' +import { resolveBasePath } from 'devframe/node/hub-internals' import { resolve } from 'pathe' export interface MountDevframeOptions { diff --git a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.d.ts similarity index 96% rename from tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts rename to tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.d.ts index 678e10b..f9dded1 100644 --- a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.d.ts @@ -1,5 +1,5 @@ /** - * Generated by tsnapi — public API snapshot of `devframe/node/internal` + * Generated by tsnapi — public API snapshot of `devframe/node/hub-internals` */ // #region Functions export declare function normalizeBasePath(_: string): string; diff --git a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js b/tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.js similarity index 93% rename from tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js rename to tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.js index 73cbb22..e958f15 100644 --- a/tests/__snapshots__/tsnapi/devframe/node/internal.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/node/hub-internals.snapshot.js @@ -1,5 +1,5 @@ /** - * Generated by tsnapi — public API snapshot of `devframe/node/internal` + * Generated by tsnapi — public API snapshot of `devframe/node/hub-internals` */ // #region Other export { getInternalContext } diff --git a/tsconfig.base.json b/tsconfig.base.json index 9ab84d6..1fac0a9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -33,8 +33,8 @@ "devframe/node/auth": [ "./packages/devframe/src/node/auth/index.ts" ], - "devframe/node/internal": [ - "./packages/devframe/src/node/internal/index.ts" + "devframe/node/hub-internals": [ + "./packages/devframe/src/node/hub-internals/index.ts" ], "devframe/node": [ "./packages/devframe/src/node/index.ts"