Skip to content

Commit 36cdc84

Browse files
committed
refactor(vite,nuxt,next): split each into /dev-spa and /hub scopes
Each framework adapter package now exposes two clearly-scoped subpaths for the two distinct jobs a consumer does, with a bare root that throws a tip: • `.../dev-spa` — build & dev-serve a SINGLE devframe's SPA with the tool (Vite: devframeVitePlugin/Bridge/Vite; Next: withDevframe + createDevframeNextHandler + `/dev-spa/client` React client; Nuxt: the module, `modules: ['@devframes/nuxt/dev-spa']`). • `.../hub` — mount a whole @devframes/hub (many integrations) inside the tool, defaulting the UI slot to @devframes/hub-ui's createUi() (overridable via `ui`, or `ui: false` for headless), with a browser client helper at `.../hub/client` over createDevframeClientHost. • the bare root (`.`) throws a helpful error pointing at both subpaths. 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 and warns nothing. @devframes/hub and @devframes/hub-ui are optional peers of all three; hub-ui loads lazily so it stays optional. The four hub examples now consume the new `/hub` exports: the full hub-vite/hub-next hosts pass `ui: false` and keep their hand-rolled clients (the point of those references), while the minimal ones use the default @devframes/hub-ui. Docs and the AGENTS.md convention section are updated; tsnapi snapshots regenerated for the new subpaths (allowing the intentional breaking rename — these packages have not shipped yet). This PR was created with the help of an agent.
1 parent 275e08d commit 36cdc84

77 files changed

Lines changed: 1817 additions & 800 deletions

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: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,18 @@ export const alias = {
5050
'@devframes/hub': r('hub/src/index.ts'),
5151
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
5252
'@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'),
5356
'@devframes/nuxt': r('nuxt/src/index.ts'),
54-
'@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'),
5561
'@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'),
5665
'@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'),

