diff --git a/.gitignore b/.gitignore index ebc185fd..ddc710af 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ *.tsbuildinfo coverage dist +.next +.nitro +.output lib-cov logs node_modules diff --git a/AGENTS.md b/AGENTS.md index bf4d5383..636126ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ **`devframe`** is the framework-neutral container for one devtool integration, portable across viewers. Build a single tool (its RPC, its SPA, its diagnostics, its CLI/build/spa/embedded outputs) without caring how it'll be displayed. A devframe app runs standalone (CLI, static deploy, embedded SPA) just as well as it mounts inside a hub. -**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. It does ship a **headless client runtime** (`createDevframeClientHost()` from `@devframes/hub/client`): booted in the host page, it assembles the shared `DevframeClientContext` (panel, docks, commands, when) and imports each dock entry's client script (`action` / `custom-render` / iframe `clientScript`) into that page — how a plugin like the a11y inspector runs code inside the page being inspected. See `examples/vite-devframe-hub/` for a working ~120-line Vite host demonstrating the protocol end to end. +**`@devframes/hub`** is the framework-neutral hub layer that sits on top of devframe and provides the multi-integration orchestration (docks, terminals, messages, commands). It does not ship UI — implementers (e.g. `@vitejs/devtools-kit`) provide their own UI on top of the hub's RPC + shared-state protocol. It does ship a **headless client runtime** (`createDevframeClientHost()` from `@devframes/hub/client`): booted in the host page, it assembles the shared `DevframeClientContext` (panel, docks, commands, when) and imports each dock entry's client script (`action` / `custom-render` / iframe `clientScript`) into that page — how a plugin like the a11y inspector runs code inside the page being inspected. See `examples/hub-vite/` for a working ~120-line Vite host demonstrating the protocol end to end. ## Stack & Structure @@ -70,7 +70,7 @@ These reinforce devframe's positioning as "the container for one devtool integra ### Hub example parity -`examples/vite-devframe-hub/` (Vite plugin + vanilla client) and `examples/next-devframe-hub/` (Next.js App Router + React client) are the two reference hosts, and they stay at **feature parity**. They mount the same set of plugins and demo devframes, expose the same dock rail / iframe stage / subsystem drawer, and speak the same hub protocol — the only differences should be the host framework's own plumbing (how static assets are mounted, how the side-car server starts, how the client is rendered). +`examples/hub-vite/` (Vite plugin + vanilla client) and `examples/hub-next/` (Next.js App Router + React client) are the two reference hosts, and they stay at **feature parity**. They mount the same set of plugins and demo devframes, expose the same dock rail / iframe stage / subsystem drawer, and speak the same hub protocol — the only differences should be the host framework's own plumbing (how static assets are mounted, how the side-car server starts, how the client is rendered). Any change to one lands in the other in the same PR: adding a dock, wiring a new hub subsystem, changing the drawer layout, adopting a new client-runtime API. Their READMEs mirror each other too. If a capability genuinely can't exist on one host, say so explicitly in both READMEs rather than letting the examples silently drift. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index fb28314f..aa06f91d 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -32,6 +32,7 @@ function guideItems(prefix: string) { { text: 'Security', link: `${prefix}/guide/security` }, { text: 'Standalone CLI', link: `${prefix}/guide/standalone-cli` }, { text: 'Hub', link: `${prefix}/guide/hub` }, + { text: 'Serve a Hub Anywhere', link: `${prefix}/guide/hub-initiate` }, { text: 'Deep Linking', link: `${prefix}/guide/deep-linking` }, { text: 'Client Scripts & Context', link: `${prefix}/guide/client-context` }, { text: 'Agent-Native (experimental)', link: `${prefix}/guide/agent-native` }, @@ -43,6 +44,7 @@ function adaptersItems(prefix: string) { { text: 'Overview', link: `${prefix}/adapters/` }, { text: 'CLI (cac)', link: `${prefix}/adapters/cac` }, { text: 'Dev', link: `${prefix}/adapters/dev` }, + { text: 'Initiate (middleware)', link: `${prefix}/adapters/initiate` }, { text: 'Build', link: `${prefix}/adapters/build` }, { text: 'Vite', link: `${prefix}/adapters/vite` }, { text: 'Embedded', link: `${prefix}/adapters/embedded` }, @@ -84,8 +86,13 @@ function examplesItems(prefix: string) { { text: 'json-render', link: `${prefix}/examples/json-render` }, { text: 'streaming-chat', link: `${prefix}/examples/streaming-chat` }, { text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` }, - { text: 'vite-devframe-hub', link: `${prefix}/examples/vite-devframe-hub` }, - { text: 'next-devframe-hub', link: `${prefix}/examples/next-devframe-hub` }, + { text: 'hub-vite', link: `${prefix}/examples/hub-vite` }, + { text: 'hub-next', link: `${prefix}/examples/hub-next` }, + { text: 'hub-vite-minimal', link: `${prefix}/examples/hub-vite-minimal` }, + { text: 'hub-next-minimal', link: `${prefix}/examples/hub-next-minimal` }, + { text: 'hub-nitro-minimal', link: `${prefix}/examples/hub-nitro-minimal` }, + { text: 'hub-hono-minimal', link: `${prefix}/examples/hub-hono-minimal` }, + { text: 'hub-rsbuild-minimal', link: `${prefix}/examples/hub-rsbuild-minimal` }, ] satisfies DefaultTheme.NavItemWithLink[] } diff --git a/docs/adapters/initiate.md b/docs/adapters/initiate.md new file mode 100644 index 00000000..e4607d10 --- /dev/null +++ b/docs/adapters/initiate.md @@ -0,0 +1,114 @@ +# Initiate (standard middleware) + +Serve a devframe from inside any app that can mount a catch-all route: `initDevframe(def, { base })` returns a live instance whose `.handler` — a web-standard `(request: Request) => Promise` — carries the whole surface (the SPA, `__connection.json` discovery, the WebSocket RPC endpoint, the auth gate, and the optional MCP route) under one mount base. + +```ts +import { initDevframe } from 'devframe/initiate' +import myDevframe from './devframe' + +const devtools = initDevframe(myDevframe, { base: '/__my-tool/', key: 'my-tool' }) +// devtools.base, devtools.handler, devtools.nodeMiddleware, devtools.websocket, +// devtools.ready, devtools.context, devtools.connectionMeta(), devtools.close() +``` + +`base` is required, so the mount path is explicit at the call site — pass the conventional `resolveBasePath(def, 'hosted')` (i.e. `def.basePath ?? /__/`) if you don't want to pick one. The instance echoes the normalized value back as `devtools.base`, so route guards and middleware reference it instead of repeating the string. The factory is synchronous and initializes eagerly; `handler`/`nodeMiddleware` await readiness internally, so hosts never race the boot. + +## Mount the handler + +::: code-group + +```ts [Vite] +import { initDevframe } from 'devframe/initiate' +// vite.config.ts — connect-style middleware + Vite's own server for the socket +import { defineConfig } from 'vite' +import myDevframe from './devframe' + +export default defineConfig({ + plugins: [{ + name: 'my-tool', + apply: 'serve', + configureServer(server) { + const devtools = initDevframe(myDevframe, { + base: '/__my-tool/', + key: 'my-tool', + server: server.httpServer ?? undefined, + }) + server.middlewares.use(devtools.nodeMiddleware) + }, + }], +}) +``` + +```ts [Nitro] +// routes/__my-tool/[...path].ts — plus routes/__my-tool/index.ts (same body) +// for the namespace root, since a catch-all doesn't match its own empty path. +import { defineHandler } from 'nitro' +import { devtools } from '../../devtools' + +export default defineHandler(event => devtools.handler(event.req)) +``` + +```ts [Hono] +// server.ts — the same file runs on Node and Bun +import { Hono } from 'hono' +import { devtools } from './devtools' + +const app = new Hono() +app.all('/__my-tool/*', c => devtools.handler(c.req.raw, c.env)) +``` + +```ts [Next.js] +import { initDevframe } from 'devframe/initiate' +// app/%5F_my-tool/[[...path]]/route.ts — Next reserves `_`-prefixed +// folders, so the segment is URL-encoded (`%5F_` decodes to `__`). +import myDevframe from '@/devframe' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +const devtools = initDevframe(myDevframe, { base: '/__my-tool/', key: 'my-tool' }) +export const GET = devtools.handler +``` + +```ts [Nuxt] +// server/middleware/devtools.ts +import { devtools } from '../devtools' + +export default defineEventHandler((event) => { + const { pathname } = new URL(toWebRequest(event).url) + // `devtools.base` is the normalized mount base — no repeated string. + if (pathname.startsWith(devtools.base) || pathname === devtools.base.slice(0, -1)) + return devtools.handler(toWebRequest(event)) +}) +``` + +```ts [SvelteKit] +// src/routes/%5F_my-tool/[...path]/+server.ts +import myDevframe from '$lib/devframe' +import { initDevframe } from 'devframe/initiate' + +const devtools = initDevframe(myDevframe, { base: '/__my-tool/', key: 'my-tool' }) +export const GET = ({ request }) => devtools.handler(request) +``` + +::: + +For frameworks with dev-time module reloading (Next, Nitro, SvelteKit), always set `key` — a re-evaluation returns the live instance instead of leaking WebSocket servers (`DF0053` reports an intentional replacement when the options changed). + +## The WebSocket binding + +Fetch handlers hand over `Request`s, so the RPC socket needs its own binding. The instance resolves it in precedence order and advertises the result in `__connection.json` — the browser client follows whatever is advertised: + +1. **`ws.port`** — an explicit side-car port. +2. **`server`** — share the host's `node:http` server; the upgrade binds at `__ws`. Zero extra ports, and the socket follows the app through proxies and HTTPS. +3. **`ws.url` alone** — advertise an external endpoint verbatim; the server behind that URL owns the transport (wire the instance's `context` into your own server with `startHttpAndWs`). Combined with `server`/`ws.port`, `ws.url` overrides only the advertisement — the tunnel pattern. +4. **Bun** — same-origin fetch upgrades: pass the `Bun.serve` server as `handler`'s second argument and wire `Bun.serve({ websocket: devtools.websocket })`. +5. **Default** — an eager side-car on a free port, started at init so the meta is stable from the first request. + +## Auth + +The instance **gates by default** — a handler mounted inside an app server is reachable by anything that can open its socket. Devframe's interactive OTP handler is wired automatically and prints its code/magic-link banner once the public origin is known (derived from the first request, or the `origin` option). Pass `auth: false` for a single-user localhost setup, or a `DevframeAuthHandler` for a custom scheme. + +## Relation to the other adapters + +`createDevServer`, `viteDevBridge`, and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate). diff --git a/docs/errors/DF0053.md b/docs/errors/DF0053.md index bb99b893..9db8a5e3 100644 --- a/docs/errors/DF0053.md +++ b/docs/errors/DF0053.md @@ -18,10 +18,10 @@ outline: deep import { initDevframe } from 'devframe/initiate' // First evaluation: -initDevframe(def, { key: 'devtools', ws: { port: 7811 } }) +initDevframe(def, { base: '/__my-tool/', key: 'devtools', ws: { port: 7811 } }) // A later reload with a different port replaces the live instance: -initDevframe(def, { key: 'devtools', ws: { port: 7812 } }) // ⚠ DF0053 +initDevframe(def, { base: '/__my-tool/', key: 'devtools', ws: { port: 7812 } }) // ⚠ DF0053 ``` ## Fix diff --git a/docs/errors/DF0054.md b/docs/errors/DF0054.md index c1b1d909..b54e9f21 100644 --- a/docs/errors/DF0054.md +++ b/docs/errors/DF0054.md @@ -17,7 +17,7 @@ outline: deep ```ts import { initDevframe } from 'devframe/initiate' -const devtools = initDevframe(def) +const devtools = initDevframe(def, { base: '/__my-tool/' }) devtools.connectionMeta() // ✗ throws DF0054 — init is still in flight await devtools.ready diff --git a/docs/errors/DF8000.md b/docs/errors/DF8000.md index 2b1dfaaf..81bc85bf 100644 --- a/docs/errors/DF8000.md +++ b/docs/errors/DF8000.md @@ -18,6 +18,7 @@ outline: deep import { initHub } from '@devframes/hub/initiate' initHub({ + base: '/__devframes/', devframes: [defineDevframe({ id: '__mcp', /* … */ })], // ✗ throws DF8000 }) ``` diff --git a/docs/errors/DF8001.md b/docs/errors/DF8001.md index 34aad8f7..94c3fef5 100644 --- a/docs/errors/DF8001.md +++ b/docs/errors/DF8001.md @@ -18,10 +18,10 @@ outline: deep import { initHub } from '@devframes/hub/initiate' // First evaluation: -initHub({ key: 'devtools', devframes: [git] }) +initHub({ base: '/__devframes/', key: 'devtools', devframes: [git] }) // A later reload with a different frame list replaces the live instance: -initHub({ key: 'devtools', devframes: [git, terminals] }) // ⚠ DF8001 +initHub({ base: '/__devframes/', key: 'devtools', devframes: [git, terminals] }) // ⚠ DF8001 ``` ## Fix diff --git a/docs/errors/DF8002.md b/docs/errors/DF8002.md index c13c309c..49331b8e 100644 --- a/docs/errors/DF8002.md +++ b/docs/errors/DF8002.md @@ -16,15 +16,15 @@ outline: deep ```ts // ✗ Bad -initHub({ devframes: [git], context: myCtx }) +initHub({ base: '/__devframes/', devframes: [git], context: myCtx }) // ✓ Good — declarative: -initHub({ devframes: [git] }) +initHub({ base: '/__devframes/', devframes: [git] }) // ✓ Good — bring your own context: const ctx = await createHubContext({ host: myHost, cwd }) await mountDevframe(ctx, git) -initHub({ context: ctx }) +initHub({ base: '/__devframes/', context: ctx }) ``` ## Fix diff --git a/docs/errors/DF8003.md b/docs/errors/DF8003.md index cbe488c2..e8396691 100644 --- a/docs/errors/DF8003.md +++ b/docs/errors/DF8003.md @@ -17,7 +17,7 @@ outline: deep ```ts import { initHub } from '@devframes/hub/initiate' -const hub = initHub({ devframes: [git] }) +const hub = initHub({ base: '/__devframes/', devframes: [git] }) hub.connectionMeta() // ✗ throws DF8003 — init is still in flight await hub.ready diff --git a/docs/errors/DF8004.md b/docs/errors/DF8004.md new file mode 100644 index 00000000..2114605d --- /dev/null +++ b/docs/errors/DF8004.md @@ -0,0 +1,35 @@ +--- +outline: deep +--- + +# DF8004: Devframe Id Is Not a Mountable URL Segment + +## Message + +> Devframe id "`{id}`" is not a mountable URL segment — the hub mounts each frame at `/`. + +## Cause + +`initHub` derives each frame's mount base from its id (`/__devframes//`), and that segment is routed by h3 — where `:` and `*` are route-pattern markers and `/` ends the segment. An id carrying those characters either crashes route registration or matches the wrong paths. + +## Example + +```ts +import { initHub } from '@devframes/hub/initiate' + +initHub({ + base: '/__devframes/', + devframes: [defineDevframe({ id: 'devframes:plugin:my-tool', /* … */ })], // ✗ throws DF8004 +}) + +// ✓ Good — route-safe id (letters, digits, `_`, `-`, `.`): +defineDevframe({ id: 'devframes_plugin_my-tool', /* … */ }) +``` + +## Fix + +Set a route-safe `id` on the definition — letters, digits, `_`, `-`, and `.` only. Plugins that accept an `id` option can be re-instantiated with a safe one; RPC function ids (the colon-namespaced `devframes:plugin::` convention) are unaffected — this constraint applies to the devframe id alone. + +## Source + +- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub` throws this while mounting the `devframes` list. diff --git a/docs/examples/hub-hono-minimal.md b/docs/examples/hub-hono-minimal.md new file mode 100644 index 00000000..dcaf53f1 --- /dev/null +++ b/docs/examples/hub-hono-minimal.md @@ -0,0 +1,27 @@ +--- +outline: deep +--- + +# hub-hono-minimal + +The minimal [Hono](https://hono.dev) host for [`@devframes/hub`](/guide/hub): one `initHub()` call behind a catch-all route, running on **Node and Bun** from the same app file, the UI supplied by `@devframes/hub-ui`. + +Package: `hub-hono-minimal` · framework: **Hono** + +## What it shows + +- `initHub({ base, devframes: [inspect, messages], ui: createUi() })` in `src/app.ts` plus `app.all(\`${hub.base}*\`, c => hub.handler(c.req.raw, c.env))`. +- On Node (`@hono/node-server`), the RPC WebSocket runs on an eager side-car port. +- On Bun (`Bun.serve({ fetch, websocket: hub.websocket })`), WebSocket upgrades complete through `hub.handler(request, server)` on the app's own origin — no side-car. The repo's `scripts/smoke-bun.ts` exercises this path end to end. + +## Run it + +```sh +pnpm install +pnpm --filter hub-hono-minimal dev # Node +pnpm --filter hub-hono-minimal dev:bun # Bun +``` + +## Source + +[`examples/hub-hono-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-hono-minimal) diff --git a/docs/examples/hub-next-minimal.md b/docs/examples/hub-next-minimal.md new file mode 100644 index 00000000..c77d5cd6 --- /dev/null +++ b/docs/examples/hub-next-minimal.md @@ -0,0 +1,28 @@ +--- +outline: deep +--- + +# hub-next-minimal + +The minimal [Next.js](https://nextjs.org) host for [`@devframes/hub`](/guide/hub): one `initHub()` call on an App Router catch-all route, the UI supplied by `@devframes/hub-ui`. + +Package: `hub-next-minimal` · framework: **React (Next.js)** + +## What it shows + +- `initHub({ base, devframes: [inspect, messages], ui: createUi() })` behind one route (`app/%5F_devframes/[[...path]]/route.ts`) delegating to `hub.handler(request)`. +- The plugins and `@devframes/hub-ui` load via a bundler-ignored dynamic `import()`, so Next resolves their published `dist` at runtime (their `import.meta.url` asset lookups don't survive static bundling). +- Next route handlers can't accept WebSocket upgrades, so the instance runs its eager side-car WS server, advertised through `__connection.json`. + +## Run it + +```sh +pnpm install +pnpm --filter hub-next-minimal dev +``` + +Open the printed URL for the host page with the floating dock, or `/__devframes/` for the standalone viewer. + +## Source + +[`examples/hub-next-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-next-minimal) diff --git a/docs/examples/hub-next.md b/docs/examples/hub-next.md new file mode 100644 index 00000000..1232669e --- /dev/null +++ b/docs/examples/hub-next.md @@ -0,0 +1,31 @@ +--- +outline: deep +--- + +# hub-next + +The same hub protocol as the [Vite host](./hub-vite), hosted from a **Next.js** App Router app with a hand-built React viewer — proof that the hub is host-runtime-agnostic. + +Package: `hub-next` · framework: **React (Next.js)** + +## What it proves + +- `initHub({ base, devframes, configure })` boots the whole hub from one call; a single App Router catch-all route (`app/%5F_devframes/[[...path]]/route.ts`) delegates to `hub.handler(request)`. +- Next route handlers can't accept WebSocket upgrades, so the instance starts its eager side-car WS server, advertised through `__connection.json`. +- The [JSON-render](/guide/json-render) hub integration with **registry replacement**: the React client renders the server-authored view with a small in-example React registry (rather than the Vue `@devframes/json-render-ui`) — the path a non-Vue host uses. +- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`. + +For the minimal counterpart — the hub UI supplied by `@devframes/hub-ui` instead of a hand-built viewer — see [hub-next-minimal](./hub-next-minimal). + +## Run it + +```sh +pnpm install +pnpm --filter hub-next dev +``` + +Open the printed URL to see the docks, commands, messages, and terminals the hub exposes. + +## Source + +[`examples/hub-next`](https://github.com/devframes/devframe/tree/main/examples/hub-next) diff --git a/docs/examples/hub-nitro-minimal.md b/docs/examples/hub-nitro-minimal.md new file mode 100644 index 00000000..69851df1 --- /dev/null +++ b/docs/examples/hub-nitro-minimal.md @@ -0,0 +1,28 @@ +--- +outline: deep +--- + +# hub-nitro-minimal + +The minimal [Nitro](https://nitro.build) host for [`@devframes/hub`](/guide/hub): one `initHub()` call behind a catch-all route, the UI supplied by `@devframes/hub-ui`. + +Package: `hub-nitro-minimal` · framework: **Nitro** + +## What it shows + +- `initHub({ base, devframes: [inspect, messages], ui: createUi() })` in `hub.ts`, delegated to by a catch-all route (`routes/__devframes/[...path].ts`, plus its `index.ts` sibling for the namespace root) via `hub.handler(event.req)`. +- `nitro.config.ts` keeps the devframe packages external so their prebuilt client assets resolve from the packages themselves rather than Nitro's build output. +- The RPC WebSocket runs on an eager side-car port, advertised through `__connection.json`. + +## Run it + +```sh +pnpm install +pnpm --filter hub-nitro-minimal dev +``` + +Open the printed URL for the host page with the floating dock, or `/__devframes/` for the standalone viewer. + +## Source + +[`examples/hub-nitro-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-nitro-minimal) diff --git a/docs/examples/hub-rsbuild-minimal.md b/docs/examples/hub-rsbuild-minimal.md new file mode 100644 index 00000000..ea52f82e --- /dev/null +++ b/docs/examples/hub-rsbuild-minimal.md @@ -0,0 +1,28 @@ +--- +outline: deep +--- + +# hub-rsbuild-minimal + +The minimal [Rsbuild](https://rsbuild.dev) host for [`@devframes/hub`](/guide/hub): one `initHub()` call mounted into the dev server's middleware stack, the UI supplied by `@devframes/hub-ui`. + +Package: `hub-rsbuild-minimal` · framework: **Rsbuild** + +## What it shows + +- `initHub({ base, devframes: [inspect, messages], ui: createUi() })` created inside `server.setup` in `rsbuild.config.ts` — lazily, so importing the config never spawns the hub's side-car. +- `server.setup` registers `hub.nodeMiddleware`, which owns the `/__devframes/` namespace and hands everything else back to Rsbuild. +- The RPC WebSocket runs on an eager side-car port, advertised through `__connection.json`; `html.tags` injects the `${hub.base}embedded.js` bootstrap. + +## Run it + +```sh +pnpm install +pnpm --filter hub-rsbuild-minimal dev +``` + +Open the printed URL for the host page with the floating dock, or `/__devframes/` for the standalone viewer. + +## Source + +[`examples/hub-rsbuild-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-rsbuild-minimal) diff --git a/docs/examples/hub-vite-minimal.md b/docs/examples/hub-vite-minimal.md new file mode 100644 index 00000000..ac177fa2 --- /dev/null +++ b/docs/examples/hub-vite-minimal.md @@ -0,0 +1,28 @@ +--- +outline: deep +--- + +# hub-vite-minimal + +The minimal [Vite](https://vite.dev) host for [`@devframes/hub`](/guide/hub): one `initHub()` call mounted as dev middleware, the UI supplied by `@devframes/hub-ui`. No hand-built viewer — the whole integration is the config file. + +Package: `hub-vite-minimal` · framework: **Vite** + +## What it shows + +- `initHub({ base, devframes: [inspect, messages], ui: createUi() })` in `vite.config.ts` — runs in Vite's Node config process, never bundled into the browser. +- `server.middlewares.use(hub.nodeMiddleware)` mounts the whole `/__devframes/` namespace; the WebSocket upgrade shares Vite's own server at `__ws`. +- `transformIndexHtml` injects ` + +`, +)) diff --git a/examples/hub-hono-minimal/src/bun.ts b/examples/hub-hono-minimal/src/bun.ts new file mode 100644 index 00000000..53d2a5d4 --- /dev/null +++ b/examples/hub-hono-minimal/src/bun.ts @@ -0,0 +1,18 @@ +import process from 'node:process' +import { app, hub } from './app' + +// Bun tier: WebSocket upgrades complete through `hub.handler(request, +// server)` on the app's own origin — no side-car port. `Bun.serve` needs +// the instance's `websocket` handlers wired alongside the fetch handler. +const port = Number(process.env.PORT ?? 5179) + +export default { + port, + fetch: app.fetch, + websocket: hub.websocket, +} + +void hub.ready.then(() => { + // eslint-disable-next-line no-console + console.log(`hono-devframe-hub (bun) on http://localhost:${port} — devtools at /__devframes/`) +}) diff --git a/examples/hub-hono-minimal/src/node.ts b/examples/hub-hono-minimal/src/node.ts new file mode 100644 index 00000000..d3a1cb61 --- /dev/null +++ b/examples/hub-hono-minimal/src/node.ts @@ -0,0 +1,10 @@ +import process from 'node:process' +import { serve } from '@hono/node-server' +import { app, hub } from './app' + +const port = Number(process.env.PORT ?? 5179) +serve({ fetch: app.fetch, port, hostname: '0.0.0.0' }) +void hub.ready.then(() => { + // eslint-disable-next-line no-console + console.log(`hono-devframe-hub on http://localhost:${port} — devtools at /__devframes/`) +}) diff --git a/examples/hub-hono-minimal/tsconfig.json b/examples/hub-hono-minimal/tsconfig.json new file mode 100644 index 00000000..11916e28 --- /dev/null +++ b/examples/hub-hono-minimal/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "esnext", + "dom" + ], + "types": [ + "node" + ] + }, + "include": [ + "src" + ], + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/examples/hub-next-minimal/package.json b/examples/hub-next-minimal/package.json new file mode 100644 index 00000000..18f6f6b1 --- /dev/null +++ b/examples/hub-next-minimal/package.json @@ -0,0 +1,27 @@ +{ + "name": "hub-next-minimal", + "type": "module", + "version": "0.8.2", + "private": true, + "description": "Minimal Next.js host for @devframes/hub — one initHub() handler on an App Router catch-all, UI from @devframes/hub-ui.", + "homepage": "https://github.com/devframes/devframe/tree/main/examples/hub-next-minimal", + "scripts": { + "dev": "next dev src/client", + "build": "next build src/client" + }, + "dependencies": { + "@devframes/hub": "workspace:*", + "@devframes/hub-ui": "workspace:*", + "@devframes/next": "workspace:*", + "@devframes/plugin-inspect": "workspace:*", + "@devframes/plugin-messages": "workspace:*", + "devframe": "workspace:*", + "next": "catalog:frontend", + "react": "catalog:frontend", + "react-dom": "catalog:frontend" + }, + "devDependencies": { + "@types/react": "catalog:types", + "@types/react-dom": "catalog:types" + } +} diff --git a/examples/hub-next-minimal/src/client/app/%5F_devframes/[[...path]]/route.ts b/examples/hub-next-minimal/src/client/app/%5F_devframes/[[...path]]/route.ts new file mode 100644 index 00000000..47b775b7 --- /dev/null +++ b/examples/hub-next-minimal/src/client/app/%5F_devframes/[[...path]]/route.ts @@ -0,0 +1,16 @@ +import { ensureHub } from '../../../hub' + +export const runtime = 'nodejs' +export const dynamic = 'force-dynamic' + +// The whole hub namespace behind one catch-all route: `initHub`'s +// web-standard `handler` serves every frame SPA, the discovery endpoints, +// and the embedded/viewer UI. Next reserves `_`-prefixed segment folders, +// so `__devframes` is URL-encoded as `%5F_devframes` in the app directory. +// MCP would speak Streamable-HTTP over GET/POST/DELETE — the same handler. +async function handler(request: Request): Promise { + const hub = await ensureHub() + return hub.handler(request) +} + +export { handler as DELETE, handler as GET, handler as POST } diff --git a/examples/hub-next-minimal/src/client/app/layout.tsx b/examples/hub-next-minimal/src/client/app/layout.tsx new file mode 100644 index 00000000..1c44cb2a --- /dev/null +++ b/examples/hub-next-minimal/src/client/app/layout.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from 'react' + +export const metadata = { + title: 'Hub Next (minimal)', +} + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + {/* The floating-dock bootstrap — one dev-only module script, the + whole embedded integration. */} + + +`, + { headers: { 'content-type': 'text/html; charset=utf-8' } }, +)) diff --git a/examples/hub-nitro-minimal/tsconfig.json b/examples/hub-nitro-minimal/tsconfig.json new file mode 100644 index 00000000..cf1d7ad8 --- /dev/null +++ b/examples/hub-nitro-minimal/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": [ + "esnext", + "dom" + ], + "types": [ + "node" + ] + }, + "include": [ + "hub.ts", + "routes", + "nitro.config.ts" + ], + "exclude": [ + "dist", + "node_modules", + ".nitro", + ".output" + ] +} diff --git a/examples/hub-rsbuild-minimal/README.md b/examples/hub-rsbuild-minimal/README.md new file mode 100644 index 00000000..46260b63 --- /dev/null +++ b/examples/hub-rsbuild-minimal/README.md @@ -0,0 +1,20 @@ +# hub-rsbuild-minimal + +The minimal [Rsbuild](https://rsbuild.dev) host for `@devframes/hub`: one `initHub()` call mounted into the dev server's middleware stack, the UI supplied by `@devframes/hub-ui`. + +```sh +pnpm --filter hub-rsbuild-minimal dev +``` + +Open the printed URL — the host page carries the floating dock via one injected script tag — or `/__devframes/` for the standalone viewer. + +## How it works + +[`rsbuild.config.ts`](./rsbuild.config.ts) is the entire host: + +- `initHub({ devframes: [inspect, messages], ui: createUi() })` runs in Rsbuild's Node config process (never bundled into the browser), so `createUi()`'s prebuilt viewer/dock and the plugins' node code work unchanged. +- `dev.setupMiddlewares` unshifts `hub.nodeMiddleware`, which owns the whole `/__devframes/` namespace and hands everything else back to Rsbuild. +- The RPC WebSocket runs on an eager side-car port, advertised through `__connection.json`; the browser client discovers it automatically. +- `html.tags` injects `