Skip to content
Open
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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ Ahead-of-time build artifacts that live under `src/` - the shadow-root styleshee
- Shared state via `devframe/utils/shared-state`; keep values serializable.
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
- 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`.
- **A plugin's default export is its `create<X>Devframe` factory, never a pre-built instance.** Don't write `const xDevframe = createXDevframe(); export default xDevframe` (or the inline `export default createXDevframe()` equivalent) - that eagerly constructs a `DevframeDefinition` the moment the module loads, at import time, whether or not any consumer wants that exact zero-config shape; a host that needs its own options (an id override, a data directory, …) ends up paying for a second, discarded instance alongside the one it actually uses. Alias the factory itself as the default export instead - `export default createXDevframe` - so importing the module costs nothing beyond defining the function, and every consumer calls it (with or without options) to get their own instance: `import createA11yDevframe from '@devframes/plugin-a11y'` then `createA11yDevframe(options)`.

### Framework adapter packages: two scopes, one shape

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:

- **`.../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']`).
- **`.../single`** - **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 `.../single/client`. Nuxt: the Nuxt module (registered as `modules: ['@devframes/nuxt/single']`).
- **`.../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.
- **The bare root (`.`) throws** a helpful error pointing at the two subpaths - never put real code on it.
- **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.
Expand Down
8 changes: 4 additions & 4 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,16 @@ export const alias = {
'@devframes/hub': r('hub/src/index.ts'),
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
'@devframes/nuxt/dev-spa': r('nuxt/src/dev-spa.ts'),
'@devframes/nuxt/single': r('nuxt/src/single.ts'),
'@devframes/nuxt/hub/client': r('nuxt/src/hub-client.ts'),
'@devframes/nuxt/hub': r('nuxt/src/hub.ts'),
'@devframes/nuxt': r('nuxt/src/index.ts'),
'@devframes/next/dev-spa/client': r('next/src/client.tsx'),
'@devframes/next/dev-spa': r('next/src/dev-spa.ts'),
'@devframes/next/single/client': r('next/src/client.tsx'),
'@devframes/next/single': r('next/src/single.ts'),
'@devframes/next/hub/client': r('next/src/hub-client.tsx'),
'@devframes/next/hub': r('next/src/hub.ts'),
'@devframes/next': r('next/src/index.ts'),
'@devframes/vite/dev-spa': r('vite/src/dev-spa.ts'),
'@devframes/vite/single': r('vite/src/single.ts'),
'@devframes/vite/hub/client': r('vite/src/hub-client.ts'),
'@devframes/vite/hub': r('vite/src/hub.ts'),
'@devframes/vite': r('vite/src/index.ts'),
Expand Down
14 changes: 7 additions & 7 deletions docs/frameworks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ The framework packages — [`@devframes/vite`](./vite), [`@devframes/nuxt`](./nu

| Scope | Subpath | You are… |
|-------|---------|----------|
| **dev-spa** | `.../dev-spa` | building & dev-serving a **single devframe's SPA** with that tool |
| **single** | `.../single` | building & dev-serving a **single devframe's SPA** with that tool |
| **hub** | `.../hub` | mounting a whole **[devframes-hub](/guide/hub)** (many integrations) inside that tool |

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.

| Package | dev-spa | hub |
|---------|---------|-----|
| Package | single | hub |
|---------|--------|-----|
| [`@devframes/vite`](./vite) | `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` | `viteDevframeHub` (+ `/hub/client`) |
| [`@devframes/nuxt`](./nuxt) | the Nuxt module (`modules: ['@devframes/nuxt/dev-spa']`) | the hub Nuxt module (+ `/hub/client`) |
| [`@devframes/next`](./next) | `withDevframe` + `createDevframeNextHandler` (+ `/dev-spa/client`) | `nextDevframeHub` (+ `/hub/client`) |
| [`@devframes/nuxt`](./nuxt) | the Nuxt module (`modules: ['@devframes/nuxt/single']`) | the hub Nuxt module (+ `/hub/client`) |
| [`@devframes/next`](./next) | `withDevframe` + `createDevframeNextHandler` (+ `/single/client`) | `nextDevframeHub` (+ `/hub/client`) |

## dev-spa: author one devframe
## single: author one devframe

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.
The `single` 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.

## hub: mount a devframes-hub

Expand Down
16 changes: 8 additions & 8 deletions docs/frameworks/next.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ outline: deep

`@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](./vite): 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.

