From a6a79e6338708d6f2b6aaa148aa082f8ceead940 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Tue, 25 Aug 2026 02:32:59 +0000 Subject: [PATCH] docs: add Project Structure guide page --- .../{9.when-clauses.md => 10.when-clauses.md} | 0 ...standalone-cli.md => 11.standalone-cli.md} | 0 .../1.guide/{11.client.md => 12.client.md} | 0 .../{12.transports.md => 13.transports.md} | 0 .../{13.security.md => 14.security.md} | 0 ...{14.agent-native.md => 15.agent-native.md} | 0 docs/content/1.guide/{15.hub.md => 16.hub.md} | 0 ...client-context.md => 17.client-context.md} | 0 ...{17.hub-initiate.md => 18.hub-initiate.md} | 0 .../{18.services.md => 19.services.md} | 0 docs/content/1.guide/2.project-structure.md | 282 ++++++++++++++++++ ...{19.deep-linking.md => 20.deep-linking.md} | 0 .../1.guide/{20.events.md => 21.events.md} | 0 ...22.build-your-own-json-render-frontend.md} | 0 ...-hub-ui.md => 23.build-your-own-hub-ui.md} | 0 .../{23.built-with.md => 24.built-with.md} | 0 docs/content/1.guide/{2.rpc.md => 3.rpc.md} | 0 .../{3.shared-state.md => 4.shared-state.md} | 0 .../{4.streaming.md => 5.streaming.md} | 0 ...{5.client-assets.md => 6.client-assets.md} | 0 ....scoped-context.md => 7.scoped-context.md} | 0 .../{7.json-render.md => 8.json-render.md} | 0 .../{8.diagnostics.md => 9.diagnostics.md} | 0 docs/content/1.guide/index.md | 1 + 24 files changed, 283 insertions(+) rename docs/content/1.guide/{9.when-clauses.md => 10.when-clauses.md} (100%) rename docs/content/1.guide/{10.standalone-cli.md => 11.standalone-cli.md} (100%) rename docs/content/1.guide/{11.client.md => 12.client.md} (100%) rename docs/content/1.guide/{12.transports.md => 13.transports.md} (100%) rename docs/content/1.guide/{13.security.md => 14.security.md} (100%) rename docs/content/1.guide/{14.agent-native.md => 15.agent-native.md} (100%) rename docs/content/1.guide/{15.hub.md => 16.hub.md} (100%) rename docs/content/1.guide/{16.client-context.md => 17.client-context.md} (100%) rename docs/content/1.guide/{17.hub-initiate.md => 18.hub-initiate.md} (100%) rename docs/content/1.guide/{18.services.md => 19.services.md} (100%) create mode 100644 docs/content/1.guide/2.project-structure.md rename docs/content/1.guide/{19.deep-linking.md => 20.deep-linking.md} (100%) rename docs/content/1.guide/{20.events.md => 21.events.md} (100%) rename docs/content/1.guide/{21.build-your-own-json-render-frontend.md => 22.build-your-own-json-render-frontend.md} (100%) rename docs/content/1.guide/{22.build-your-own-hub-ui.md => 23.build-your-own-hub-ui.md} (100%) rename docs/content/1.guide/{23.built-with.md => 24.built-with.md} (100%) rename docs/content/1.guide/{2.rpc.md => 3.rpc.md} (100%) rename docs/content/1.guide/{3.shared-state.md => 4.shared-state.md} (100%) rename docs/content/1.guide/{4.streaming.md => 5.streaming.md} (100%) rename docs/content/1.guide/{5.client-assets.md => 6.client-assets.md} (100%) rename docs/content/1.guide/{6.scoped-context.md => 7.scoped-context.md} (100%) rename docs/content/1.guide/{7.json-render.md => 8.json-render.md} (100%) rename docs/content/1.guide/{8.diagnostics.md => 9.diagnostics.md} (100%) diff --git a/docs/content/1.guide/9.when-clauses.md b/docs/content/1.guide/10.when-clauses.md similarity index 100% rename from docs/content/1.guide/9.when-clauses.md rename to docs/content/1.guide/10.when-clauses.md diff --git a/docs/content/1.guide/10.standalone-cli.md b/docs/content/1.guide/11.standalone-cli.md similarity index 100% rename from docs/content/1.guide/10.standalone-cli.md rename to docs/content/1.guide/11.standalone-cli.md diff --git a/docs/content/1.guide/11.client.md b/docs/content/1.guide/12.client.md similarity index 100% rename from docs/content/1.guide/11.client.md rename to docs/content/1.guide/12.client.md diff --git a/docs/content/1.guide/12.transports.md b/docs/content/1.guide/13.transports.md similarity index 100% rename from docs/content/1.guide/12.transports.md rename to docs/content/1.guide/13.transports.md diff --git a/docs/content/1.guide/13.security.md b/docs/content/1.guide/14.security.md similarity index 100% rename from docs/content/1.guide/13.security.md rename to docs/content/1.guide/14.security.md diff --git a/docs/content/1.guide/14.agent-native.md b/docs/content/1.guide/15.agent-native.md similarity index 100% rename from docs/content/1.guide/14.agent-native.md rename to docs/content/1.guide/15.agent-native.md diff --git a/docs/content/1.guide/15.hub.md b/docs/content/1.guide/16.hub.md similarity index 100% rename from docs/content/1.guide/15.hub.md rename to docs/content/1.guide/16.hub.md diff --git a/docs/content/1.guide/16.client-context.md b/docs/content/1.guide/17.client-context.md similarity index 100% rename from docs/content/1.guide/16.client-context.md rename to docs/content/1.guide/17.client-context.md diff --git a/docs/content/1.guide/17.hub-initiate.md b/docs/content/1.guide/18.hub-initiate.md similarity index 100% rename from docs/content/1.guide/17.hub-initiate.md rename to docs/content/1.guide/18.hub-initiate.md diff --git a/docs/content/1.guide/18.services.md b/docs/content/1.guide/19.services.md similarity index 100% rename from docs/content/1.guide/18.services.md rename to docs/content/1.guide/19.services.md diff --git a/docs/content/1.guide/2.project-structure.md b/docs/content/1.guide/2.project-structure.md new file mode 100644 index 00000000..61705174 --- /dev/null +++ b/docs/content/1.guide/2.project-structure.md @@ -0,0 +1,282 @@ +--- +title: 'Project Structure' +description: 'A recommended folder layout for a devframe package — one definition, a node/client split, a namespaced RPC registry, and a mount-path-portable SPA that ships as its own assets package.' +--- + +A recommended folder layout for a devframe package. One [`defineDevframe`](/guide/devframe-definition) call drives every surface — a standalone CLI, a Vite host, a hub dock — so the layout's whole job is to keep the **node** side and the **browser** side apart, expose each through its own package subpath, and let the SPA travel to any mount path. + +The [`starter/`](https://github.com/devframes/devframe/tree/main/starter) template is the smallest expression of this shape; the eight [built-in plugins](/plugins) are the fullest. This page distills the convention they share. + +## The shape at a glance + +``` +my-plugin/ +├── bin.mjs # CLI entry: createCac(createMyDevframe()).parse() +├── package.json # exports map, bin, files +├── tsdown.config.ts # node + client library build +├── tsconfig.json # extends the repo base +├── uno.config.ts # SPA styling (design system) +├── src/ +│ ├── index.ts # createDevframe factory (default export) +│ ├── cli.ts # createCli — wraps the factory in createCac +│ ├── node/ +│ │ └── index.ts # setup(ctx) — registers RPC on a context +│ ├── rpc/ +│ │ ├── index.ts # serverFunctions[] + `declare module 'devframe'` +│ │ └── functions/ +│ │ └── *.ts # one defineRpcFunction per file +│ ├── client/ +│ │ └── index.ts # thin connect() browser helper +│ ├── shared/ # serializable types shared node ↔ browser +│ ├── diagnostics.ts # coded warnings/errors (node-side) +│ └── spa/ # the UI (Vue / Solid / Preact / React / vanilla) +│ ├── index.html +│ ├── main.ts # render entry; applies `.dark` from OS pref +│ ├── App.vue # (or app.tsx, …) +│ └── vite.config.ts # base: './', builds into the assets package +└── assets-pkg/ # sibling, lockstep-versioned prebuilt SPA package + └── package.json # @scope/my-plugin--assets +``` + +Not every part is mandatory. A minimal tool collapses to `src/index.ts` + one RPC file + one SPA (as in the starter); a rich tool adds an in-page agent, a query engine, or a data-source registry. The **seams** below stay the same regardless of size. + +## The node / client seam + +The single most important rule: **node-only code and browser-only code never share a module graph.** A `node:fs` import that leaks into the SPA bundle breaks the build; a DOM reference that leaks into the server breaks at runtime. Enforce the split with folders and with separate build entries. + +| Folder | Runs in | May import | +|--------|---------|------------| +| `src/index.ts`, `src/node/`, `src/rpc/`, `src/cli.ts`, `src/diagnostics.ts` | Node | `node:*`, `devframe`, server deps | +| `src/client/`, `src/spa/` | Browser | `devframe/client`, DOM, UI framework | +| `src/shared/`, `src/types.ts` | Both | nothing environment-specific (plain serializable types/constants) | + +`tsdown` compiles the two sides as **separate rolldown graphs** so a stray node import can never reach the client bundle: + +```ts [tsdown.config.ts] +import { defineConfig } from 'tsdown' + +const tsconfig = '../../tsconfig.base.json' + +// Browser-loaded entry — isolated graph. +const clientEntries = { 'client/index': 'src/client/index.ts' } + +// Node-side entries — definition, CLI, setup module, RPC registry. +const serverEntries = { + 'index': 'src/index.ts', + 'cli': 'src/cli.ts', + 'node/index': 'src/node/index.ts', + 'rpc/index': 'src/rpc/index.ts', +} + +export default defineConfig([ + { clean: true, platform: 'browser', tsconfig, dts: false, outExtensions: () => ({ js: '.mjs' }), entry: clientEntries }, + { clean: false, platform: 'node', tsconfig, dts: false, entry: serverEntries }, + // One combined dts pass so the `declare module 'devframe'` RPC augmentation resolves once. + { clean: false, platform: 'neutral', tsconfig, dts: { emitDtsOnly: true }, outExtensions: () => ({ dts: '.d.mts' }), entry: { ...clientEntries, ...serverEntries } }, +]) +``` + +The SPA under `src/spa/` is a Vite app and builds separately (`vite build --config src/spa/vite.config.ts`), not through `tsdown`. + +## The definition factory — `src/index.ts` + +A plugin's default export is its **factory**, never a pre-built instance — so importing the module costs nothing until a consumer calls it with their own options ([why](https://github.com/devframes/devframe/blob/main/AGENTS.md)): + +```ts [src/index.ts] +import type { DevframeDefinition, RemoteAssets } from 'devframe' +import { defineDevframe } from 'devframe' +import pkg from '../package.json' with { type: 'json' } +import { setupMyPlugin } from './node/index' + +const DEFAULT_ID = 'my-plugin' + +// The SPA ships in the sibling assets package (see below), served on demand. +const distDir: RemoteAssets = { package: `${pkg.name}--assets`, version: pkg.version } + +export interface MyPluginOptions { + id?: string + name?: string + icon?: string + basePath?: string + port?: number +} + +export function createMyPluginDevframe(options: MyPluginOptions = {}): DevframeDefinition { + const id = options.id ?? DEFAULT_ID + return defineDevframe({ + id, + name: options.name ?? 'My Plugin', + version: pkg.version, + packageName: pkg.name, + importMetaUrl: import.meta.url, + homepage: pkg.homepage, + description: pkg.description, + icon: options.icon ?? 'ph:gauge-duotone', + basePath: options.basePath, + cli: { command: id, port: options.port ?? 9000, distDir }, + setup(ctx) { + setupMyPlugin(ctx) + }, + }) +} + +export default createMyPluginDevframe +``` + +Metadata comes from `package.json` (one source of truth for version/name/homepage/description). `importMetaUrl: import.meta.url` is what lets the definition resolve its own dependencies and remote assets — always pass it. + +`setup` stays thin: it delegates to a `setup(ctx)` function in `src/node/` so host adapters that wire their own context can reuse it. + +```ts [src/node/index.ts] +import type { DevframeNodeContext } from 'devframe' +import { serverFunctions } from '../rpc/index' + +export function setupMyPlugin(ctx: DevframeNodeContext): void { + for (const fn of serverFunctions) + ctx.rpc.register(fn) +} +``` + +## The RPC registry — `src/rpc/` + +One `defineRpcFunction` per file under `src/rpc/functions/`, collected in `src/rpc/index.ts`, which also augments the `devframe` module so every registered id is typed: + +```ts [src/rpc/index.ts] +import type { RpcDefinitionsToFunctions } from 'devframe/rpc' +import { getState } from './functions/get-state' +import { listItems } from './functions/list-items' + +export const serverFunctions = [getState, listItems] as const + +declare module 'devframe' { + interface DevframeRpcServerFunctions extends RpcDefinitionsToFunctions {} +} +``` + +Namespace every RPC id with the plugin's slug — `defineRpcFunction({ name: 'devframes:plugin:my-plugin:get-state', … })` — matching the `@devframes/plugin-` package name. Inside `setup` you can instead take a scoped context (`ctx.scope('my-plugin')`) so bare names auto-prefix; see [Scoped Context](/guide/scoped-context). See [RPC](/guide/rpc) for the function types. + +## The browser helper — `src/client/index.ts` + +A thin, typed wrapper over [`connectDevframe`](/guide/client), published as the `./client` subpath. The SPA derives its base from `document.baseURI`, so no options are needed in the common case: + +```ts [src/client/index.ts] +import type { DevframeRpcClient, DevframeRpcClientOptions } from 'devframe/client' +import { connectDevframe } from 'devframe/client' + +export type { DevframeRpcClient } +export type { MyState } from '../shared/types' // re-export the serializable types + +export function connectMyPlugin(options?: DevframeRpcClientOptions): Promise { + return connectDevframe(options) +} +``` + +## The SPA — `src/spa/` + +The UI is a standalone Vite app. Pick any framework — the built-ins use Vue, Solid, React (Next), and Svelte; the starter uses vanilla TypeScript. Two rules make it portable: + +1. **`base: './'`** — relative asset URLs so the same build serves under `/`, `/__my-plugin/`, or any hub mount path. Discover the runtime base from `document.baseURI`; never hardcode a mount path or inject one at build time. +2. **Style with the shared [design system](https://github.com/antfu/design)** via `uno.config.ts` (`mergeConfigs([designConfig, …])`), so the surface looks like one product across frameworks. + +```ts [src/spa/vite.config.ts] +import { fileURLToPath } from 'node:url' +import { devframeVite } from '@devframes/vite/single' +import vue from '@vitejs/plugin-vue' +import UnoCSS from 'unocss/vite' +import { defineConfig } from 'vite' +import createMyPluginDevframe from '../index' + +export default defineConfig({ + base: './', + root: fileURLToPath(new URL('.', import.meta.url)), + plugins: [ + vue(), + UnoCSS(), + // Bridge dev-mode RPC/WS so the SPA has a live backend while you iterate. + devframeVite(createMyPluginDevframe(), { bridge: true, base: '/' }), + ], + build: { + // Emit into the sibling assets package rather than this slim node package. + outDir: fileURLToPath(new URL('../../assets-pkg/dist', import.meta.url)), + emptyOutDir: true, + }, +}) +``` + +The `main` entry applies dark mode from the OS preference and mounts the app; see any plugin's `src/spa/main.ts`. + +## Shipping the SPA as an assets package + +The built-in plugins keep the node package slim and publish the compiled SPA as a **sibling, lockstep-versioned package** named `--assets`. The definition points `clientAssets`/`distDir` at it as `RemoteAssets` (`{ package, version }`), and `importMetaUrl` lets a locally installed copy be served with zero network — otherwise the assets stream on demand through devframe's caching back-proxy. + +``` +my-plugin/ +├── package.json # @scope/my-plugin (node code) +└── assets-pkg/ + └── package.json # @scope/my-plugin--assets (prebuilt SPA, files: ["dist"]) +``` + +The assets package is otherwise empty — its `build` just delegates to the plugin's SPA build. This split is optional: a self-contained tool can instead point `clientAssets` at a local `dist/client` directory resolved via `fileURLToPath(new URL('../dist/client', import.meta.url))`, which is what the [starter](https://github.com/devframes/devframe/tree/main/starter) does. + +## The CLI entry — `bin.mjs` + `src/cli.ts` + +Standalone tools ship a `bin`. `src/cli.ts` wraps the factory in [`createCac`](/guide/standalone-cli); `bin.mjs` is the three-line executable: + +```ts [src/cli.ts] +import { createCac } from 'devframe/adapters/cac' +import createMyPluginDevframe from './index' + +export function createMyPluginCli() { + return createCac(createMyPluginDevframe()) +} +``` + +```js [bin.mjs] +#!/usr/bin/env node +import { createMyPluginCli } from './dist/cli.mjs' + +createMyPluginCli().parse() +``` + +This gives `dev`, `build`, and `mcp` subcommands for free. + +## The package manifest + +The `exports` map mirrors the build entries — one subpath per seam — and `files` ships only `bin.mjs` and `dist`: + +```jsonc [package.json] +{ + "name": "@devframes/plugin-my-plugin", + "type": "module", + "exports": { + ".": "./dist/index.mjs", // the factory + "./client": "./dist/client/index.mjs", // browser helper + "./cli": "./dist/cli.mjs", // createCac wrapper + "./node": "./dist/node/index.mjs", // setup(ctx) for host adapters + "./package.json": "./package.json" + }, + "types": "./dist/index.d.mts", + "bin": { "devframe-my-plugin": "./bin.mjs" }, + "files": ["bin.mjs", "dist"], + "sideEffects": false +} +``` + +## Optional pieces + +Reach for these only when the tool needs them — the built-ins show each in context: + +- **`src/inject/`** — an **in-page agent** the hub imports into the host page as a dock [client script](/guide/client-context) (the [a11y](/plugins/a11y) scanner, the [data-inspector](/plugins/data-inspector) collector). Builds separately with Vite into `dist/inject`. +- **`src/shared/`** — protocol types and constants shared across the node/client seam (the a11y `protocol.ts`). Keep these free of environment-specific imports so either side can bundle them. +- **`src/engine/`, `src/registry/`** — domain subsystems that deserve their own module (the data-inspector's jora query engine and data-source registry). +- **`src/diagnostics.ts`** — [structured diagnostics](/guide/diagnostics) for any node-side warning or error, via `defineDiagnostics` from `devframe/utils/nostics`. Node-side only; browser code keeps using `console`/`throw`. +- **`playground/`** — Vite hosts that dev-serve the SPA against a live backend, one per consumption scope (a `single` bridge and a `hub` dock), as in the starter. +- **`test/` and `e2e/`** — `vitest` specs that boot the devframe in-process and exercise RPC over a real WebSocket, plus Playwright tests against a playground. + +## What's next + +- [Devframe Definition](/guide/devframe-definition) — every field of `defineDevframe` +- [RPC](/guide/rpc) — the function types you register +- [Client Assets](/guide/client-assets) — local dirs vs remote assets packages +- [Standalone CLI](/guide/standalone-cli) — the `createCac` command shell +- [Built-in Plugins](/plugins) — the layout applied at full scale diff --git a/docs/content/1.guide/19.deep-linking.md b/docs/content/1.guide/20.deep-linking.md similarity index 100% rename from docs/content/1.guide/19.deep-linking.md rename to docs/content/1.guide/20.deep-linking.md diff --git a/docs/content/1.guide/20.events.md b/docs/content/1.guide/21.events.md similarity index 100% rename from docs/content/1.guide/20.events.md rename to docs/content/1.guide/21.events.md diff --git a/docs/content/1.guide/21.build-your-own-json-render-frontend.md b/docs/content/1.guide/22.build-your-own-json-render-frontend.md similarity index 100% rename from docs/content/1.guide/21.build-your-own-json-render-frontend.md rename to docs/content/1.guide/22.build-your-own-json-render-frontend.md diff --git a/docs/content/1.guide/22.build-your-own-hub-ui.md b/docs/content/1.guide/23.build-your-own-hub-ui.md similarity index 100% rename from docs/content/1.guide/22.build-your-own-hub-ui.md rename to docs/content/1.guide/23.build-your-own-hub-ui.md diff --git a/docs/content/1.guide/23.built-with.md b/docs/content/1.guide/24.built-with.md similarity index 100% rename from docs/content/1.guide/23.built-with.md rename to docs/content/1.guide/24.built-with.md diff --git a/docs/content/1.guide/2.rpc.md b/docs/content/1.guide/3.rpc.md similarity index 100% rename from docs/content/1.guide/2.rpc.md rename to docs/content/1.guide/3.rpc.md diff --git a/docs/content/1.guide/3.shared-state.md b/docs/content/1.guide/4.shared-state.md similarity index 100% rename from docs/content/1.guide/3.shared-state.md rename to docs/content/1.guide/4.shared-state.md diff --git a/docs/content/1.guide/4.streaming.md b/docs/content/1.guide/5.streaming.md similarity index 100% rename from docs/content/1.guide/4.streaming.md rename to docs/content/1.guide/5.streaming.md diff --git a/docs/content/1.guide/5.client-assets.md b/docs/content/1.guide/6.client-assets.md similarity index 100% rename from docs/content/1.guide/5.client-assets.md rename to docs/content/1.guide/6.client-assets.md diff --git a/docs/content/1.guide/6.scoped-context.md b/docs/content/1.guide/7.scoped-context.md similarity index 100% rename from docs/content/1.guide/6.scoped-context.md rename to docs/content/1.guide/7.scoped-context.md diff --git a/docs/content/1.guide/7.json-render.md b/docs/content/1.guide/8.json-render.md similarity index 100% rename from docs/content/1.guide/7.json-render.md rename to docs/content/1.guide/8.json-render.md diff --git a/docs/content/1.guide/8.diagnostics.md b/docs/content/1.guide/9.diagnostics.md similarity index 100% rename from docs/content/1.guide/8.diagnostics.md rename to docs/content/1.guide/9.diagnostics.md diff --git a/docs/content/1.guide/index.md b/docs/content/1.guide/index.md index fefe9793..79fbcd2c 100644 --- a/docs/content/1.guide/index.md +++ b/docs/content/1.guide/index.md @@ -170,6 +170,7 @@ The CLI adapter serves the SPA at `/`; embedded in a host (`vite`, `embedded`) t ## What's next - [Devframe Definition](/guide/devframe-definition) — `defineDevframe` and `DevframeNodeContext` +- [Project Structure](/guide/project-structure) — a recommended folder layout for a devframe package - [The Standard Handler](/adapters/initiate) — mount into any host - [Adapters](/adapters) — convenience entry points - [Hub](/guide/hub) — compose many devframes