Skip to content

Commit ddd0ec4

Browse files
committed
docs(hub-ui-onboard): guide page, host integration recipes, reference tables
1 parent d4f9073 commit ddd0ec4

6 files changed

Lines changed: 223 additions & 22 deletions

File tree

‎docs/content/1.guide/18.hub-initiate.md‎

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,7 @@ To add a language, add its tag and native name to `packages/hub-ui/src/locales.t
6565

6666
## Onboarding without the hub installed
6767

68-
`@devframes/hub-ui-onboard` is for a host that wants DevTools to be an opt-in install. It depends on neither `devframe` nor `@devframes/hub` and ships one 20 kB browser file. `createOnboarding()` returns a handler that serves a floating button at `<base>embedded.js`, the same URL the hub uses, plus three routes under `<base>__onboard/`: `GET status`, `POST install` and `POST disable`.
69-
70-
```ts
71-
import { createOnboarding } from '@devframes/hub-ui-onboard'
72-
73-
const onboarding = createOnboarding({
74-
packages: ['@devframes/hub', '@devframes/hub-ui'],
75-
branding: { productName: 'My DevTools', primaryColor: '#646cff' },
76-
async onInstalled() {
77-
// Resolve from the project root; under pnpm the new packages are only reachable from there.
78-
const hub = await startHub()
79-
return hub.handler // every later request at <base> goes here, no restart
80-
},
81-
})
82-
83-
server.middlewares.use(onboarding.nodeMiddleware)
84-
if (!onboarding.disabled)
85-
injectScript(onboarding.scriptSrc)
86-
```
87-
88-
Install runs the project's package manager (detected from the lockfile) with the configured `packages` only; the request body controls nothing, and a cross-origin `POST` is refused. The button polls `status` once a second and, when `onInstalled` returned a handler, loads the real `embedded.js` and removes itself. Without a handler it asks the user to restart. When every named package is already in `node_modules`, `onboarding.installed` is `true`, `onInstalled` runs on the first request, and the button never shows. The panel has two ways out: "Hide for now" hides the button for the browser tab (`sessionStorage`), and "Disable entirely" writes `<stateDir>/hub-ui-onboard.json` (default `node_modules/.devframe`) so the host reads `onboarding.disabled` on the next start and injects nothing. Mount the onboarding only when the user did not set the host's own devtools option, so an explicit `devtools: true` or `false` in the host configuration always wins. [`examples/hub-onboard-vite`](https://github.com/devframes/devframe/tree/main/examples/hub-onboard-vite) shows the whole flow on Vite.
68+
A host that wants DevTools as an opt-in install ships `@devframes/hub-ui-onboard` instead of the hub: a 20 kB floating button at the same `<base>embedded.js` URL, an Install action that runs the project's package manager, and an `onInstalled` hook that hands `base` to the real hub in the same process. See [Opt-in DevTools with Onboarding](/guide/hub-ui-onboard).
8969

9070
## Renderer modules
9171

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
title: 'Opt-in DevTools with Onboarding'
3+
navigation:
4+
icon: i-lucide-download
5+
description: '@devframes/hub-ui-onboard lets a host ship a 20 kB floating button instead of the hub, install the hub on demand, and hand the hub base to it without a restart.'
6+
---
7+
8+
`@devframes/hub-ui-onboard` lets a host ship a 20 kB floating button instead of the hub, install the hub on demand, and hand the hub base to it in the same process.
9+
10+
## Why
11+
12+
A hub UI provider, its Vue runtime, and the devframes it mounts add tens of megabytes to a framework's install size. A host that wants DevTools as an opt-in can move those packages to optional peers and ship only this package: one browser file, a handful of routes, and three small runtime dependencies (a package-manager detector, a process runner, the diagnostics library). The user still discovers DevTools through the usual floating button; the first click installs them.
13+
14+
## What the user sees
15+
16+
The button sits at the bottom left, dimmed until hovered. It opens a panel with the product name and logo, one sentence, the exact command the install will run (for example `pnpm add -D @nuxt/devtools`), and three actions:
17+
18+
- **Install** runs the command in the project. The panel shows progress, then either the real dock replaces the button in place, or the panel asks for a restart.
19+
- **Hide for now** removes the button for the current browser tab.
20+
- **Disable entirely** writes a state file so the host stops injecting the button on every later start.
21+
22+
The panel follows the shared design tokens, the host's `primaryColor`, and the user's hub color scheme, so the swap to the real dock looks like one product.
23+
24+
## Create the onboarding
25+
26+
```ts
27+
import { createOnboarding } from '@devframes/hub-ui-onboard'
28+
29+
const onboarding = createOnboarding({
30+
packages: ['@devframes/hub', '@devframes/hub-ui'],
31+
branding: { productName: 'My DevTools', logo: '/logo.svg', primaryColor: '#646cff' },
32+
})
33+
```
34+
35+
`createOnboarding()` returns four things:
36+
37+
- `handler(request)`: a web-standard `Request => Response` handler for every path under `base` (default `/__devframes/`).
38+
- `nodeMiddleware(req, res, next)`: the same handler as Connect middleware for Vite, Express, Fastify with `@fastify/middie`, or a plain `node:http` server. It calls `next()` for paths outside `base`.
39+
- `scriptSrc`: `<base>embedded.js`, the URL to inject as `<script type="module">`.
40+
- `disabled` and `installed`: what the host needs to decide whether to inject the script at all (below).
41+
42+
`packages` are package specs as the package manager accepts them. The package manager comes from the lockfile (`npm`, `pnpm`, `yarn`, `bun`, `deno`), `dev: true` adds `-D`, and `cwd` (default `process.cwd()`) is the project that receives the dependency. In a workspace, point `cwd` at the package that runs the dev server. The packages are fixed at creation, and a `POST` from another origin is refused.
43+
44+
## Hand the base to the hub
45+
46+
Return a handler from `onInstalled` and the hub takes over `base` in the same process. The button then loads the real `embedded.js` from that handler and removes itself.
47+
48+
```ts
49+
import { createRequire } from 'node:module'
50+
import { join } from 'node:path'
51+
import { pathToFileURL } from 'node:url'
52+
53+
const require = createRequire(join(cwd, 'package.json'))
54+
const load = <T>(id: string): Promise<T> => import(pathToFileURL(require.resolve(id)).href)
55+
56+
const onboarding = createOnboarding({
57+
cwd,
58+
packages: ['@devframes/hub', '@devframes/hub-ui'],
59+
async onInstalled() {
60+
const [{ initHub }, { createUi }] = await Promise.all([
61+
load<typeof import('@devframes/hub/initiate')>('@devframes/hub/initiate'),
62+
load<typeof import('@devframes/hub-ui')>('@devframes/hub-ui'),
63+
])
64+
const hub = initHub({ base: '/__devframes/', cwd, server: httpServer, ui: createUi(), devframes: [] })
65+
return hub.handler
66+
},
67+
})
68+
```
69+
70+
Resolve the new packages from the project's `package.json`, as above. Under pnpm they are dependencies of the project, so a bare `import('@devframes/hub')` from the host's own file fails. Pass the live `node:http` server so the hub attaches its WebSocket to it; `initHub` accepts a server after it started listening.
71+
72+
`onInstalled` is also how the onboarding short-circuits. When every named package is already in `node_modules` at creation, `onboarding.installed` is `true`, `onInstalled` runs on the first request, and the user never sees the button. A host can therefore mount the onboarding unconditionally during development: it serves the hub when the packages exist and the button when they are missing.
73+
74+
Without `onInstalled`, or when it returns nothing, the panel reports the install and asks for a restart. The next start finds the packages installed.
75+
76+
## When to inject the button
77+
78+
Inject `scriptSrc` only when `onboarding.disabled` is `false`. The user set that flag with "Disable entirely"; it lives in `<stateDir>/hub-ui-onboard.json`, default `<cwd>/node_modules/.devframe`.
79+
80+
Mount the onboarding only when the user did not set the host's own devtools option. An explicit `devtools: true` means the host installs or requires DevTools itself; an explicit `devtools: false` means no button. The onboarding covers the unset case, and a user who disabled it from the panel turns it back on by setting the option.
81+
82+
## Hosts
83+
84+
### Vite
85+
86+
A plugin mounts the middleware and injects the tag. `examples/hub-onboard-vite` is the complete version with the hub hand-off.
87+
88+
```ts [vite.config.ts]
89+
import type { Plugin } from 'vite'
90+
import { createOnboarding } from '@devframes/hub-ui-onboard'
91+
92+
function hubOnboarding(): Plugin {
93+
const onboarding = createOnboarding({ packages: ['@devframes/hub', '@devframes/hub-ui'] })
94+
return {
95+
name: 'hub-onboarding',
96+
apply: 'serve',
97+
configureServer(server) {
98+
server.middlewares.use(onboarding.nodeMiddleware)
99+
},
100+
transformIndexHtml() {
101+
return onboarding.disabled
102+
? []
103+
: [{ tag: 'script', attrs: { type: 'module', src: onboarding.scriptSrc }, injectTo: 'body' }]
104+
},
105+
}
106+
}
107+
```
108+
109+
### Nuxt
110+
111+
Nuxt runs Vite, so a module reuses the plugin above and adds the tag to `app.head`. Gate it on the dev server, and skip it when the user set your own devtools option.
112+
113+
```ts [modules/devtools-onboarding.ts]
114+
import { createOnboarding } from '@devframes/hub-ui-onboard'
115+
import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
116+
117+
export default defineNuxtModule({
118+
setup(_, nuxt) {
119+
if (!nuxt.options.dev)
120+
return
121+
const onboarding = createOnboarding({
122+
cwd: nuxt.options.rootDir,
123+
packages: ['@nuxt/devtools'],
124+
branding: { productName: 'Nuxt DevTools', primaryColor: '#00dc82' },
125+
})
126+
addVitePlugin({
127+
name: 'devtools-onboarding',
128+
configureServer: server => server.middlewares.use(onboarding.nodeMiddleware),
129+
})
130+
if (!onboarding.disabled)
131+
(nuxt.options.app.head.script ??= []).push({ type: 'module', src: onboarding.scriptSrc })
132+
},
133+
})
134+
```
135+
136+
### Next.js
137+
138+
A route handler forwards `Request` objects, and the root layout renders the tag.
139+
140+
```ts [app/__devframes/[[...path]]/route.ts]
141+
import { onboarding } from '../../../devtools-onboarding'
142+
143+
export const GET = (request: Request) => onboarding.handler(request)
144+
export const POST = (request: Request) => onboarding.handler(request)
145+
```
146+
147+
```tsx [app/layout.tsx]
148+
import { onboarding } from '../devtools-onboarding'
149+
150+
export default function RootLayout({ children }: { children: React.ReactNode }) {
151+
return (
152+
<html lang="en">
153+
<body>
154+
{children}
155+
{process.env.NODE_ENV === 'development' && !onboarding.disabled && (
156+
<script type="module" src={onboarding.scriptSrc} />
157+
)}
158+
</body>
159+
</html>
160+
)
161+
}
162+
```
163+
164+
A Next route handler cannot accept a WebSocket upgrade, so a hub started from `onInstalled` uses a side-car socket (`ws: { sidecar: true }`); see [Next](/frameworks/next#mounting-a-hub).
165+
166+
### Any Node server
167+
168+
```ts
169+
import { createServer } from 'node:http'
170+
171+
createServer((req, res) => {
172+
onboarding.nodeMiddleware(req, res, () => {
173+
res.statusCode = 404
174+
res.end()
175+
})
176+
}).listen(3000)
177+
```
178+
179+
Frameworks with a `Request => Response` surface (Hono, Nitro, Deno) mount `onboarding.handler` under `base` instead.
180+
181+
## Strings and branding
182+
183+
`branding` takes `productName`, `logo` (one URL or `{ light, dark }`) and `primaryColor`, the same three fields hub-ui's `DevframeBranding` starts with. Every string in the panel derives from `productName` and is overridable through `messages`; the keys are listed in the [Hub API reference](/references/hub-api#onboarding-options-and-routes).
184+
185+
## Errors
186+
187+
The Node side reports through `DF9000` to `DF9004`. An install failure or a throwing `onInstalled` also reaches the panel as `{ state: 'error', error: { code, message } }`, with a Retry button. See the [error reference](/errors).

‎docs/content/1.guide/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,5 @@ The CLI adapter serves the SPA at `/`; embedded in a host framework (`vite`, `em
178178
- [The Standard Handler](/adapters/initiate): mount into any host framework
179179
- [Adapters](/adapters): convenience entry points
180180
- [Hub](/guide/hub): compose many devframes
181+
- [Opt-in DevTools with Onboarding](/guide/hub-ui-onboard): ship a 20 kB button and install the hub on demand
181182
- [Pluggable, Extensible, and Playful DevTools](/posts/pluggable-extensible-playful-devtools): the vision and story behind Devframe

