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
81 changes: 13 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,63 +6,21 @@

<h1 align="center">Devframe</h1>

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]

Framework-neutral foundation for building generic DevTools. Describe one devframe — its RPC, its data, its SPA, its CLI shape — and deploy the same definition through any of seven adapters.

Documentation: [https://devfra.me/](https://devfra.me/).

## Install

```sh
pnpm add devframe
```

## Hello, Devframe

```ts
import { defineDevframe, defineRpcFunction } from 'devframe'
import { createCli } from 'devframe/adapters/cli'

const devframe = defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
setup(ctx) {
ctx.rpc.register(defineRpcFunction({
name: 'my-devframe:hello',
type: 'static',
jsonSerializable: true,
handler: () => ({ message: 'hello' }),
}))
},
})

await createCli(devframe).parse()
```

## Adapters

| Adapter | Use case |
|---------|----------|
| `cli` | Standalone CLI tool with `dev` / `build` / `mcp` subcommands. |
| `build` | Generates a static, self-contained SPA snapshot. |
| `vite` | Mounts the devframe into Vite DevTools (or any compatible host) via `@vitejs/devtools-kit`. |
| `embedded` | Overlays inside another devframe's UI. |
| `mcp` | Surfaces the devframe's RPC to coding agents over MCP. |
<p align="center">
<a href="https://npmx.dev/package/devframe"><img src="https://img.shields.io/npm/v/devframe?style=flat&colorA=080f12&colorB=517158" alt="npm version" /></a>
<a href="https://npmx.dev/package/devframe"><img src="https://img.shields.io/npm/dm/devframe?style=flat&colorA=080f12&colorB=517158" alt="npm downloads" /></a>
<a href="https://bundlephobia.com/result?p=devframe"><img src="https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=517158&label=minzip" alt="bundle" /></a>
<a href="https://www.jsdocs.io/package/devframe"><img src="https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=517158" alt="JSDocs" /></a>
<a href="https://github.com/devframes/devframe/blob/main/LICENSE.md"><img src="https://img.shields.io/github/license/devframes/devframe.svg?style=flat&colorA=080f12&colorB=517158" alt="License" /></a>
</p>

## Repo layout
<p align="center">
Framework-neutral foundation for building generic DevTools.
</p>

| Path | Description |
|------|-------------|
| [`packages/devframe`](./packages/devframe) | The published [`devframe`](https://www.npmjs.com/package/devframe) npm package. |
| [`packages/nuxt`](./packages/nuxt) | The [`@devframes/nuxt`](https://www.npmjs.com/package/@devframes/nuxt) Nuxt module adapter. |
| [`docs`](./docs) | VitePress documentation site, deployed at https://devfra.me/. |
| [`examples`](./examples) | End-to-end demos: [`devframe-counter`](./examples/devframe-counter), [`devframe-files-inspector`](./examples/devframe-files-inspector), and [`devframe-streaming-chat`](./examples/devframe-streaming-chat). |
| [`tests`](./tests) | Public-API snapshot tests via [`tsnapi`](https://github.com/posva/tsnapi). |
<p align="center">
<b>Documentation:</b> <a href="https://devfra.me/">https://devfra.me/</a>
</p>

## Sponsors

Expand All @@ -75,16 +33,3 @@ await createCli(devframe).parse()
## License

[MIT](./LICENSE.md) License © [Anthony Fu](https://github.com/antfu)

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/devframe?style=flat&colorA=080f12&colorB=517158
[npm-version-href]: https://npmx.dev/package/devframe
[npm-downloads-src]: https://img.shields.io/npm/dm/devframe?style=flat&colorA=080f12&colorB=517158
[npm-downloads-href]: https://npmx.dev/package/devframe
[bundle-src]: https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=517158&label=minzip
[bundle-href]: https://bundlephobia.com/result?p=devframe
[license-src]: https://img.shields.io/github/license/devframes/devframe.svg?style=flat&colorA=080f12&colorB=517158
[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE.md
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=517158
[jsdocs-href]: https://www.jsdocs.io/package/devframe
64 changes: 52 additions & 12 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { fileURLToPath } from 'node:url'
import { globSync } from 'tinyglobby'
import { defineConfig } from 'vitepress'
import { withMermaid } from 'vitepress-plugin-mermaid'
import pkg from '../../packages/devframe/package.json' with { type: 'json' }

const errorsDir = fileURLToPath(new URL('../errors/', import.meta.url))

const repo = 'https://github.com/devframes/devframe'
const brandColor = '#517158'

function listErrorCodes(prefix: string): string[] {
return globSync(`${prefix}*.md`, { cwd: errorsDir })
.map(f => f.replace(/\.md$/, ''))
Expand All @@ -16,26 +20,53 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] {
return [
{ text: 'Introduction', link: `${prefix}/guide/` },
{ text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` },
{ text: 'Adapters', link: `${prefix}/guide/adapters` },
{ text: 'RPC', link: `${prefix}/guide/rpc` },
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
{ text: 'Utilities', link: `${prefix}/guide/utilities` },
{ text: 'Client', link: `${prefix}/guide/client` },
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
{ text: 'Nuxt Helper', link: `${prefix}/guide/nuxt` },
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
]
}

function adaptersItems(prefix: string): DefaultTheme.NavItemWithLink[] {
return [
{ text: 'Overview', link: `${prefix}/adapters/` },
{ text: 'CLI', link: `${prefix}/adapters/cli` },
{ text: 'Dev', link: `${prefix}/adapters/dev` },
{ text: 'Build', link: `${prefix}/adapters/build` },
{ text: 'Vite', link: `${prefix}/adapters/vite` },
{ text: 'Embedded', link: `${prefix}/adapters/embedded` },
{ text: 'MCP', link: `${prefix}/adapters/mcp` },
]
}

function helpersItems(prefix: string): DefaultTheme.NavItemWithLink[] {
return [
{ text: 'Overview', link: `${prefix}/helpers/` },
{ text: 'Utilities', link: `${prefix}/helpers/utilities` },
{ text: 'Vite Bridge', link: `${prefix}/helpers/vite-bridge` },
{ text: 'Nuxt Module', link: `${prefix}/helpers/nuxt` },
{ text: 'Open Helpers', link: `${prefix}/helpers/open-helpers` },
]
}

export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
return [
{
text: 'Guide',
items: guideItems(prefix),
},
{
text: 'Adapters',
items: adaptersItems(prefix),
},
{
text: 'Helpers',
items: helpersItems(prefix),
},
{
text: 'Error Reference',
link: `${prefix}/errors/`,
Expand All @@ -48,10 +79,19 @@ export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
]
}

export function devframeNav(prefix = ''): DefaultTheme.NavItemWithLink[] {
export function devframeNav(prefix = ''): DefaultTheme.NavItem[] {
return [
...guideItems(prefix),
{ text: 'Error Reference', link: `${prefix}/errors/` },
{ text: 'Guide', items: guideItems(prefix) },
{ text: 'Adapters', items: adaptersItems(prefix) },
{ text: 'Helpers', items: helpersItems(prefix) },
{ text: 'Errors', link: `${prefix}/errors/` },
{
text: `v${pkg.version}`,
items: [
{ text: 'Release Notes', link: `${repo}/releases` },
{ text: 'Contributing', link: `${repo}/blob/main/CONTRIBUTING.md` },
],
},
]
}

Expand All @@ -60,22 +100,22 @@ export default withMermaid(defineConfig({
description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.',
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
['link', { rel: 'apple-touch-icon', href: '/logo.svg' }],
['link', { rel: 'mask-icon', href: '/logo.svg', color: brandColor }],
['meta', { name: 'theme-color', content: brandColor }],
],
themeConfig: {
logo: { light: '/logo.svg', dark: '/logo.svg' },
nav: [
{ text: 'Guide', items: guideItems('') },
{ text: 'Error Reference', link: '/errors/' },
],
nav: devframeNav(),
sidebar: devframeSidebar(),
search: {
provider: 'local',
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/devframes/devframe' },
{ icon: 'github', link: repo },
],
editLink: {
pattern: 'https://github.com/devframes/devframe/edit/main/docs/:path',
pattern: `${repo}/edit/main/docs/:path`,
text: 'Suggest changes to this page',
},
footer: {
Expand Down
41 changes: 41 additions & 0 deletions docs/adapters/build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
outline: deep
---

# Build

Produces a self-contained static deploy of a devframe:

1. Copies the author's SPA dist (`cli.distDir` or `options.distDir`) into `<outDir>`.
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. |

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.
110 changes: 110 additions & 0 deletions docs/adapters/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
outline: deep
---

# CLI

The CLI adapter wraps a `DevframeDefinition` in a `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.

```ts
import { defineDevframe } from 'devframe'
import { createCli } from 'devframe/adapters/cli'

const devframe = defineDevframe({
id: 'my-devframe',
name: 'My Devframe',
cli: { distDir: './client/dist' },
setup(ctx) { /* register docks, RPC, etc. */ },
})

await createCli(devframe).parse()
```

Running the resulting binary:

```sh
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 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).

## Options

`createCli(def, options?)` accepts:

| Option | Default | Description |
|--------|---------|-------------|
| `defaultPort` | `9999` (or `def.cli?.port`) | Port used by the dev command when `--port` isn't provided. |
| `configureCli` | — | `(cli: CAC) => void` — final hook to add commands/flags at the assembly stage, after the definition's `cli.configure` runs. |
| `onReady` | — | `(info: { origin, port, app }) => void \| Promise<void>` — called once the dev server is listening. Use this to print your own startup banner. |

`createCli` returns a `CliHandle`:

```ts
interface CliHandle {
cli: CAC // raw cac instance — mutate before calling parse()
parse: (argv?: string[]) => Promise<void>
}
```

The `cli` property lets the caller add ad-hoc commands and flags right before `parse()` when a `configureCli` callback is inconvenient.

## Definition-level `cli` fields

```ts
defineDevframe({
id: 'my-devframe',
cli: {
command: 'my-devframe', // binary name; default: the id
distDir: './client/dist', // required for dev/build/spa
port: 7777, // preferred port
portRange: [7777, 9000], // passed through to get-port-please
random: false, // passed through to get-port-please
host: '127.0.0.1', // default host; --host overrides
open: true, // auto-open the browser on dev start
auth: false, // skip the trust handshake (single-user localhost)
configure(cli) { // contribute capability flags/commands
cli.option('--config <file>', 'Custom config file')
.option('--no-files', 'Skip file matching')
},
},
setup(ctx, { flags }) {
// `flags` is the parsed cac flag bag — includes both devframe's
// built-ins (`--port`, `--host`, `--open`) and anything declared in
// `cli.configure` or `configureCli`.
},
})
```

`distDir` is the only required field; everything else has sensible defaults. The `configure` hook runs *before* the `configureCli` option passed to `createCli`, so the final tool author always has the last word on flags.

## Headless logging

Devframe leaves startup output to the application. Wire `onReady` to print your own banner:

```ts
await createCli(devframe, {
onReady({ origin }) {
console.log(`ESLint Config Inspector ready at ${origin}`)
},
}).parse()
```

Structured diagnostics (via `logs-sdk`) continue to surface through their normal reporters.

## Use your own CLI framework

To integrate devframe into an existing commander / yargs program — or to expose a different command structure than `createCli`'s `dev` / `build` / `mcp` triplet — drop down to the peer factories. Same `DevframeDefinition`, different shell:

| Building block | Entry | Purpose |
|----------------|-------|---------|
| [`createDevServer(def, opts?)`](./dev) | `devframe/adapters/dev` | h3 + WebSocket RPC + SPA mount |
| [`createBuild(def, opts?)`](./build) | `devframe/adapters/build` | Static deploy |
| [`createMcpServer(def, opts?)`](./mcp) | `devframe/adapters/mcp` | stdio MCP server |
| `parseCliFlags(schema, raw)` | `devframe/adapters/cli` | Validate a flag bag against a `CliFlagsSchema` |

See the [Standalone CLI guide](/guide/standalone-cli#use-your-own-cli-framework) for a worked commander example.
Loading
Loading