From 6d10149368bdbbf74ab64f784c7f63f3955c2231 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 20 Aug 2026 06:15:04 +0000 Subject: [PATCH] feat(devframe): promote cli.distDir to top-level clientAssets --- docs/adapters/build.md | 4 +- docs/adapters/cac.md | 6 +-- docs/adapters/dev.md | 2 +- docs/errors/DF0059.md | 8 ++- docs/errors/DF0060.md | 8 ++- docs/errors/DF0061.md | 10 ++-- docs/errors/DF0064.md | 2 +- docs/errors/DF0065.md | 16 +++--- docs/errors/DF8106.md | 4 +- docs/examples/json-render.md | 2 +- docs/frameworks/nuxt.md | 4 +- docs/frameworks/vite.md | 2 +- docs/guide/client-assets.md | 53 +++++++++++++++---- docs/guide/devframe-definition.md | 50 ++++++++++++++--- docs/guide/hub.md | 2 +- docs/guide/index.md | 4 +- docs/guide/json-render.md | 4 +- docs/guide/standalone-cli.md | 19 ++++--- examples/files-inspector/src/devframe.ts | 2 +- examples/files-inspector/tests/_utils.ts | 2 +- .../src/client/devframe/demo-devframe.ts | 4 +- .../src/client/devframe/tabbed-devframe.ts | 4 +- examples/hub-vite/src/devframe.ts | 4 +- examples/hub-vite/src/tabbed-tool.ts | 4 +- examples/json-render/README.md | 2 +- examples/json-render/src/devframe.ts | 2 +- .../next-runtime-snapshot/src/devframe.ts | 2 +- .../next-runtime-snapshot/tests/_utils.ts | 2 +- examples/streaming-chat/src/devframe.ts | 2 +- examples/streaming-chat/tests/_utils.ts | 2 +- .../src/adapters/__tests__/build.test.ts | 2 +- packages/devframe/src/adapters/build.ts | 10 ++-- packages/devframe/src/adapters/dev.ts | 13 ++--- packages/devframe/src/adapters/initiate.ts | 12 +++-- packages/devframe/src/define.ts | 13 ++++- packages/devframe/src/types/devframe.ts | 22 ++++++-- packages/devframe/src/types/host.ts | 4 +- packages/devframe/src/types/remote-assets.ts | 2 +- .../hub/src/node/__tests__/initiate.test.ts | 2 +- .../node/__tests__/install-devframe.test.ts | 2 +- packages/hub/src/node/install-devframe.ts | 6 ++- packages/json-render-ui/src/spa.ts | 12 ++--- .../json-render-ui/src/spa/vite.config.ts | 2 +- packages/next/src/handler.ts | 5 +- packages/next/test/handler.test.ts | 23 ++++++-- packages/vite/src/single.ts | 9 ++-- packages/vite/test/single.test.ts | 2 +- skills/devframe/SKILL.md | 4 +- .../tsnapi/devframe/index.snapshot.d.ts | 2 + .../tsnapi/devframe/index.snapshot.js | 1 + 50 files changed, 238 insertions(+), 142 deletions(-) diff --git a/docs/adapters/build.md b/docs/adapters/build.md index fc5d7806..7f34dbb1 100644 --- a/docs/adapters/build.md +++ b/docs/adapters/build.md @@ -6,7 +6,7 @@ outline: deep Produces a self-contained static deploy of a devframe: -1. Copies the author's SPA dist (`cli.distDir` or `options.distDir`) into ``. +1. Copies the author's SPA dist (`clientAssets` or `options.distDir`) into ``. 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`. @@ -23,7 +23,7 @@ await createBuild(devframe, { | Option | Default | Description | |--------|---------|-------------| | `outDir` | `dist-static` | Output directory. Cleared on each build. | -| `distDir` | `def.cli?.distDir` | Override the SPA dist directory (a local path or a [remote assets](/guide/client-assets) package, materialized in full at build time). | +| `distDir` | `def.clientAssets` (falls back to deprecated `def.cli?.distDir`) | Override the SPA dist directory (a local path or a [remote assets](/guide/client-assets) package, materialized in full at build time). | | `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. diff --git a/docs/adapters/cac.md b/docs/adapters/cac.md index 5caf67d6..6fbbc19a 100644 --- a/docs/adapters/cac.md +++ b/docs/adapters/cac.md @@ -21,7 +21,7 @@ import { createCac } from 'devframe/adapters/cac' const devframe = defineDevframe({ id: 'my-devframe', name: 'My Devframe', - cli: { distDir: './client/dist' }, + clientAssets: './client/dist', setup(ctx) { /* register docks, RPC, etc. */ }, }) @@ -66,9 +66,9 @@ The `cli` property lets the caller add ad-hoc commands and flags right before `p ```ts defineDevframe({ id: 'my-devframe', + clientAssets: './client/dist', // built SPA served as the UI cli: { command: 'my-devframe', // binary name; default: the id - 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 @@ -87,7 +87,7 @@ defineDevframe({ }) ``` -`distDir` is the only required field; everything else has sensible defaults. The `configure` hook runs *before* the `configureCli` option passed to `createCac`, so the final tool author always has the last word on flags. +The top-level [`clientAssets`](/guide/client-assets) supplies the SPA the dev/build commands serve; everything under `cli` has sensible defaults. The `configure` hook runs *before* the `configureCli` option passed to `createCac`, so the final tool author always has the last word on flags. ## Headless logging diff --git a/docs/adapters/dev.md b/docs/adapters/dev.md index a15c813b..4fcd70b3 100644 --- a/docs/adapters/dev.md +++ b/docs/adapters/dev.md @@ -26,7 +26,7 @@ process.on('SIGINT', () => handle.close().then(() => process.exit(0))) | `host` | `def.cli?.host ?? 'localhost'` | Bind host. | | `port` | resolved via `resolveDevServerPort` | Port to listen on. | | `flags` | `{}` | Parsed flag bag forwarded to `setup(ctx, { flags })`. | -| `distDir` | `def.cli?.distDir` | Required — throws when neither is set. | +| `distDir` | `def.clientAssets` (falls back to deprecated `def.cli?.distDir`) | SPA dist override. When unset the server runs in bridge mode (meta + WS only). | | `basePath` | `resolveBasePath(def, 'standalone')` | Mount path override. | | `app` | fresh h3 app | Pre-configured h3 app to mount onto (custom middleware, auth, extra static assets). | | `openBrowser` | resolves from `flags.open` / `def.cli?.open` | Explicit on/off override. `false` disables; a string opens that relative path. | diff --git a/docs/errors/DF0059.md b/docs/errors/DF0059.md index e851c003..f803de64 100644 --- a/docs/errors/DF0059.md +++ b/docs/errors/DF0059.md @@ -16,11 +16,9 @@ A remote-assets source (`{ package, version }` passed where a static mount accep ```ts defineDevframe({ - cli: { - distDir: { - package: '@devframes/plugin-git-client', - version: '1.2.3', - }, + clientAssets: { + package: '@devframes/plugin-git-client', + version: '1.2.3', }, }) ``` diff --git a/docs/errors/DF0060.md b/docs/errors/DF0060.md index 5afe8d7f..4a4103df 100644 --- a/docs/errors/DF0060.md +++ b/docs/errors/DF0060.md @@ -16,11 +16,9 @@ A file of a remote-assets source was requested that is neither in the locally in ```ts defineDevframe({ - cli: { - distDir: { - package: '@devframes/plugin-git-client', - version: '1.2.3', - }, + clientAssets: { + package: '@devframes/plugin-git-client', + version: '1.2.3', }, }) ``` diff --git a/docs/errors/DF0061.md b/docs/errors/DF0061.md index 9d21cf65..377bbbcb 100644 --- a/docs/errors/DF0061.md +++ b/docs/errors/DF0061.md @@ -16,12 +16,10 @@ A remote-assets source found a locally installed copy of its assets package (res ```ts defineDevframe({ - cli: { - distDir: { - package: '@devframes/plugin-git-client', - version: '2.0.0', - resolveFrom: import.meta.url, - }, + clientAssets: { + package: '@devframes/plugin-git-client', + version: '2.0.0', + resolveFrom: import.meta.url, }, }) ``` diff --git a/docs/errors/DF0064.md b/docs/errors/DF0064.md index 875990e2..1b5963a1 100644 --- a/docs/errors/DF0064.md +++ b/docs/errors/DF0064.md @@ -10,7 +10,7 @@ outline: deep ## Cause -A static build (`createBuild`) with a remote-assets `distDir` needs every asset file up front — the output must be self-contained. Materialization walks the provider's file listing and downloads each file, and one of those steps failed: the provider has no `listFiles` (custom providers may omit it), the listing request failed, or an individual file download errored. +A static build (`createBuild`) with remote-assets `clientAssets` needs every asset file up front — the output must be self-contained. Materialization walks the provider's file listing and downloads each file, and one of those steps failed: the provider has no `listFiles` (custom providers may omit it), the listing request failed, or an individual file download errored. ## Example diff --git a/docs/errors/DF0065.md b/docs/errors/DF0065.md index 65bbb432..d7ef98f0 100644 --- a/docs/errors/DF0065.md +++ b/docs/errors/DF0065.md @@ -16,11 +16,9 @@ A remote-assets source's `package` and `version` are interpolated into CDN URLs ```ts defineDevframe({ - cli: { - distDir: { - package: '@devframes/plugin-git-client', - version: '../etc', // ✗ not a semver version - }, + clientAssets: { + package: '@devframes/plugin-git-client', + version: '../etc', // ✗ not a semver version }, }) ``` @@ -31,11 +29,9 @@ Use a valid npm package name and an exact version: ```ts defineDevframe({ - cli: { - distDir: { - package: '@devframes/plugin-git-client', - version: '1.2.3', - }, + clientAssets: { + package: '@devframes/plugin-git-client', + version: '1.2.3', }, }) ``` diff --git a/docs/errors/DF8106.md b/docs/errors/DF8106.md index 5e7a5d00..3001be4f 100644 --- a/docs/errors/DF8106.md +++ b/docs/errors/DF8106.md @@ -12,7 +12,7 @@ outline: deep A mounted devframe's SPA loads in an iframe at its own base (e.g. `/__terminals/`) and calls `connectDevframe()`, which fetches `./__connection.json` relative to that base to discover the RPC/WebSocket endpoint. `ctx.install` serves that file at each base by calling the host's `mountConnectionMeta(base)` alongside `mountStatic`. -This diagnostic is reported when a devframe with a servable `cli.distDir` is mounted on a `DevframeHost` that does not implement `mountConnectionMeta`. The SPA's `./__connection.json` fetch then falls through to the host's HTML fallback, so the SPA cannot discover the endpoint and its panel stays empty or stuck loading — previously a silent failure. +This diagnostic is reported when a devframe with servable `clientAssets` is mounted on a `DevframeHost` that does not implement `mountConnectionMeta`. The SPA's `./__connection.json` fetch then falls through to the host's HTML fallback, so the SPA cannot discover the endpoint and its panel stays empty or stuck loading — previously a silent failure. The SPA can still connect when it shares an origin with the hub UI, by inheriting the connection meta from the parent window. Cross-origin, sandboxed, or directly-opened iframes have no such parent to inherit from. @@ -35,4 +35,4 @@ A static-snapshot host that bakes `__connection.json` into its served files can ## Source -- [`packages/hub/src/node/install-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/install-devframe.ts) — `ctx.install()` emits this when a devframe with a servable `distDir` is installed on a host lacking `mountConnectionMeta`. +- [`packages/hub/src/node/install-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/install-devframe.ts) — `ctx.install()` emits this when a devframe with servable `clientAssets` is installed on a host lacking `mountConnectionMeta`. diff --git a/docs/examples/json-render.md b/docs/examples/json-render.md index 7e189fe1..a21c8de1 100644 --- a/docs/examples/json-render.md +++ b/docs/examples/json-render.md @@ -20,7 +20,7 @@ Package: `json-render` · frontend: **prebuilt `@devframes/json-render-ui/spa`** - **Action bridge** — the `Refresh` button's `press` action is dispatched as an RPC call of the same name; the handler bumps a counter and patches state, with per-action loading and error surfacing. -- **Out-of-box SPA** — `createJsonRenderDevframe` points `cli.distDir` at the +- **Out-of-box SPA** — `createJsonRenderDevframe` points `clientAssets` at the prebuilt `@devframes/json-render-ui/spa`, which discovers the view from the view index and renders it — no client build in the example. - **Static output** — `cli:build` snapshots the spec + state as a read-only diff --git a/docs/frameworks/nuxt.md b/docs/frameworks/nuxt.md index 7d606a1c..0be5d062 100644 --- a/docs/frameworks/nuxt.md +++ b/docs/frameworks/nuxt.md @@ -100,7 +100,7 @@ export default defineNuxtConfig({ ### Relationship to `createCac` -The bridge handles the **dev workflow**. Production deploys still go through `createCac` (or `createBuild`), which produces a static `__connection.json` + `__rpc-dump/` snapshot from `cli.distDir`: +The bridge handles the **dev workflow**. Production deploys still go through `createCac` (or `createBuild`), which produces a static `__connection.json` + `__rpc-dump/` snapshot from `clientAssets`: ``` my-tool/ @@ -110,7 +110,7 @@ my-tool/ │ └── app/ # Nuxt SPA — uses `@devframes/nuxt` └── dist/ ├── cli.mjs # bundled Node entry - └── public/ # Nuxt build output, pointed at by cli.distDir + └── public/ # Nuxt build output, pointed at by clientAssets ``` In dev (`nuxt dev`) the bridge is live. In production (` build`) the SPA loads the static dump. diff --git a/docs/frameworks/vite.md b/docs/frameworks/vite.md index e9b25c94..c49e7b7e 100644 --- a/docs/frameworks/vite.md +++ b/docs/frameworks/vite.md @@ -26,7 +26,7 @@ export default defineConfig({ ## `devframeVitePlugin` — static mount -Mounts `def.cli.distDir` at `options.base` (`/__/` by default) with SPA fallback. No RPC server is started — useful when you only need the SPA bundle served from a known path. `distDir` may be a local directory or a [remote assets](/guide/client-assets) package. +Mounts `def.clientAssets` at `options.base` (`/__/` by default) with SPA fallback. No RPC server is started — useful when you only need the SPA bundle served from a known path. `clientAssets` may be a local directory or a [remote assets](/guide/client-assets) package. | Option | Default | Description | |--------|---------|-------------| diff --git a/docs/guide/client-assets.md b/docs/guide/client-assets.md index 821410ba..dec72657 100644 --- a/docs/guide/client-assets.md +++ b/docs/guide/client-assets.md @@ -4,11 +4,11 @@ outline: deep # Client Assets -A devframe's UI is a built single-page app served as its client. `cli.distDir` tells devframe where those assets live — either a **local directory** bundled with your tool, or a **published npm package** fetched on demand. +A devframe's UI is a built single-page app served as its client. The top-level `clientAssets` field tells devframe where those assets live — either a **local directory** bundled with your tool, or a **published npm package** fetched on demand. ## Mounting a local build -The basic form points `cli.distDir` at the directory your SPA build produces. Resolve it from the module so it works from both source and the published package: +The basic form points `clientAssets` at the directory your SPA build produces. Resolve it from the module so it works from both source and the published package: ```ts import { fileURLToPath } from 'node:url' @@ -19,9 +19,7 @@ export default defineDevframe({ id: 'my-tool', version: pkg.version, packageName: pkg.name, - cli: { - distDir: fileURLToPath(new URL('../dist/spa', import.meta.url)), - }, + clientAssets: fileURLToPath(new URL('../dist/spa', import.meta.url)), setup(ctx) { // … }, @@ -30,20 +28,53 @@ export default defineDevframe({ devframe serves that directory with SPA fallback (an unknown path resolves to `index.html`, so client-side routing works) and no-store caching for dev. Build your SPA with a relative base (`vite: { base: './' }`) so the bundle is mount-path portable — it discovers its runtime base from `document.baseURI` and works at `/`, `/__my-tool/`, or any mount point without rewriting. -The [`dev`](/adapters/dev), [`build`](/adapters/build), and [Vite](/frameworks/vite) adapters all consume this same `distDir`. +The [`dev`](/adapters/dev), [`build`](/adapters/build), and [Vite](/frameworks/vite) adapters all consume this same `clientAssets`. + +The earlier home for this value, `cli.distDir`, is deprecated but still read as a fallback when `clientAssets` is unset, so existing definitions keep working — move it up to the top level at your convenience. + +## Programmatic hosting from `setup` + +`clientAssets` is the declarative way to serve the tool's *primary* UI — the adapters resolve it and mount it at the base path for you. When you need to host assets yourself — mount a second static bundle at another path, decide the source at runtime, or serve extra directories alongside the main SPA — reach for `ctx.views.hostStatic` inside `setup`: + +```ts +export default defineDevframe({ + id: 'my-tool', + version: pkg.version, + packageName: pkg.name, + importMetaUrl: import.meta.url, + clientAssets: fileURLToPath(new URL('../dist/spa', import.meta.url)), + setup(ctx) { + // Serve an extra static bundle at a sibling base. + ctx.views.hostStatic( + '/docs/', + fileURLToPath(new URL('../dist/docs', import.meta.url)), + ) + + // A remote source works here too — same shape as `clientAssets`. + ctx.views.hostStatic('/legacy/', { + package: '@acme/my-tool-legacy-ui', + version: pkg.version, + }) + }, +}) +``` + +`hostStatic(baseUrl, source, defaultResolveFrom?)` accepts the same `StaticAssetsSource` (a local directory or a remote declaration) as `clientAssets`. In `dev` mode it registers the middleware live; in `build` mode it copies the files into the static output, so a programmatically hosted bundle survives `createBuild` too. The optional `defaultResolveFrom` overrides the context's own `importMetaUrl` as the resolution base for a remote source — a hub mounting assets on behalf of a plugin passes that plugin's `importMetaUrl` so they resolve against its dependency graph. + +Under the hood the adapters resolve `clientAssets` (falling back to the deprecated `cli.distDir`) with the exported `resolveClientAssets(def)` helper and hand it to the host's static mount — `hostStatic` is that same mechanism, exposed for your own bases. ## Remote assets -Instead of a directory, `cli.distDir` can name a **published npm package** that holds the built UI. The assets are then fetched on demand and cached locally, so the node package doesn't bundle its SPA — keeping the installed footprint small, since a plugin's UI is usually the bulk of its tarball. +Instead of a directory, `clientAssets` can name a **published npm package** that holds the built UI. The assets are then fetched on demand and cached locally, so the node package doesn't bundle its SPA — keeping the installed footprint small, since a plugin's UI is usually the bulk of its tarball. -Give `cli.distDir` a `RemoteAssets` object naming the package and exact version: +Give `clientAssets` a `RemoteAssets` object naming the package and exact version: ```ts import type { RemoteAssets } from 'devframe' import { defineDevframe } from 'devframe' import pkg from '../package.json' with { type: 'json' } -const distDir: RemoteAssets = { +const clientAssets: RemoteAssets = { package: '@acme/my-tool-assets', version: pkg.version, } @@ -53,7 +84,7 @@ export default defineDevframe({ version: pkg.version, packageName: pkg.name, importMetaUrl: import.meta.url, - cli: { distDir }, + clientAssets, setup(ctx) { // … }, @@ -108,7 +139,7 @@ That page also posts its failure to `window.parent` (`DEVFRAME_REMOTE_ASSETS_ERR A custom provider supplies the file URL, and optionally a file listing (used for correct 404s, SPA fallback, and static builds): ```ts -const distDir: RemoteAssets = { +const clientAssets: RemoteAssets = { package: '@acme/my-tool-assets', version: pkg.version, provider: { diff --git a/docs/guide/devframe-definition.md b/docs/guide/devframe-definition.md index 94c77f2e..9898b0eb 100644 --- a/docs/guide/devframe-definition.md +++ b/docs/guide/devframe-definition.md @@ -54,6 +54,7 @@ export default defineDevframe({ | `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? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. | | `services` | `DevframeServiceInput[]` | Wire services this devframe consumes — descriptors (`{ package, version?, required?, options? }`) the adapter imports against the plugin's own dependencies, or ready definitions. See [Cross-Plugin Services](./services#wire-services). | +| `clientAssets` | `string \| RemoteAssets` | The built SPA served as the devframe's UI — a local dist directory, or a [remote assets](./client-assets) package fetched on demand. Consumed by every adapter that serves the UI (`dev`, `build`, `vite`, `next`, hub). Supersedes the deprecated `cli.distDir`. See [Client Assets](./client-assets). | | `rpc` | `{ snapshot?: (string \| { method, inputs })[] }` | RPC-level config. `rpc.snapshot` opts an RPC function this devframe doesn't own (e.g. a wire service's) into the static build's dump. A bare method id bakes the no-argument call; `{ method, inputs }` bakes one record per argument-tuple, where `inputs` is a list of tuples or an async `(ctx) => tuples` provider (so it can enumerate at build time via the service's node API). The first tuple's result becomes the fallback. | | `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`. | | `cli` | `DevframeCliOptions` | Defaults for the CLI adapter. See [CLI options](#cli-options) below. | @@ -94,11 +95,9 @@ export default defineDevframe({ importMetaUrl: import.meta.url, homepage: pkg.homepage, description: pkg.description, - cli: { - // Served from the locally installed `my-devframe--assets` — resolved via - // `importMetaUrl`, so it works under pnpm's strict layout with zero network. - distDir: { package: `${pkg.name}--assets`, version: pkg.version }, - }, + // Served from the locally installed `my-devframe--assets` — resolved via + // `importMetaUrl`, so it works under pnpm's strict layout with zero network. + clientAssets: { package: `${pkg.name}--assets`, version: pkg.version }, services: [ // Imported from `my-devframe`'s own dependency graph. { package: '@scope/my-service', version: pkg.version }, @@ -109,6 +108,43 @@ export default defineDevframe({ For a remote assets source, `importMetaUrl` is the default `resolveFrom`; a per-source `resolveFrom` still wins, and an explicit `resolveFrom: null` opts out of the installed-copy lookup. See [Client Assets](./client-assets) and [Cross-Plugin Services](./services#wire-services) for the full resolution order. +### Serving the UI with `clientAssets` + +The devframe's UI is a built single-page app, and `clientAssets` — a top-level field — points at it. It takes either a **local dist directory** or a **remote assets package** ([full reference](./client-assets)). Every adapter that serves the UI (`dev`, `build`, the [Vite](/frameworks/vite) plugin, the [Next](/frameworks/next) handler, and the hub install path) reads this one value, so the UI is declared once and travels with the definition across viewers. + +```ts +import { fileURLToPath } from 'node:url' + +export default defineDevframe({ + id: 'my-devframe', + name: 'My Devframe', + version: pkg.version, + packageName: pkg.name, + importMetaUrl: import.meta.url, + homepage: pkg.homepage, + description: pkg.description, + // A local build resolved from the module, so it works from both source + // and the published package. + clientAssets: fileURLToPath(new URL('../dist/spa', import.meta.url)), + setup(ctx) { /* … */ }, +}) +``` + +This value used to live under `cli.distDir`. That field is now **deprecated** but still honored as a fallback when `clientAssets` is unset, so existing definitions keep working unchanged: + +```ts +export default defineDevframe({ + // … + cli: { + // ⚠️ Deprecated — prefer the top-level `clientAssets`. Still read as a + // fallback for back-compat. + distDir: fileURLToPath(new URL('../dist/spa', import.meta.url)), + }, +}) +``` + +For assets you host yourself — a second bundle, a runtime-decided source — call `ctx.views.hostStatic` in `setup` instead. See [Client Assets](./client-assets#programmatic-hosting-from-setup). + ### Runtime flags The `ctx.mode` field is either `'dev'` or `'build'`. Use it to gate work that should only run in one runtime: @@ -212,9 +248,9 @@ Each devframe-level host has a dedicated page: defineDevframe({ id: 'my-devframe', name: 'My Devframe', + clientAssets: './client/dist', // built SPA served as the UI cli: { command: 'my-devframe', // binary name; default: the `id` - 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 @@ -236,7 +272,7 @@ defineDevframe({ | Field | Type | Description | |-------|------|-------------| | `command` | `string` | Binary name surfaced in `--help`. Default: the definition's `id`. | -| `distDir` | `string \| RemoteAssets` | SPA dist directory, or a [remote assets](./client-assets) package fetched on demand. **Required** for `dev` / `build`. | +| `distDir` | `string \| RemoteAssets` | **Deprecated** — moved to the top-level [`clientAssets`](#serving-the-ui-with-clientassets). Still read as a fallback when `clientAssets` is unset. | | `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. | diff --git a/docs/guide/hub.md b/docs/guide/hub.md index 9f04247a..4362d9ca 100644 --- a/docs/guide/hub.md +++ b/docs/guide/hub.md @@ -152,7 +152,7 @@ const host: DevframeHost = { } ``` -A host that omits `mountConnectionMeta` while mounting a devframe with a servable `distDir` triggers a [`DF8106`](https://devfra.me/errors/DF8106) diagnostic and falls back to same-origin window inheritance, which connects an embedded SPA only when it shares an origin with the hub UI. When the hub mounts several devframe SPAs at different bases in the same page, inheritance still works: the connection meta is published together with the base it was resolved against, so each same-origin child resolves the RPC/WS endpoint against the publisher's base rather than its own. +A host that omits `mountConnectionMeta` while mounting a devframe with servable `clientAssets` triggers a [`DF8106`](https://devfra.me/errors/DF8106) diagnostic and falls back to same-origin window inheritance, which connects an embedded SPA only when it shares an origin with the hub UI. When the hub mounts several devframe SPAs at different bases in the same page, inheritance still works: the connection meta is published together with the base it was resolved against, so each same-origin child resolves the RPC/WS endpoint against the publisher's base rather than its own. ### Bundled hosts (Next.js) diff --git a/docs/guide/index.md b/docs/guide/index.md index 6a08dfdb..5e23af12 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -58,9 +58,7 @@ const devframe = defineDevframe({ homepage: 'https://github.com/me/my-devframe', description: 'A one-line summary of what the tool does.', icon: 'ph:gauge-duotone', - cli: { - distDir: 'client/dist', - }, + clientAssets: 'client/dist', setup(ctx) { ctx.rpc.register(defineRpcFunction({ name: 'my-devframe:hello', diff --git a/docs/guide/json-render.md b/docs/guide/json-render.md index 9cf02d84..ed174fea 100644 --- a/docs/guide/json-render.md +++ b/docs/guide/json-render.md @@ -93,7 +93,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 +with `createJsonRenderDevframe` — it points `clientAssets` at the shipped SPA (`jsonRenderSpaDir`): ```ts @@ -153,7 +153,7 @@ It ships as two self-contained prebuilt bundles — the standalone SPA (`@devframes/json-render-ui/hub`) — each inlining Vue, the upstream renderer, and the compiled `@antfu/design` styles. A consuming app wires nothing and pulls no frontend package into its own graph: the SPA is served verbatim as -`cli.distDir`, and the hub module is imported natively by the viewer from the +`clientAssets`, and the hub module is imported natively by the viewer from the renderer manifest. ## Rendering inside a hub diff --git a/docs/guide/standalone-cli.md b/docs/guide/standalone-cli.md index a0253718..1d2e6f62 100644 --- a/docs/guide/standalone-cli.md +++ b/docs/guide/standalone-cli.md @@ -33,14 +33,14 @@ import { createCac } from 'devframe/adapters/cac' import { colors as c } from 'devframe/utils/colors' import { resolve } from 'pathe' -const distDir = resolve(import.meta.dirname, '../dist/public') +const clientAssets = resolve(import.meta.dirname, '../dist/public') const devframe = defineDevframe({ id: 'my-tool', name: 'My Tool', + clientAssets, cli: { command: 'my-tool', - distDir, port: 7777, portRange: [7777, 9000], open: true, // auth defaults to on; `--open` embeds the current OTP so the tab lands authenticated @@ -93,12 +93,12 @@ export default defineNuxtConfig({ modules: ['@devframes/nuxt/single'], nitro: { preset: 'static', - output: { dir: './dist' }, // matches createCac's distDir of ./dist/public + output: { dir: './dist' }, // matches the definition's clientAssets of ./dist/public }, }) ``` -Build with `nuxt build` and point `cli.distDir` at `./dist/public`. The SPA discovers its effective base at runtime — no `--base` rewrite needed. See the [Nuxt docs](/frameworks/nuxt) for the full reference. +Build with `nuxt build` and point `clientAssets` at `./dist/public`. The SPA discovers its effective base at runtime — no `--base` rewrite needed. See the [Nuxt docs](/frameworks/nuxt) for the full reference. ## Next.js SPA setup @@ -118,7 +118,7 @@ export default { - **`assetPrefix: '.'`** is the setting that makes the build base-agnostic. Assets are referenced as `./_next/...` so the same bundle works at `/`, `/__my-tool/`, and any other mount path the host adapter chooses. Without it, Next.js bakes in `/_next/...` and the build only works at the root. - **`trailingSlash: true`** emits `foo/index.html` rather than `foo.html`, which composes cleanly with devframe's static-handler directory-with-index resolution. -`next build` writes the export to `/out/` next to `next.config.mjs`. Copy or move that to wherever you point `cli.distDir`: +`next build` writes the export to `/out/` next to `next.config.mjs`. Copy or move that to wherever you point `clientAssets`: ```json [package.json] { @@ -133,9 +133,7 @@ import { fileURLToPath } from 'node:url' defineDevframe({ id: 'my-tool', - cli: { - distDir: fileURLToPath(new URL('../dist/client', import.meta.url)), - }, + clientAssets: fileURLToPath(new URL('../dist/client', import.meta.url)), // … }) ``` @@ -185,8 +183,8 @@ const appFlags = defineCliFlags({ defineDevframe({ id: 'my-tool', name: 'My Tool', + clientAssets, cli: { - distDir, flags: appFlags, }, setup(ctx, info) { @@ -312,7 +310,8 @@ import { createDevServer } from 'devframe/adapters/dev' const devframe = defineDevframe({ id: 'my-tool', name: 'My Tool', - cli: { distDir: './dist/public', port: 7777 }, + clientAssets: './dist/public', + cli: { port: 7777 }, setup(ctx, { flags }) { /* ... */ }, }) diff --git a/examples/files-inspector/src/devframe.ts b/examples/files-inspector/src/devframe.ts index 0405dc7b..703d9696 100644 --- a/examples/files-inspector/src/devframe.ts +++ b/examples/files-inspector/src/devframe.ts @@ -16,10 +16,10 @@ export default defineDevframe({ description: pkg.description, icon: 'ph:folder-open-duotone', basePath: BASE_PATH, + clientAssets: distDir, cli: { command: 'devframe-files-inspector', port: 9876, - distDir, // Single-user localhost demo - skip the trust handshake so the served // SPA can call RPC without an OTP round-trip. auth: false, diff --git a/examples/files-inspector/tests/_utils.ts b/examples/files-inspector/tests/_utils.ts index 738405c6..7438063b 100644 --- a/examples/files-inspector/tests/_utils.ts +++ b/examples/files-inspector/tests/_utils.ts @@ -59,7 +59,7 @@ export interface InspectorServer extends StartedServer { export async function startInspectorServer( { cwd }: { cwd: string }, ): Promise { - const distDir = devframe.cli!.distDir! + const distDir = devframe.clientAssets! if (typeof distDir !== 'string') throw new TypeError('these tests serve the local dist directory — build the SPA first') const basePath = devframe.basePath! diff --git a/examples/hub-next/src/client/devframe/demo-devframe.ts b/examples/hub-next/src/client/devframe/demo-devframe.ts index d2f2d04a..5af5c167 100644 --- a/examples/hub-next/src/client/devframe/demo-devframe.ts +++ b/examples/hub-next/src/client/devframe/demo-devframe.ts @@ -18,9 +18,7 @@ export default defineDevframe({ description: 'A tiny demo devframe mounted into the Next.js hub via its `devframes` list.', icon: 'ph:rocket-duotone', basePath: '/__next-demo-tool/', - cli: { - distDir: resolve(HERE, '../../../spa/next-demo-tool'), - }, + clientAssets: resolve(HERE, '../../../spa/next-demo-tool'), async setup(rawCtx) { const ctx = rawCtx as unknown as DevframeHubContext diff --git a/examples/hub-next/src/client/devframe/tabbed-devframe.ts b/examples/hub-next/src/client/devframe/tabbed-devframe.ts index 46cb55f4..de285e0d 100644 --- a/examples/hub-next/src/client/devframe/tabbed-devframe.ts +++ b/examples/hub-next/src/client/devframe/tabbed-devframe.ts @@ -24,9 +24,7 @@ export default defineDevframe({ description: 'A multi-view SPA hosted as shared-iframe hub docks with soft navigation.', icon: 'ph:squares-four-duotone', basePath: '/__next-tabbed-tool/', - cli: { - distDir: resolve(HERE, '../../../spa/next-tabbed-tool'), - }, + clientAssets: resolve(HERE, '../../../spa/next-tabbed-tool'), async setup(rawCtx) { const ctx = rawCtx as unknown as DevframeHubContext await ctx.messages.add({ diff --git a/examples/hub-vite/src/devframe.ts b/examples/hub-vite/src/devframe.ts index dfe6e434..42a747ca 100644 --- a/examples/hub-vite/src/devframe.ts +++ b/examples/hub-vite/src/devframe.ts @@ -24,9 +24,7 @@ export default defineDevframe({ description: 'A tiny demo devframe that plugs into the hub via its `devframes` list.', icon: 'ph:rocket-duotone', basePath: '/__demo-tool/', - cli: { - distDir: fileURLToPath(new URL('../spa/demo-tool/', import.meta.url)), - }, + clientAssets: fileURLToPath(new URL('../spa/demo-tool/', import.meta.url)), async setup(rawCtx) { const ctx = rawCtx as unknown as DevframeHubContext diff --git a/examples/hub-vite/src/tabbed-tool.ts b/examples/hub-vite/src/tabbed-tool.ts index c01bb917..8167ef59 100644 --- a/examples/hub-vite/src/tabbed-tool.ts +++ b/examples/hub-vite/src/tabbed-tool.ts @@ -24,9 +24,7 @@ export default defineDevframe({ description: 'A multi-view SPA hosted as shared-iframe hub docks with soft navigation.', icon: 'ph:squares-four-duotone', basePath: '/__tabbed-tool/', - cli: { - distDir: fileURLToPath(new URL('../spa/tabbed-tool/', import.meta.url)), - }, + clientAssets: fileURLToPath(new URL('../spa/tabbed-tool/', import.meta.url)), async setup(rawCtx) { const ctx = rawCtx as unknown as DevframeHubContext await ctx.messages.add({ diff --git a/examples/json-render/README.md b/examples/json-render/README.md index 51783159..1597671d 100644 --- a/examples/json-render/README.md +++ b/examples/json-render/README.md @@ -20,7 +20,7 @@ dashboard** that exercises **every base-catalog component** — `Stack`, `Card`, name into the `KeyValueTable`. Each is dispatched as an RPC call of the same name, with per-action loading and error surfacing. - **`@devframes/json-render-ui/spa`** — the prebuilt out-of-box SPA. This example - has **no client build**: `createJsonRenderDevframe(...)` points `cli.distDir` at + has **no client build**: `createJsonRenderDevframe(...)` points `clientAssets` at the shipped renderer, which discovers the view from the JSON-render view index and renders it with `JsonRenderView`. diff --git a/examples/json-render/src/devframe.ts b/examples/json-render/src/devframe.ts index 3276f741..04dab65f 100644 --- a/examples/json-render/src/devframe.ts +++ b/examples/json-render/src/devframe.ts @@ -2,7 +2,7 @@ import { createJsonRenderDevframe } from '@devframes/json-render-ui/spa' import pkg from '../package.json' with { type: 'json' } import { createDashboardView } from './dashboard.ts' -// `createJsonRenderDevframe` points `cli.distDir` at the prebuilt +// `createJsonRenderDevframe` points `clientAssets` 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({ diff --git a/examples/next-runtime-snapshot/src/devframe.ts b/examples/next-runtime-snapshot/src/devframe.ts index 559813e0..e9160a93 100644 --- a/examples/next-runtime-snapshot/src/devframe.ts +++ b/examples/next-runtime-snapshot/src/devframe.ts @@ -20,10 +20,10 @@ export default defineDevframe({ description: pkg.description, icon: 'ph:gauge-duotone', basePath: BASE_PATH, + clientAssets: distDir, cli: { command: 'next-runtime-snapshot', port: 9899, - distDir, auth: false, }, setup(ctx) { diff --git a/examples/next-runtime-snapshot/tests/_utils.ts b/examples/next-runtime-snapshot/tests/_utils.ts index 32801992..f20a6913 100644 --- a/examples/next-runtime-snapshot/tests/_utils.ts +++ b/examples/next-runtime-snapshot/tests/_utils.ts @@ -23,7 +23,7 @@ export interface SnapshotServer extends StartedServer { * `packages/devframe/src/rpc/transports/ws.test.ts`. */ export async function startSnapshotServer(): Promise { - const distDir = devframe.cli!.distDir! + const distDir = devframe.clientAssets! if (typeof distDir !== 'string') throw new TypeError('these tests serve the local dist directory — build the SPA first') const basePath = devframe.basePath! diff --git a/examples/streaming-chat/src/devframe.ts b/examples/streaming-chat/src/devframe.ts index 991e478f..634686f3 100644 --- a/examples/streaming-chat/src/devframe.ts +++ b/examples/streaming-chat/src/devframe.ts @@ -20,10 +20,10 @@ export default defineDevframe({ description: pkg.description, icon: 'ph:chat-circle-dots-duotone', basePath: BASE_PATH, + clientAssets: distDir, cli: { command: 'devframe-streaming-chat', port: 9897, - distDir, // Single-user localhost demo - skip the trust handshake that the // Vite-side surface requires. auth: false, diff --git a/examples/streaming-chat/tests/_utils.ts b/examples/streaming-chat/tests/_utils.ts index 2dd14ea5..93269c9c 100644 --- a/examples/streaming-chat/tests/_utils.ts +++ b/examples/streaming-chat/tests/_utils.ts @@ -29,7 +29,7 @@ export async function startStreamingChatServer(): Promise> = {} packageName: 'devframe-build-test', homepage: 'https://example.test', description: 'Test devframe.', - cli: { distDir }, + clientAssets: distDir, setup: () => {}, ...overrides, }) diff --git a/packages/devframe/src/adapters/build.ts b/packages/devframe/src/adapters/build.ts index 4debfc7a..1b61a45f 100644 --- a/packages/devframe/src/adapters/build.ts +++ b/packages/devframe/src/adapters/build.ts @@ -14,6 +14,7 @@ import { DEVFRAME_RPC_DUMP_DIRNAME, DEVFRAME_RPC_DUMP_MANIFEST_FILENAME, } from '../constants' +import { resolveClientAssets } from '../define' import { createHostContext } from '../node/context' import { diagnostics } from '../node/diagnostics' import { createH3DevframeHost } from '../node/host-h3' @@ -26,8 +27,9 @@ export interface CreateBuildOptions { /** * Override the SPA dist to copy into `outDir` — a local directory or a * remote-assets declaration (materialized in full at build time). When - * omitted the adapter reads `devframe.cli?.distDir` — authors typically - * set this once on the definition itself. + * omitted the adapter reads `devframe.clientAssets` (or the deprecated + * `devframe.cli?.distDir`) — authors typically set this once on the + * definition itself. */ distDir?: StaticAssetsSource /** @@ -61,9 +63,9 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt throw diagnostics.DF0042({ id: d.id }) const outDir = resolve(options.outDir ?? 'dist-static') - const distSource = options.distDir ?? d.cli?.distDir + const distSource = options.distDir ?? resolveClientAssets(d) if (!distSource) - throw new Error(`[devframe] createBuild: no distDir for "${d.id}". Set \`cli.distDir\` on the definition or pass it as an option.`) + throw new Error(`[devframe] createBuild: no client assets for "${d.id}". Set \`clientAssets\` on the definition or pass it as an option.`) if (existsSync(outDir)) await fs.rm(outDir, { recursive: true }) diff --git a/packages/devframe/src/adapters/dev.ts b/packages/devframe/src/adapters/dev.ts index 10857841..9b495ddd 100644 --- a/packages/devframe/src/adapters/dev.ts +++ b/packages/devframe/src/adapters/dev.ts @@ -32,11 +32,12 @@ export interface CreateDevServerOptions { */ flags?: Record /** - * Override `def.cli?.distDir`. When neither this option nor - * `def.cli?.distDir` is set, the dev server runs in **bridge mode** — - * only `__connection.json` and the WS endpoint are mounted; the SPA - * is expected to be hosted elsewhere (e.g. by a parent Vite/Nuxt - * dev server via `devframeViteBridge` from `@devframes/vite`). + * Override the definition's `clientAssets` (or deprecated `cli.distDir`). + * When neither this option nor the definition's client assets are set, the + * dev server runs in **bridge mode** — only `__connection.json` and the WS + * endpoint are mounted; the SPA is expected to be hosted elsewhere (e.g. by + * a parent Vite/Nuxt dev server via `devframeViteBridge` from + * `@devframes/vite`). */ distDir?: StaticAssetsSource /** @@ -125,7 +126,7 @@ export interface CreateDevServerOptions { * h3 + WebSocket RPC + (optionally) the author's SPA mounted at the * resolved base path. * - * When `distDir` is omitted (and `def.cli?.distDir` is unset) the + * When `distDir` is omitted (and the definition's client assets are unset) the * server runs in **bridge mode**: only `__connection.json` and the WS * endpoint are mounted, with no SPA mount. The SPA is expected to be * hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see diff --git a/packages/devframe/src/adapters/initiate.ts b/packages/devframe/src/adapters/initiate.ts index 802aebc9..e1971775 100644 --- a/packages/devframe/src/adapters/initiate.ts +++ b/packages/devframe/src/adapters/initiate.ts @@ -15,6 +15,7 @@ import { H3 } from 'h3' import { resolve } from 'pathe' import { joinURL } from 'ufo' import { DEVFRAME_CONNECTION_META_FILENAME } from '../constants' +import { resolveClientAssets } from '../define' import { createHostContext } from '../node/context' import { diagnostics } from '../node/diagnostics' import { createH3DevframeHost } from '../node/host-h3' @@ -33,10 +34,11 @@ export interface InitDevframeOptions { */ base: string /** - * Override `def.cli?.distDir`. When neither is set — or `false` is passed - * to suppress the definition's own `distDir` — the handler runs in - * **bridge mode**: only `__connection.json`, the WS endpoint, and the MCP - * route (when enabled) are served; the SPA is hosted elsewhere. + * Override the definition's `clientAssets` (or deprecated `cli.distDir`). + * When neither is set — or `false` is passed to suppress the definition's + * own client assets — the handler runs in **bridge mode**: only + * `__connection.json`, the WS endpoint, and the MCP route (when enabled) are + * served; the SPA is hosted elsewhere. */ distDir?: StaticAssetsSource | false /** @@ -247,7 +249,7 @@ export function initDevframe( options: InitDevframeOptions, ): DevframeInstance { const base = normalizeBasePath(options.base) - const distDir = options.distDir === false ? undefined : options.distDir ?? def.cli?.distDir + const distDir = options.distDir === false ? undefined : options.distDir ?? resolveClientAssets(def) const app = options.app ?? new H3() const host = options.host ?? def.cli?.host ?? 'localhost' diff --git a/packages/devframe/src/define.ts b/packages/devframe/src/define.ts index 3046ebe6..4b9bc1a0 100644 --- a/packages/devframe/src/define.ts +++ b/packages/devframe/src/define.ts @@ -1,4 +1,4 @@ -import type { DevframeDefinition, DevframeNodeContext } from 'devframe/types' +import type { DevframeDefinition, DevframeNodeContext, StaticAssetsSource } from 'devframe/types' import { createDefineWrapperWithContext } from 'devframe/rpc' export const defineRpcFunction = createDefineWrapperWithContext() @@ -10,3 +10,14 @@ export const defineRpcFunction = createDefineWrapperWithContext `ok:${id}` }) ctx.agent.registerTool({ diff --git a/packages/hub/src/node/__tests__/install-devframe.test.ts b/packages/hub/src/node/__tests__/install-devframe.test.ts index 0fa31ec7..a6cf7122 100644 --- a/packages/hub/src/node/__tests__/install-devframe.test.ts +++ b/packages/hub/src/node/__tests__/install-devframe.test.ts @@ -147,7 +147,7 @@ describe('ctx.install', () => { ;(ctx.host as { mountConnectionMeta?: unknown }).mountConnectionMeta = mountConnectionMeta const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) - await ctx.install(makeDevframe({ cli: { distDir: '/tmp/demo-dist' } })) + await ctx.install(makeDevframe({ clientAssets: '/tmp/demo-dist' })) expect(mountConnectionMeta).toHaveBeenCalledWith('/__demo/') expect(warn).not.toHaveBeenCalled() diff --git a/packages/hub/src/node/install-devframe.ts b/packages/hub/src/node/install-devframe.ts index df8024ba..7b4fadf6 100644 --- a/packages/hub/src/node/install-devframe.ts +++ b/packages/hub/src/node/install-devframe.ts @@ -1,6 +1,7 @@ import type { DevframeDefinition } from 'devframe/types' import type { DevframeViewIframe } from '../types/docks' import type { DevframeHubContext } from './context' +import { resolveClientAssets } from 'devframe' import { resolveBasePath } from 'devframe/node/hub-internals' import { resolve } from 'pathe' import { diagnostics } from './diagnostics' @@ -86,7 +87,8 @@ export async function prepareDevframe( ? resolveBasePath(d, 'hosted') : resolveBasePath({ ...d, id, basePath: undefined }, 'hosted')) - if (d.cli?.distDir) { + const clientAssets = resolveClientAssets(d) + if (clientAssets) { // Serve the hub's connection meta under the devframe's base so its SPA // discovers the RPC/WS endpoint via `connectDevframe()`'s relative // `./__connection.json` fetch — instead of relying on inheriting it from a @@ -101,7 +103,7 @@ export async function prepareDevframe( await ctx.host.mountConnectionMeta(base) else diagnostics.DF8106({ id, name: d.name, base }) - const distSource = d.cli.distDir + const distSource = clientAssets // Resolve the plugin's assets against *its* dependency graph, not the // hub's: pass the devframe's own `importMetaUrl` as the default // `resolveFrom`. diff --git a/packages/json-render-ui/src/spa.ts b/packages/json-render-ui/src/spa.ts index 4e172eb8..58c0fe0c 100644 --- a/packages/json-render-ui/src/spa.ts +++ b/packages/json-render-ui/src/spa.ts @@ -3,12 +3,12 @@ import { fileURLToPath } from 'node:url' /** * Absolute path to the prebuilt standalone SPA assets shipped by this package - * (`dist/spa`). Point a devframe's `cli.distDir` at it to serve the out-of-box + * (`dist/spa`). Point a devframe's `clientAssets` at it to serve the out-of-box * renderer with no client build: * * ```ts * import { jsonRenderSpaDir } from '@devframes/json-render-ui/spa' - * defineDevframe({ cli: { command: 'my-app', distDir: jsonRenderSpaDir } }) + * defineDevframe({ clientAssets: jsonRenderSpaDir, cli: { command: 'my-app' } }) * ``` * * 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}. 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 `clientAssets` to the SPA assets (an explicit + * `clientAssets`, or the deprecated `cli.distDir`, still wins). The author + * supplies everything else (id, name, `setup`, port, …) as usual. * * ```ts * export default createJsonRenderDevframe({ @@ -33,6 +33,6 @@ export const jsonRenderSpaDir: string = fileURLToPath(new URL('./spa/', import.m export function createJsonRenderDevframe(definition: DevframeDefinition): DevframeDefinition { return { ...definition, - cli: { ...definition.cli, distDir: definition.cli?.distDir ?? jsonRenderSpaDir }, + clientAssets: definition.clientAssets ?? definition.cli?.distDir ?? jsonRenderSpaDir, } } diff --git a/packages/json-render-ui/src/spa/vite.config.ts b/packages/json-render-ui/src/spa/vite.config.ts index dcd6b751..58101d14 100644 --- a/packages/json-render-ui/src/spa/vite.config.ts +++ b/packages/json-render-ui/src/spa/vite.config.ts @@ -8,7 +8,7 @@ import { alias } from '../../../../alias' // the bundle is mount-path portable — it discovers its runtime base from // `document.baseURI` and connects via `connectDevframe()`. devframe's dev/build // adapters serve this directory verbatim (no HTML rewriting) when an app wires -// `cli.distDir = jsonRenderSpaDir`. +// `clientAssets = jsonRenderSpaDir`. export default defineConfig({ base: './', root: fileURLToPath(new URL('.', import.meta.url)), diff --git a/packages/next/src/handler.ts b/packages/next/src/handler.ts index 94659d19..735facdb 100644 --- a/packages/next/src/handler.ts +++ b/packages/next/src/handler.ts @@ -3,6 +3,7 @@ import type { InitDevframeOptions } from 'devframe/initiate' import { homedir } from 'node:os' import { join } from 'node:path' import process from 'node:process' +import { resolveClientAssets } from 'devframe' import { initDevframe } from 'devframe/initiate' export interface CreateDevframeNextHandlerOptions { @@ -112,10 +113,10 @@ export function createDevframeNextHandler( def: DevframeDefinition, options: CreateDevframeNextHandlerOptions = {}, ): DevframeNextHandler { - const distDir = def.cli?.distDir + const distDir = resolveClientAssets(def) if (!distDir) { throw new Error( - `[@devframes/next] createDevframeNextHandler("${def.id}") needs a built SPA to serve, but "cli.distDir" is not set on the devframe definition.`, + `[@devframes/next] createDevframeNextHandler("${def.id}") needs a built SPA to serve, but "clientAssets" is not set on the devframe definition.`, ) } diff --git a/packages/next/test/handler.test.ts b/packages/next/test/handler.test.ts index 24b1835f..437a1d25 100644 --- a/packages/next/test/handler.test.ts +++ b/packages/next/test/handler.test.ts @@ -5,7 +5,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { createDevframeNextHandler } from '../src/handler' -function makeDef(distDir: string): DevframeDefinition { +function makeDef(clientAssets: string): DevframeDefinition { return { id: 'test-next', name: 'Test Next', @@ -13,7 +13,7 @@ function makeDef(distDir: string): DevframeDefinition { packageName: '@test/next', homepage: '', description: '', - cli: { distDir }, + clientAssets, setup() {}, } } @@ -60,8 +60,23 @@ describe('createDevframeNextHandler', () => { it('throws when the definition has no built SPA', () => { const def = makeDef('') - def.cli = undefined - expect(() => createDevframeNextHandler(def)).toThrow(/cli\.distDir/) + def.clientAssets = undefined + expect(() => createDevframeNextHandler(def)).toThrow(/clientAssets/) + }) + + it('falls back to the deprecated cli.distDir', async () => { + const dist = mkdtempSync(join(tmpdir(), 'df-next-legacy-')) + writeFileSync(join(dist, 'index.html'), 'ok') + + const def = makeDef('') + def.clientAssets = undefined + def.cli = { distDir: dist } + + handler = createDevframeNextHandler(def, { host: '127.0.0.1' }) + await handler.ready + + const index = await handler.fetch(new Request('http://localhost:3000/__test-next/')) + expect(index.status).toBe(200) }) it('forwards the mcp option and advertises the side-car endpoint', async () => { diff --git a/packages/vite/src/single.ts b/packages/vite/src/single.ts index 9fe89556..af1f6418 100644 --- a/packages/vite/src/single.ts +++ b/packages/vite/src/single.ts @@ -4,6 +4,7 @@ import type { DevframeAuthHandler } from 'devframe/node/auth' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' import type { Plugin } from 'vite' import process from 'node:process' +import { resolveClientAssets } from 'devframe' import { initDevframe } from 'devframe/initiate' import { diagnostics, normalizeBasePath, resolveBasePath } from 'devframe/internal' import { resolveStaticAssetsSource } from 'devframe/utils/remote-assets' @@ -49,7 +50,7 @@ export interface DevframeVitePluginOptions { } /** - * Statically mount a devframe's built SPA (`def.cli.distDir`) at + * Statically mount a devframe's built SPA (`def.clientAssets`) at * `options.base` inside an existing Vite dev server. No RPC server is * started — reach for {@link devframeViteBridge} when the mounted UI * needs a live RPC/WebSocket connection back to the devframe. @@ -61,7 +62,7 @@ export interface DevframeVitePluginOptions { */ export function devframeVitePlugin(d: DevframeDefinition, options: DevframeVitePluginOptions = {}): DevframeVitePlugin { const base = normalizeMountBase(options.base ?? resolveBasePath(d, 'hosted')) - const distDir = d.cli?.distDir + const distDir = resolveClientAssets(d) return { name: `devframe:${d.id}`, @@ -122,7 +123,7 @@ export interface DevframeViteBridgeOptions { /** * Bridge a devframe's RPC + WebSocket backend into an existing Vite dev - * server: the host app owns the SPA (`distDir` is never mounted), and this + * server: the host app owns the SPA (`clientAssets` is never mounted), and this * plugin serves discovery (`__connection.json`), the WebSocket RPC * upgrade (`__ws`, shared on Vite's own HTTP server), and the * optional MCP route through {@link initDevframe}'s node middleware — so @@ -159,7 +160,7 @@ export function devframeViteBridge(d: DevframeDefinition, options: DevframeViteB const created = initDevframe(d, { base, // The host app owns the SPA in bridge mode — never mount the - // definition's own distDir here. + // definition's own client assets here. distDir: false, flags: options.flags, host: options.host, diff --git a/packages/vite/test/single.test.ts b/packages/vite/test/single.test.ts index b3efe862..e485a1a3 100644 --- a/packages/vite/test/single.test.ts +++ b/packages/vite/test/single.test.ts @@ -97,7 +97,7 @@ describe('devframeVitePlugin (static mount)', () => { const distDir = mkdtempSync(join(tmpdir(), 'devframe-vite-plugin-')) writeFileSync(join(distDir, 'index.html'), '

hi

') - const plugin = devframeVitePlugin(defineTestDef({ cli: { distDir } }), { base: '/__static-test/' }) + const plugin = devframeVitePlugin(defineTestDef({ clientAssets: distDir }), { base: '/__static-test/' }) vite = fakeViteServer() await vite.listen(vitePort, host) await plugin.configureServer?.(vite as any) diff --git a/skills/devframe/SKILL.md b/skills/devframe/SKILL.md index d9e0881e..a474d7c2 100644 --- a/skills/devframe/SKILL.md +++ b/skills/devframe/SKILL.md @@ -77,7 +77,7 @@ export default defineDevframe({ homepage: pkg.homepage, description: pkg.description, icon: 'ph:magnifying-glass-duotone', - cli: { distDir: './client/dist' }, + clientAssets: './client/dist', // built SPA served as the UI setup(ctx) { const my = ctx.scope('my-inspector') // preferred — auto-namespaces ids my.rpc.register(defineRpcFunction({ @@ -540,7 +540,7 @@ Built-in context: `clientType` (`'embedded' | 'standalone'`), `dockOpen`, `palet | Subcommand | Action | |------------|--------| -| *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `cli.distDir` served at the base | +| *(default)* | Dev server (port 9999 or `--port`) — WebSocket RPC, `clientAssets` served at the base | | `build` | Static snapshot → `./dist-static/` (`--out-dir`) | | `mcp` | stdio MCP server | diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 58acf736..7cab9b7e 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -147,6 +147,7 @@ export interface DevframeDefinition { build?: boolean; }; services?: DevframeServiceInput[]; + clientAssets?: StaticAssetsSource; rpc?: DevframeRpcOptions; setup: (_: DevframeNodeContext, _?: DevframeSetupInfo) => void | Promise; cli?: DevframeCliOptions; @@ -506,6 +507,7 @@ export type StaticAssetsSource = string | RemoteAssets; // #region Functions export declare function defineDevframe(_: DevframeDefinition): DevframeDefinition; +export declare function resolveClientAssets(_: DevframeDefinition): StaticAssetsSource | undefined; // #endregion // #region Variables diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.js b/tests/__snapshots__/tsnapi/devframe/index.snapshot.js index e681572e..508aeb23 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.js @@ -3,6 +3,7 @@ */ // #region Functions export function defineDevframe(_) {} +export function resolveClientAssets(_) {} // #endregion // #region Variables