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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ on:
pull_request:
branches: [main]

permissions: {}
permissions:
contents: read

jobs:
unit-test:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
.cache
.DS_Store
.eslintcache
.idea
*.log
*.tgz
*.tsbuildinfo
coverage
dist
lib-cov
logs
node_modules
temp
.turbo
.vitepress/cache
.vitepress/dist
packages/devframe/skills
59 changes: 55 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,63 @@
# devframe
# Devframe

[![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]

_description_
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` | Runs as a Vite plugin alongside the host app's dev server. |
| `kit` | Mounts into a DevTools Kit aggregator (e.g. `@vitejs/devtools-kit`). |
| `embedded` | Overlays inside another devframe's UI. |
| `mcp` | Surfaces the devframe's RPC to coding agents over MCP. |

## Repo layout

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

## Sponsors

Expand All @@ -18,7 +69,7 @@ _description_

## License

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

<!-- Badges -->

Expand All @@ -29,6 +80,6 @@ _description_
[bundle-src]: https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=1fa669&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=1fa669
[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE
[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=1fa669
[jsdocs-href]: https://www.jsdocs.io/package/devframe
53 changes: 53 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
import { join, relative } from 'pathe'

const root = fileURLToPath(new URL('.', import.meta.url))
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))

export const alias = {
'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'),
'devframe/rpc/transports/ws-client': r('devframe/src/rpc/transports/ws-client.ts'),
'devframe/rpc/client': r('devframe/src/rpc/client.ts'),
'devframe/rpc/server': r('devframe/src/rpc/server.ts'),
'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': r('devframe/src/node/index.ts'),
'devframe/constants': r('devframe/src/constants.ts'),
'devframe/utils/colors': r('devframe/src/utils/colors.ts'),
'devframe/utils/events': r('devframe/src/utils/events.ts'),
'devframe/utils/hash': r('devframe/src/utils/hash.ts'),
'devframe/utils/human-id': r('devframe/src/utils/human-id.ts'),
'devframe/utils/launch-editor': r('devframe/src/utils/launch-editor.ts'),
'devframe/utils/nanoid': r('devframe/src/utils/nanoid.ts'),
'devframe/utils/open': r('devframe/src/utils/open.ts'),
'devframe/utils/promise': r('devframe/src/utils/promise.ts'),
'devframe/utils/serve-static': r('devframe/src/utils/serve-static.ts'),
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
'devframe/utils/streaming-channel': r('devframe/src/utils/streaming-channel.ts'),
'devframe/utils/structured-clone': r('devframe/src/utils/structured-clone.ts'),
'devframe/utils/when': r('devframe/src/utils/when.ts'),
'devframe/adapters/cli': r('devframe/src/adapters/cli.ts'),
'devframe/adapters/dev': r('devframe/src/adapters/dev.ts'),
'devframe/adapters/build': r('devframe/src/adapters/build.ts'),
'devframe/adapters/vite': r('devframe/src/adapters/vite.ts'),
'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'),
'devframe/adapters/mcp': r('devframe/src/adapters/mcp.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'),
'devframe/client': r('devframe/src/client/index.ts'),
'devframe': r('devframe/src'),
}

// update tsconfig.base.json
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)}`]]),
)
const newRaw = JSON.stringify(tsconfig, null, 2)
if (newRaw !== raw)
fs.writeFileSync(join(root, 'tsconfig.base.json'), `${newRaw}\n`, 'utf-8')
103 changes: 103 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { DefaultTheme } from 'vitepress'
import { fileURLToPath } from 'node:url'
import { globSync } from 'tinyglobby'
import { defineConfig } from 'vitepress'
import { withMermaid } from 'vitepress-plugin-mermaid'

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

function listErrorCodes(prefix: string): string[] {
return globSync(`${prefix}*.md`, { cwd: errorsDir })
.map(f => f.replace(/\.md$/, ''))
.sort()
}

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` },
]
}

export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
return [
{
text: 'Guide',
items: guideItems(prefix),
},
{
text: 'Error Reference',
link: `${prefix}/errors/`,
collapsed: true,
items: listErrorCodes('DF').map(code => ({
text: code,
link: `${prefix}/errors/${code}`,
})),
},
]
}

export function devframeNav(prefix = ''): DefaultTheme.NavItemWithLink[] {
return [
...guideItems(prefix),
{ text: 'Error Reference', link: `${prefix}/errors/` },
]
}

export default withMermaid(defineConfig({
title: 'Devframe',
description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.',
themeConfig: {
nav: [
{ text: 'Guide', items: guideItems('') },
{ text: 'Error Reference', link: '/errors/' },
],
sidebar: devframeSidebar(),
search: {
provider: 'local',
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/devframes/devframe' },
],
editLink: {
pattern: 'https://github.com/devframes/devframe/edit/main/docs/:path',
text: 'Suggest changes to this page',
},
footer: {
message: 'Released under the MIT License.',
copyright: 'Copyright © 2025-present Anthony Fu & Contributors',
},
lastUpdated: {
text: 'Last updated',
},
},
mermaid: {
theme: 'base',
flowchart: {
curve: 'basis',
padding: 20,
nodeSpacing: 50,
rankSpacing: 60,
useMaxWidth: true,
},
sequence: {
actorMargin: 80,
boxMargin: 10,
boxTextMargin: 5,
noteMargin: 10,
messageMargin: 40,
useMaxWidth: true,
},
},
}))
21 changes: 21 additions & 0 deletions docs/errors/DF0006.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
outline: deep
---

# DF0006: RPC Function Not Registered

## Message

> RPC function "`{name}`" is not registered

## Cause

`RpcFunctionsHost.invokeLocal()` was called with a method name that has not been registered on this host.

## Fix

Register the function with `ctx.rpc.register(defineRpcFunction({ name }))` before invoking it.

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

# DF0007: AsyncLocalStorage Not Set

## Message

> AsyncLocalStorage is not set, it likely to be an internal bug of the DevTools foundation

## 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.

## Fix

Only call `getCurrentRpcSession()` from RPC handlers executed by the server. Report as a bug if you hit this inside a handler.

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

# DF0008: View distDir Not Found

## Message

> distDir `{distDir}` does not exist

## Cause

`DevToolsViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk.

## Fix

Verify the `distDir` path resolves correctly (run your SPA build first, and check the resolved absolute path).

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

# DF0012: Storage Parse Failed

## Message

> Failed to parse storage file: `{filepath}`, falling back to defaults.

## Cause

The persisted storage file (e.g. `auth.json`) could not be parsed as JSON. Storage falls back to the initial value so the devtool can continue.

## Fix

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

# DF0013: Shared State Not Found

## Message

> Shared state of "`{key}`" is not found, please provide an initial value for the first time

## Cause

`RpcSharedStateHost.get()` was called for a key that has no stored state yet, and no `initialValue` was passed.

## Fix

Pass `initialValue` on the first call: `ctx.rpc.sharedState.get(key, { initialValue: ... })`.

## 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.
Loading
Loading