Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agents/01-positioning.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 01 - Positioning

## What each layer is

**`devframe`** is the framework-neutral container for one devtool integration, portable across hub UI providers. It packages a single tool - its RPC, its SPA, its diagnostics, its CLI/build/embedded outputs - without caring how it will be displayed. A devframe runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub.

**`@devframes/hub`** is the framework-neutral hub layer on top of devframe: multi-devframe orchestration (docks, terminals, messages, commands). It ships no UI - hub UI providers (e.g. `@vitejs/devtools-kit`) provide their own UI over the hub's RPC + shared-state protocol. It does ship a **headless client runtime** (`createDevframeClientRuntime()` 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 built-in devframe like the a11y inspector runs its page script inside the user app's page. `examples/custom-hub-vite/` is a working ~120-line Vite host demonstrating the protocol end to end.

## Design principles

These enforce the split "devframe provides primitives, the hub provides UX". When in doubt, err toward the primitive.

- **Single-integration scope.** Devframe describes one tool. A feature that only makes sense when multiple tools share a UI - docking, a unified command palette, cross-tool toasts, terminal aggregation - MUST live in a hub package, not in `devframe`.
- **Headless by default.** Core and adapters MUST NOT ship default startup banners, opinionated stdout logging, or default styling. Provide hooks (`onReady`, `cli.configure`, …) and let the application print its own branding. Structured diagnostics via `nostics` are fine ([08](./08-diagnostics.md)); 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`, `build`). Authors override via `DevframeDefinition.basePath`. Adapter code that may run standalone MUST NOT hardcode a mount path.
- **SPAs own their basePath at runtime.** SPAs are built with relative asset paths (`vite.base: './'`) and 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 RPC client (`connectDevframe`) resolves `.connection.json` relative to the runtime base automatically.
- **CLI flags compose from both sides.** The `cac` instance backing `createCac` is exposed to the `DevframeDefinition` (`cli.configure(cli)`) for capabilities the tool contributes, and to the `createCac` caller for flags added at final assembly. Parsed flag values are forwarded to `setup(ctx, { flags })`. `createCac` itself MUST NOT hardcode domain-specific flags.
14 changes: 14 additions & 0 deletions .agents/02-terminology.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 02 - Terminology

The docs' canonical vocabulary lives in [`docs/content/8.references/1.terms.md`](../docs/content/8.references/1.terms.md) - one name per concept. Every docs, README, and comment edit MUST follow it.

- Bare `client`, `host`, `server`, `agent`, `plugin`, `embedded`, or `standalone` MUST NOT appear in prose. Use a fixed compound from the terms page or a code-formatted API/package name. One exception: the directional `client → server` arrows in the RPC/events reference tables.
- The tool is **"a devframe"** - never "integration", "frame", or "app". The ready-to-run `@devframes/plugin-*` packages are **built-in devframes**: devframe has no plugin concept; the `plugin-` npm prefix only sets those packages apart from core packages. "Vite plugin" stays for the bundler mechanism, and RPC ids keep the literal `devframes:plugin:<slug>:` namespace because it mirrors the package names on the wire.
- **host framework** is the environment a devframe or hub mounts into (a Vite dev server, a Next.js app, a Hono server); named forms like "the Vite host" are fine. **host page** is the browser document where the client runtime boots; **user app** is the application being developed and inspected.
- A devframe's two halves are the **node side** and the **browser side**.
- Browser-side terms: **client runtime** (`createDevframeClientRuntime()`), **client context**, **client script**, **page script** (a devframe's script in the user app's page - never "agent"; **coding agent** is the only agent), **RPC client** (`connectDevframe()`), **SPA**, **panel** (a devframe's SPA as a rendered surface), **surface** (any rendered browser view - say "API", not "API surface").
- Hub terms: **hub UI provider** (a hub UI implementation - never "shell" or bare "viewer"; "external viewer" stays for cross-origin surfaces in the security docs), **dock entry** / **dock rail** / **dock panel**, **mounted devframe** (never "frame").
- The three communication paths: **RPC** (browser side ↔ node side), the **client context** (client scripts ↔ client runtime), and the **in-page channel** (page script ↔ panel, same-origin in-browser).
- Storage scopes: **workspace scope** (committable, per-repo), **project scope** (per-checkout), **global scope** (per-user). The project scope MUST NOT be described as "per-workspace".
- **framework kits** are `@devframes/vite` / `@devframes/nuxt` / `@devframes/next`; refer to external products by their full names (`@vitejs/devtools-kit`, `@nuxt/devtools`).
- Qualify the rest: embedded/standalone only as attached adjectives (embedded adapter, standalone SPA), sessions (terminal / MCP / trust session), entries (dock entry / entry point / browser entry), bridges (RPC bridge).
60 changes: 60 additions & 0 deletions .agents/03-stack-and-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# 03 - Stack, layout, and commands

## Stack

ESM TypeScript library. Bundled with `tsdown`. Tested with `vitest`. pnpm workspaces with catalog dependencies (`pnpm-workspace.yaml`); workspace globs reserve `playground`, `docs`, `packages/*`, `examples/*` for future additions.

Per-package layout:

- `src/` - library code; entry `src/index.ts`
- `test/` - vitest specs; API snapshots via `tsnapi` under `test/__snapshots__/`
- `dist/` - `tsdown` build output (shipped to the npm tarball via `files`)

## Commands

```sh
pnpm install # requires pnpm@11.x
pnpm build # tsdown
pnpm dev # tsdown --watch
pnpm test # pnpm build && vitest (api snapshot guards against stale dist)
pnpm typecheck # turbo run typecheck (per-package tsc --noEmit)
pnpm lint --fix # ESLint via @antfu/eslint-config
pnpm knip # unused files/dependencies/exports across every workspace
pnpm start # tsx src/index.ts
```

Before opening a PR, all five gates MUST pass:

```sh
pnpm lint && pnpm knip && pnpm test && pnpm typecheck && pnpm build
```

Commits and PR titles MUST follow Conventional Commits (`feat:`, `fix:`, …).

## Testing

The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots compare against fresh `dist/`. `tsdown-stale-guard` enforces this in `test/api-snapshot.test.ts`.

## Typechecking

`pnpm typecheck` fans out through Turbo: every workspace package MUST own a `"typecheck": "tsc --noEmit"` script and its own `tsconfig.json` (extending `tsconfig.base.json` with an explicit `include`). Cross-package imports resolve to source through the `paths` aliases in `tsconfig.base.json`, so no prior build is needed. A new package under `packages/*` or `plugins/*` joins the fan-out the moment it ships that script - always add one so type errors can't be silently skipped.

`scripts/verify-typecheck-coverage.ts` runs first and fails the command (and CI, which just runs `pnpm typecheck`) if any workspace package has a `tsconfig.json` but no `typecheck` script. A package that genuinely can't typecheck yet needs a documented exception in that script, not a missing script.

## Generated artifacts under `src/`

Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/` and `packages/json-render-ui/src/.generated/` - are **generated, not committed** (`.generated` is gitignored). Each owning package builds its own with `pnpm run build:css`; three things guarantee the file is on disk before anything imports it: the root `postinstall` runs `turbo run build:css`, the Turbo `typecheck` task depends on both `build:css` tasks, and each package's `build` script chains `build:css` first. A new generated-under-`src` artifact MUST follow the same shape - its own build script, declared `outputs` in `turbo.json`, and a `typecheck` dependency - and MUST NOT be checked in: a minified single-line blob conflicts on every concurrent edit.

## `starter/`

The top-level, self-contained template for creating a new devframe (Vanilla TS, Vite SPA, playgrounds, tests). It uses real versions in its `package.json` (no catalogs, no `workspace:*`) so it is copy-paste ready for users; pnpm links its `devframe`/`@devframes/*` dependencies to the local workspace copies during development. When `bumpp -r` bumps the repo versions, `bumpp.config.ts` runs `scripts/sync-starter-version.ts` to update the starter's dependencies to match.

## knip

`pnpm knip` finds unused files, dependencies, and exports across every workspace (config in `knip.jsonc`), running against source directly - no prior build needed. Most workspaces need no configuration; `knip.jsonc` only carries per-workspace overrides for what knip's defaults can't infer:

- **Non-`index.ts` `exports` subpaths.** knip's package.json→`dist`→`src` source mapping needs a workspace `tsconfig.json` `outDir`, which conflicts with this repo's cross-workspace `src/*.ts` imports, so multi-entry packages list their `exports`-mapped entry files explicitly - keep that list in sync with each `tsdown.config.ts`.
- **Config files knip's plugins don't discover** in a nested location (a Next.js app rooted below the workspace root, `storybook-solidjs-vite` not matching the Storybook plugin trigger).
- **Dependencies referenced dynamically** outside the static import graph (icon collections consumed by UnoCSS at build time, built-in devframe packages loaded via a runtime `import()` string).

Prefer fixing the underlying gap or a scoped `ignoreDependencies`/`entry` override over a blanket `ignore`.
29 changes: 29 additions & 0 deletions .agents/04-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 04 - Code conventions

## RPC

RPC functions MUST use `defineRpcFunction`. Built-in devframes MUST namespace IDs `devframes:plugin:<slug>:<fn-name>` - the literal `plugin:` token mirrors the `@devframes/plugin-<slug>` package name on the wire; it is npm namespacing, not a concept.

## Event names come from the centralized maps

Every event, broadcast, shared-state key, and channel name lives in one of two source-of-truth maps: `DEVFRAME_EVENTS` (`packages/devframe/src/events.ts`, re-exported from `devframe/constants`) for the core runtime, and `HUB_EVENTS` (`packages/hub/src/events.ts`, re-exported from `@devframes/hub/constants`) for the hub. Call sites (`.events.emit`/`.on`, `rpc.broadcast({ method })`, `sharedState.get(key)`, `defineHubRpcFunction({ name })`, `rpc.call`) MUST reference `DEVFRAME_EVENTS.*` / `HUB_EVENTS.*`, never a re-typed string literal.

The two maps and the [`docs/content/8.references/3.events.md`](../docs/content/8.references/3.events.md) Events Reference stay in lockstep: adding, renaming, or removing a name means editing the map **and** that page in the same change; every name in the maps appears in the tables, and vice versa.

Permitted literals: unavoidable type-position keys (the `EventEmitter<…>` maps in `types/*` and the `DevframeRpcClientFunctions`/`DevframeRpcServerFunctions` augmentations), which mirror the maps; and a package that deliberately avoids a hub dependency (e.g. `@devframes/plugin-terminals`, which models the hub bridge structurally) keeps a local literal rather than importing `HUB_EVENTS`.

## Validator neutrality

`devframe` and every `@devframes/*` package MUST NOT carry a schema validator (`valibot`, `zod`, `arktype`, …) in their runtime `dependencies`. `args`/`returns`/flag schemas are typed against [Standard Schema](https://standardschema.dev/) (`@standard-schema/spec`, types-only); first-party code that needs to author a schema uses the built-in zero-dep `devframe/utils/simple-schema` builder (deliberately minimal - not a general validator). JSON-schema conversion uses each schema's own Standard JSON Schema converter (`~standard.jsonSchema`, implemented by e.g. zod 4) when present and degrades to a permissive object otherwise - no converter library, no vendor dependency.

Docs, by contrast, SHOULD point *users* at a real validator for their own integrations - recommend **valibot** (lightest) or **zod** (worth reusing if they already pull it via the JSON-render or MCP integrations).

## Imports, state, dependencies

- Shared state goes through `devframe/utils/shared-state`; values MUST stay serializable.
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.
- Dependencies go through the pnpm catalogs in `pnpm-workspace.yaml` (`cli`, `inlined`, `testing`, `types`) - add to a catalog and reference as `catalog:<name>`; versions MUST NOT be pinned in a `package.json`.

## Default exports are factories

A built-in devframe's default export MUST be its `create<X>Devframe` factory, never a pre-built instance. `export default createXDevframe()` (or the two-line equivalent) eagerly constructs a `DevframeDefinition` at import time whether or not any consumer wants that exact zero-config shape; a consumer that needs its own options (an id override, a data directory, …) pays for a second, discarded instance. Alias the factory itself - `export default createXDevframe` - so importing costs nothing and every consumer calls it to get their own instance: `import createA11yDevframe from '@devframes/plugin-a11y'` then `createA11yDevframe(options)`.
22 changes: 22 additions & 0 deletions .agents/05-framework-kits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 05 - Framework kits: two scopes, one shape

The framework kits - `@devframes/vite`, `@devframes/nuxt`, `@devframes/next` - each split their surface into two clearly-scoped subpaths, because a consumer is always doing one of two distinct jobs. All three kits MUST stay parallel.

## `.../single` - author one devframe

Build & dev-serve a single devframe's SPA with that tool. Vite: the `devframeVitePlugin` / `devframeViteBridge` / `devframeVite` plugins. Next: `withDevframe` + `createDevframeNextHandler`, with its React client at `.../single/client`. Nuxt: the Nuxt module (registered as `modules: ['@devframes/nuxt/single']`).

## `.../hub` - stand up devtools

Mount a whole `@devframes/hub` (many devframes) inside that tool. Wraps `initHub`, defaults the UI slot to `@devframes/hub-ui`'s `createUi()` (overridable via `ui`, or `ui: false` for headless), and ships a browser client helper at `.../hub/client` (a thin, lifecycle-managing wrapper over `@devframes/hub/client`'s `createDevframeClientRuntime`).

`@devframes/hub` and `@devframes/hub-ui` are **optional peers** of these packages; `hub-ui` MUST be loaded lazily (a bundler-ignored dynamic `import()` in the Next hub) so it stays optional and its `import.meta.url` asset lookups resolve at request time.

## Rules

- The bare root (`.`) MUST throw a helpful error pointing at the two subpaths - real code never lives on it.
- Vite and Nuxt already have native hub UI providers (`@vitejs/devtools-kit`, `@nuxt/devtools`), so `@devframes/vite/hub` and `@devframes/nuxt/hub` still work but emit a one-time `console.warn` recommending those (silenced with `{ quiet: true }`). `@devframes/next/hub` has no native counterpart and warns nothing.

## How the examples consume the kits

The **full hub examples** (`examples/custom-hub-vite`, `examples/custom-hub-next`) consume `.../hub` on the node side but hand-roll their own hub UI provider against `@devframes/hub/client` with `ui: false` - that hand-rolled hub UI provider is the whole point of those reference hosts ([07](./07-hub-examples.md)). The **minimal** ones (`examples/hub-vite`, `examples/hub-next`, `examples/hub-deno`, `examples/hub-fastify`, `examples/hub-hono`, `examples/hub-nitro`, `examples/hub-rsbuild`, `examples/hub-sveltekit`) consume `.../hub` with the default `@devframes/hub-ui` and inject its `embedded.js`, needing no browser-side code.
Loading
Loading