Skip to content
Closed
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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
- Dependencies go through the pnpm catalogs in `pnpm-workspace.yaml` (`cli`, `inlined`, `testing`, `types`) — add to a catalog and reference as `catalog:<name>`, don't pin versions in `package.json`.

### Design system

All five built-in plugins — and every example under `examples/` — share one design system, `@internal/design`, so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is vanilla DOM, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a private, source-only package — never built or published; consumers import its TypeScript/CSS directly (resolved through `alias.ts` for bundlers and the package `exports` for config loaders), so editing it needs no rebuild.

- **Respect the skills.** This design system is built to the `antfu` and `antfu-design` skills (UnoCSS-first, class-based semantic tokens, dual light/dark, anti-slop) — load and follow them when building or changing any UI here. The surfaces deliberately echo the upstream devtools they descend from; reference their UI/UX when in doubt: [`antfu/node-modules-inspector`](https://github.com/antfu/node-modules-inspector), [`antfu/vite-plugin-inspect`](https://github.com/antfu/vite-plugin-inspect), [`eslint/config-inspector`](https://github.com/eslint/config-inspector), and [`vitejs/devtools` → `packages/rolldown`](https://github.com/vitejs/devtools/tree/main/packages/rolldown).
- **One preset to extend.** Each consumer's `uno.config.ts` is just `presets: [presetDevframe()]` (imported from `@internal/design/preset`). The preset bundles `presetWind4` + `presetIcons` (Phosphor) + the directive/variant-group transformers, the semantic token theme, and the shared `df-*` component shortcuts (which it safelists). Don't re-declare presets, palettes, or shortcuts per consumer.
- **One token source.** Import `@internal/design/theme.css` once on the page (after the generated UnoCSS stylesheet so its base layer wins). Token *values* (the `--df-*` custom properties, light + dark via the `.dark` class) live only there — never hardcode a palette. The brand primary is devframe's sage green; flip the OS preference onto `<html class="dark">` from the SPA entry.
- **Shared component vocabulary.** Build UI from the `df-*` classes (`df-btn`, `df-badge`, `df-tab`, `df-navtab`, `df-nav`, `df-nav-brand`, `df-toolbar`, `df-card`, `df-input`, `df-dot`, `df-tag-*`, …) and the semantic token utilities (`bg-primary`, `text-muted-foreground`, `bg-card`, `border-border`, …). Markup differs per framework; the classes resolve identically, which is what keeps the surfaces consistent.
- **Component builders.** Prefer the framework-neutral recipes from `@internal/design/components` (`button`, `iconButton`, `badge`, `tab`, `tabsList`, `navTab`, `nav`, `navBrand`, `toolbar`, `card`, `panel`, `input`, `link`, `dot`, `spinner`, `tag`) over hand-written class strings — they return the canonical `df-*` classes and read the same in React (`className=`), Svelte/Vue (`class=`), vanilla DOM and Solid. Because these classes are assembled at runtime, the preset safelists the `df-*` vocabulary so UnoCSS always emits it; add new component classes to `DF_SAFELIST` when you extend the set.
- **One nav, three buttons, one tab selector — strictly.** Every surface opens with the same top bar: `nav()` (a single row, one fixed height) led by `navBrand()` (a primary-tinted `i-ph-*` icon + the product name). Buttons come in exactly three forms and nothing else: a **text button** (`button({ variant, size })`), an **icon button** (`iconButton()` — bordered), and a **borderless icon button** (`iconButton({ variant: 'ghost' })`). Multi-view tools (inspect, git) switch views with the one shared segmented selector — `tabsList()` wrapping `tab()` buttons that carry `data-state="active"` and a leading `i-ph-*` icon per tab. Don't invent bespoke nav bars, button shapes, or tab styles.
- **Icons** come from the shared Phosphor set (`i-ph-*`, duotone preferred) via `presetIcons` — use them everywhere instead of per-consumer icon libraries or bespoke SVG.
- **A surface keeping its own component CSS** (inspect, a11y) re-bases its color/radius tokens onto the `--df-*` variables rather than hardcoding a palette, so it tracks the shared theme.
- **Plain `.ts`/vanilla views** must opt `.ts` into UnoCSS extraction (`content.pipeline.include` for Vite, or `content.filesystem` globs for the `@unocss/postcss` setup Next uses), since UnoCSS only scans framework files by default.

### Devframe design principles

These reinforce devframe's positioning as "the container for one devtool integration, portable to multiple viewers". When in doubt, err on the side of "devframe provides primitives, the hub provides UX".
Expand Down
18 changes: 16 additions & 2 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export const alias = {
'@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'),
'@internal/design/preset': r('design/src/preset.ts'),
'@internal/design/components': r('design/src/components.ts'),
'@internal/design/tokens': r('design/src/tokens.ts'),
'@internal/design/theme.css': r('design/src/theme.css'),
'@internal/design': r('design/src/index.ts'),
'@devframes/plugin-code-server/client': p('code-server/src/client/index.ts'),
'@devframes/plugin-code-server/node': p('code-server/src/node/index.ts'),
'@devframes/plugin-code-server/constants': p('code-server/src/constants.ts'),
Expand All @@ -60,6 +65,7 @@ export const alias = {
'@devframes/plugin-terminals/cli': p('terminals/src/cli.ts'),
'@devframes/plugin-terminals/vite': p('terminals/src/vite.ts'),
'@devframes/plugin-terminals': p('terminals/src/index.ts'),
'@devframes/plugin-git': p('git/src/index.ts'),
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
'devframe/client': r('devframe/src/client/index.ts'),
'devframe': r('devframe/src'),
Expand All @@ -68,13 +74,21 @@ export const alias = {
'@devframes/plugin-inspect/cli': p('inspect/src/cli.ts'),
'@devframes/plugin-inspect/vite': p('inspect/src/vite.ts'),
'@devframes/plugin-inspect': p('inspect/src/index.ts'),
'@devframes/plugin-a11y/client': p('a11y/src/client/index.ts'),
'@devframes/plugin-a11y/node': p('a11y/src/node/index.ts'),
'@devframes/plugin-a11y/cli': p('a11y/src/cli.ts'),
'@devframes/plugin-a11y/vite': p('a11y/src/vite.ts'),
'@devframes/plugin-a11y': p('a11y/src/index.ts'),
}

// update tsconfig.base.json
// update tsconfig.base.json — CSS aliases exist for Vite resolution only;
// TypeScript resolves `*.css` side-effect imports through ambient shims.
const raw = fs.readFileSync(join(root, 'tsconfig.base.json'), 'utf-8').trim()
const tsconfig = JSON.parse(raw)
tsconfig.compilerOptions.paths = Object.fromEntries(
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
Object.entries(alias)
.filter(([key]) => !key.endsWith('.css'))
.map(([key, value]) => [key, [`./${relative(root, value)}`]]),
)
const newRaw = JSON.stringify(tsconfig, null, 2)
if (newRaw !== raw)
Expand Down
32 changes: 32 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ function helpersItems(prefix: string): DefaultTheme.NavItemWithLink[] {
]
}

function pluginsItems(prefix: string): DefaultTheme.NavItemWithLink[] {
return [
{ text: 'Overview', link: `${prefix}/plugins/` },
{ text: 'Devframe Inspector', link: `${prefix}/plugins/inspect` },
{ text: 'Accessibility Inspector', link: `${prefix}/plugins/a11y` },
{ text: 'Git', link: `${prefix}/plugins/git` },
{ text: 'Terminals', link: `${prefix}/plugins/terminals` },
{ text: 'Code Server', link: `${prefix}/plugins/code-server` },
]
}

function examplesItems(prefix: string): DefaultTheme.NavItemWithLink[] {
return [
{ text: 'Overview', link: `${prefix}/examples/` },
{ text: 'files-inspector', link: `${prefix}/examples/files-inspector` },
{ text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` },
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
{ text: 'minimal-vite-devframe-hub', link: `${prefix}/examples/minimal-vite-devframe-hub` },
{ text: 'minimal-next-devframe-hub', link: `${prefix}/examples/minimal-next-devframe-hub` },
]
}

export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
return [
{
Expand All @@ -71,6 +93,14 @@ export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
text: 'Helpers',
items: helpersItems(prefix),
},
{
text: 'Plugins',
items: pluginsItems(prefix),
},
{
text: 'Examples',
items: examplesItems(prefix),
},
{
text: 'Error Reference',
link: `${prefix}/errors/`,
Expand All @@ -88,6 +118,8 @@ export function devframeNav(prefix = ''): DefaultTheme.NavItem[] {
{ text: 'Guide', items: guideItems(prefix) },
{ text: 'Adapters', items: adaptersItems(prefix) },
{ text: 'Helpers', items: helpersItems(prefix) },
{ text: 'Plugins', items: pluginsItems(prefix) },
{ text: 'Examples', items: examplesItems(prefix) },
{ text: 'Errors', link: `${prefix}/errors/` },
{
text: `v${pkg.version}`,
Expand Down
26 changes: 26 additions & 0 deletions docs/adapters/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,34 @@ process.on('SIGINT', () => handle.close().then(() => process.exit(0)))
| `basePath` | `resolveBasePath(def, 'standalone')` | Mount path override. |
| `app` | fresh h3 app | Pre-configured h3 app to mount onto (custom middleware, auth, extra static assets). |
| `openBrowser` | resolves from `flags.open` / `def.cli?.open` | Explicit on/off override. `false` disables; a string opens that relative path. |
| `ws` | `def.cli?.ws` | How the browser reaches the RPC WebSocket — see below. |
| `onReady` | — | Callback when the WS server is bound. |

## WebSocket endpoint

By default the RPC socket shares the HTTP server's port and binds to the `__devframe_ws` route next to `__connection.json`. The descriptor advertises a *relative* path, so the client connects to its own origin — the link follows the page through a reverse proxy that rewrites the domain, port, or subpath. Configure the three connection scenarios via `def.cli.ws` (or the `ws` call-site option):

```ts
defineDevframe({
// 1. Same server, a custom route (default route is `__devframe_ws`):
cli: { ws: { route: '__sockets' } },

// 2. A dedicated port on the same host:
cli: { ws: { port: 9788 } },

// 3. A remote, fully-qualified endpoint (e.g. a tunnel/relay):
cli: { ws: { url: 'wss://devtools.example.com/relay/__devframe_ws' } },
})
```

| Field | Scenario | Advertised `websocket` |
|-------|----------|------------------------|
| `route` | same server, different route | `{ path: <route> }` (same origin) |
| `port` | different port | `{ port, path: <route> }` (page host) |
| `url` | remote, different origin | the URL string, used verbatim |

Precedence is `url` > `port` > `route`. In the remote case the dev server still hosts the socket locally on `route`; point your tunnel at it.

## Port resolution

`resolveDevServerPort(def, opts?)` resolves a port up-front (to print or log it) before the server starts:
Expand Down
30 changes: 30 additions & 0 deletions docs/examples/files-inspector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
outline: deep
---

# files-inspector

Lists the files in the current working directory and renders them through a **Preact** SPA. A node-modules-inspector-style demo that exercises every devframe surface end to end.

Package: `files-inspector-example` · framework: **Preact + Vite**

## What it shows

- **CLI dev server** — `node bin.mjs` boots an HTTP + WebSocket server backing live RPC.
- **Static build** — `node bin.mjs build` produces a self-contained directory (SPA + baked RPC dump) deployable to any static host.
- **Runtime base discovery** — the client is built with `vite.base: './'` and reads `document.baseURI` at runtime, so the same `dist/client` works under any base path without rebuilding.
- **Two RPC types** — `:list-files` is a `query` baked into the dump; `:get-cwd` is a `static` RPC.

## Run it

```sh
pnpm -C examples/files-inspector run build # build the Preact client
pnpm -C examples/files-inspector run dev # CLI dev server (live RPC)
pnpm -C examples/files-inspector run cli:build # static deploy → dist/static
```

The dev server prints its URL. Serve `dist/static` from any static host — relative asset paths make it portable.

## Source

[`examples/files-inspector`](https://github.com/devframes/devframe/tree/main/examples/files-inspector)
32 changes: 32 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
outline: deep
---

# Examples

End-to-end examples that exercise the full adapter surface, each a runnable app in the repository. Like the [built-in plugins](/plugins/), they are written across different UI frameworks on purpose: the node-side definition stays the same while the browser bundle varies, so the set demonstrates that devframe is framework-agnostic at both the plugin and the host level.

| 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. |
| [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. |
| [minimal-vite-devframe-hub](./minimal-vite-devframe-hub) | Vanilla TypeScript (Vite) | A ~120-line Vite host wiring `@devframes/hub` end to end. |
| [minimal-next-devframe-hub](./minimal-next-devframe-hub) | React (Next.js) | The same hub protocol, hosted from a Next.js route handler. |

## Two kinds of example

The first three are **single-tool devframes** — one `DevframeDefinition` deployed through the [adapters](/adapters/), showing how RPC, streaming, and a chosen SPA framework fit together.

The last two are **hub hosts** built on [`@devframes/hub`](/guide/hub). They are protocol witnesses: each is a small host that exercises every hub subsystem (docks, commands, messages, terminals) so you can read one file and see the whole shape. One is a Vite plugin; the other a Next.js route handler — same hub, different host runtime.

## Run any example

Each example ships its own scripts; from the repository root:

```sh
pnpm install
pnpm --filter <example-name> dev
```

See the individual pages for the package name, the build / static-deploy commands, and what to look for in the running app.
30 changes: 30 additions & 0 deletions docs/examples/minimal-next-devframe-hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
outline: deep
---

# minimal-next-devframe-hub

The same hub protocol as the [Vite host](./minimal-vite-devframe-hub), hosted from a **Next.js** App Router app. It wires [`@devframes/hub`](/guide/hub) by lazily starting a side-car RPC / WebSocket server from a Node route handler — proof that the hub is host-runtime-agnostic.

Package: `minimal-next-devframe-hub` · framework: **React (Next.js)**

## What it proves

- `createHubContext()` boots a hub without any Vite-specific code path.
- A `DevframeHost` implementation plugs the Next host specifics into the hub uniformly.
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
- 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`, which starts the singleton host on demand.

## Run it

```sh
pnpm install
pnpm --filter minimal-next-devframe-hub dev
```

Open the printed URL to see the docks, commands, messages, and terminals lists, plus a button that dispatches a sample command through `hub:commands:execute`.

## Source

[`examples/minimal-next-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-next-devframe-hub)
30 changes: 30 additions & 0 deletions docs/examples/minimal-vite-devframe-hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
outline: deep
---

# minimal-vite-devframe-hub

A protocol-witness host: roughly 120 lines of Vite plugin code that wire [`@devframes/hub`](/guide/hub) into a Vite dev server. The browser UI is plain **vanilla TypeScript**, so nothing distracts from the hub protocol itself. Every framework's hub host follows the same shape.

Package: `minimal-vite-devframe-hub` · framework: **Vanilla TypeScript (Vite)**

## What it proves

- `createHubContext()` boots a hub without any Vite-specific code path.
- A `DevframeHost` implementation plugs framework specifics (storage paths, origin resolution) into the hub uniformly.
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock.
- 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.

## Run it

```sh
pnpm install
pnpm --filter minimal-vite-devframe-hub dev
```

Open the printed URL to see the docks, commands, messages, and terminals lists the hub exposes, plus a button that dispatches a sample command through `hub:commands:execute`.

## Source

[`examples/minimal-vite-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/minimal-vite-devframe-hub)
32 changes: 32 additions & 0 deletions docs/examples/next-runtime-snapshot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
outline: deep
---

# next-runtime-snapshot

A **Next.js App Router** SPA over RPC, surfacing the host Node runtime — system info, memory, and environment variables. It shows that a React + Next.js build is a drop-in replacement for a Preact + Vite SPA: devframe serves the static export, and the client calls into the host Node process through the same type-safe RPC.

Package: `next-runtime-snapshot-example` · framework: **React (Next.js)**

## What it shows

- `…:system` — a `static` RPC. Runs once at build time when baked into a static dump, otherwise resolved live over WebSocket. Returns Node version, platform / arch, pid, cwd, and start time.
- `…:memory` — a `query` RPC the UI re-invokes from a refresh button.
- `…:env` — a `query` with valibot-validated args, listing environment variables matching a regex and redacting keys that look secret.
- Next.js App Router with `'use client'` components calling `connectDevframe()` once, then sharing the scoped client through React context.

The Next.js config carries three non-defaults that each map to a devframe design principle: `output: 'export'` (devframe owns the server), `assetPrefix: '.'` (relative assets so the same build works at any base), and `trailingSlash: true` (composes with devframe's directory-with-index static resolution).

## Run it

```sh
pnpm -C examples/next-runtime-snapshot run build # next build → static export
pnpm -C examples/next-runtime-snapshot run dev # devframe CLI dev server
pnpm -C examples/next-runtime-snapshot run cli:build # static deploy → dist/static
```

The three cards populate from RPC; the static deploy still works because the `static` and `query` RPCs that opted into the dump are baked at build time.

## Source

[`examples/next-runtime-snapshot`](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot)
Loading