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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Positioning

**`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.
**`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/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. It does ship a **headless client runtime** (`createDevframeClientHost()` from `@devframes/hub/client`): booted in the host page, it assembles the shared `DevframeClientContext` (panel, docks, commands, when) and imports each dock entry's client script (`action` / `custom-render` / iframe `clientScript`) into that page - how a plugin like the a11y inspector runs code inside the page being inspected. See `examples/hub-vite/` for a working ~120-line Vite host demonstrating the protocol end to end.

Expand Down Expand Up @@ -79,8 +79,8 @@ These reinforce devframe's positioning as "the container for one devtool integra

- **Single-integration scope.** Devframe describes one tool. If a feature only makes sense when multiple tools share a UI - docking, a unified command palette, cross-tool toasts, terminal aggregation - it belongs in a hub package, not here.
- **Headless by default.** No default startup banners, no opinionated logging to stdout, no default styling. Provide hooks (`onReady`, `cli.configure`, etc.); let the application print its own branding. Structured diagnostics via `nostics` are fine - ad-hoc `console.log`s baked into adapters are not.
- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`) and `/` for *standalone* adapters (`cli`, `spa`, `build`). Authors override via `DevframeDefinition.basePath`. Don't hardcode mount paths in adapter code paths that may run standalone.
- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim - no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically.
- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`) and `/` for *standalone* adapters (`cli`, `build`). Authors override via `DevframeDefinition.basePath`. Don't hardcode mount paths in adapter code paths that may run standalone.
- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` copies SPA output verbatim - no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically.
- **CLI flags compose from both sides.** The `cac` instance backing `createCac` is exposed both to the `DevframeDefinition` (`cli.configure(cli)`) - for capabilities contributed by the tool itself - and to the `createCac` caller - for flags added at the final assembly stage. Parsed flag values are forwarded to `setup(ctx, { flags })`. Never hardcode domain-specific flags into `createCac`.

### Hub example parity
Expand Down
12 changes: 1 addition & 11 deletions docs/adapters/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,22 @@ Produces a self-contained static deploy of a devframe:
2. Runs `setup(ctx)` with `mode: 'build'`.
3. Collects RPC dumps for every `'static'` function and any `'query'` function with `dump.inputs` / `snapshot: true`.
4. Writes `<outDir>/__connection.json` (`{ backend: 'static' }`) and sharded dump files under `<outDir>/__rpc-dump/` — both at the SPA root so the deployed client discovers them via relative paths from `document.baseURI`.
5. When `def.spa` is set, also writes `<outDir>/spa-loader.json` describing how the SPA hydrates its data.

```ts
import { createBuild } from 'devframe/adapters/build'
import devframe from './devframe'

