Skip to content

Commit 4872040

Browse files
committed
feat: extract devframe into standalone monorepo
Imports the devframe code from vite-devtools and converts the single-package boilerplate into a multi-package monorepo (packages, examples, docs, tests, skills). Sets up pnpm catalogs, Turborepo pipeline, ESLint config, tsconfig path aliases via alias.ts, and the VitePress docs site. The files-inspector example has its Vite DevTools kit integration removed (kit package lives outside this repo).
1 parent 7beb69e commit 4872040

284 files changed

Lines changed: 23602 additions & 614 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
.cache
22
.DS_Store
3+
.eslintcache
34
.idea
45
*.log
56
*.tgz
7+
*.tsbuildinfo
68
coverage
79
dist
810
lib-cov
911
logs
1012
node_modules
1113
temp
14+
.turbo
15+
.vitepress/cache
16+
.vitepress/dist
17+
packages/devframe/skills

README.md

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,63 @@
1-
# devframe
1+
# Devframe
22

33
[![npm version][npm-version-src]][npm-version-href]
44
[![npm downloads][npm-downloads-src]][npm-downloads-href]
55
[![bundle][bundle-src]][bundle-href]
66
[![JSDocs][jsdocs-src]][jsdocs-href]
77
[![License][license-src]][license-href]
88

