Skip to content

Commit 3bfcd2a

Browse files
authored
feat(vite,nuxt,next): dev-spa vs hub scoped subpaths + extract @devframes/vite (#202)
1 parent 570570f commit 3bfcd2a

123 files changed

Lines changed: 2615 additions & 940 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.

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
4444
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
4545
- 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`.
4646

47+
### Framework adapter packages: two scopes, one shape
48+
49+
The framework adapter packages - `@devframes/vite`, `@devframes/nuxt`, `@devframes/next` - each split their surface into **two clearly-scoped subpaths**, because a consumer is always doing one of two distinct jobs. Keep all three parallel:
50+
51+
- **`.../dev-spa`** - **build & dev-serve a single devframe's SPA** with that tool (the "I'm authoring one devframe" scope). Vite: the `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` plugins. Next: `withDevframe` + `createDevframeNextHandler`, with its React client at `.../dev-spa/client`. Nuxt: the Nuxt module (registered as `modules: ['@devframes/nuxt/dev-spa']`).
52+
- **`.../hub`** - **mount a whole `@devframes/hub` (many integrations) inside that tool** (the "I'm standing up devtools" scope). Wraps `initHub`, defaults the UI slot to `@devframes/hub-ui`'s `createUi()` (overridable via `ui`, or `ui: false` for headless), and ships a browser client helper at `.../hub/client` (a thin, lifecycle-managing wrapper over `@devframes/hub/client`'s `createDevframeClientHost`). `@devframes/hub` and `@devframes/hub-ui` are **optional peers** of these packages; `hub-ui` is loaded lazily (a bundler-ignored dynamic `import()` in the Next hub) so it stays optional and its `import.meta.url` asset lookups resolve at request time.
53+
- **The bare root (`.`) throws** a helpful error pointing at the two subpaths - never put real code on it.
54+
- **Vite and Nuxt already have native hub viewers** (`@vitejs/devtools-kit`, `@nuxt/devtools`), so `@devframes/vite/hub` and `@devframes/nuxt/hub` still work but emit a one-time `console.warn` recommending those (silence with `{ quiet: true }`). `@devframes/next/hub` has no native counterpart, so it warns nothing.
55+
- The **full hub examples** (`examples/hub-vite`, `examples/hub-next`) consume `.../hub` for the server but keep hand-rolling their own client UI against `@devframes/hub/client` with `ui: false` - that hand-rolled client is the whole point of those reference hosts. The **minimal** ones (`examples/hub-*-minimal`) consume `.../hub` with the default `@devframes/hub-ui` and inject its `embedded.js`, needing no client code.
56+
4757
### Design system
4858

4959
All five built-in plugins - and every example under `examples/` - share one design system, [`@antfu/design`](https://github.com/antfu/design), so they look and feel like one product across frameworks (Git is React/Next, terminals is Svelte, code-server is Vue, inspect is Vue, a11y is Solid, the examples are Preact/Next/vanilla). It's a dev dependency consumed at build time: its UnoCSS preset and shipped styles drive every surface, and its Vue components are the canonical reference every framework matches. There is no shared internal design package - each app wires the preset itself and owns its own component ports.

alias.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export const alias = {
3939
'devframe/adapters/cac': r('devframe/src/adapters/cac.ts'),
4040
'devframe/adapters/dev': r('devframe/src/adapters/dev.ts'),
4141
'devframe/adapters/build': r('devframe/src/adapters/build.ts'),
42-
'devframe/helpers/vite': r('devframe/src/helpers/vite.ts'),
4342
'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'),
4443
'devframe/initiate': r('devframe/src/adapters/initiate.ts'),
4544
'devframe/adapters/mcp': r('devframe/src/adapters/mcp/index.ts'),
@@ -51,9 +50,19 @@ export const alias = {
5150
'@devframes/hub': r('hub/src/index.ts'),
5251
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
5352
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
53+
'@devframes/nuxt/dev-spa': r('nuxt/src/dev-spa.ts'),
54+
'@devframes/nuxt/hub/client': r('nuxt/src/hub-client.ts'),
55+
'@devframes/nuxt/hub': r('nuxt/src/hub.ts'),
5456
'@devframes/nuxt': r('nuxt/src/index.ts'),
55-
'@devframes/next/client': r('next/src/client.tsx'),
57+
'@devframes/next/dev-spa/client': r('next/src/client.tsx'),
58+
'@devframes/next/dev-spa': r('next/src/dev-spa.ts'),
59+
'@devframes/next/hub/client': r('next/src/hub-client.tsx'),
60+
'@devframes/next/hub': r('next/src/hub.ts'),
5661
'@devframes/next': r('next/src/index.ts'),
62+
'@devframes/vite/dev-spa': r('vite/src/dev-spa.ts'),
63+
'@devframes/vite/hub/client': r('vite/src/hub-client.ts'),
64+
'@devframes/vite/hub': r('vite/src/hub.ts'),
65+
'@devframes/vite': r('vite/src/index.ts'),
5766
'@devframes/json-render/core': r('json-render/src/core.ts'),
5867
'@devframes/json-render/hub': r('json-render/src/hub.ts'),
5968
'@devframes/json-render/node': r('json-render/src/node/index.ts'),

docs/.vitepress/config.ts

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,60 @@ function listErrorCodes(prefix: string): string[] {
1616
.sort()
1717
}
1818

19-
function guideItems(prefix: string) {
19+
function guideGroups(prefix: string) {
2020
return [
21-
{ text: 'Introduction', link: `${prefix}/guide/` },
22-
{ text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` },
23-
{ text: 'Scoped Context', link: `${prefix}/guide/scoped-context` },
24-
{ text: 'Cross-Plugin Services', link: `${prefix}/guide/services` },
25-
{ text: 'RPC', link: `${prefix}/guide/rpc` },
26-
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
27-
{ text: 'JSON-Render', link: `${prefix}/guide/json-render` },
28-
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
29-
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
30-
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
31-
{ text: 'Client', link: `${prefix}/guide/client` },
32-
{ text: 'Transports', link: `${prefix}/guide/transports` },
33-
{ text: 'Security', link: `${prefix}/guide/security` },
34-
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
35-
{ text: 'Hub', link: `${prefix}/guide/hub` },
36-
{ text: 'Serve a Hub Anywhere', link: `${prefix}/guide/hub-initiate` },
37-
{ text: 'Deep Linking', link: `${prefix}/guide/deep-linking` },
38-
{ text: 'Client Scripts & Context', link: `${prefix}/guide/client-context` },
39-
{ text: 'Build Your Own Hub UI', link: `${prefix}/guide/build-your-own-hub-ui` },
40-
{ text: 'Build Your Own JSON-Render Frontend', link: `${prefix}/guide/build-your-own-json-render-frontend` },
41-
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
42-
] satisfies DefaultTheme.NavItemWithLink[]
21+
{
22+
text: 'Fundamentals',
23+
items: [
24+
{ text: 'Introduction', link: `${prefix}/guide/` },
25+
{ text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` },
26+
{ text: 'Scoped Context', link: `${prefix}/guide/scoped-context` },
27+
{ text: 'Cross-Plugin Services', link: `${prefix}/guide/services` },
28+
{ text: 'RPC', link: `${prefix}/guide/rpc` },
29+
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
30+
{ text: 'Streaming', link: `${prefix}/guide/streaming` },
31+
{ text: 'When Clauses', link: `${prefix}/guide/when-clauses` },
32+
{ text: 'Structured Diagnostics', link: `${prefix}/guide/diagnostics` },
33+
],
34+
},
35+
{
36+
text: 'Client & Security',
37+
items: [
38+
{ text: 'Client', link: `${prefix}/guide/client` },
39+
{ text: 'Transports', link: `${prefix}/guide/transports` },
40+
{ text: 'Security', link: `${prefix}/guide/security` },
41+
{ text: 'Deep Linking', link: `${prefix}/guide/deep-linking` },
42+
],
43+
},
44+
{
45+
text: 'JSON-Render',
46+
items: [
47+
{ text: 'JSON-Render', link: `${prefix}/guide/json-render` },
48+
{ text: 'Build Your Own JSON-Render Frontend', link: `${prefix}/guide/build-your-own-json-render-frontend` },
49+
],
50+
},
51+
{
52+
text: 'Hub',
53+
items: [
54+
{ text: 'Hub', link: `${prefix}/guide/hub` },
55+
{ text: 'Serve a Hub Anywhere', link: `${prefix}/guide/hub-initiate` },
56+
{ text: 'Client Scripts & Context', link: `${prefix}/guide/client-context` },
57+
{ text: 'Build Your Own Hub UI', link: `${prefix}/guide/build-your-own-hub-ui` },
58+
],
59+
},
60+
{
61+
text: 'Recipes & Advanced',
62+
items: [
63+
{ text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` },
64+
{ text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` },
65+
],
66+
},
67+
] satisfies { text: string, items: DefaultTheme.NavItemWithLink[] }[]
68+
}
69+
70+
/** Flattened guide list — used by the top nav dropdown, which renders one level. */
71+
function guideItems(prefix: string) {
72+
return guideGroups(prefix).flatMap(group => group.items) satisfies DefaultTheme.NavItemWithLink[]
4373
}
4474

4575
function adaptersItems(prefix: string) {
@@ -49,19 +79,25 @@ function adaptersItems(prefix: string) {
4979
{ text: 'Dev', link: `${prefix}/adapters/dev` },
5080
{ text: 'Initiate (middleware)', link: `${prefix}/adapters/initiate` },
5181
{ text: 'Build', link: `${prefix}/adapters/build` },
52-
{ text: 'Vite', link: `${prefix}/adapters/vite` },
82+
{ text: 'Vite DevTools', link: `${prefix}/adapters/vite` },
5383
{ text: 'Embedded', link: `${prefix}/adapters/embedded` },
5484
{ text: 'MCP', link: `${prefix}/adapters/mcp` },
5585
] satisfies DefaultTheme.NavItemWithLink[]
5686
}
5787

88+
function frameworksItems(prefix: string) {
89+
return [
90+
{ text: 'Overview', link: `${prefix}/frameworks/` },
91+
{ text: 'Vite', link: `${prefix}/frameworks/vite` },
92+
{ text: 'Nuxt', link: `${prefix}/frameworks/nuxt` },
93+
{ text: 'Next', link: `${prefix}/frameworks/next` },
94+
] satisfies DefaultTheme.NavItemWithLink[]
95+
}
96+
5897
function helpersItems(prefix: string) {
5998
return [
6099
{ text: 'Overview', link: `${prefix}/helpers/` },
61100
{ text: 'Utilities', link: `${prefix}/helpers/utilities` },
62-
{ text: 'Vite Bridge', link: `${prefix}/helpers/vite-bridge` },
63-
{ text: 'Nuxt Module', link: `${prefix}/helpers/nuxt` },
64-
{ text: 'Next Helper', link: `${prefix}/helpers/next` },
65101
{ text: 'Common RPC Functions', link: `${prefix}/helpers/common-rpc-functions` },
66102
{ text: 'Interactive Auth', link: `${prefix}/helpers/interactive-auth` },
67103
] satisfies DefaultTheme.NavItemWithLink[]
@@ -103,12 +139,17 @@ export function devframeSidebar(prefix = ''): DefaultTheme.SidebarItem[] {
103139
return [
104140
{
105141
text: 'Guide',
106-
items: guideItems(prefix),
142+
// Labelled, collapsible subsections instead of one long flat list.
143+
items: guideGroups(prefix).map(group => ({ ...group, collapsed: false })),
107144
},
108145
{
109146
text: 'Adapters',
110147
items: adaptersItems(prefix),
111148
},
149+
{
150+
text: 'Frameworks',
151+
items: frameworksItems(prefix),
152+
},
112153
{
113154
text: 'Helpers',
114155
items: helpersItems(prefix),
@@ -137,6 +178,7 @@ export function devframeNav(prefix = ''): DefaultTheme.NavItem[] {
137178
text: 'Adapters',
138179
items: [
139180
...adaptersItems(prefix),
181+
{ text: 'Frameworks', items: frameworksItems(prefix) },
140182
{ text: 'Helpers', items: helpersItems(prefix) },
141183
],
142184
},

docs/adapters/initiate.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,4 @@ The instance **gates by default** — a handler mounted inside an app server is
126126

127127
## Relation to the other adapters
128128

129-
`createDevServer`, `viteDevBridge`, and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate).
129+
`createDevServer`, `devframeViteBridge` (`@devframes/vite`), and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate).

docs/adapters/mcp.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ defineDevframe({
5151
Both hosted bridges forward the same option to their side-car dev server and advertise the endpoint (with its port) in the `__connection.json` they serve:
5252

5353
```ts
54-
// Vite
55-
viteDevBridge(devframe, { devMiddleware: true, mcp: true })
54+
// Vite (@devframes/vite)
55+
devframeViteBridge(devframe, { mcp: true })
5656

5757
// Next.js (@devframes/next)
5858
createDevframeNextHandler(devframe, { mcp: true })
@@ -90,6 +90,6 @@ It exposes two gateway tools (the wire names of the `devframe:connect:*` ids —
9090
- **`devframe_connect_list-instances`** — discover running devframe dev servers and list each one's MCP tools. Instances running without an MCP route are listed with a hint to restart with `--mcp`.
9191
- **`devframe_connect_call-tool`** — invoke one tool on one instance (`{ port, tool, args }`) over its Streamable-HTTP endpoint.
9292

93-
Discovery reads the **instance registry**: every `createDevServer` (CLI `dev`, `viteDevBridge`, `@devframes/next`'s handler) writes a record to `~/.devframe/instances/<pid>-<port>.json` on boot and removes it on close; readers prune records whose liveness probe fails. The connector dials each instance's endpoint with the instance's own loopback origin, so it clears the route's origin gate without any configuration. In-process hosts register explicitly with `registerDevframeInstance` from `devframe/node` — see `createDevframeNextHost().mountMcp` for serving MCP on a Next app's own origin. `--port <n>` probes an explicit port besides the registry; `DEVFRAME_INSTANCES_DIR` relocates the registry and `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts a server out.
93+
Discovery reads the **instance registry**: every `createDevServer` (CLI `dev`, `devframeViteBridge`, `@devframes/next`'s handler) writes a record to `~/.devframe/instances/<pid>-<port>.json` on boot and removes it on close; readers prune records whose liveness probe fails. The connector dials each instance's endpoint with the instance's own loopback origin, so it clears the route's origin gate without any configuration. In-process hosts register explicitly with `registerDevframeInstance` from `devframe/node` — see `createDevframeNextHost().mountMcp` for serving MCP on a Next app's own origin. `--port <n>` probes an explicit port besides the registry; `DEVFRAME_INSTANCES_DIR` relocates the registry and `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts a server out.
9494

9595
See the [Agent-Native](/guide/agent-native) page for the full API, safety model, and Claude Desktop integration example.

docs/errors/DF0033.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ outline: deep
1010
1111
## Cause
1212

13-
`viteDevBridge({ devMiddleware })` could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons:
13+
`devframeViteBridge()` (from `@devframes/vite`) could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons:
1414

1515
- The preferred port is in use and no fallback range was configured.
1616
- Calling `def.setup(ctx)` threw — the devframe's own setup logic surfaced an error.
@@ -20,10 +20,10 @@ This is a soft warning — the surrounding Vite dev server keeps running, but th
2020

2121
## Fix
2222

23-
- Pin a port via `cli.port` / `cli.portRange` on the devframe definition, or via `devMiddleware.port` on `viteDevBridge`.
23+
- Pin a port via `cli.port` / `cli.portRange` on the devframe definition, or via `port` on `devframeViteBridge`.
2424
- Inspect the `reason` (or the attached `cause`) for the underlying error — fix the setup function or free the port.
2525
- For Nuxt: pass `devMiddleware: { port: <free-port> }` to the `@devframes/nuxt` module.
2626

2727
## Source
2828

29-
- [`packages/devframe/src/helpers/vite.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/helpers/vite.ts)`viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`.
29+
- [`packages/vite/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/vite/src/index.ts)`devframeViteBridge()` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`.

docs/errors/DF0052.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The instance's side-car / shared-server transport binding tried to bind the HTTP
2121

2222
## Fix
2323

24-
- Free the port, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`.
24+
- Free the port, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `port` on `devframeViteBridge` (`@devframes/vite`).
2525
- The original node error is available as `error.cause` — check `error.cause.code` (e.g. `'EADDRINUSE'`) to branch on the failure kind programmatically.
2626

2727
## Source

docs/frameworks/index.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Frameworks
6+
7+
The framework packages — [`@devframes/vite`](./vite), [`@devframes/nuxt`](./nuxt), and [`@devframes/next`](./next) — integrate devframe with a specific meta-framework's dev server. Each one splits into **two clearly-scoped subpaths**, because you're always doing one of two distinct jobs:
8+
9+
| Scope | Subpath | You are… |
10+
|-------|---------|----------|
11+
| **dev-spa** | `.../dev-spa` | building & dev-serving a **single devframe's SPA** with that tool |
12+
| **hub** | `.../hub` | mounting a whole **[devframes-hub](/guide/hub)** (many integrations) inside that tool |
13+
14+
The bare package root (`@devframes/vite`, `@devframes/nuxt`, `@devframes/next`) has no export — it throws with a pointer to the two subpaths, so an accidental bare import fails loudly instead of resolving to nothing.
15+
16+
| Package | dev-spa | hub |
17+
|---------|---------|-----|
18+
| [`@devframes/vite`](./vite) | `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` | `viteDevframeHub` (+ `/hub/client`) |
19+
| [`@devframes/nuxt`](./nuxt) | the Nuxt module (`modules: ['@devframes/nuxt/dev-spa']`) | the hub Nuxt module (+ `/hub/client`) |
20+
| [`@devframes/next`](./next) | `withDevframe` + `createDevframeNextHandler` (+ `/dev-spa/client`) | `nextDevframeHub` (+ `/hub/client`) |
21+
22+
## dev-spa: author one devframe
23+
24+
The `dev-spa` scope is for when the thing you're building **is** a devframe — you author its UI with Vite/Nuxt/Next and want its RPC backend running during development. See each package's page for the details; for the framework-neutral CLI/build/embedded outputs, reach for the [adapters](/adapters/) instead.
25+
26+
## hub: mount a devframes-hub
27+
28+
The `hub` scope mounts an [`@devframes/hub`](/guide/hub) — many integrations under one namespace, one merged RPC registry — inside the tool's dev server. Each `hub` entry wraps [`initHub`](/guide/hub-initiate), defaults the UI slot to [`@devframes/hub-ui`](/guide/build-your-own-hub-ui)'s `createUi()` (override with `ui`, or `ui: false` for a headless hub you drive with the matching `/hub/client` helper), and mounts everything behind one catch-all.
29+
30+
- **[Vite](./vite#mounting-a-hub)**`viteDevframeHub()` shares Vite's dev server and injects the floating dock.
31+
- **[Nuxt](./nuxt#mounting-a-hub)** — the hub Nuxt module wires the Vite hub plugin into `nuxt dev`.
32+
- **[Next](./next#mounting-a-hub)**`nextDevframeHub()` serves the hub from one App Router route on a side-car socket.
33+
34+
Vite and Nuxt already have native hub viewers ([Vite DevTools](https://devtools.vite.dev), [Nuxt DevTools](https://devtools.nuxt.com)) that integrate the same hub protocol, so `@devframes/vite/hub` and `@devframes/nuxt/hub` print a one-time recommendation to prefer those (silence with `{ quiet: true }`). Next has no native counterpart, so `@devframes/next/hub` stays quiet.

0 commit comments

Comments
 (0)