await createBuild(devframe, {
outDir: 'dist-static',
base: '/',
})
```

| Option | Default | Description |
|--------|---------|-------------|
| `outDir` | `dist-static` | Output directory. Cleared on each build. |
| `base` | `/` | Absolute URL base the output is served from. |
| `distDir` | `def.cli?.distDir` | Override the SPA dist directory. |
| `pretty` | `false` | Pretty-print dump JSON (larger on disk). |

The resulting directory hosts on any static web server (`serve`, nginx, GitHub Pages, …). The client auto-detects `static` mode by resolving `./__connection.json` against `document.baseURI` and runs in read-only form.

`createBuild` copies the SPA verbatim, so deploying under a custom URL base just means building the SPA with relative asset paths (`vite.base: './'`) — the client discovers the effective base at runtime.

When `def.spa` is set on the definition, `createBuild` also writes `spa-loader.json` next to `index.html` describing how the deployed SPA sources its data:

- `'none'` — use the baked RPC dump only (read-only static view).
- `'query'` — hydrate from URL search params.
- `'upload'` — accept a drag-and-drop file.

Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers.
4 changes: 2 additions & 2 deletions docs/adapters/cac.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# CLI (cac)

The cac adapter wraps a `DevframeDefinition` in a [`cac`](https://github.com/cacjs/cac)-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, builds SPA bundles, or starts an MCP server.
The cac adapter wraps a `DevframeDefinition` in a [`cac`](https://github.com/cacjs/cac)-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, or starts an MCP server.

`cac` is an optional peer dependency, pulled in only through this adapter — install it alongside `devframe` to opt into `createCac`:

Expand Down Expand Up @@ -68,7 +68,7 @@ defineDevframe({
id: 'my-devframe',
cli: {
command: 'my-devframe', // binary name; default: the id
distDir: './client/dist', // required for dev/build/spa
distDir: './client/dist', // required for dev/build
port: 7777, // preferred port
portRange: [7777, 9000], // passed through to get-port-please
random: false, // passed through to get-port-please
Expand Down
2 changes: 1 addition & 1 deletion docs/adapters/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ A devframe's SPA basePath depends on which adapter is running it:

| Adapter kind | Default basePath | Reason |
|--------------|------------------|--------|
| `cli`, `spa`, `build` (standalone) | `/` | The devframe owns the origin. |
| `cli`, `build` (standalone) | `/` | The devframe owns the origin. |
| `vite`, `embedded` (hosted) | `/__<id>/` | The devframe shares the origin with a host app and namespaces itself. |

Override either side explicitly with `DevframeDefinition.basePath`:
Expand Down
32 changes: 32 additions & 0 deletions docs/errors/DF0058.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
outline: deep
---

# DF0058: Dev Server Disabled By The Definition

## Message

> "`{id}`" declares `capabilities.dev: false` — it does not support a live dev server (its value is a static export only).

## Cause

`createDevServer` runs unconditionally when called directly, but a definition can opt out of the live dev server via `capabilities.dev: false` — for a devframe whose value is inherently static (e.g. a report generator with nothing to serve live), a dev server would only ever produce an empty or misleading surface. This diagnostic covers a caller invoking `createDevServer()` directly.

## Example

```ts
// ✗ Bad — starts a dev server for a devframe that opted out of it
await createDevServer(reportDevframe) // throws DF0058

// ✓ Good — run it anyway when you know what you're doing
await createDevServer(reportDevframe, { force: true })
```

## Fix

- Pass `{ force: true }` to `createDevServer()` to run it anyway.
- Otherwise, drop `capabilities.dev: false` on the definition if a live dev server should be supported after all.

## Source

- [`packages/devframe/src/adapters/dev.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/dev.ts) — `createDevServer()` throws this when `capabilities.dev` is `false` and `force` isn't set.
2 changes: 1 addition & 1 deletion docs/examples/built-with.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ Real-world devframes:

End-to-end examples in this repo, exercising the full adapter surface:

- [**files-inspector**](https://github.com/devframes/devframe/tree/main/examples/files-inspector) — lists files in cwd via RPC; exercises CLI dev/build/spa surfaces.
- [**files-inspector**](https://github.com/devframes/devframe/tree/main/examples/files-inspector) — lists files in cwd via RPC; exercises CLI dev/build surfaces.
- [**streaming-chat**](https://github.com/devframes/devframe/tree/main/examples/streaming-chat) — streams synthetic chat tokens from server to client via `ctx.rpc.streaming`.
- [**next-runtime-snapshot**](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot) — Next.js App Router SPA over RPC, surfacing the host Node runtime (system info, memory, env).
2 changes: 1 addition & 1 deletion docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ End-to-end examples that exercise the full adapter surface, each a runnable app

| Example | UI framework | What it shows |
|---------|--------------|---------------|
| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build / spa surfaces. |
| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build surfaces. |
| [json-render](./json-render) | Vue | A server-authored JSON-render view rendered by `@devframes/json-render-ui`, with live state and an action bridge. |
| [streaming-chat](./streaming-chat) | Preact | Streams synthetic chat tokens server → client, with history kept in shared state. |
| [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. |
Expand Down
2 changes: 1 addition & 1 deletion docs/frameworks/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ my-tool/
└── public/ # Nuxt build output, pointed at by cli.distDir
```

In dev (`nuxt dev`) the bridge is live. In production (`<your-cli> build` then `<your-cli> spa`) the SPA loads the static dump.
In dev (`nuxt dev`) the bridge is live. In production (`<your-cli> build`) the SPA loads the static dump.

## How it works

Expand Down
39 changes: 4 additions & 35 deletions docs/guide/devframe-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,11 @@ export default defineDevframe({
| `homepage` | `string` | **Required.** Project homepage or documentation URL. |
| `description` | `string` | **Required.** One-line summary of what the tool does. |
| `icon` | `string \| { light, dark }` | Optional Iconify name or URL; supports light/dark pairs. |
| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `spa` / `build`), `/.<id>/` for hosted (`vite` / `embedded`). |
| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `build`), `/.<id>/` for hosted (`vite` / `embedded`). |
| `duplicationStrategy` | `'warn' \| 'silent' \| 'throw' \| 'duplicate'` | How a hub reacts when another devframe sharing this `id` is mounted onto the same hub. Defaults to `'warn'`. See [Hub](./hub). Hub adapters consult it; standalone adapters ignore it. |
| `capabilities` | `{ dev?, build?, spa? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. |
| `capabilities` | `{ dev?, build? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. |
| `setup` | `(ctx, info?) => void \| Promise<void>` | **Required.** Server-side entry point. Runs in every runtime. The optional second argument carries runtime metadata — most notably the parsed CLI `flags` when running under `createCac`. |
| `setupBrowser` | `(ctx) => void \| Promise<void>` | Browser-only entry used by the SPA adapter. |
| `cli` | `DevframeCliOptions` | Defaults for the CLI adapter. See [CLI options](#cli-options) below. |
| `spa` | `DevframeSpaOptions` | Defaults for the SPA adapter (`base`, `loader`). |

### Sourcing metadata from `package.json`

Expand Down Expand Up @@ -171,23 +169,6 @@ Each devframe-level host has a dedicated page:
- [Agent-Native](./agent-native) — `ctx.agent`
- [Cross-Plugin Services](./services) — `ctx.services`

## Browser setup

The SPA adapter supports a `setupBrowser(ctx)` hook that runs inside the deployed client bundle. Use it for tools that perform their own in-browser work — parsing a dropped file, calling public APIs from the client, etc.

```ts
defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
setup(ctx) { /* server-side */ },
setupBrowser(ctx) {
// `ctx.rpc` is the write-disabled static client in SPA mode.
},
})
```

Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers.

## CLI options

`cli` configures the CLI adapter's defaults and plugs additional flags/commands into the CAC instance:
Expand All @@ -198,7 +179,7 @@ defineDevframe({
name: 'My Devframe',
cli: {
command: 'my-devframe', // binary name; default: the `id`
distDir: './client/dist', // required for dev / build / spa
distDir: './client/dist', // required for dev / build
port: 9876, // preferred port; default: 9999
portRange: [9876, 10000], // forwarded to get-port-please
random: false, // forwarded to get-port-please
Expand All @@ -220,7 +201,7 @@ defineDevframe({
| Field | Type | Description |
|-------|------|-------------|
| `command` | `string` | Binary name surfaced in `--help`. Default: the definition's `id`. |
| `distDir` | `string` | SPA dist directory. **Required** for `dev` / `build` / `spa`. |
| `distDir` | `string` | SPA dist directory. **Required** for `dev` / `build`. |
| `port` | `number` | Preferred port for the dev server. |
| `portRange` | `[number, number]` | Port scan range, passed through to `get-port-please`. |
| `random` | `boolean` | Prefer a random open port. |
Expand All @@ -231,18 +212,6 @@ defineDevframe({

`setup(ctx, info)` receives `info.flags` populated from both devframe's built-in flags and any you declared via `configure` — saves duplicating flag parsing.

## SPA options

```ts
defineDevframe({
id: 'my-devframe',
spa: {
base: '/',
loader: 'query', // 'query' | 'upload' | 'none'
},
})
```

See [Adapters](/adapters/) for how each adapter consumes these.

## Multiple runtimes, one definition
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por
- **One tool per definition.** A devframe describes a single integration. Deploy it through any adapter; host-level features that only matter when several tools share a UI (palettes, cross-tool toasts, unified terminals) come from whichever host you mount into — Vite DevTools is one example.
- **Headless.** Hook into `onReady`, `cli.configure`, and friends to print your own startup banners and styling — Devframe stays out of the way.
- **App-owned file watching.** Wire your own watcher (chokidar, fs.watch, …) and signal change via `ctx.rpc.sharedState.set(...)` or event-typed RPCs.
- **Context-aware mount paths.** Standalone adapters (`cli`, `spa`, `build`) serve at `/` by default; hosted adapters (`vite`, `embedded`) serve at `/.<id>/`. Override via `DevframeDefinition.basePath`.
- **Context-aware mount paths.** Standalone adapters (`cli`, `build`) serve at `/` by default; hosted adapters (`vite`, `embedded`) serve at `/.<id>/`. Override via `DevframeDefinition.basePath`.
- **SPAs own their base at runtime.** Build with relative asset paths (`vite.base: './'`); `connectDevframe` discovers the effective base from the executing script's location.
- **CLI flags compose.** The `cac` instance is exposed to both the devframe (`cli.configure`) and the caller of `createCac`, so capability flags and app flags merge cleanly.

Expand Down
2 changes: 1 addition & 1 deletion docs/guide/json-render.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ validation.
`@devframes/json-render-ui/spa` ships a prebuilt renderer, so an app serves a
JSON-render UI without authoring or building any client. Wrap the definition
with `createJsonRenderDevframe` — it points `cli.distDir` at the shipped SPA
(`jsonRenderSpaDir`) and sets `spa.loader: 'none'`:
(`jsonRenderSpaDir`):

```ts
import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa'
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/standalone-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# Standalone CLI with Devframe

This recipe walks through building a standalone CLI devframe on top of Devframe — the shape where a user runs `npx my-tool` and gets a local dev server serving a Vue / Nuxt / React SPA backed by type-safe RPC, plus `build` / `spa` / `mcp` subcommands for free.
This recipe walks through building a standalone CLI devframe on top of Devframe — the shape where a user runs `npx my-tool` and gets a local dev server serving a Vue / Nuxt / React SPA backed by type-safe RPC, plus `build` / `mcp` subcommands for free.

It's the pattern used by tools like an ESLint config inspector or a bundler-config viewer: a binary that opens a browser.

Expand Down Expand Up @@ -345,7 +345,7 @@ For typed flag schemas, `parseCliFlags(schema, rawBag)` (from `devframe/adapters

## Why this shape

- **One command, one binary.** `createCac` is a complete CLI — dev, build, spa, mcp all from a single `defineDevframe` value.
- **One command, one binary.** `createCac` is a complete CLI — dev, build, mcp all from a single `defineDevframe` value.
- **Headless.** Your `onReady` callback owns startup output, so your tool's stdout stays yours.
- **Base-agnostic.** Same SPA build works at `/` (dev, standalone static) and at any deployment base.
- **Typed end-to-end.** RPC function definitions flow their types through to the client `rpc.call` site.
Expand Down
2 changes: 1 addition & 1 deletion examples/files-inspector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ pnpm -C examples/files-inspector run test # E2E tests
|------|---------|
| `src/devframe.ts` | The single `DevframeDefinition` consumed by every adapter. |
| `src/client/` | Preact SPA: `index.html`, `main.tsx`, `app.tsx`, `routes/*`, `vite.config.ts`. |
| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `spa`, `mcp`. |
| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `mcp`. |
| `tests/` | E2E tests for CLI dev server and static build. |
2 changes: 1 addition & 1 deletion examples/files-inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"version": "0.9.0-beta.4",
"private": true,
"description": "Devframe demo that lists files in the working directory over RPC across the dev, build, and spa surfaces.",
"description": "Devframe demo that lists files in the working directory over RPC across the dev and build surfaces.",
"homepage": "https://github.com/devframes/devframe/tree/main/examples/files-inspector",
"main": "src/devframe.ts",
"bin": {
Expand Down
1 change: 0 additions & 1 deletion examples/files-inspector/src/devframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default defineDevframe({
// register the instance for `devframe connect` discovery.
mcp: true,
},
spa: { loader: 'none' },
setup(ctx) {
// A scoped context auto-namespaces every registered id with `NAMESPACE:`.
const my = ctx.scope(NAMESPACE)
Expand Down
Loading
Loading