9-
_description_
9+
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.
10+
11+
Documentation: [https://devfra.me/](https://devfra.me/).
12+
13+
## Install
14+
15+
```sh
16+
pnpm add devframe
17+
```
18+
19+
## Hello, Devframe
20+
21+
```ts
22+
import { defineDevframe, defineRpcFunction } from 'devframe'
23+
import { createCli } from 'devframe/adapters/cli'
24+
25+
const devframe = defineDevframe({
26+
id: 'my-devframe',
27+
name: 'My Devframe',
28+
setup(ctx) {
29+
ctx.rpc.register(defineRpcFunction({
30+
name: 'my-devframe:hello',
31+
type: 'static',
32+
jsonSerializable: true,
33+
handler: () => ({ message: 'hello' }),
34+
}))
35+
},
36+
})
37+
38+
await createCli(devframe).parse()
39+
```
40+
41+
## Adapters
42+
43+
| Adapter | Use case |
44+
|---------|----------|
45+
| `cli` | Standalone CLI tool with `dev` / `build` / `mcp` subcommands. |
46+
| `build` | Generates a static, self-contained SPA snapshot. |
47+
| `vite` | Runs as a Vite plugin alongside the host app's dev server. |
48+
| `kit` | Mounts into a DevTools Kit aggregator (e.g. `@vitejs/devtools-kit`). |
49+
| `embedded` | Overlays inside another devframe's UI. |
50+
| `mcp` | Surfaces the devframe's RPC to coding agents over MCP. |
51+
52+
## Repo layout
53+
54+
| Path | Description |
55+
|------|-------------|
56+
| [`packages/devframe`](./packages/devframe) | The published [`devframe`](https://www.npmjs.com/package/devframe) npm package. |
57+
| [`packages/nuxt`](./packages/nuxt) | The [`@devframes/nuxt`](https://www.npmjs.com/package/@devframes/nuxt) Nuxt module adapter. |
58+
| [`docs`](./docs) | VitePress documentation site, deployed at https://devfra.me/. |
59+
| [`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). |
60+
| [`tests`](./tests) | Public-API snapshot tests via [`tsnapi`](https://github.com/posva/tsnapi). |
1061

1162
## Sponsors
1263

@@ -18,7 +69,7 @@ _description_
1869

1970
## License
2071

21-
[MIT](./LICENSE) License © [Anthony Fu](https://github.com/antfu)
72+
[MIT](./LICENSE.md) License © [Anthony Fu](https://github.com/antfu)
2273

2374
<!-- Badges -->
2475

@@ -29,6 +80,6 @@ _description_
2980
[bundle-src]: https://img.shields.io/bundlephobia/minzip/devframe?style=flat&colorA=080f12&colorB=1fa669&label=minzip
3081
[bundle-href]: https://bundlephobia.com/result?p=devframe
3182
[license-src]: https://img.shields.io/github/license/devframes/devframe.svg?style=flat&colorA=080f12&colorB=1fa669
32-
[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE
83+
[license-href]: https://github.com/devframes/devframe/blob/main/LICENSE.md
3384
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
3485
[jsdocs-href]: https://www.jsdocs.io/package/devframe

alias.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import fs from 'node:fs'
2+
import { fileURLToPath } from 'node:url'
3+
import { join, relative } from 'pathe'
4+
5+
const root = fileURLToPath(new URL('.', import.meta.url))
6+
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))
7+
8+
export const alias = {
9+
'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'),
10+
'devframe/rpc/transports/ws-client': r('devframe/src/rpc/transports/ws-client.ts'),
11+
'devframe/rpc/client': r('devframe/src/rpc/client.ts'),
12+
'devframe/rpc/server': r('devframe/src/rpc/server.ts'),
13+
'devframe/rpc': r('devframe/src/rpc'),
14+
'devframe/types': r('devframe/src/types/index.ts'),
15+
'devframe/node/auth': r('devframe/src/node/auth/index.ts'),
16+
'devframe/node/internal': r('devframe/src/node/internal/index.ts'),
17+
'devframe/node': r('devframe/src/node/index.ts'),
18+
'devframe/constants': r('devframe/src/constants.ts'),
19+
'devframe/utils/colors': r('devframe/src/utils/colors.ts'),
20+
'devframe/utils/events': r('devframe/src/utils/events.ts'),
21+
'devframe/utils/hash': r('devframe/src/utils/hash.ts'),
22+
'devframe/utils/human-id': r('devframe/src/utils/human-id.ts'),
23+
'devframe/utils/launch-editor': r('devframe/src/utils/launch-editor.ts'),
24+
'devframe/utils/nanoid': r('devframe/src/utils/nanoid.ts'),
25+
'devframe/utils/open': r('devframe/src/utils/open.ts'),
26+
'devframe/utils/promise': r('devframe/src/utils/promise.ts'),
27+
'devframe/utils/serve-static': r('devframe/src/utils/serve-static.ts'),
28+
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
29+
'devframe/utils/streaming-channel': r('devframe/src/utils/streaming-channel.ts'),
30+
'devframe/utils/structured-clone': r('devframe/src/utils/structured-clone.ts'),
31+
'devframe/utils/when': r('devframe/src/utils/when.ts'),
32+
'devframe/adapters/cli': r('devframe/src/adapters/cli.ts'),
33+
'devframe/adapters/dev': r('devframe/src/adapters/dev.ts'),
34+
'devframe/adapters/build': r('devframe/src/adapters/build.ts'),
35+
'devframe/adapters/vite': r('devframe/src/adapters/vite.ts'),
36+
'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'),
37+
'devframe/adapters/mcp': r('devframe/src/adapters/mcp.ts'),
38+
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
39+
'@devframes/nuxt': r('nuxt/src/index.ts'),
40+
'devframe/recipes/open-helpers': r('devframe/src/recipes/open-helpers.ts'),
41+
'devframe/client': r('devframe/src/client/index.ts'),
42+
'devframe': r('devframe/src'),
43+
}
44+
45+
// update tsconfig.base.json
46+
const raw = fs.readFileSync(join(root, 'tsconfig.base.json'), 'utf-8').trim()
47+
const tsconfig = JSON.parse(raw)
48+
tsconfig.compilerOptions.paths = Object.fromEntries(
49+
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
50+
)
51+
const newRaw = JSON.stringify(tsconfig, null, 2)
52+
if (newRaw !== raw)
53+
fs.writeFileSync(join(root, 'tsconfig.base.json'), `${newRaw}\n`, 'utf-8')

docs/.vitepress/config.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { DefaultTheme } from 'vitepress'
2+
import { fileURLToPath } from 'node:url'
3+
import { globSync } from 'tinyglobby'
4+
import { defineConfig } from 'vitepress'
5+
import { withMermaid } from 'vitepress-plugin-mermaid'
6+
7+
const errorsDir = fileURLToPath(new URL('../errors/', import.meta.url))
8+
9+
function listErrorCodes(prefix: string): string[] {
10+
return globSync(`${prefix}*.md`, { cwd: errorsDir })
11+
.map(f => f.replace(/\.md$/, ''))
12+
.sort()
13+
}
14+
15+
function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] {
16+
return [
17+
{ text: 'Introduction', link: `${prefix}/guide/` },
18+
{ text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` },
19+
{ text: 'Adapters', link: `${prefix}/guide/adapters` },
20+
{ text: 'RPC', link: `${prefix}/guide/rpc` },
21+
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
22+
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
23+
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
24+
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
25+
{ text: 'Utilities', link: `${prefix}/guide/utilities` },
26+
{ text: 'Client', link: `${prefix}/guide/client` },
27+
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
28+
{ text: 'Nuxt Helper', link: `${prefix}/guide/nuxt` },
29+
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
30+
]
31+
}
32+
33+
export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
34+
return [
35+
{
36+
text: 'Guide',
37+
items: guideItems(prefix),
38+
},
39+
{
40+
text: 'Error Reference',
41+
link: `${prefix}/errors/`,
42+
collapsed: true,
43+
items: listErrorCodes('DF').map(code => ({
44+
text: code,
45+
link: `${prefix}/errors/${code}`,
46+
})),
47+
},
48+
]
49+
}
50+
51+
export function devframeNav(prefix = ''): DefaultTheme.NavItemWithLink[] {
52+
return [
53+
...guideItems(prefix),
54+
{ text: 'Error Reference', link: `${prefix}/errors/` },
55+
]
56+
}
57+
58+
export default withMermaid(defineConfig({
59+
title: 'Devframe',
60+
description: 'Framework-neutral foundation for building generic DevTools — RPC layer, hosts, and adapters.',
61+
themeConfig: {
62+
nav: [
63+
{ text: 'Guide', items: guideItems('') },
64+
{ text: 'Error Reference', link: '/errors/' },
65+
],
66+
sidebar: devframeSidebar(),
67+
search: {
68+
provider: 'local',
69+
},
70+
socialLinks: [
71+
{ icon: 'github', link: 'https://github.com/devframes/devframe' },
72+
],
73+
editLink: {
74+
pattern: 'https://github.com/devframes/devframe/edit/main/docs/:path',
75+
text: 'Suggest changes to this page',
76+
},
77+
footer: {
78+
message: 'Released under the MIT License.',
79+
copyright: 'Copyright © 2025-present Anthony Fu & Contributors',
80+
},
81+
lastUpdated: {
82+
text: 'Last updated',
83+
},
84+
},
85+
mermaid: {
86+
theme: 'base',
87+
flowchart: {
88+
curve: 'basis',
89+
padding: 20,
90+
nodeSpacing: 50,
91+
rankSpacing: 60,
92+
useMaxWidth: true,
93+
},
94+
sequence: {
95+
actorMargin: 80,
96+
boxMargin: 10,
97+
boxTextMargin: 5,
98+
noteMargin: 10,
99+
messageMargin: 40,
100+
useMaxWidth: true,
101+
},
102+
},
103+
}))

