diff --git a/AGENTS.md b/AGENTS.md index 06557b50..ddc0262c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Positioning -**`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. +**`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. **`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI - implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. It does ship a **headless client runtime** (`createDevframeClientHost()` from `@devframes/hub/client`): booted in the host page, it assembles the shared `DevframeClientContext` (panel, docks, commands, when) and imports each dock entry's client script (`action` / `custom-render` / iframe `clientScript`) into that page - how a plugin like the a11y inspector runs code inside the page being inspected. See `examples/hub-vite/` for a working ~120-line Vite host demonstrating the protocol end to end. @@ -79,8 +79,8 @@ These reinforce devframe's positioning as "the container for one devtool integra - **Single-integration scope.** Devframe describes one tool. If a feature only makes sense when multiple tools share a UI - docking, a unified command palette, cross-tool toasts, terminal aggregation - it belongs in a hub package, not here. - **Headless by default.** No default startup banners, no opinionated logging to stdout, no default styling. Provide hooks (`onReady`, `cli.configure`, etc.); let the application print its own branding. Structured diagnostics via `nostics` are fine - ad-hoc `console.log`s baked into adapters are not. -- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`) and `/` for *standalone* adapters (`cli`, `spa`, `build`). Authors override via `DevframeDefinition.basePath`. Don't hardcode mount paths in adapter code paths that may run standalone. -- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` / `createSpa` copy SPA output verbatim - no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically. +- **Mount path depends on adapter context.** Given `id: 'foo'`, the default mount path is `/__foo/` for *hosted* adapters (`vite`, `embedded`) and `/` for *standalone* adapters (`cli`, `build`). Authors override via `DevframeDefinition.basePath`. Don't hardcode mount paths in adapter code paths that may run standalone. +- **SPAs own their basePath at runtime.** Build SPAs with relative asset paths (`vite.base: './'`); discover the effective base in the browser from the executing script's location / `document.baseURI`. `createBuild` copies SPA output verbatim - no HTML rewriting, no build-time `--base` injection. The client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically. - **CLI flags compose from both sides.** The `cac` instance backing `createCac` is exposed both to the `DevframeDefinition` (`cli.configure(cli)`) - for capabilities contributed by the tool itself - and to the `createCac` caller - for flags added at the final assembly stage. Parsed flag values are forwarded to `setup(ctx, { flags })`. Never hardcode domain-specific flags into `createCac`. ### Hub example parity diff --git a/docs/adapters/build.md b/docs/adapters/build.md index 0e63cca4..d1f4e215 100644 --- a/docs/adapters/build.md +++ b/docs/adapters/build.md @@ -10,7 +10,6 @@ Produces a self-contained static deploy of a devframe: 2. Runs `setup(ctx)` with `mode: 'build'`. 3. Collects RPC dumps for every `'static'` function and any `'query'` function with `dump.inputs` / `snapshot: true`. 4. Writes `/__connection.json` (`{ backend: 'static' }`) and sharded dump files under `/__rpc-dump/` — both at the SPA root so the deployed client discovers them via relative paths from `document.baseURI`. -5. When `def.spa` is set, also writes `/spa-loader.json` describing how the SPA hydrates its data. ```ts import { createBuild } from 'devframe/adapters/build' @@ -18,24 +17,15 @@ import devframe from './devframe' await createBuild(devframe, { outDir: 'dist-static', - base: '/', }) ``` | Option | Default | Description | |--------|---------|-------------| | `outDir` | `dist-static` | Output directory. Cleared on each build. | -| `base` | `/` | Absolute URL base the output is served from. | | `distDir` | `def.cli?.distDir` | Override the SPA dist directory. | +| `pretty` | `false` | Pretty-print dump JSON (larger on disk). | The resulting directory hosts on any static web server (`serve`, nginx, GitHub Pages, …). The client auto-detects `static` mode by resolving `./__connection.json` against `document.baseURI` and runs in read-only form. `createBuild` copies the SPA verbatim, so deploying under a custom URL base just means building the SPA with relative asset paths (`vite.base: './'`) — the client discovers the effective base at runtime. - -When `def.spa` is set on the definition, `createBuild` also writes `spa-loader.json` next to `index.html` describing how the deployed SPA sources its data: - -- `'none'` — use the baked RPC dump only (read-only static view). -- `'query'` — hydrate from URL search params. -- `'upload'` — accept a drag-and-drop file. - -Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers. diff --git a/docs/adapters/cac.md b/docs/adapters/cac.md index 98d2e21c..5caf67d6 100644 --- a/docs/adapters/cac.md +++ b/docs/adapters/cac.md @@ -4,7 +4,7 @@ outline: deep # CLI (cac) -The cac adapter wraps a `DevframeDefinition` in a [`cac`](https://github.com/cacjs/cac)-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, builds SPA bundles, or starts an MCP server. +The cac adapter wraps a `DevframeDefinition` in a [`cac`](https://github.com/cacjs/cac)-powered command-line interface. From one entry it spins up an `h3` dev server with WebSocket RPC, builds static snapshots, or starts an MCP server. `cac` is an optional peer dependency, pulled in only through this adapter — install it alongside `devframe` to opt into `createCac`: @@ -68,7 +68,7 @@ defineDevframe({ id: 'my-devframe', cli: { command: 'my-devframe', // binary name; default: the id - distDir: './client/dist', // required for dev/build/spa + distDir: './client/dist', // required for dev/build port: 7777, // preferred port portRange: [7777, 9000], // passed through to get-port-please random: false, // passed through to get-port-please diff --git a/docs/adapters/index.md b/docs/adapters/index.md index a4f0a7ab..d8e217d6 100644 --- a/docs/adapters/index.md +++ b/docs/adapters/index.md @@ -25,7 +25,7 @@ A devframe's SPA basePath depends on which adapter is running it: | Adapter kind | Default basePath | Reason | |--------------|------------------|--------| -| `cli`, `spa`, `build` (standalone) | `/` | The devframe owns the origin. | +| `cli`, `build` (standalone) | `/` | The devframe owns the origin. | | `vite`, `embedded` (hosted) | `/__/` | The devframe shares the origin with a host app and namespaces itself. | Override either side explicitly with `DevframeDefinition.basePath`: diff --git a/docs/errors/DF0058.md b/docs/errors/DF0058.md new file mode 100644 index 00000000..8dfb0ed8 --- /dev/null +++ b/docs/errors/DF0058.md @@ -0,0 +1,32 @@ +--- +outline: deep +--- + +# DF0058: Dev Server Disabled By The Definition + +## Message + +> "`{id}`" declares `capabilities.dev: false` — it does not support a live dev server (its value is a static export only). + +## Cause + +`createDevServer` runs unconditionally when called directly, but a definition can opt out of the live dev server via `capabilities.dev: false` — for a devframe whose value is inherently static (e.g. a report generator with nothing to serve live), a dev server would only ever produce an empty or misleading surface. This diagnostic covers a caller invoking `createDevServer()` directly. + +## Example + +```ts +// ✗ Bad — starts a dev server for a devframe that opted out of it +await createDevServer(reportDevframe) // throws DF0058 + +// ✓ Good — run it anyway when you know what you're doing +await createDevServer(reportDevframe, { force: true }) +``` + +## Fix + +- Pass `{ force: true }` to `createDevServer()` to run it anyway. +- Otherwise, drop `capabilities.dev: false` on the definition if a live dev server should be supported after all. + +## Source + +- [`packages/devframe/src/adapters/dev.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/adapters/dev.ts) — `createDevServer()` throws this when `capabilities.dev` is `false` and `force` isn't set. diff --git a/docs/examples/built-with.md b/docs/examples/built-with.md index 45f7b8dd..c1ef055a 100644 --- a/docs/examples/built-with.md +++ b/docs/examples/built-with.md @@ -12,6 +12,6 @@ Real-world devframes: End-to-end examples in this repo, exercising the full adapter surface: -- [**files-inspector**](https://github.com/devframes/devframe/tree/main/examples/files-inspector) — lists files in cwd via RPC; exercises CLI dev/build/spa surfaces. +- [**files-inspector**](https://github.com/devframes/devframe/tree/main/examples/files-inspector) — lists files in cwd via RPC; exercises CLI dev/build surfaces. - [**streaming-chat**](https://github.com/devframes/devframe/tree/main/examples/streaming-chat) — streams synthetic chat tokens from server to client via `ctx.rpc.streaming`. - [**next-runtime-snapshot**](https://github.com/devframes/devframe/tree/main/examples/next-runtime-snapshot) — Next.js App Router SPA over RPC, surfacing the host Node runtime (system info, memory, env). diff --git a/docs/examples/index.md b/docs/examples/index.md index 4d17876a..35e4065d 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -8,7 +8,7 @@ End-to-end examples that exercise the full adapter surface, each a runnable app | Example | UI framework | What it shows | |---------|--------------|---------------| -| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build / spa surfaces. | +| [files-inspector](./files-inspector) | Preact | Lists files in the cwd via RPC; exercises the CLI dev / build surfaces. | | [json-render](./json-render) | Vue | A server-authored JSON-render view rendered by `@devframes/json-render-ui`, with live state and an action bridge. | | [streaming-chat](./streaming-chat) | Preact | Streams synthetic chat tokens server → client, with history kept in shared state. | | [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. | diff --git a/docs/frameworks/nuxt.md b/docs/frameworks/nuxt.md index 1cd21e27..ffd6a605 100644 --- a/docs/frameworks/nuxt.md +++ b/docs/frameworks/nuxt.md @@ -113,7 +113,7 @@ my-tool/ └── public/ # Nuxt build output, pointed at by cli.distDir ``` -In dev (`nuxt dev`) the bridge is live. In production (` build` then ` spa`) the SPA loads the static dump. +In dev (`nuxt dev`) the bridge is live. In production (` build`) the SPA loads the static dump. ## How it works diff --git a/docs/guide/devframe-definition.md b/docs/guide/devframe-definition.md index 888f5b54..efc65d6d 100644 --- a/docs/guide/devframe-definition.md +++ b/docs/guide/devframe-definition.md @@ -48,13 +48,11 @@ export default defineDevframe({ | `homepage` | `string` | **Required.** Project homepage or documentation URL. | | `description` | `string` | **Required.** One-line summary of what the tool does. | | `icon` | `string \| { light, dark }` | Optional Iconify name or URL; supports light/dark pairs. | -| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `spa` / `build`), `/./` for hosted (`vite` / `embedded`). | +| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `build`), `/./` for hosted (`vite` / `embedded`). | | `duplicationStrategy` | `'warn' \| 'silent' \| 'throw' \| 'duplicate'` | How a hub reacts when another devframe sharing this `id` is mounted onto the same hub. Defaults to `'warn'`. See [Hub](./hub). Hub adapters consult it; standalone adapters ignore it. | -| `capabilities` | `{ dev?, build?, spa? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. | +| `capabilities` | `{ dev?, build? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. | | `setup` | `(ctx, info?) => void \| Promise` | **Required.** Server-side entry point. Runs in every runtime. The optional second argument carries runtime metadata — most notably the parsed CLI `flags` when running under `createCac`. | -| `setupBrowser` | `(ctx) => void \| Promise` | Browser-only entry used by the SPA adapter. | | `cli` | `DevframeCliOptions` | Defaults for the CLI adapter. See [CLI options](#cli-options) below. | -| `spa` | `DevframeSpaOptions` | Defaults for the SPA adapter (`base`, `loader`). | ### Sourcing metadata from `package.json` @@ -171,23 +169,6 @@ Each devframe-level host has a dedicated page: - [Agent-Native](./agent-native) — `ctx.agent` - [Cross-Plugin Services](./services) — `ctx.services` -## Browser setup - -The SPA adapter supports a `setupBrowser(ctx)` hook that runs inside the deployed client bundle. Use it for tools that perform their own in-browser work — parsing a dropped file, calling public APIs from the client, etc. - -```ts -defineDevframe({ - id: 'my-devframe', - name: 'My Devframe', - setup(ctx) { /* server-side */ }, - setupBrowser(ctx) { - // `ctx.rpc` is the write-disabled static client in SPA mode. - }, -}) -``` - -Deployed SPAs that use `setupBrowser` ship their own client entry that registers the handlers. - ## CLI options `cli` configures the CLI adapter's defaults and plugs additional flags/commands into the CAC instance: @@ -198,7 +179,7 @@ defineDevframe({ name: 'My Devframe', cli: { command: 'my-devframe', // binary name; default: the `id` - distDir: './client/dist', // required for dev / build / spa + distDir: './client/dist', // required for dev / build port: 9876, // preferred port; default: 9999 portRange: [9876, 10000], // forwarded to get-port-please random: false, // forwarded to get-port-please @@ -220,7 +201,7 @@ defineDevframe({ | Field | Type | Description | |-------|------|-------------| | `command` | `string` | Binary name surfaced in `--help`. Default: the definition's `id`. | -| `distDir` | `string` | SPA dist directory. **Required** for `dev` / `build` / `spa`. | +| `distDir` | `string` | SPA dist directory. **Required** for `dev` / `build`. | | `port` | `number` | Preferred port for the dev server. | | `portRange` | `[number, number]` | Port scan range, passed through to `get-port-please`. | | `random` | `boolean` | Prefer a random open port. | @@ -231,18 +212,6 @@ defineDevframe({ `setup(ctx, info)` receives `info.flags` populated from both devframe's built-in flags and any you declared via `configure` — saves duplicating flag parsing. -## SPA options - -```ts -defineDevframe({ - id: 'my-devframe', - spa: { - base: '/', - loader: 'query', // 'query' | 'upload' | 'none' - }, -}) -``` - See [Adapters](/adapters/) for how each adapter consumes these. ## Multiple runtimes, one definition diff --git a/docs/guide/index.md b/docs/guide/index.md index e101ff76..6a08dfdb 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -15,7 +15,7 @@ Devframe keeps its surface focused on one tool, so the same definition stays por - **One tool per definition.** A devframe describes a single integration. Deploy it through any adapter; host-level features that only matter when several tools share a UI (palettes, cross-tool toasts, unified terminals) come from whichever host you mount into — Vite DevTools is one example. - **Headless.** Hook into `onReady`, `cli.configure`, and friends to print your own startup banners and styling — Devframe stays out of the way. - **App-owned file watching.** Wire your own watcher (chokidar, fs.watch, …) and signal change via `ctx.rpc.sharedState.set(...)` or event-typed RPCs. -- **Context-aware mount paths.** Standalone adapters (`cli`, `spa`, `build`) serve at `/` by default; hosted adapters (`vite`, `embedded`) serve at `/./`. Override via `DevframeDefinition.basePath`. +- **Context-aware mount paths.** Standalone adapters (`cli`, `build`) serve at `/` by default; hosted adapters (`vite`, `embedded`) serve at `/./`. Override via `DevframeDefinition.basePath`. - **SPAs own their base at runtime.** Build with relative asset paths (`vite.base: './'`); `connectDevframe` discovers the effective base from the executing script's location. - **CLI flags compose.** The `cac` instance is exposed to both the devframe (`cli.configure`) and the caller of `createCac`, so capability flags and app flags merge cleanly. diff --git a/docs/guide/json-render.md b/docs/guide/json-render.md index ea2493de..e2084e58 100644 --- a/docs/guide/json-render.md +++ b/docs/guide/json-render.md @@ -94,7 +94,7 @@ validation. `@devframes/json-render-ui/spa` ships a prebuilt renderer, so an app serves a JSON-render UI without authoring or building any client. Wrap the definition with `createJsonRenderDevframe` — it points `cli.distDir` at the shipped SPA -(`jsonRenderSpaDir`) and sets `spa.loader: 'none'`: +(`jsonRenderSpaDir`): ```ts import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' diff --git a/docs/guide/standalone-cli.md b/docs/guide/standalone-cli.md index 88c0bff1..72e42c48 100644 --- a/docs/guide/standalone-cli.md +++ b/docs/guide/standalone-cli.md @@ -4,7 +4,7 @@ outline: deep # Standalone CLI with Devframe -This recipe walks through building a standalone CLI devframe on top of Devframe — the shape where a user runs `npx my-tool` and gets a local dev server serving a Vue / Nuxt / React SPA backed by type-safe RPC, plus `build` / `spa` / `mcp` subcommands for free. +This recipe walks through building a standalone CLI devframe on top of Devframe — the shape where a user runs `npx my-tool` and gets a local dev server serving a Vue / Nuxt / React SPA backed by type-safe RPC, plus `build` / `mcp` subcommands for free. It's the pattern used by tools like an ESLint config inspector or a bundler-config viewer: a binary that opens a browser. @@ -345,7 +345,7 @@ For typed flag schemas, `parseCliFlags(schema, rawBag)` (from `devframe/adapters ## Why this shape -- **One command, one binary.** `createCac` is a complete CLI — dev, build, spa, mcp all from a single `defineDevframe` value. +- **One command, one binary.** `createCac` is a complete CLI — dev, build, mcp all from a single `defineDevframe` value. - **Headless.** Your `onReady` callback owns startup output, so your tool's stdout stays yours. - **Base-agnostic.** Same SPA build works at `/` (dev, standalone static) and at any deployment base. - **Typed end-to-end.** RPC function definitions flow their types through to the client `rpc.call` site. diff --git a/examples/files-inspector/README.md b/examples/files-inspector/README.md index 4cf21e39..30224f42 100644 --- a/examples/files-inspector/README.md +++ b/examples/files-inspector/README.md @@ -27,5 +27,5 @@ pnpm -C examples/files-inspector run test # E2E tests |------|---------| | `src/devframe.ts` | The single `DevframeDefinition` consumed by every adapter. | | `src/client/` | Preact SPA: `index.html`, `main.tsx`, `app.tsx`, `routes/*`, `vite.config.ts`. | -| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `spa`, `mcp`. | +| `bin.mjs` | `createCac(devframe).parse()` - exposes `dev`, `build`, `mcp`. | | `tests/` | E2E tests for CLI dev server and static build. | diff --git a/examples/files-inspector/package.json b/examples/files-inspector/package.json index d7d23cbb..814eb681 100644 --- a/examples/files-inspector/package.json +++ b/examples/files-inspector/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.9.0-beta.4", "private": true, - "description": "Devframe demo that lists files in the working directory over RPC across the dev, build, and spa surfaces.", + "description": "Devframe demo that lists files in the working directory over RPC across the dev and build surfaces.", "homepage": "https://github.com/devframes/devframe/tree/main/examples/files-inspector", "main": "src/devframe.ts", "bin": { diff --git a/examples/files-inspector/src/devframe.ts b/examples/files-inspector/src/devframe.ts index 19830e48..97e39525 100644 --- a/examples/files-inspector/src/devframe.ts +++ b/examples/files-inspector/src/devframe.ts @@ -26,7 +26,6 @@ export default defineDevframe({ // register the instance for `devframe connect` discovery. mcp: true, }, - spa: { loader: 'none' }, setup(ctx) { // A scoped context auto-namespaces every registered id with `NAMESPACE:`. const my = ctx.scope(NAMESPACE) diff --git a/examples/files-inspector/tests/static-build.test.ts b/examples/files-inspector/tests/static-build.test.ts index a06951eb..5ccb2131 100644 --- a/examples/files-inspector/tests/static-build.test.ts +++ b/examples/files-inspector/tests/static-build.test.ts @@ -89,23 +89,4 @@ describe('static build (CLI build surface)', () => { ) as { output: string[] } expect(record.output).toEqual(['README.md', 'package.json', 'sample.txt']) }) - - it('writes spa-loader.json honoring a custom base when def.spa is set', async () => { - // The example's devframe sets `spa: { loader: 'none' }`, which opts - // into the spa-loader sidecar. A `--base` override should be reflected - // verbatim in the loader descriptor without forcing a rebuild - the - // SPA bundle itself uses runtime base discovery, so the descriptor is - // the only place the deploy base needs to land. - const out = await mkdtemp(path.join(os.tmpdir(), 'devframe-files-inspector-base-')) - try { - await createBuild(devframe, { outDir: out, base: '/custom-base/' }) - const loader = JSON.parse( - await readFile(path.join(out, 'spa-loader.json'), 'utf-8'), - ) as { version: number, mode: string, base: string } - expect(loader).toEqual({ version: 1, mode: 'none', base: '/custom-base/' }) - } - finally { - await rm(out, { recursive: true, force: true }) - } - }) }) diff --git a/examples/json-render/src/devframe.ts b/examples/json-render/src/devframe.ts index c4afb962..3276f741 100644 --- a/examples/json-render/src/devframe.ts +++ b/examples/json-render/src/devframe.ts @@ -2,9 +2,9 @@ import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' import pkg from '../package.json' with { type: 'json' } import { createDashboardView } from './dashboard.ts' -// `createJsonRenderDevframe` presets `spa.loader: 'none'` and points -// `cli.distDir` at the prebuilt `@devframes/json-render-ui` SPA, so this -// example serves the out-of-box renderer with no client build of its own. +// `createJsonRenderDevframe` points `cli.distDir` at the prebuilt +// `@devframes/json-render-ui` SPA, so this example serves the out-of-box +// renderer with no client build of its own. export default createJsonRenderDevframe({ id: 'example:json-render', name: 'JSON-Render', diff --git a/examples/next-runtime-snapshot/src/devframe.ts b/examples/next-runtime-snapshot/src/devframe.ts index 34fa7d62..0c075065 100644 --- a/examples/next-runtime-snapshot/src/devframe.ts +++ b/examples/next-runtime-snapshot/src/devframe.ts @@ -25,7 +25,6 @@ export default defineDevframe({ distDir, auth: false, }, - spa: { loader: 'none' }, setup(ctx) { // A scoped context auto-namespaces every registered id with `NAMESPACE:`. const my = ctx.scope(NAMESPACE) diff --git a/examples/streaming-chat/src/devframe.ts b/examples/streaming-chat/src/devframe.ts index bf6548e1..a837c66f 100644 --- a/examples/streaming-chat/src/devframe.ts +++ b/examples/streaming-chat/src/devframe.ts @@ -27,7 +27,6 @@ export default defineDevframe({ // Vite-side surface requires. auth: false, }, - spa: { loader: 'none' }, async setup(ctx) { // A scoped context auto-namespaces channel + state ids with `NAMESPACE:`. const my = ctx.scope(NAMESPACE) diff --git a/packages/devframe/src/adapters/__tests__/cac.test.ts b/packages/devframe/src/adapters/__tests__/cac.test.ts index 159dc84c..1c075737 100644 --- a/packages/devframe/src/adapters/__tests__/cac.test.ts +++ b/packages/devframe/src/adapters/__tests__/cac.test.ts @@ -26,12 +26,9 @@ describe('adapters/cac', () => { expect(cli.commands.map(c => c.name)).not.toContain('build') }) - it('still registers build when capabilities.build is true or a record', () => { + it('still registers build when capabilities.build is true', () => { const truthy = createCac(baseDevframe({ capabilities: { build: true } })) expect(truthy.cli.commands.map(c => c.name)).toContain('build') - - const record = createCac(baseDevframe({ capabilities: { build: { dump: true } } })) - expect(record.cli.commands.map(c => c.name)).toContain('build') }) it('always registers the dev and mcp commands regardless of capabilities.build', () => { diff --git a/packages/devframe/src/adapters/__tests__/dev.test.ts b/packages/devframe/src/adapters/__tests__/dev.test.ts index 39729ded..21e5acd9 100644 --- a/packages/devframe/src/adapters/__tests__/dev.test.ts +++ b/packages/devframe/src/adapters/__tests__/dev.test.ts @@ -46,6 +46,44 @@ async function connectRaw(url: string, origin?: string): Promise<'open' | 'close } describe('adapters/dev', () => { + it('rejects a definition with capabilities.dev: false by default', async () => { + const devframe = defineDevframe({ + id: 'devframe-dev-disabled', + name: 'Dev Disabled', + version: '0.0.0', + packageName: 'devframe-test', + homepage: 'https://example.test', + description: 'Test devframe.', + capabilities: { dev: false }, + setup: () => {}, + }) + await expect( + createDevServer(devframe, { host: '127.0.0.1', port: 0, openBrowser: false }), + ).rejects.toThrow(/capabilities\.dev: false/) + }) + + it('proceeds past capabilities.dev: false when force is set', async () => { + const devframe = defineDevframe({ + id: 'devframe-dev-disabled-forced', + name: 'Dev Disabled Forced', + version: '0.0.0', + packageName: 'devframe-test', + homepage: 'https://example.test', + description: 'Test devframe.', + capabilities: { dev: false }, + setup: () => {}, + }) + const host = '127.0.0.1' + const port = await getPort({ port: 19398, host }) + const handle = await createDevServer(devframe, { host, port, openBrowser: false, force: true }) + try { + expect(handle.port).toBe(port) + } + finally { + await handle.close() + } + }) + it('createDevServer starts, exposes __connection.json, and closes', async () => { const distDir = makeTmpDist() const devframe = defineDevframe({ diff --git a/packages/devframe/src/adapters/_shared.ts b/packages/devframe/src/adapters/_shared.ts index 0579a2f6..1619116a 100644 --- a/packages/devframe/src/adapters/_shared.ts +++ b/packages/devframe/src/adapters/_shared.ts @@ -9,7 +9,7 @@ const DEFAULT_PORT = 9999 /** * Resolve the mount base path for a devframe's SPA. Hosted adapters * (`vite`, `embedded`) default to `/__/` so they don't collide - * with the host app; standalone adapters (`cli`, `spa`, `build`) + * with the host app; standalone adapters (`cli`, `build`) * default to `/` because they own the origin. * * The devframe author can override with `basePath` on the definition. diff --git a/packages/devframe/src/adapters/build.ts b/packages/devframe/src/adapters/build.ts index 5b833d08..d8c06d93 100644 --- a/packages/devframe/src/adapters/build.ts +++ b/packages/devframe/src/adapters/build.ts @@ -16,13 +16,10 @@ import { diagnostics } from '../node/diagnostics' import { createH3DevframeHost } from '../node/host-h3' import { collectStaticRpcDump } from '../rpc/dump/static' import { strictJsonStringify } from '../rpc/serialization' -import { resolveBasePath } from './_shared' export interface CreateBuildOptions { /** Output directory. Defaults to `dist-static`. */ outDir?: string - /** Absolute URL base the output is served from (default: `/`). */ - base?: string /** * Override the SPA dist directory to copy into `outDir`. When omitted * the adapter reads `devframe.cli?.distDir` — authors typically set this @@ -51,11 +48,9 @@ export interface CreateBuildOptions { * - Copy the author's SPA dist into `/`. * - Write `/__connection.json` (`{ backend: 'static' }`) and the * sharded RPC dump under `/__rpc-dump/` so the deployed SPA - * discovers both via relative paths from `document.baseURI`. - * - When `def.spa` is configured, also write `/spa-loader.json` - * describing the SPA's data-loader mode (`'query'` / `'upload'` / - * `'none'`). The output is mount-path agnostic — the same bundle - * works at `/`, `/devframe/`, or any base, no rewriting required. + * discovers both via relative paths from `document.baseURI`. The + * output is mount-path agnostic — the same bundle works at `/`, + * `/devframe/`, or any base, no rewriting required. */ export async function createBuild(d: DevframeDefinition, options: CreateBuildOptions = {}): Promise { if (d.capabilities?.build === false && !options.force) @@ -118,19 +113,5 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt 'utf-8', ) - if (d.spa) { - const base = options.base ?? resolveBasePath(d, 'standalone') - const spaLoader = { - version: 1, - mode: d.spa.loader ?? 'none', - base, - } - await fs.writeFile( - resolve(outDir, 'spa-loader.json'), - JSON.stringify(spaLoader, null, 2), - 'utf-8', - ) - } - console.log(c.green`[devframe] built "${d.id}" -> ${outDir}`) } diff --git a/packages/devframe/src/adapters/cac.ts b/packages/devframe/src/adapters/cac.ts index e12af3ad..c2487b61 100644 --- a/packages/devframe/src/adapters/cac.ts +++ b/packages/devframe/src/adapters/cac.ts @@ -112,10 +112,9 @@ export function createCac(d: DevframeDefinition, options: CreateCacOptions = {}) cli .command('build', 'Build a self-contained static deploy of the devframe') .option('--out-dir ', 'Output directory', { default: 'dist-static' }) - .option('--base ', 'URL base', { default: '/' }) .option('--pretty', 'Pretty-print dump JSON (larger on disk)') - .action(async (flags: { outDir: string, base?: string, pretty?: boolean }) => { - await createBuild(d, { outDir: flags.outDir, base: flags.base, pretty: flags.pretty }) + .action(async (flags: { outDir: string, pretty?: boolean }) => { + await createBuild(d, { outDir: flags.outDir, pretty: flags.pretty }) }) } diff --git a/packages/devframe/src/adapters/dev.ts b/packages/devframe/src/adapters/dev.ts index 59881515..13b8fa7d 100644 --- a/packages/devframe/src/adapters/dev.ts +++ b/packages/devframe/src/adapters/dev.ts @@ -111,6 +111,12 @@ export interface CreateDevServerOptions { * otherwise — wire this if you want a startup banner. */ onReady?: (info: { origin: string, port: number, app: H3 }) => void | Promise + /** + * Proceed even when the definition declares `capabilities.dev: false`. + * `createDevServer` otherwise refuses such a definition (`DF0058`) — this + * only matters for a caller invoking it directly, bypassing the CLI. + */ + force?: boolean } /** @@ -135,6 +141,9 @@ export async function createDevServer( def: DevframeDefinition, options: CreateDevServerOptions = {}, ): Promise { + if (def.capabilities?.dev === false && !options.force) + throw diagnostics.DF0058({ id: def.id }) + const host = options.host ?? def.cli?.host ?? 'localhost' const requestedPort = options.port ?? await resolveDevServerPort(def, { host }) const flags = options.flags ?? {} diff --git a/packages/devframe/src/constants.ts b/packages/devframe/src/constants.ts index 40e1bc87..d1945fde 100644 --- a/packages/devframe/src/constants.ts +++ b/packages/devframe/src/constants.ts @@ -44,7 +44,6 @@ export const DEVFRAME_SSE_SESSION_HEADER = 'x-birpc-session' export const DEVFRAME_MCP_ROUTE = '__mcp' export const DEVFRAME_RPC_DUMP_MANIFEST_FILENAME = '__rpc-dump/index.json' export const DEVFRAME_DOCK_IMPORTS_FILENAME = '__client-imports.js' -export const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID = '/__devframe-client-imports.js' export const DEVFRAME_RPC_DUMP_DIRNAME = '__rpc-dump' /** diff --git a/packages/devframe/src/node/diagnostics.ts b/packages/devframe/src/node/diagnostics.ts index 657c6dfc..f72118f2 100644 --- a/packages/devframe/src/node/diagnostics.ts +++ b/packages/devframe/src/node/diagnostics.ts @@ -132,5 +132,9 @@ export const diagnostics = defineDiagnostics({ why: () => 'This instance disables its WebSocket transport (`ws: false`), so there is no socket to drive upgrades into.', fix: 'Clients connect over the SSE endpoint instead — no upgrade wiring is needed. Remove `ws: false` if the instance should serve a WebSocket after all.', }, + DF0058: { + why: (p: { id: string }) => `"${p.id}" declares \`capabilities.dev: false\` — it does not support a live dev server (its value is a static export only).`, + fix: 'Pass `{ force: true }` to `createDevServer()` to run it anyway, or drop `capabilities.dev: false` on the definition.', + }, }, }) diff --git a/packages/devframe/src/node/host-h3.ts b/packages/devframe/src/node/host-h3.ts index 90093c74..935fb542 100644 --- a/packages/devframe/src/node/host-h3.ts +++ b/packages/devframe/src/node/host-h3.ts @@ -4,8 +4,6 @@ import process from 'node:process' import { join } from 'pathe' export interface CreateH3DevframeHostOptions { - /** The h3 app instance — registered once the CLI adapter lands. */ - app?: unknown /** * Host the standalone server listens on, e.g. `http://localhost:9999`. * Consumed by `resolveOrigin` for dock entries that need an absolute URL. @@ -15,8 +13,8 @@ export interface CreateH3DevframeHostOptions { origin: string | (() => string) /** * Register a static-file handler at `base` serving files from `distDir`. - * Wired into the h3 app once the CLI adapter lands (commit 5). For now - * the CLI isn't running, so the default is a no-op. + * `mountStatic` forwards to it; when omitted the host serves no SPA + * (bridge mode, where the SPA is hosted elsewhere). */ mount?: (base: string, distDir: string) => void | Promise /** diff --git a/packages/devframe/src/types/devframe.ts b/packages/devframe/src/types/devframe.ts index 383f80f5..e05aaff8 100644 --- a/packages/devframe/src/types/devframe.ts +++ b/packages/devframe/src/types/devframe.ts @@ -3,13 +3,11 @@ import type { CliFlagsSchema } from '../adapters/flags' import type { DevframeAuthHandler } from '../node/auth/handler' import type { DevframeNodeContext } from './context' -export type DevframeRuntime = 'cli' | 'build' | 'spa' | 'vite' | 'embedded' - /** * Classification of how a devframe is being deployed. Hosted adapters * (`vite`, `embedded`) share their origin with a host app and must * namespace their mount path under `/__/`. Standalone adapters - * (`cli`, `spa`, `build`) own the origin and default to `/`. + * (`cli`, `build`) own the origin and default to `/`. */ export type DevframeDeploymentKind = 'standalone' | 'hosted' @@ -132,7 +130,7 @@ export interface DevframeCliOptions { /** * Authentication for the standalone dev server. * - * - `undefined` / `true` — the standalone adapters (`cli` / `spa` / + * - `undefined` / `true` — the standalone adapters (`cli` / * served `build`) auto-wire devframe's interactive OTP auth * (`createInteractiveAuth`): an untrusted client can only reach * `anonymous:` methods until it exchanges the printed one-time code. @@ -182,7 +180,7 @@ export interface DevframeCliOptions { sse?: boolean | DevframeSseOptions /** * Capability-side CAC hook. Called with the CAC instance after the - * adapter registers its built-in commands (`build` / `spa` / `mcp`) + * adapter registers its built-in commands (`build` / `mcp`) * but before `createCac`'s own `configureCli` caller. Use this to * contribute tool-specific flags and subcommands from the definition * itself. @@ -224,7 +222,7 @@ export interface DevframeCliOptions { * * Every field is optional. `title` / `icon` default to the definition's * `name` / `icon` when omitted here; the rest are unset by default. - * Standalone adapters (`cli` / `spa` / `build`) ignore this entirely. + * Standalone adapters (`cli` / `build`) ignore this entirely. */ export interface DevframeDockDefaults { /** Dock entry title. Defaults to the definition's `name`. */ @@ -259,24 +257,6 @@ export interface DevframeDockDefaults { groupId?: string } -export interface DevframeSpaOptions { - base?: string - /** - * How the deployed SPA loads its data. - * - `'query'` — read from URL search params. - * - `'upload'` — accept a file drag-drop. - * - `'none'` — use the baked RPC dump only. - */ - loader?: 'query' | 'upload' | 'none' -} - -export interface DevframeBrowserContext { - /** - * The connected RPC client (may be write-disabled in static/spa modes). - */ - rpc: unknown -} - /** * Runtime information threaded into `setup(ctx, info)`. Adapters * populate the fields that make sense for their deployment. In @@ -303,45 +283,44 @@ export interface DevframeDefinition { * Default dock attributes applied when a hub mounts this devframe as an * iframe dock entry. Consulted only by the hub install path (`ctx.install`), * which merge these beneath the per-mount `dock` overrides; standalone - * adapters (`cli` / `spa` / `build`) ignore it. + * adapters (`cli` / `build`) ignore it. * * @see {@link DevframeDockDefaults} */ dock?: DevframeDockDefaults /** * Mount path override. Defaults depend on the adapter: - * `/` for standalone (`cli` / `spa` / `build`), `/__/` for hosted + * `/` for standalone (`cli` / `build`), `/__/` for hosted * (`vite` / `embedded`). */ basePath?: string /** * How a hub reacts when another devframe sharing this one's `id` is * mounted onto the same hub. Consulted only by hub adapters - * (`ctx.install`); standalone adapters (`cli` / `spa` / `build`) + * (`ctx.install`); standalone adapters (`cli` / `build`) * ignore it. * * @default 'warn' */ duplicationStrategy?: DevframeDuplicationStrategy /** - * Declares which runtimes meaningfully support this devframe. Consulted - * by adapters that can act on a plain `false` before doing any work: - * `createCac` skips registering the `build` subcommand entirely when - * `capabilities.build` is `false` — useful for a devframe whose value is - * inherently live (e.g. it manages real files on disk), so a static - * export would only ever produce a broken, write-less shell of the tool. - * The `Record` shape is reserved for finer-grained - * sub-capabilities and currently unconsumed by any adapter. + * Declares which runtimes meaningfully support this devframe. Adapters + * act on a `false` before doing any work: + * + * - `build: false` — `createCac` skips registering the `build` subcommand, + * and `createBuild` refuses (throws `DF0042`) unless `{ force: true }`. + * Useful for a devframe whose value is inherently live (e.g. it manages + * real files on disk), so a static export would only ever produce a + * broken, write-less shell of the tool. + * - `dev: false` — `createDevServer` refuses (throws `DF0058`) unless + * `{ force: true }`. Useful for a devframe that only makes sense as a + * static export (e.g. a report generator with nothing to serve live). */ capabilities?: { - dev?: boolean | Record - build?: boolean | Record - spa?: boolean | Record + dev?: boolean + build?: boolean } /** Server-side setup — the primary entrypoint. Runs in every runtime. */ setup: (ctx: DevframeNodeContext, info?: DevframeSetupInfo) => void | Promise - /** Browser-only setup for the SPA adapter (bundled into the client). */ - setupBrowser?: (ctx: DevframeBrowserContext) => void | Promise cli?: DevframeCliOptions - spa?: DevframeSpaOptions } diff --git a/packages/devframe/src/types/diagnostics.ts b/packages/devframe/src/types/diagnostics.ts index 03d48158..15fd09eb 100644 --- a/packages/devframe/src/types/diagnostics.ts +++ b/packages/devframe/src/types/diagnostics.ts @@ -1,14 +1,5 @@ import type { defineDiagnostics, Diagnostic, DiagnosticDefinition } from 'nostics' -/** - * A diagnostics definition object built with `defineDiagnostics`. Typed as - * `unknown` because each integration's definition has a distinct narrow shape - * (e.g. specific code keys like `DF0001` / `MYP0001`), and TypeScript's mapped - * types don't allow assigning a narrow-keyed result to a generically-keyed - * one. The host stores them in a heterogeneous registry. - */ -export type DevframeDiagnosticsDefinition = ReturnType> - /** * The shared diagnostics lookup exposed by the host. A `Proxy` that resolves * any registered code name to its `nostics` handle (a callable that builds diff --git a/packages/devframe/src/types/host.ts b/packages/devframe/src/types/host.ts index 0555f227..d08a4a4f 100644 --- a/packages/devframe/src/types/host.ts +++ b/packages/devframe/src/types/host.ts @@ -3,10 +3,10 @@ // snapshot, embedded, etc.). // // Host classes (docks, views, ...) call into this interface so they stay -// framework-neutral. Concrete implementations live in each adapter: -// - packages/kit/src/node/vite-host.ts — Vite-backed (dev mode) -// - packages/devframe/src/node/host-h3.ts — h3 CLI server -// - (build/spa/embedded) — added as the respective adapters land +// framework-neutral. The h3-backed implementation +// (`packages/devframe/src/node/host-h3.ts`) serves every standalone runtime +// (CLI dev server, static build, embedded); hosted runtimes provide their own +// (e.g. `@devframes/vite`). export interface DevframeHost { /** diff --git a/packages/devframe/src/types/index.ts b/packages/devframe/src/types/index.ts index 85028ab8..b42b6754 100644 --- a/packages/devframe/src/types/index.ts +++ b/packages/devframe/src/types/index.ts @@ -8,5 +8,4 @@ export * from './rpc' export * from './rpc-augments' export * from './scope' export * from './services' -export * from './utils' export * from './views' diff --git a/packages/devframe/src/types/utils.ts b/packages/devframe/src/types/utils.ts deleted file mode 100644 index 58d7bc88..00000000 --- a/packages/devframe/src/types/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Thenable = T | Promise - -export type EntriesToObject = { - [K in T[number] as K[0]]: K[1] -} - -export type PartialWithoutId = Partial> & { id: string } diff --git a/packages/hub/src/types/index.ts b/packages/hub/src/types/index.ts index e81a1c1a..11e2585c 100644 --- a/packages/hub/src/types/index.ts +++ b/packages/hub/src/types/index.ts @@ -18,7 +18,6 @@ export type { RpcDefinitionsFilter, RpcDefinitionsToFunctions } from 'devframe/r export type { ConnectionMeta, DevframeCapabilities, - DevframeDiagnosticsDefinition, DevframeDiagnosticsHost, DevframeDiagnosticsLogger, DevframeHost, @@ -27,11 +26,9 @@ export type { DevframeRpcServerFunctions, DevframeRpcSharedStates, DevframeViewHost, - EntriesToObject, EventEmitter, EventsMap, EventUnsubscribe, - PartialWithoutId, RpcBroadcastOptions, RpcFunctionsHost, RpcSharedStateGetOptions, @@ -39,5 +36,4 @@ export type { RpcStreamingChannel, RpcStreamingChannelOptions, RpcStreamingHost, - Thenable, } from 'devframe/types' diff --git a/packages/json-render-ui/src/spa.ts b/packages/json-render-ui/src/spa.ts index 2e40420c..4e172eb8 100644 --- a/packages/json-render-ui/src/spa.ts +++ b/packages/json-render-ui/src/spa.ts @@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url' * * ```ts * import { jsonRenderSpaDir } from '@devframes/json-render-ui/spa' - * defineDevframe({ cli: { command: 'my-app', distDir: jsonRenderSpaDir }, spa: { loader: 'none' } }) + * defineDevframe({ cli: { command: 'my-app', distDir: jsonRenderSpaDir } }) * ``` * * This entry is node-safe: it imports no Vue and no `@antfu/design`, so a build @@ -18,9 +18,9 @@ export const jsonRenderSpaDir: string = fileURLToPath(new URL('./spa/', import.m /** * Wrap a devframe definition so it serves the prebuilt {@link jsonRenderSpaDir - * standalone SPA}. Presets `spa.loader: 'none'` and defaults `cli.distDir` to - * the SPA assets (an explicit `cli.distDir` still wins). The author supplies - * everything else (id, name, `setup`, port, …) as usual. + * standalone SPA}. Defaults `cli.distDir` to the SPA assets (an explicit + * `cli.distDir` still wins). The author supplies everything else (id, name, + * `setup`, port, …) as usual. * * ```ts * export default createJsonRenderDevframe({ @@ -33,7 +33,6 @@ export const jsonRenderSpaDir: string = fileURLToPath(new URL('./spa/', import.m export function createJsonRenderDevframe(definition: DevframeDefinition): DevframeDefinition { return { ...definition, - spa: { loader: 'none', ...definition.spa }, cli: { ...definition.cli, distDir: definition.cli?.distDir ?? jsonRenderSpaDir }, } } diff --git a/plugins/a11y/src/cli.ts b/plugins/a11y/src/cli.ts index 50b0c9ba..1fee09de 100644 --- a/plugins/a11y/src/cli.ts +++ b/plugins/a11y/src/cli.ts @@ -6,7 +6,7 @@ import a11yDevframe from './index.ts' * Build the standalone CLI for the a11y inspector — backs the package `bin` * (`devframes_plugin_a11y`) and `pnpx @devframes/plugin-a11y`. Wraps the * default {@link createA11yDevframe} definition with devframe's - * `dev` / `build` / `spa` command shell. + * `dev` / `build` command shell. */ export function createA11yCli(): CacHandle { return createCac(a11yDevframe) diff --git a/plugins/a11y/src/index.ts b/plugins/a11y/src/index.ts index be518fa1..d0250520 100644 --- a/plugins/a11y/src/index.ts +++ b/plugins/a11y/src/index.ts @@ -87,7 +87,6 @@ export function createA11yDevframe(options: A11yDevframeOptions = {}): DevframeD port: options.port ?? 9899, distDir, }, - spa: { loader: 'none' }, setup(ctx) { setupA11y(ctx, { dockId: id, diff --git a/plugins/assets/src/index.ts b/plugins/assets/src/index.ts index d749d1ba..01557a84 100644 --- a/plugins/assets/src/index.ts +++ b/plugins/assets/src/index.ts @@ -120,7 +120,6 @@ export function createAssetsDevframe(options: AssetsDevframeOptions = {}): Devfr cli.option('--read-only', 'Disable upload, rename, delete, and folder creation') }, }, - spa: { loader: 'none' }, dock: { category: '~builtin' }, async setup(ctx, info) { const readOnlyFlag = info?.flags?.readOnly === true diff --git a/plugins/code-server/src/index.ts b/plugins/code-server/src/index.ts index ef134519..09fba77c 100644 --- a/plugins/code-server/src/index.ts +++ b/plugins/code-server/src/index.ts @@ -60,7 +60,6 @@ export function createCodeServerDevframe(options: CodeServerOptions = {}): Devfr // Hosted adapters supply their own auth layer and ignore this. auth: options.auth ?? true, }, - spa: { loader: 'none' }, async setup(ctx) { const { setupCodeServer } = await import('./node/index') await setupCodeServer(ctx, options) diff --git a/plugins/data-inspector/src/index.ts b/plugins/data-inspector/src/index.ts index 30ceb9e2..bbc6479f 100644 --- a/plugins/data-inspector/src/index.ts +++ b/plugins/data-inspector/src/index.ts @@ -76,7 +76,6 @@ export function createDataInspectorDevframe(options: DataInspectorDevframeOption distDir: existsSync(distDir) ? distDir : undefined, auth: options.auth ?? true, }, - spa: { loader: 'none' }, dock: { category: '~builtin' }, setup(ctx) { setupDataInspector(ctx, { exampleSource: options.exampleSource }) diff --git a/plugins/data-inspector/src/spa/vite.config.ts b/plugins/data-inspector/src/spa/vite.config.ts index 1cf06970..369867c4 100644 --- a/plugins/data-inspector/src/spa/vite.config.ts +++ b/plugins/data-inspector/src/spa/vite.config.ts @@ -8,7 +8,7 @@ import { dataInspectorVitePlugin } from '../vite' // The data-inspector SPA. `base: './'` keeps every asset URL relative so the // bundle is mount-path portable — it discovers its runtime base from // `document.baseURI` and connects via `connectDevframe()`. The build is -// copied verbatim by `createBuild`/`createSpa`; no HTML rewriting. +// copied verbatim by `createBuild`; no HTML rewriting. // // `dataInspectorVitePlugin({ bridge: true })` dogfoods the plugin: it // runs a side-car RPC + WS backend (with the built-in example source) next to diff --git a/plugins/git/src/index.ts b/plugins/git/src/index.ts index e0edbc71..898094ca 100644 --- a/plugins/git/src/index.ts +++ b/plugins/git/src/index.ts @@ -75,7 +75,6 @@ export function createGitDevframe(options: GitDevframeOptions = {}): DevframeDef cli.option('--write', 'Enable staging, unstaging, and committing from the UI') }, }, - spa: { loader: 'none' }, setup(ctx, info) { const write = options.write ?? info?.flags?.write === true configureGit(ctx, { diff --git a/plugins/inspect/src/cli.ts b/plugins/inspect/src/cli.ts index 6f072582..7bc6a143 100644 --- a/plugins/inspect/src/cli.ts +++ b/plugins/inspect/src/cli.ts @@ -6,7 +6,7 @@ import inspectDevframe from './index' * Build the standalone CLI for the inspector — backs the package `bin` * (`devframe-inspect`) and `pnpx @devframes/plugin-inspect`. Wraps the * default {@link createInspectDevframe} definition with devframe's - * `dev` / `build` / `spa` / `mcp` command shell. + * `dev` / `build` / `mcp` command shell. */ export function createInspectCli(): CacHandle { return createCac(inspectDevframe) diff --git a/plugins/inspect/src/index.ts b/plugins/inspect/src/index.ts index b4671fcd..78553b7c 100644 --- a/plugins/inspect/src/index.ts +++ b/plugins/inspect/src/index.ts @@ -38,7 +38,7 @@ export interface InspectDevframeOptions { /** * Build a {@link DevframeDefinition} for the Devframe Inspector. The - * same definition runs standalone (`/cli`, `/spa`, `/build`) and mounts + * same definition runs standalone (`/cli`, `/build`) and mounts * into a host (`/vite`, hub). * * @experimental This plugin is experimental and may change without a major @@ -64,7 +64,6 @@ export function createInspectDevframe(options: InspectDevframeOptions = {}): Dev // Hosted adapters (Vite/hub) supply their own auth layer and ignore this. auth: options.auth ?? true, }, - spa: { loader: 'none' }, dock: { category: '~builtin', }, diff --git a/plugins/inspect/src/spa/vite.config.ts b/plugins/inspect/src/spa/vite.config.ts index 3edfcbae..d83059b1 100644 --- a/plugins/inspect/src/spa/vite.config.ts +++ b/plugins/inspect/src/spa/vite.config.ts @@ -8,7 +8,7 @@ import { inspectVitePlugin } from '../vite' // The inspector SPA. `base: './'` keeps every asset URL relative so the // bundle is mount-path portable — it discovers its runtime base from // `document.baseURI` and connects via `connectDevframe()`. The build is -// copied verbatim by `createBuild`/`createSpa`; no HTML rewriting. +// copied verbatim by `createBuild`; no HTML rewriting. export default defineConfig({ base: './', root: fileURLToPath(new URL('.', import.meta.url)), diff --git a/plugins/messages/src/cli.ts b/plugins/messages/src/cli.ts index 6cee4f1e..05160c94 100644 --- a/plugins/messages/src/cli.ts +++ b/plugins/messages/src/cli.ts @@ -6,7 +6,7 @@ import messagesDevframe from './index' * Build the standalone CLI for the messages panel — backs the package `bin` * (`devframe-messages`) and `pnpx @devframes/plugin-messages`. Wraps the * default {@link createMessagesDevframe} definition with devframe's - * `dev` / `build` / `spa` / `mcp` command shell. + * `dev` / `build` / `mcp` command shell. */ export function createMessagesCli(): CacHandle { return createCac(messagesDevframe) diff --git a/plugins/messages/src/index.ts b/plugins/messages/src/index.ts index 6a558818..709d2359 100644 --- a/plugins/messages/src/index.ts +++ b/plugins/messages/src/index.ts @@ -38,7 +38,7 @@ export interface MessagesDevframeOptions { * Build a {@link DevframeDefinition} for the hub message feed panel — * a portable view over `ctx.messages`, ported from vitejs/devtools' * built-in Messages view. The same definition runs standalone - * (`/cli`, `/spa`, `/build`) and mounts into a host (`/vite`, hub); + * (`/cli`, `/build`) and mounts into a host (`/vite`, hub); * a hub host is what feeds it live entries. * * @experimental This plugin is experimental and may change without a major @@ -67,7 +67,6 @@ export function createMessagesDevframe(options: MessagesDevframeOptions = {}): D dock: { category: '~builtin', }, - spa: { loader: 'none' }, setup(ctx) { setupMessages(ctx) }, diff --git a/plugins/messages/src/spa/vite.config.ts b/plugins/messages/src/spa/vite.config.ts index 9a3ae4af..3d579d3d 100644 --- a/plugins/messages/src/spa/vite.config.ts +++ b/plugins/messages/src/spa/vite.config.ts @@ -9,7 +9,7 @@ import { createMessagesDevDevframe } from './dev-host' // The messages panel SPA. `base: './'` keeps every asset URL relative so the // bundle is mount-path portable — it discovers its runtime base from // `document.baseURI` and connects via `connectDevframe()`. The build is -// copied verbatim by `createBuild`/`createSpa`; no HTML rewriting. +// copied verbatim by `createBuild`; no HTML rewriting. // // `pnpm dev` self-hosts through the demo-seeded dev harness (a stand-in hub // messages host) so the feed is lively without a full hub host. diff --git a/plugins/og/src/index.ts b/plugins/og/src/index.ts index d5b39ed7..05214515 100644 --- a/plugins/og/src/index.ts +++ b/plugins/og/src/index.ts @@ -46,7 +46,6 @@ export function createOgDevframe(options: OgDevframeOptions = {}): DevframeDefin distDir: existsSync(distDir) ? distDir : undefined, auth: options.auth ?? true, }, - spa: { loader: 'query' }, dock: { category: '~builtin' }, setup(ctx) { setupOg(ctx, { defaultUrl: options.defaultUrl, fetch: options.fetch }) diff --git a/plugins/og/test/static-build.test.ts b/plugins/og/test/static-build.test.ts index 5182a429..769e3003 100644 --- a/plugins/og/test/static-build.test.ts +++ b/plugins/og/test/static-build.test.ts @@ -32,12 +32,10 @@ describe('open Graph static build', () => { await rm(outDir, { recursive: true, force: true }) }) - it('copies the Vue SPA and query loader metadata', async () => { + it('copies the Vue SPA with relative asset URLs', async () => { const html = await readFile(path.join(outDir, 'index.html'), 'utf8') - const loader = JSON.parse(await readFile(path.join(outDir, 'spa-loader.json'), 'utf8')) expect(html).toContain('') expect(html).toMatch(/src="\.\/assets\/[^"?]+\.js"/) - expect(loader).toMatchObject({ mode: 'query' }) }) it('bakes the default target into the RPC dump', async () => { diff --git a/plugins/terminals/src/index.ts b/plugins/terminals/src/index.ts index 32ba788c..0f9b4999 100644 --- a/plugins/terminals/src/index.ts +++ b/plugins/terminals/src/index.ts @@ -61,7 +61,6 @@ export function createTerminalsDevframe(options: TerminalsOptions = {}): Devfram // the tab lands already trusted. auth: options.auth ?? true, }, - spa: { loader: 'none' }, dock: { category: '~builtin', }, diff --git a/skills/devframe/SKILL.md b/skills/devframe/SKILL.md index d3416c5f..af2f4124 100644 --- a/skills/devframe/SKILL.md +++ b/skills/devframe/SKILL.md @@ -5,7 +5,7 @@ description: > framework- and build-tool-agnostic foundation for defining a devtool once and serving it in many places. Covers DevframeDefinition, picking the right deployment adapter - (cli / build / spa / vite / embedded / mcp), designing RPC + (cli / build / vite / embedded / mcp), designing RPC contracts, exposing an agent-native surface over MCP, and wiring the author's SPA client. For host-level features (docks, terminals, palette, etc.), the devframe can be mounted into a @@ -85,7 +85,7 @@ export default defineDevframe({ `setup(ctx)` registers RPC functions, shared state, diagnostics, and any other devframe-level wiring. Host adapters can augment `ctx` with extra surfaces - for example, mounting into Vite DevTools via `createPluginFromDevframe(d)` exposes `docks`, `terminals`, `messages`, and `commands` on the augmented context, and the kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For richer host-side behaviour (custom-render, terminals, palette commands) pass `options.setup` to `createPluginFromDevframe`. -See `templates/counter-devframe.ts` for a runnable counter example, `templates/spa-devframe.ts` for an SPA-ready shape, and `templates/vite-client.ts` for the author's client entry. +See `templates/counter-devframe.ts` for a runnable counter example and `templates/vite-client.ts` for the author's client entry. ## Scoped context (preferred) @@ -504,7 +504,7 @@ const data = await my.rpc.call('get-stats', { limit: 10 }) `connectDevframe` auto-detects the backend via `/.devframe/.connection.json`: - **websocket** (dev mode) - full read/write, requires auth handshake. Listen for token updates on the `devframe-auth` BroadcastChannel. -- **static** (build / spa output) - read-only, resolves calls from the baked RPC dump. +- **static** (build output) - read-only, resolves calls from the baked RPC dump. Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(defineRpcFunction(...))` to receive server broadcasts, `my.rpc.callOptional(...)` when a missing handler should resolve to `undefined` instead of throwing, and `my.settings.{project,global}` for persisted per-workspace / per-user settings synced from the server. @@ -530,13 +530,12 @@ At runtime, static clients look up the argument hash in the dump; misses resolve ## CLI adapter subcommands -`createCac(devframe).parse()` gives the tool four subcommands out of the box: +`createCac(devframe).parse()` gives the tool three subcommands out of the box: | Subcommand | Action | |------------|--------| | *(default)* | Dev server on port 9999 (or `--port`) - WebSocket RPC, `cli.distDir` served at `/.devframe/` | | `build` | Static snapshot → `./dist-static/` (configurable via `--out-dir`) | -| `spa` | Deployable SPA → `./dist-spa/` | | `mcp` | stdio MCP server | **Bring your own CLI framework?** `createCac` (`devframe/adapters/cac`) is just a cac wrapper around three peer factories - `createDevServer` (`devframe/adapters/dev`), `createBuild` (`devframe/adapters/build`), and `createMcpServer` (`devframe/adapters/mcp`). Use them directly with commander/yargs/oclif when `createCac`'s baked-in command structure doesn't fit. `cac` is an optional peer dependency pulled in only through `devframe/adapters/cac`, so bring-your-own-CLI tools run without installing it. `createDevServer` returns a `StartedServer` handle (`origin`, `port`, `app`, `wss`, `close()`) so you can wire SIGINT / hot-reload teardown into the surrounding program. `parseCliFlags(schema, raw)` and `defineCliFlags(...)` (both from `devframe/adapters/cac`) validate an arbitrary flag bag against a `CliFlagsSchema` - the helpers are framework-agnostic. diff --git a/skills/devframe/templates/spa-devframe.ts b/skills/devframe/templates/spa-devframe.ts deleted file mode 100644 index 801de29e..00000000 --- a/skills/devframe/templates/spa-devframe.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Devframe with setupBrowser + SPA query-loader — deployable as a static site. -// Host adapters (e.g. the `vite` adapter for Vite DevTools) auto-derive their -// mount entry from `id` / `name` / `icon`. -import { defineDevframe, defineRpcFunction } from 'devframe' -// Recommended: source version/packageName/homepage/description from your -// package.json so the published metadata stays in sync. The import-attribute -// form resolves under both bundlers and Node's native TypeScript execution. -import pkg from '../package.json' with { type: 'json' } -import * as v from 'valibot' - -export default defineDevframe({ - id: 'my-inspector', - name: 'My Inspector', - version: pkg.version, - packageName: pkg.name, - homepage: pkg.homepage, - description: pkg.description, - icon: 'ph:magnifying-glass-duotone', - setup(ctx) { - const my = ctx.scope('my-inspector') - my.rpc.register(defineRpcFunction({ - name: 'analyze', // -> my-inspector:analyze - type: 'query', - args: [v.object({ url: v.string() })], - returns: v.object({ url: v.string(), verdict: v.literal('ok') }), - handler: ({ url }: { url: string }) => { - // Server-side implementation (used by CLI/build adapters). - return { url, verdict: 'ok' as const } - }, - })) - }, - setupBrowser() { - // Browser-side implementation — used by the SPA adapter so the - // deployed static site can answer RPC without a server. - // (Wire up an in-browser handler here once the SPA adapter lands.) - }, - spa: { loader: 'query' }, -}) diff --git a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts index 3906052a..9aff9c44 100644 --- a/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts @@ -376,7 +376,6 @@ export declare const defineHubRpcFunction: void | Promise; + force?: boolean; } // #endregion diff --git a/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.d.ts deleted file mode 100644 index 2b011197..00000000 --- a/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `devframe/adapters/spa` - */ -// #region Interfaces -export interface CreateSpaOptions { - outDir?: string; - base?: string; - pretty?: boolean; -} -// #endregion - -// #region Functions -export declare function createSpa(_: DevtoolDefinition, _?: CreateSpaOptions): Promise; -// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.js b/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.js deleted file mode 100644 index 102d91f9..00000000 --- a/tests/__snapshots__/tsnapi/devframe/adapters/spa.snapshot.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Generated by tsnapi — public API snapshot of `devframe/adapters/spa` - */ -// #region Functions -export async function createSpa(_, _) {} -// #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts index 2b5dcb2b..8a69f9ca 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.d.ts @@ -11,7 +11,6 @@ export declare const DEVFRAME_AUTH_TOKEN_QUERY_PARAM: string; export declare const DEVFRAME_CONNECTION_KEY: string; export declare const DEVFRAME_CONNECTION_META_FILENAME: string; export declare const DEVFRAME_DOCK_IMPORTS_FILENAME: string; -export declare const DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID: string; export declare const DEVFRAME_MCP_ROUTE: string; export declare const DEVFRAME_OTP_URL_PARAM: string; export declare const DEVFRAME_RPC_DUMP_DIRNAME: string; diff --git a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js index 12586280..76076a12 100644 --- a/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/constants.snapshot.js @@ -11,7 +11,6 @@ export var DEVFRAME_AUTH_TOKEN_QUERY_PARAM /* const */ export var DEVFRAME_CONNECTION_KEY /* const */ export var DEVFRAME_CONNECTION_META_FILENAME /* const */ export var DEVFRAME_DOCK_IMPORTS_FILENAME /* const */ -export var DEVFRAME_DOCK_IMPORTS_VIRTUAL_ID /* const */ export var DEVFRAME_MCP_ROUTE /* const */ export var DEVFRAME_OTP_URL_PARAM /* const */ export var DEVFRAME_RPC_DUMP_DIRNAME /* const */ diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 8d837a04..449b7cde 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -107,9 +107,6 @@ export interface DevframeAgentHostEvents { 'agent:resource:unregistered': (_: string) => void; 'agent:manifest:changed': () => void; } -export interface DevframeBrowserContext { - rpc: unknown; -} export interface DevframeCapabilities { rpc?: boolean; views?: boolean; @@ -150,14 +147,11 @@ export interface DevframeDefinition { basePath?: string; duplicationStrategy?: DevframeDuplicationStrategy; capabilities?: { - dev?: boolean | Record; - build?: boolean | Record; - spa?: boolean | Record; + dev?: boolean; + build?: boolean; }; setup: (_: DevframeNodeContext, _?: DevframeSetupInfo) => void | Promise; - setupBrowser?: (_: DevframeBrowserContext) => void | Promise; cli?: DevframeCliOptions; - spa?: DevframeSpaOptions; } export interface DevframeDiagnosticsHost { readonly logger: DevframeDiagnosticsLogger; @@ -329,10 +323,6 @@ export interface DevframeSettingsStore = Record; } -export interface DevframeSpaOptions { - base?: string; - loader?: 'query' | 'upload' | 'none'; -} export interface DevframeSseOptions { route?: string; } @@ -426,20 +416,12 @@ export interface ScopedBroadcastOptions { // #region Types export type AgentToolProvider = () => readonly AgentToolInput[]; export type DevframeDeploymentKind = 'standalone' | 'hosted'; -export type DevframeDiagnosticsDefinition = ReturnType>; export type DevframeDiagnosticsLogger = Record; export type DevframeDuplicationStrategy = 'warn' | 'silent' | 'throw' | 'duplicate'; export type DevframeRpcTransportKind = 'websocket' | 'sse'; -export type DevframeRuntime = 'cli' | 'build' | 'spa' | 'vite' | 'embedded'; export type DevframeServiceId = keyof DevframeServicesRegistry | (string & {}); export type DevframeServiceOf = ID extends keyof DevframeServicesRegistry ? DevframeServicesRegistry[ID] : unknown; export type DevframeStorageScope = 'workspace' | 'project' | 'global'; -export type EntriesToObject = { [K in T[number] as K[0]]: K[1]; }; -export type PartialWithoutId = Partial> & { - id: string; -}; export type RpcFunctionsHost = RpcFunctionsCollectorBase & { invokeLocal: >(_: T, ..._: Args) => Promise>>; broadcast: >(_: RpcBroadcastOptions) => Promise; @@ -452,7 +434,6 @@ export type ScopedRpcFn = `${NS}: export type ScopedServerFunctions = { [K in keyof DevframeRpcServerFunctions as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcServerFunctions[K]; }; export type ScopedSharedStates = { [K in keyof DevframeRpcSharedStates as K extends `${NS}:${infer R}` ? R : never]: DevframeRpcSharedStates[K]; }; export type SettingsForNamespace = NS extends keyof DevframeSettingsRegistry ? DevframeSettingsRegistry[NS] extends Record ? DevframeSettingsRegistry[NS] : Record : Record; -export type Thenable = T | Promise; // #endregion // #region Functions diff --git a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts index 05628c64..b76e3ff9 100644 --- a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts @@ -3,7 +3,6 @@ */ // #region Interfaces export interface CreateH3DevframeHostOptions { - app?: unknown; origin: string | (() => string); mount?: (_: string, _: string) => void | Promise; appName: string; @@ -245,6 +244,12 @@ export declare const diagnostics: import("nostics").Diagnostics<{ readonly why: () => string; readonly fix: "Clients connect over the SSE endpoint instead — no upgrade wiring is needed. Remove `ws: false` if the instance should serve a WebSocket after all."; }; + readonly DF0058: { + readonly why: (p: { + id: string; + }) => string; + readonly fix: "Pass `{ force: true }` to `createDevServer()` to run it anyway, or drop `capabilities.dev: false` on the definition."; + }; }, readonly [typeof devframeReporter]>; // #endregion diff --git a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts index 481ccc85..19d42a06 100644 --- a/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/types.snapshot.d.ts @@ -16,14 +16,12 @@ export { ConnectionMetaSse } export { ConnectionMetaWebsocket } export { DevframeAgentHost } export { DevframeAgentHostEvents } -export { DevframeBrowserContext } export { DevframeCapabilities } export { DevframeCliOptions } export { DevframeConnectionConfigsRegistry } export { DevframeDefineDiagnosticsOptions } export { DevframeDefinition } export { DevframeDeploymentKind } -export { DevframeDiagnosticsDefinition } export { DevframeDiagnosticsHost } export { DevframeDiagnosticsLogger } export { DevframeDockDefaults } @@ -38,7 +36,6 @@ export { DevframeRpcConnectionRequest } export { DevframeRpcServerFunctions } export { DevframeRpcSharedStates } export { DevframeRpcTransportKind } -export { DevframeRuntime } export { DevframeScopedNodeContext } export { DevframeScopedNodeRpc } export { DevframeScopedStreamingHost } @@ -50,17 +47,14 @@ export { DevframeSettings } export { DevframeSettingsRegistry } export { DevframeSettingsStore } export { DevframeSetupInfo } -export { DevframeSpaOptions } export { DevframeSseOptions } export { DevframeStorageScope } export { DevframeViewHost } export { DevframeWsOptions } -export { EntriesToObject } export { EventEmitter } export { EventsMap } export { EventUnsubscribe } export { McpRouteOptions } -export { PartialWithoutId } export { RpcBroadcastOptions } export { RpcFunctionAgentOptions } export { RpcFunctionsHost } @@ -75,5 +69,4 @@ export { ScopedRpcFn } export { ScopedServerFunctions } export { ScopedSharedStates } export { SettingsForNamespace } -export { Thenable } // #endregion \ No newline at end of file