‎docs/guide/standalone-cli.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ For the Nuxt side, add the devframe helper module — it sets `app.baseURL: './'
9090
```ts [nuxt.config.ts]
9191
export default defineNuxtConfig({
9292
ssr: false,
93-
modules: ['@devframes/nuxt'],
93+
modules: ['@devframes/nuxt/dev-spa'],
9494
nitro: {
9595
preset: 'static',
9696
output: { dir: './dist' }, // matches createCac's distDir of ./dist/public

‎docs/helpers/next.md‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,19 @@ outline: deep
99
1010
`@devframes/next` hosts devframes from a Next.js App Router app. Next runs on webpack/Turbopack rather than Vite, so it hosts through a route handler instead of the [Vite Bridge](./vite-bridge): the package serves each devframe's SPA and its `__connection.json` from a single `fetch` handler your catch-all route delegates to, reusing devframe's own [`serveStaticHandler`](/adapters/dev) for SPA fallback, content types, and path-traversal guarding.
1111

12-
It comes in three parts:
12+
`@devframes/next` splits into two scopes: `@devframes/next/dev-spa` (author one devframe with Next) and [`@devframes/next/hub`](#mounting-a-hub) (mount a whole devframes-hub). The bare `@devframes/next` import throws with a pointer to both.
13+
14+
The `dev-spa` scope comes in two parts:
1315

1416
1. **`withDevframe()`** — applies the one Next config setting a devframe host needs.
1517
2. **`createDevframeNextHandler()`** — hosts a single devframe (the common case).
16-
3. **`createDevframeNextHost()`** — the lower-level primitive for a hub mounting many devframes at once.
1718

18-
Plus a React client surface at `@devframes/next/client`.
19+
Plus a React client surface at `@devframes/next/dev-spa/client`.
1920

2021
## Config
2122

2223
```ts [next.config.mjs]
23-
import { withDevframe } from '@devframes/next'
24+
import { withDevframe } from '@devframes/next/dev-spa'
2425

2526
export default withDevframe({
2627
// ...your own Next config
@@ -34,7 +35,7 @@ export default withDevframe({
3435
`createDevframeNextHandler(definition)` statically serves the devframe's built SPA and starts a side-car RPC/WebSocket server, advertising it at `<base>/__connection.json`. Delegate your catch-all route to its `fetch`:
3536

3637
```ts [app/__my-tool/[[...path]]/route.ts]
37-
import { createDevframeNextHandler } from '@devframes/next'
38+
import { createDevframeNextHandler } from '@devframes/next/dev-spa'
3839
import myDevframe from '@/devframe'
3940

4041
export const runtime = 'nodejs'
@@ -87,11 +88,11 @@ export async function GET(request: Request): Promise<Response> {
8788

8889
## React client
8990

90-
`@devframes/next/client` connects to the RPC backend and provides the client to your component tree — the React counterpart to `@devframes/nuxt`'s `$rpc` plugin. Children render immediately, so your shell and a connection indicator stay visible while the client connects.
91+
`@devframes/next/dev-spa/client` connects to the RPC backend and provides the client to your component tree — the React counterpart to `@devframes/nuxt`'s `$rpc` plugin. Children render immediately, so your shell and a connection indicator stay visible while the client connects.
9192

9293
```tsx [app/providers.tsx]
9394
'use client'
94-
import { RpcProvider } from '@devframes/next/client'
95+
import { RpcProvider } from '@devframes/next/dev-spa/client'
9596

9697
export function Providers({ children }: { children: React.ReactNode }) {
9798
return <RpcProvider baseURL="/__my-tool/">{children}</RpcProvider>
@@ -102,7 +103,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
102103

103104
```tsx [app/panel.tsx]
104105
'use client'
105-
import { useRpc, useRpcStatus } from '@devframes/next/client'
106+
import { useRpc, useRpcStatus } from '@devframes/next/dev-spa/client'
106107

107108
export function Panel() {
108109
const rpc = useRpc()?.scope('my-tool:')
@@ -119,6 +120,24 @@ Both hooks throw outside a `<RpcProvider>`. Theming and layout stay app-owned.
119120

120121
Route handlers that call `fetch` pin `export const runtime = 'nodejs'`: the static handler streams built SPA files from disk, and the side-car RPC/WS server is a Node process.
121122

123+
## Mounting a hub
124+
125+
`@devframes/next/hub` mounts a whole [devframes-hub](/guide/hub) — many integrations under one namespace — from a single catch-all route. `nextDevframeHub()` returns a route handle memoized on `globalThis` (so Next's dev-time route re-evaluation reuses one instance); `createNextDevframeHub()` is the underlying builder. The UI defaults to `@devframes/hub-ui` (loaded through a bundler-ignored dynamic `import()` so its asset lookups resolve at request time); pass `ui` to swap it or `ui: false` for a headless hub you drive with the React client at `@devframes/next/hub/client` (`useDevframeHubClient()`).
126+
127+
```ts [app/__devframes/[[...path]]/route.ts]
128+
import { nextDevframeHub } from '@devframes/next/hub'
129+
130+
export const runtime = 'nodejs'
131+
export const dynamic = 'force-dynamic'
132+
133+
const hub = nextDevframeHub({ devframes: [] })
134+
export const GET = (req: Request) => hub.handler(req)
135+
export const POST = (req: Request) => hub.handler(req)
136+
export const DELETE = (req: Request) => hub.handler(req)
137+
```
138+
139+
Unlike Vite and Nuxt, Next has no native hub viewer, so this scope prints no recommendation. `createDevframeNextHost()` remains available from `@devframes/next/hub` as the lower-level "bring your own `DevframeHost`" seam for `initHub({ context })`.
140+
122141
## See also
123142

124143
- [Vite Bridge](./vite-bridge) — the equivalent for Vite-based hosts

‎docs/helpers/nuxt.md‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ outline: deep
44

55
# Nuxt Helper
66

7-
The `@devframes/nuxt` module wires a Nuxt-built SPA as a devframe client, and optionally serves the dev-time RPC bridge alongside `nuxt dev`. It runs inside the Nuxt app that consumes your devframe.
7+
The `@devframes/nuxt/dev-spa` module wires a Nuxt-built SPA as a devframe client, and optionally serves the dev-time RPC bridge alongside `nuxt dev`. It runs inside the Nuxt app that consumes your devframe.
8+
9+
`@devframes/nuxt` splits into two scopes: `@devframes/nuxt/dev-spa` (this page — author one devframe with Nuxt) and [`@devframes/nuxt/hub`](#mounting-a-hub) (mount a whole devframes-hub). The bare `@devframes/nuxt` import throws with a pointer to both.
810

911
It handles the four things every Nuxt-powered standalone devtool needs:
1012

@@ -17,7 +19,7 @@ It handles the four things every Nuxt-powered standalone devtool needs:
1719

1820
```ts [nuxt.config.ts]
1921
export default defineNuxtConfig({
20-
modules: ['@devframes/nuxt'],
22+
modules: ['@devframes/nuxt/dev-spa'],
2123
})
2224
```
2325

@@ -45,7 +47,7 @@ export function usePayload() {
4547

4648
```ts [nuxt.config.ts]
4749
export default defineNuxtConfig({
48-
modules: ['@devframes/nuxt'],
50+
modules: ['@devframes/nuxt/dev-spa'],
4951
devframe: {
5052
baseURL: './', // where the devframe snapshot lives, relative to the page
5153
skipAppDefaults: false, // opt out of the app.baseURL / vite.base defaults
@@ -64,7 +66,7 @@ Pass your devframe definition to wire `nuxt dev` up to the RPC backend:
6466
import devframe from './src/devframe' // defineDevframe(...) export
6567
6668
export default defineNuxtConfig({
67-
modules: [['@devframes/nuxt', { devframe }]],
69+
modules: [['@devframes/nuxt/dev-spa', { devframe }]],
6870
})
6971
```
7072

@@ -81,7 +83,7 @@ The bridge is **on by default** whenever `devframe` is set. Skip it (back to cli
8183

8284
```ts [nuxt.config.ts]
8385
export default defineNuxtConfig({
84-
modules: [['@devframes/nuxt', {
86+
modules: [['@devframes/nuxt/dev-spa', {
8587
devframe,
8688
devMiddleware: {
8789
port: 7777,
@@ -128,6 +130,18 @@ At build time the module:
128130

129131
At runtime the built SPA fetches `./__connection.json` (resolved against `document.baseURI`) and branches on the `backend` field — `websocket` in dev, `static` from a `createBuild` snapshot.
130132

133+
## Mounting a hub
134+
135+
`@devframes/nuxt/hub` mounts a whole [devframes-hub](/guide/hub) — many integrations under one namespace — alongside `nuxt dev`, wiring `@devframes/vite`'s hub plugin into Nuxt's Vite dev server and injecting `@devframes/hub-ui`'s floating dock. The UI defaults to `@devframes/hub-ui`; pass `ui` to swap it or `ui: false` for a headless hub you drive with `@devframes/nuxt/hub/client`.
136+
137+
```ts [nuxt.config.ts]
138+
export default defineNuxtConfig({
139+
modules: [['@devframes/nuxt/hub', { devframes: [] }]],
140+
})
141+
```
142+
143+
Nuxt DevTools (`@nuxt/devtools`) integrates the same hub protocol natively and is the recommended path for a Nuxt app, so this module prints a one-time recommendation to that effect (silence it with `{ quiet: true }`).
144+
131145
## See also
132146

133147
- [Standalone CLI recipe](/guide/standalone-cli) — end-to-end walk-through

‎docs/helpers/vite-bridge.md‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ outline: deep
44

55
# @devframes/vite
66

7-
Two Vite plugins for mounting a devframe inside an existing Vite dev server — `devframeVitePlugin` (static mount) and `devframeViteBridge` (RPC bridge) — plus `devframeVite`, a convenience wrapper that picks between them. Used by [`@devframes/nuxt`](./nuxt) and available for any Vite-based host (Astro, SolidStart, plain Vite apps).
7+
`@devframes/vite` splits into two scopes: **`@devframes/vite/dev-spa`** (this page — dev-serve one devframe's SPA with Vite) and [**`@devframes/vite/hub`**](#mounting-a-hub) (mount a whole devframes-hub inside a Vite app). The bare `@devframes/vite` import throws with a pointer to both.
8+
9+
The `dev-spa` scope exports two Vite plugins for mounting a single devframe inside an existing Vite dev server — `devframeVitePlugin` (static mount) and `devframeViteBridge` (RPC bridge) — plus `devframeVite`, a convenience wrapper that picks between them. Used by [`@devframes/nuxt`](./nuxt) and available for any Vite-based host (Astro, SolidStart, plain Vite apps).
810

911
This sits below the [`vite` adapter](/adapters/vite) on the abstraction ladder: the adapter targets the full Vite DevTools dock; these are the lower-level Vite plugins you reach for when you want a devframe to ride along with an existing app's dev server without the DevTools dock.
1012

1113
```ts
12-
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite'
14+
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/dev-spa'
1315
import { defineConfig } from 'vite'
1416
import devframe from './devframe'
1517

@@ -50,3 +52,18 @@ To mount the RPC socket onto the Vite server's own port instead of a side-car
5052
## `devframeVite` — convenience wrapper
5153

5254
`devframeVite(def, { bridge, ...bridgeOptions })` forwards to `devframeViteBridge` when `bridge: true`, or `devframeVitePlugin` otherwise — handy when a single call site needs to switch between the two modes. Reach for the two plugins directly when a devframe needs both mounted at once (e.g. a bridge for RPC alongside a static mount serving its own bundled UI, as the built-in `terminals`/`code-server` plugins do).
55+
56+
## Mounting a hub
57+
58+
`@devframes/vite/hub` mounts a whole [devframes-hub](/guide/hub) — many integrations under one namespace, one merged RPC registry — inside a Vite dev server with one `viteDevframeHub()` plugin. It wraps `initHub`, shares Vite's HTTP server for the WebSocket, defaults the dock UI to `@devframes/hub-ui` (injecting its `embedded.js` bootstrap into the host page), and mounts everything as connect middleware.
59+
60+
```ts
61+
import { viteDevframeHub } from '@devframes/vite/hub'
62+
import { defineConfig } from 'vite'
63+
64+
export default defineConfig({
65+
plugins: [viteDevframeHub({ devframes: [] })],
66+
})
67+
```
68+
69+
Pass `ui` to swap the viewer or `ui: false` for a headless hub you drive with the client helper at `@devframes/vite/hub/client` (`mountDevframeHubClient()`). Vite DevTools (`@vitejs/devtools-kit`) integrates the same hub protocol natively and is the recommended path for a Vite app, so this plugin prints a one-time recommendation to that effect (silence it with `{ quiet: true }`).

‎examples/hub-next-minimal/src/client/hub.ts‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import type { createUi as CreateUi } from '@devframes/hub-ui'
21
import type { HubInstance } from '@devframes/hub/initiate'
32
import type { DevframeJsonRenderSpec } from '@devframes/json-render'
43
import type { jsonRenderUiRenderer as JsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
54
import type { DevframeJsonRenderDockEntry } from '@devframes/json-render/hub'
65
import type { DevframeDefinition } from 'devframe'
7-
import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate'
6+
import { createNextDevframeHub } from '@devframes/next/hub'
87

98
// A server-authored JSON-render dock: the whole view is this serializable
109
// spec — no client build. It renders through whatever `'json-render'`
@@ -52,8 +51,7 @@ const BUILTIN_PLUGIN_PACKAGES = [
5251
] as const
5352

5453
async function loadHub(): Promise<HubInstance> {
55-
const [hubUi, jsonRenderUi, dataInspector, assets, ...builtins] = await Promise.all([
56-
import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/hub-ui'),
54+
const [jsonRenderUi, dataInspector, assets, ...builtins] = await Promise.all([
5755
import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/json-render-ui/hub'),
5856
import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-data-inspector'),
5957
import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-assets'),
@@ -70,13 +68,12 @@ async function loadHub(): Promise<HubInstance> {
7068
(dataInspector.createDataInspectorDevframe as (options: { id: string }) => DevframeDefinition)({ id: 'devframes_plugin_data-inspector' }),
7169
(assets.createAssetsDevframe as (options: { watch: boolean }) => DevframeDefinition)({ watch: false }),
7270
]
73-
// Next route handlers can't accept WebSocket upgrades, so the socket asks
74-
// for a side-car server of its own, advertised via `__connection.json`.
75-
return initHub({
76-
base: DEVFRAMES_HUB_BASE,
77-
ws: { sidecar: true },
71+
// `@devframes/next/hub` runs the socket on a side-car (Next routes can't
72+
// accept WS upgrades) and defaults the UI to `@devframes/hub-ui` (loaded
73+
// through its own bundler-ignored dynamic import) — the minimal host needs
74+
// no client code, just the injected `embedded.js`.
75+
return createNextDevframeHub({
7876
devframes,
79-
ui: (hubUi.createUi as typeof CreateUi)(),
8077
// Serve the reference json-render frontend as a prebuilt renderer module
8178
// — the one-liner that makes `'json-render'` docks render in the prebuilt
8279
// viewer. Swap it for any community implementation of the same contract.

‎examples/hub-next-minimal/src/client/next.config.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { withDevframe } from '@devframes/next'
1+
import { withDevframe } from '@devframes/next/dev-spa'
22

33
// `withDevframe` applies the settings a devframe host requires (currently
44
// `skipTrailingSlashRedirect: true`, so mounted SPAs' relative assets under

0 commit comments

Comments
 (0)