`@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.
`@devframes/next` splits into two scopes: `@devframes/next/single` (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.

The `dev-spa` scope comes in two parts:
The `single` scope comes in two parts:

1. **`withDevframe()`** — applies the one Next config setting a devframe host needs.
2. **`createDevframeNextHandler()`** — hosts a single devframe (the common case).

Plus a React client surface at `@devframes/next/dev-spa/client`.
Plus a React client surface at `@devframes/next/single/client`.

## Config

```ts [next.config.mjs]
import { withDevframe } from '@devframes/next/dev-spa'
import { withDevframe } from '@devframes/next/single'

export default withDevframe({
// ...your own Next config
Expand All @@ -35,7 +35,7 @@ export default withDevframe({
`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`:

```ts [app/__my-tool/[[...path]]/route.ts]
import { createDevframeNextHandler } from '@devframes/next/dev-spa'
import { createDevframeNextHandler } from '@devframes/next/single'
import myDevframe from '@/devframe'

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

## React client

`@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.
`@devframes/next/single/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.

```tsx [app/providers.tsx]
'use client'
import { RpcProvider } from '@devframes/next/dev-spa/client'
import { RpcProvider } from '@devframes/next/single/client'

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

```tsx [app/panel.tsx]
'use client'
import { useRpc, useRpcStatus } from '@devframes/next/dev-spa/client'
import { useRpc, useRpcStatus } from '@devframes/next/single/client'

export function Panel() {
const rpc = useRpc()?.scope('my-tool:')
Expand Down
12 changes: 6 additions & 6 deletions docs/frameworks/nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ outline: deep

# Nuxt

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.
The `@devframes/nuxt/single` 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.

`@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.
`@devframes/nuxt` splits into two scopes: `@devframes/nuxt/single` (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.

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

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

```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@devframes/nuxt/dev-spa'],
modules: ['@devframes/nuxt/single'],
})
```

Expand Down Expand Up @@ -47,7 +47,7 @@ export function usePayload() {

```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: ['@devframes/nuxt/dev-spa'],
modules: ['@devframes/nuxt/single'],
devframe: {
baseURL: './', // where the devframe snapshot lives, relative to the page
skipAppDefaults: false, // opt out of the app.baseURL / vite.base defaults
Expand All @@ -66,7 +66,7 @@ Pass your devframe definition to wire `nuxt dev` up to the RPC backend:
import devframe from './src/devframe' // defineDevframe(...) export

export default defineNuxtConfig({
modules: [['@devframes/nuxt/dev-spa', { devframe }]],
modules: [['@devframes/nuxt/single', { devframe }]],
})
```

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

```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [['@devframes/nuxt/dev-spa', {
modules: [['@devframes/nuxt/single', {
devframe,
devMiddleware: {
port: 7777,
Expand Down
6 changes: 3 additions & 3 deletions docs/frameworks/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ outline: deep

# Vite

`@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.
`@devframes/vite` splits into two scopes: **`@devframes/vite/single`** (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.

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).
The `single` 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).

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.

```ts
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/dev-spa'
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/single'
import { defineConfig } from 'vite'
import devframe from './devframe'

Expand Down
4 changes: 3 additions & 1 deletion docs/guide/hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ Dev servers with a module bundler (Next's Turbopack/webpack) statically analyse
const pkgs = ['@devframes/plugin-git', '@devframes/plugin-terminals']
const defs = await Promise.all(
pkgs.map(p => import(/* webpackIgnore: true */ /* turbopackIgnore: true */ p)),
).then(mods => mods.map(m => m.default))
// Each package's default export is its `create<X>Devframe` factory, not a
// pre-built instance — call it to get one.
).then(mods => mods.map(m => m.default()))

for (const def of defs)
await ctx.install(def)
Expand Down
Loading
Loading