Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adapters/initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Fetch handlers hand over `Request`s, so the RPC socket needs a binding of its ow
3. **`ws: { sidecar: true }`** — a side-car server on a free port, for hosts whose handlers never see upgrades (Next.js route handlers, Nitro, Rsbuild).
4. **The host's own upgrades** — with none of the above, the socket waits for the host to hand upgrade events over: `devtools.attach(server)` routes a server's `upgrade` events (returning a detach function), and `devtools.handleUpgrade(req, socket, head)` completes a single one from a listener you already own. This is the tier for hosts whose server exists only after the instance does, and it builds the transport lazily — an instance nobody attaches costs nothing.

`ws.url` controls the *advertisement* instead: the browser dials it verbatim. On its own it means an external server owns the transport and its auth (wire the instance's `context` into that server with `startHttpAndWs`); alongside a local binding it overrides only what is advertised — the tunnel pattern, where a relay forwards to the socket bound here.
`ws.url` controls the *advertisement* instead: the browser dials it verbatim. On its own it means an external server owns the transport and its auth (wire the instance's `context` into that server by composing `createContextRpcServer` with a WS transport); alongside a local binding it overrides only what is advertised — the tunnel pattern, where a relay forwards to the socket bound here.

Whichever combination is active, `__connection.json` describes it and the browser client follows. Asking a configured instance to also take over host upgrades reports `DF0055` (a local binding already owns the socket) or `DF0056` (`ws.url` handed it to someone else).

Expand Down
2 changes: 1 addition & 1 deletion docs/errors/DF0007.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ outline: deep

## Cause

`getCurrentRpcSession()` was called outside the RPC dispatch context. Usually indicates the RPC server hasn't been composed with `startHttpAndWs` or the caller is running before the async context is established.
`getCurrentRpcSession()` was called outside the RPC dispatch context. Usually indicates the RPC server hasn't been composed with the context transport binding (`createContextRpcServer`) or the caller is running before the async context is established.

## Fix

Expand Down
13 changes: 6 additions & 7 deletions docs/errors/DF0036.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,16 @@ outline: deep

## Cause

`startHttpAndWs` was configured with an `authorize` gate (either directly, or via a [`DevframeAuthHandler`](../guide/security) passed as `auth`) and the calling session hasn't satisfied it — the call is neither to an `anonymous:`-prefixed method (see `isAnonymousRpcMethod`) nor made by a trusted session.
The RPC server was configured with an `authorize` gate (either directly, or via a [`DevframeAuthHandler`](../guide/security) passed as `auth`) and the calling session hasn't satisfied it — the call is neither to an `anonymous:`-prefixed method (see `isAnonymousRpcMethod`) nor made by a trusted session.

## Example

```ts
import { startHttpAndWs } from 'devframe/node'
import { createInteractiveAuth } from 'devframe/recipes/interactive-auth'
import { createDevServer } from 'devframe/adapters/dev'

const auth = createInteractiveAuth(ctx)

await startHttpAndWs({ context: ctx, port: 9999, auth })
// `auth` defaults to devframe's interactive gate; `initDevframe` / `initHub`
// gate hosted instances the same way through their own `auth` option.
await createDevServer(def)

// A browser that hasn't completed the handshake yet can still reach the
// handshake methods themselves…
Expand All @@ -38,4 +37,4 @@ await client.call('some-plugin:do-something') // ✗ throws DF0036

## Source

- [`packages/devframe/src/node/server.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/server.ts) — `startHttpAndWs`'s resolver throws this when `authorize`/`auth.authorize` rejects a call.
- [`packages/devframe/src/node/rpc-core.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/rpc-core.ts) — `createContextRpcServer`'s resolver throws this when `authorize`/`auth.authorize` rejects a call.
6 changes: 3 additions & 3 deletions docs/errors/DF0052.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ outline: deep

## Cause

`startHttpAndWs` tried to bind the HTTP server it owns to `host:port` and the underlying `listen()` call failed — most commonly `EADDRINUSE` (another process, often a previous devframe instance, is already bound to that port) or `EACCES` (insufficient permissions, typically a privileged port). The WS RPC transport is torn down before this error surfaces, so nothing is leaked.
The instance's side-car / shared-server transport binding tried to bind the HTTP server it owns to `host:port` and the underlying `listen()` call failed — most commonly `EADDRINUSE` (another process, often a previous devframe instance, is already bound to that port) or `EACCES` (insufficient permissions, typically a privileged port). The WS RPC transport is torn down before this error surfaces, so nothing is leaked.

## Example

```ts
// A previous instance is still bound to 4096:
// await startHttpAndWs({ context, host: 'localhost', port: 4096 }) → DF0052
// await createDevServer(def, { host: 'localhost', port: 4096 }) → DF0052
```

## Fix
Expand All @@ -26,4 +26,4 @@ outline: deep

## Source

- [`packages/devframe/src/node/server.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/server.ts) — `startHttpAndWs()` throws this when its owned HTTP server's `listen()` fails.
- [`packages/devframe/src/node/instance-shell.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/instance-shell.ts) — the instance shell's HTTP+WS binding throws this when its owned HTTP server's `listen()` fails.
2 changes: 1 addition & 1 deletion docs/errors/DF0056.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const tunnelled = initDevframe(def, {

## Fix

Drop `ws.url` to have the instance serve the socket, or pair it with `server` / `ws.port` / `ws.sidecar` for the tunnel pattern — a local binding that the advertised relay forwards to. To serve RPC from a server you wire yourself, run `startHttpAndWs({ context, server, path })` against the instance's `context` and keep `ws.url` pointed at it.
Drop `ws.url` to have the instance serve the socket, or pair it with `server` / `ws.port` / `ws.sidecar` for the tunnel pattern — a local binding that the advertised relay forwards to. To serve RPC from a server you wire yourself, compose `createContextRpcServer` (`devframe/internal`) with a WS transport (`devframe/rpc/transports/ws-server`) against the instance's `context` and keep `ws.url` pointed at it.

## Source

Expand Down
2 changes: 1 addition & 1 deletion docs/errors/DF8002.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ initHub({ base: '/__devframes/', devframes: [git] })

// ✓ Good — bring your own context:
const ctx = await createHubContext({ host: myHost, cwd })
await mountDevframe(ctx, git)
await ctx.install(git)
initHub({ base: '/__devframes/', context: ctx })
```

Expand Down
6 changes: 3 additions & 3 deletions docs/errors/DF8105.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ outline: deep

## Cause

`mountDevframe(ctx, def)` was called with a devframe whose `id` already belongs to another devframe mounted on the same hub. Devframes are deduplicated by `id`, and the definition's `duplicationStrategy` is `'warn'` (the default) or `'throw'`.
`ctx.install(def)` was called with a devframe whose `id` already belongs to another devframe mounted on the same hub. Devframes are deduplicated by `id`, and the definition's `duplicationStrategy` is `'warn'` (the default) or `'throw'`.

## Fix

Expand All @@ -21,8 +21,8 @@ Set `duplicationStrategy` on the definition to choose how duplicates are handled
- `'throw'` — surface duplicates as a thrown error.
- `'duplicate'` — let every instance coexist under a disambiguated dock id (`my-tool`, `my-tool-2`, …).

Otherwise, remove the redundant `mountDevframe` call so each devframe is mounted once.
Otherwise, remove the redundant `ctx.install` call (or the duplicate `devframes` entry) so each devframe is mounted once.

## Source

- [`packages/hub/src/node/mount-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/mount-devframe.ts) — `mountDevframe()` emits this when a devframe sharing an already-mounted `id` is mounted and the strategy is not `'duplicate'`.
- [`packages/hub/src/node/install-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/install-devframe.ts) — `ctx.install()` emits this when a devframe sharing an already-mounted `id` is installed and the strategy is not `'duplicate'`.
4 changes: 2 additions & 2 deletions docs/errors/DF8106.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ outline: deep

## Cause

A mounted devframe's SPA loads in an iframe at its own base (e.g. `/__terminals/`) and calls `connectDevframe()`, which fetches `./__connection.json` relative to that base to discover the RPC/WebSocket endpoint. `mountDevframe` serves that file at each base by calling the host's `mountConnectionMeta(base)` alongside `mountStatic`.
A mounted devframe's SPA loads in an iframe at its own base (e.g. `/__terminals/`) and calls `connectDevframe()`, which fetches `./__connection.json` relative to that base to discover the RPC/WebSocket endpoint. `ctx.install` serves that file at each base by calling the host's `mountConnectionMeta(base)` alongside `mountStatic`.

This diagnostic is reported when a devframe with a servable `cli.distDir` is mounted on a `DevframeHost` that does not implement `mountConnectionMeta`. The SPA's `./__connection.json` fetch then falls through to the host's HTML fallback, so the SPA cannot discover the endpoint and its panel stays empty or stuck loading — previously a silent failure.

Expand All @@ -35,4 +35,4 @@ A static-snapshot host that bakes `__connection.json` into its served files can

## Source

- [`packages/hub/src/node/mount-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/mount-devframe.ts) — `mountDevframe()` emits this when a devframe with a servable `distDir` is mounted on a host lacking `mountConnectionMeta`.
- [`packages/hub/src/node/install-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/install-devframe.ts) — `ctx.install()` emits this when a devframe with a servable `distDir` is installed on a host lacking `mountConnectionMeta`.
6 changes: 3 additions & 3 deletions docs/guide/client-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ A script that fails to import is logged and retried on the next dock update.
Build the script as a single self-contained ES module — it loads outside any chunk graph or import map. Attach it when mounting the devframe:

```ts
await mountDevframe(ctx, myDevframe, {
await ctx.install(myDevframe, {
dock: { clientScript: { importFrom: `/@fs/${myAgentBundlePath}` } },
})
```
Expand All @@ -152,14 +152,14 @@ The [a11y inspector](/plugins/a11y)'s in-page agent is the canonical client scri

## Iframe panels

Dock iframes are their own documents, so they connect themselves instead of reading the host page's context: the panel SPA calls `connectDevframe()`, which discovers `./__connection.json` relative to its own base — `mountDevframe` serves the hub's connection meta under every dock base for exactly this. The client script (host page) and the iframe panel then share the server through RPC and shared state, or a same-origin `BroadcastChannel` when the loop must survive static builds.
Dock iframes are their own documents, so they connect themselves instead of reading the host page's context: the panel SPA calls `connectDevframe()`, which discovers `./__connection.json` relative to its own base — `ctx.install` serves the hub's connection meta under every dock base for exactly this. The client script (host page) and the iframe panel then share the server through RPC and shared state, or a same-origin `BroadcastChannel` when the loop must survive static builds.

## Shared-iframe soft navigation

A tool with many internal views — Nuxt DevTools' tabs, say — can surface each view as its own hub dock while they all share **one** live iframe, switching between them with client-side (soft) navigation instead of reloading. One iframe dock is the **anchor**: it owns a `frameId` and opts in with `subTabs`.

```ts
await mountDevframe(ctx, nuxtDevtools, {
await ctx.install(nuxtDevtools, {
dock: { frameId: 'nuxt-devtools', subTabs: { protocol: 'postmessage' } },
})
```
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ A devframe's SPA and RPC client code are byte-identical in both cases — that i

## Bring your own context

Hosts that assemble `createHubContext` + `mountDevframe` themselves (with their own `DevframeHost` serving the frames) pass the finished context instead of a `devframes` list:
Hosts that assemble `createHubContext` + `ctx.install` themselves (with their own `DevframeHost` serving the frames) pass the finished context instead of a `devframes` list:

```ts
const hub = initHub({ base: DEVFRAMES_HUB_BASE, context: ctx })
Expand Down
12 changes: 6 additions & 6 deletions docs/guide/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,20 +119,20 @@ This is what lets a downstream analyzer spawn a `vite build`, show its progress

## 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)`:
`ctx.install(def)` is the framework-neutral primitive that registers any `DevframeDefinition` as a dock and runs its `setup(ctx)`. It's the imperative counterpart to `initHub`'s declarative `devframes` list:

```ts
import { createHubContext, mountDevframe } from '@devframes/hub/node'
import { createHubContext } from '@devframes/hub/node'

const ctx = await createHubContext({ cwd, host, mode: 'dev' })
await mountDevframe(ctx, myDevframe)
await ctx.install(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`.
Framework kits typically wrap this in a plugin shell. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a Vite `Plugin` whose `devtools.setup` calls into `ctx.install`.

### Connecting embedded SPAs

A mounted devframe's SPA loads in an iframe at its base (`/__<id>/`) and calls `connectDevframe()`, which fetches `./__connection.json` relative to that base. `mountDevframe` serves it there by calling the host's `mountConnectionMeta(base)` alongside `mountStatic`, so the SPA discovers the RPC/WS endpoint directly. Implement `mountConnectionMeta` on your `DevframeHost` to serve the same connection meta you expose at the hub's own base:
A mounted devframe's SPA loads in an iframe at its base (`/__<id>/`) and calls `connectDevframe()`, which fetches `./__connection.json` relative to that base. `ctx.install` serves it there by calling the host's `mountConnectionMeta(base)` alongside `mountStatic`, so the SPA discovers the RPC/WS endpoint directly. Implement `mountConnectionMeta` on your `DevframeHost` to serve the same connection meta you expose at the hub's own base:

```ts
const host: DevframeHost = {
Expand Down Expand Up @@ -165,7 +165,7 @@ const defs = await Promise.all(
).then(mods => mods.map(m => m.default))

for (const def of defs)
await mountDevframe(ctx, def)
await ctx.install(def)
```

Each mounted SPA is served at `/__<id>/` and references its assets relatively (`./_next/…`, `./assets/…`). Disable the bundler's trailing-slash redirect so those paths resolve under the mount base:
Expand Down
Loading
Loading