‎docs/content/3.frameworks/index.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ For framework-neutral CLI/build/embedded outputs, use the [adapters](/adapters)
2929
Each `hub` scope wraps [`initHub`](/guide/hub-initiate) and defaults the UI to [`@devframes/hub-ui`](/guide/build-your-own-hub-ui)'s `createUi()` (`ui` to override, `ui: false` for headless). Per tool: **[Vite](/frameworks/vite#mounting-a-hub)**, **[Nuxt](/frameworks/nuxt#mounting-a-hub)**, **[Next](/frameworks/next#mounting-a-hub)**.
3030

3131
`@devframes/vite/hub` and `@devframes/nuxt/hub` recommend the native hub UI providers ([Vite DevTools](https://devtools.vite.dev), [Nuxt DevTools](https://devtools.nuxt.com)) once (silence with `{ quiet: true }`). Next has none, so `@devframes/next/hub` stays quiet.
32+
33+
A framework that wants DevTools as an opt-in install ships [`@devframes/hub-ui-onboard`](/guide/hub-ui-onboard) instead of the hub and lets the user install it from a floating button.

‎docs/content/8.references/6.hub-api.md‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ What `initHub()` serves under its `base`: [The namespace](/guide/hub-initiate#th
8484
| `__client-imports.js` | dock client-script import map for hub UI providers |
8585
| `__mcp` | aggregate MCP endpoint over the tool registry (`mcp: 'auto'` default: mounted once agent tools exist) |
8686

87+
## Onboarding options and routes
88+
89+
What `createOnboarding()` from `@devframes/hub-ui-onboard` accepts and serves: [Opt-in DevTools with Onboarding](/guide/hub-ui-onboard).
90+
91+
| Option | Default | Meaning |
92+
| --- | --- | --- |
93+
| `packages` | required | Package specs the Install button adds |
94+
| `base` | `/__devframes/` | Mount base, shared with the hub that takes over later |
95+
| `cwd` | `process.cwd()` | Project that receives the dependency and whose lockfile picks the package manager |
96+
| `stateDir` | `<cwd>/node_modules/.devframe` | Directory of the `hub-ui-onboard.json` state file |
97+
| `dev` | `true` | Install as a devDependency |
98+
| `branding` | `{}` | `productName` (default `Devframes`), `logo` (URL or `{ light, dark }`), `primaryColor` |
99+
| `messages` | derived from `productName` | Overrides for `title`, `description`, `install`, `hide`, `disable`, `installing`, `restart`, `retry` |
100+
| `onInstalled` | none | Runs after a verified install, or on the first request when the packages already exist; a returned handler takes over `base` |
101+
102+
| Route | Serves |
103+
| --- | --- |
104+
| `GET <base>embedded.js` | the floating button, a self-contained ES module |
105+
| `GET <base>__onboard/status` | `{ state, command, branding, messages, error? }` |
106+
| `POST <base>__onboard/install` | starts the install (`202`), same-origin only |
107+
| `POST <base>__onboard/disable` | writes the state file (`204`), same-origin only |
108+
109+
| `state` | Meaning |
110+
| --- | --- |
111+
| `idle` | nothing installed yet; the panel offers Install |
112+
| `installing` | the package manager is running; the button polls once a second |
113+
| `installed` | installed, no hub handler to hand off to; the panel asks for a restart |
114+
| `ready` | `onInstalled` returned a handler; the button loads the real `embedded.js` and removes itself |
115+
| `error` | the install or `onInstalled` failed; `error.code` is a `DF90xx` diagnostic |
116+
| `disabled` | the user chose Disable entirely; the button renders nothing |
117+
87118
## `buildHub` options
88119

89120
The options of `buildHub()` from `@devframes/hub/build`: [Static builds](/guide/hub-initiate#static-builds). `devframes`, `services`, `rpcDeclarations`, `configure`, `ui`, `renderers`, `name`, `version`, `cwd`, and `getStorageDir` carry the same contracts as their `initHub` counterparts.

‎docs/content/8.references/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Lookup pages the guides link into:
1212
- [Events Reference](/references/events): every event, broadcast, shared-state key, and channel name, by direction and reach.
1313
- [Node-Side API](/references/node-api): `DevframeDefinition` fields, CLI options, storage scopes, RPC function types, broadcast options, streaming lifecycle, remote assets, the `ctx.services` host and wire-service fields, diagnostics prefixes, and the auth surface.
1414
- [Browser-Side API](/references/browser-api): `connectDevframe` options, RPC client events, connection statuses, and in-page channel error codes.
15-
- [Hub API](/references/hub-api): hub subsystems, launcher fields, duplication strategies, dock categories, the hub UI protocol, the namespace routes, the client runtime, the client context, and dock entry types.
15+
- [Hub API](/references/hub-api): hub subsystems, launcher fields, duplication strategies, dock categories, the hub UI protocol, the namespace routes, the onboarding options and routes, the client runtime, the client context, and dock entry types.
1616
- [Utilities](/references/utilities): the small, stable helpers under `devframe/utils/*`, bundled into `devframe`.
1717
- [Interactive Auth](/references/interactive-auth): the OTP auth recipe: handshake, resolver gate, connect-time trust, banner.
1818

0 commit comments

Comments
 (0)