Skip to content

Commit 3e6ef47

Browse files
authored
feat!: delete the mirror fringe; rename adapters' /dev-spa to /single; plugins default-export their factory (#238)
1 parent 9def6c7 commit 3e6ef47

145 files changed

Lines changed: 468 additions & 569 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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ Ahead-of-time build artifacts that live under `src/` - the shadow-root styleshee
4545
- Shared state via `devframe/utils/shared-state`; keep values serializable.
4646
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
4747
- 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`.
48+
- **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)`.
4849

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

5152
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:
5253

53-
- **`.../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']`).
54+
- **`.../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']`).
5455
- **`.../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.
5556
- **The bare root (`.`) throws** a helpful error pointing at the two subpaths - never put real code on it.
5657
- **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.

alias.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,16 @@ export const alias = {
5151
'@devframes/hub': r('hub/src/index.ts'),
5252
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
5353
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
54-
'@devframes/nuxt/dev-spa': r('nuxt/src/dev-spa.ts'),
54+
'@devframes/nuxt/single': r('nuxt/src/single.ts'),
5555
'@devframes/nuxt/hub/client': r('nuxt/src/hub-client.ts'),
5656
'@devframes/nuxt/hub': r('nuxt/src/hub.ts'),
5757
'@devframes/nuxt': r('nuxt/src/index.ts'),
58-
'@devframes/next/dev-spa/client': r('next/src/client.tsx'),
59-
'@devframes/next/dev-spa': r('next/src/dev-spa.ts'),
58+
'@devframes/next/single/client': r('next/src/client.tsx'),
59+
'@devframes/next/single': r('next/src/single.ts'),
6060
'@devframes/next/hub/client': r('next/src/hub-client.tsx'),
6161
'@devframes/next/hub': r('next/src/hub.ts'),
6262
'@devframes/next': r('next/src/index.ts'),
63-
'@devframes/vite/dev-spa': r('vite/src/dev-spa.ts'),
63+
'@devframes/vite/single': r('vite/src/single.ts'),
6464
'@devframes/vite/hub/client': r('vite/src/hub-client.ts'),
6565
'@devframes/vite/hub': r('vite/src/hub.ts'),
6666
'@devframes/vite': r('vite/src/index.ts'),

docs/frameworks/index.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ The framework packages — [`@devframes/vite`](./vite), [`@devframes/nuxt`](./nu
88

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

1414
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.
1515

16-
| Package | dev-spa | hub |
17-
|---------|---------|-----|
16+
| Package | single | hub |
17+
|---------|--------|-----|
1818
| [`@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`) |
19+
| [`@devframes/nuxt`](./nuxt) | the Nuxt module (`modules: ['@devframes/nuxt/single']`) | the hub Nuxt module (+ `/hub/client`) |
20+
| [`@devframes/next`](./next) | `withDevframe` + `createDevframeNextHandler` (+ `/single/client`) | `nextDevframeHub` (+ `/hub/client`) |
2121

22-
## dev-spa: author one devframe
22+
## single: author one devframe
2323

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.
24+
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.
2525

2626
## hub: mount a devframes-hub
2727

docs/frameworks/next.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +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](./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.
1111

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.
12+
`@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.
1313

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

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

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

2121
## Config
2222

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

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

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

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

8989
## React client
9090

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.
91+
`@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.
9292

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

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

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

108108
export function Panel() {
109109
const rpc = useRpc()?.scope('my-tool:')

docs/frameworks/nuxt.md

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

55
# Nuxt
66

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.
7+
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.
88

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.
9+
`@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.
1010

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

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

2020
```ts [nuxt.config.ts]
2121
export default defineNuxtConfig({
22-
modules: ['@devframes/nuxt/dev-spa'],
22+
modules: ['@devframes/nuxt/single'],
2323
})
2424
```
2525

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

4848
```ts [nuxt.config.ts]
4949
export default defineNuxtConfig({
50-
modules: ['@devframes/nuxt/dev-spa'],
50+
modules: ['@devframes/nuxt/single'],
5151
devframe: {
5252
baseURL: './', // where the devframe snapshot lives, relative to the page
5353
skipAppDefaults: false, // opt out of the app.baseURL / vite.base defaults
@@ -66,7 +66,7 @@ Pass your devframe definition to wire `nuxt dev` up to the RPC backend:
6666
import devframe from './src/devframe' // defineDevframe(...) export
6767
6868
export default defineNuxtConfig({
69-
modules: [['@devframes/nuxt/dev-spa', { devframe }]],
69+
modules: [['@devframes/nuxt/single', { devframe }]],
7070
})
7171
```
7272

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

8484
```ts [nuxt.config.ts]
8585
export default defineNuxtConfig({
86-
modules: [['@devframes/nuxt/dev-spa', {
86+
modules: [['@devframes/nuxt/single', {
8787
devframe,
8888
devMiddleware: {
8989
port: 7777,

docs/frameworks/vite.md

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

55
# Vite
66

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.
7+
`@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.
88

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).
9+
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).
1010

1111
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.
1212

1313
```ts
14-
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/dev-spa'
14+
import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite/single'
1515
import { defineConfig } from 'vite'
1616
import devframe from './devframe'
1717

docs/guide/hub.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ Dev servers with a module bundler (Next's Turbopack/webpack) statically analyse
162162
const pkgs = ['@devframes/plugin-git', '@devframes/plugin-terminals']
163163
const defs = await Promise.all(
164164
pkgs.map(p => import(/* webpackIgnore: true */ /* turbopackIgnore: true */ p)),
165-
).then(mods => mods.map(m => m.default))
165+
// Each package's default export is its `create<X>Devframe` factory, not a
166+
// pre-built instance — call it to get one.
167+
).then(mods => mods.map(m => m.default()))
166168

167169
for (const def of defs)
168170
await ctx.install(def)

0 commit comments

Comments
 (0)