docs/errors/DF0006.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0006: RPC Function Not Registered
6+
7+
## Message
8+
9+
> RPC function "`{name}`" is not registered
10+
11+
## Cause
12+
13+
`RpcFunctionsHost.invokeLocal()` was called with a method name that has not been registered on this host.
14+
15+
## Fix
16+
17+
Register the function with `ctx.rpc.register(defineRpcFunction({ name }))` before invoking it.
18+
19+
## Source
20+
21+
- [`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.

docs/errors/DF0007.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0007: AsyncLocalStorage Not Set
6+
7+
## Message
8+
9+
> AsyncLocalStorage is not set, it likely to be an internal bug of the DevTools foundation
10+
11+
## Cause
12+
13+
`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.
14+
15+
## Fix
16+
17+
Only call `getCurrentRpcSession()` from RPC handlers executed by the server. Report as a bug if you hit this inside a handler.
18+
19+
## Source
20+
21+
- [`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.

docs/errors/DF0008.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0008: View distDir Not Found
6+
7+
## Message
8+
9+
> distDir `{distDir}` does not exist
10+
11+
## Cause
12+
13+
`DevToolsViewHost.hostStatic()` was asked to mount a directory that doesn't exist on disk.
14+
15+
## Fix
16+
17+
Verify the `distDir` path resolves correctly (run your SPA build first, and check the resolved absolute path).
18+
19+
## Source
20+
21+
- [`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.

docs/errors/DF0012.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0012: Storage Parse Failed
6+
7+
## Message
8+
9+
> Failed to parse storage file: `{filepath}`, falling back to defaults.
10+
11+
## Cause
12+
13+
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.
14+
15+
## Fix
16+
17+
Delete the file to reset to defaults, or investigate how it became malformed.
18+
19+
## Source
20+
21+
- [`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`.

docs/errors/DF0013.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0013: Shared State Not Found
6+
7+
## Message
8+
9+
> Shared state of "`{key}`" is not found, please provide an initial value for the first time
10+
11+
## Cause
12+
13+
`RpcSharedStateHost.get()` was called for a key that has no stored state yet, and no `initialValue` was passed.
14+
15+
## Fix
16+
17+
Pass `initialValue` on the first call: `ctx.rpc.sharedState.get(key, { initialValue: ... })`.
18+
19+
## Source
20+
21+
- [`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.

0 commit comments

Comments
 (0)