Skip to content

Commit 6013aa3

Browse files
committed
feat(hub-ui-onboard): add a tiny stand-in that installs the hub UI on demand
A host can now make DevTools an opt-in install without losing discovery. `@devframes/hub-ui-onboard` ships one ~20 kB browser file served at the hub base, offers Install / Disable, runs the project's package manager with the configured packages, and hands the base to the real hub in the same process when the host returns a handler from `onInstalled`. It depends on neither `devframe` nor `@devframes/hub`; node-side errors use the new `DF90xx` range through `nostics` directly.
1 parent 1b2c23f commit 6013aa3

41 files changed

Lines changed: 1389 additions & 11 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/03-stack-and-commands.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots co
4343

4444
## Generated artifacts under `src/`
4545

46-
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.
46+
Ahead-of-time build artifacts that live under `src/` - the shadow-root stylesheets in `packages/hub-ui/src/client/.generated/`, `packages/hub-ui-onboard/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 every `build:css` task, 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.
4747

4848
## `starter/`
4949

‎.agents/06-design-system.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ Each consumer's `uno.config.ts` composes the same stack: `presetAnthonyDesign({
1212

1313
## Wind4 by default, Wind3 for shadow roots
1414

15-
Ordinary surfaces (plugins served in iframes, examples in the page) use `presetWind4()`. A surface whose stylesheet is injected into a **shadow root** (`@devframes/hub-ui`'s dock custom element, `@devframes/json-render-ui`'s renderer module) MUST build on **`presetWind3()`** instead - via `createDesignConfig({ base: presetWind3() })`, or `presetWind3()` directly. Wind4 keeps `@antfu/design`'s theme in a document `:root {}` block and registers its `--un-*` custom properties with `@property { inherits: false }`, neither of which reaches a shadow tree - its `color-mix(var(--colors-*))` semantic utilities (`bg-base`, `color-base`, …) resolve to nothing inside a shadow root. Wind3 bakes the same shortcuts to concrete `rgb()` + `.dark` variants, self-contained in the shadow tree.
15+
Ordinary surfaces (plugins served in iframes, examples in the page) use `presetWind4()`. A surface whose stylesheet is injected into a **shadow root** (`@devframes/hub-ui`'s dock custom element, `@devframes/json-render-ui`'s renderer module, `@devframes/hub-ui-onboard`'s floating button) MUST build on **`presetWind3()`** instead - via `createDesignConfig({ base: presetWind3() })`, or `presetWind3()` directly. Wind4 keeps `@antfu/design`'s theme in a document `:root {}` block and registers its `--un-*` custom properties with `@property { inherits: false }`, neither of which reaches a shadow tree - its `color-mix(var(--colors-*))` semantic utilities (`bg-base`, `color-base`, …) resolve to nothing inside a shadow root. Wind3 bakes the same shortcuts to concrete `rgb()` + `.dark` variants, self-contained in the shadow tree.
1616

17-
Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both handled in the shared `design/build-shadow-css.ts` pipeline, consumed by `packages/{hub-ui,json-render-ui}/scripts/build-css.ts`; the Vite `unocss/vite` path for standalone SPAs and Storybook is not affected):
17+
Two shadow-root gotchas the ahead-of-time CSS builder MUST compensate for (both handled in the shared `design/build-shadow-css.ts` pipeline, consumed by `packages/{hub-ui,hub-ui-onboard,json-render-ui}/scripts/build-css.ts`; a surface that renders none of `@antfu/design`'s Vue components passes `scanDesignComponents: false` to keep its stylesheet small; the Vite `unocss/vite` path for standalone SPAs and Storybook is not affected):
1818

1919
- **Plain-vs-variant shortcut drop.** When a semantic shortcut also appears **variant-prefixed** in the scanned sources (e.g. `@antfu/design`'s Tabs emits `data-[state=active]:bg-base`), a single-pass `generate(tokens)` drops the *plain* `.bg-base` / `.color-base` rule - so emit the surface tokens (`design/uno.config.ts`'s exported `shadowSurfaceSafelist`) in a **dedicated `generate()` pass** and append them.
2020
- **`--un-*` collision with a Wind4 host.** `@property` registrations are document-global, so a host page built on Wind4 registers `--un-bg-opacity` / `--un-border-opacity` / `--un-text-opacity` as `@property { syntax: '<percentage>' }` for the whole document, including our shadow tree - which invalidates the *unitless* values Wind3 writes (`--un-border-opacity: 0.13`) and collapses the dependent `rgb(… / var(--un-*))` color (a visibly wrong border/background). Rename every `--un-` in the shadow stylesheet to a private prefix with `design/uno.config.ts`'s exported `namespaceShadowCssVars()` so it's immune to whatever the host registered.

‎.agents/08-diagnostics.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
All node-side warnings and errors use structured diagnostics via [`nostics`](https://www.npmjs.com/package/nostics). Node-side code MUST NOT use raw `console.warn`, `console.error`, or `throw new Error` with ad-hoc messages - always define a coded diagnostic. Browser-only code is out of scope and keeps using `console.*` / `throw`.
44

5-
Import `defineDiagnostics` (and `Diagnostic` for `instanceof` checks) from `devframe/utils/nostics`, never from `nostics` directly - it pre-wires devframe's ANSI console reporter, so a plugin's `diagnostics.ts` never builds its own reporter (`colors`, `ansiFormatter`) or depends on `nostics` itself.
5+
Import `defineDiagnostics` (and `Diagnostic` for `instanceof` checks) from `devframe/utils/nostics`, never from `nostics` directly - it pre-wires devframe's ANSI console reporter, so a plugin's `diagnostics.ts` never builds its own reporter (`colors`, `ansiFormatter`) or depends on `nostics` itself. One exception: `@devframes/hub-ui-onboard` MUST stay free of `devframe` (a host ships it while devframe is not installed), so it imports `defineDiagnostics` and `createConsoleReporter` from `nostics` directly.
66

77
## Code ranges
88

@@ -16,6 +16,7 @@ Prefix: **`DF`**. Codes are sequential 4-digit numbers (e.g. `DF0033`) - check t
1616
- `DF83xx` - messages
1717
- `DF84xx` - commands
1818
- `DF85xx` - built-in RPC commands
19+
- `DF90xx` - `@devframes/hub-ui-onboard` (install, state file, hand-off)
1920

2021
## Adding a new error
2122

‎alias.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const alias = {
6161
'@devframes/hub/types': r('hub/src/types/index.ts'),
6262
'@devframes/hub': r('hub/src/index.ts'),
6363
'@devframes/hub-ui': r('hub-ui/src/index.ts'),
64+
'@devframes/hub-ui-onboard': r('hub-ui-onboard/src/index.ts'),
6465
'@devframes/nuxt/runtime/plugin.client': r('nuxt/src/runtime/plugin.client.ts'),
6566
'@devframes/nuxt/single': r('nuxt/src/single.ts'),
6667
'@devframes/nuxt/hub/client': r('nuxt/src/hub-client.ts'),

‎design/build-shadow-css.ts‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export interface BuildShadowCssOptions {
4242
* shadow trees on the same host page never collide.
4343
*/
4444
varPrefix: string
45+
/**
46+
* Also scan `@antfu/design`'s Vue components so the classes they use ship
47+
* in the stylesheet. Default `true`; a surface that renders none of those
48+
* components turns it off to keep the stylesheet small.
49+
*/
50+
scanDesignComponents?: boolean
4551
}
4652

4753
export interface BuildShadowCssResult {
@@ -63,7 +69,7 @@ export interface BuildShadowCssResult {
6369
* exempt from the `no-console` lint rule) prints its own summary line.
6470
*/
6571
export async function buildShadowCss(options: BuildShadowCssOptions): Promise<BuildShadowCssResult> {
66-
const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix } = options
72+
const { srcDir, globs, config, primaryRampPath, userStylePath, varPrefix, scanDesignComponents = true } = options
6773
const generatedCss = join(srcDir, '.generated/css.ts')
6874

6975
const require = createRequire(import.meta.url)
@@ -81,11 +87,13 @@ export async function buildShadowCss(options: BuildShadowCssOptions): Promise<Bu
8187
// package's component sources too so those classes ship in the injected
8288
// CSS.
8389
const designComponentsDir = join(require.resolve('@antfu/design/package.json'), '..', 'components')
84-
const designFiles = await glob('**/*.vue', {
85-
cwd: designComponentsDir,
86-
absolute: true,
87-
ignore: IGNORE,
88-
})
90+
const designFiles = scanDesignComponents
91+
? await glob('**/*.vue', {
92+
cwd: designComponentsDir,
93+
absolute: true,
94+
ignore: IGNORE,
95+
})
96+
: []
8997

9098
const generator = await createGenerator(config)
9199

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,30 @@ interface DevframeHubUi {
6363

6464
To add a language, add its tag and native name to `packages/hub-ui/src/locales.ts`, translate a copy of `packages/hub-ui/src/client/i18n/locales/en.json` under that tag, and register the file in the `messages` map in `packages/hub-ui/src/client/i18n/index.ts`. The i18n test fails on a missing key or a changed `{slot}`.
6565

66+
## Onboarding without the hub installed
67+
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. Disable writes `<stateDir>/hub-ui-onboard.json` (default `node_modules/.devframe`); 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.
89+
6690
## Renderer modules
6791

6892
A dock type's renderer (e.g. [JSON-Render](/guide/json-render)) composes via `initHub({ renderers })`. Each registration `{ type, file, importName? }` (`file` = a prebuilt ES module exporting a `DockRenderer`) is served at `<base>__renderers/<type>.mjs` and published into the `devframe:dock-renderers` manifest; client runtimes import it lazily on first mount:

‎docs/content/6.errors/DF9000.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
title: 'DF9000: No Packages to Install'
3+
description: '`createOnboarding()` received no packages to install.'
4+
---
5+
6+
## Message
7+
8+
> `createOnboarding()` received no packages to install.
9+
10+
## Cause
11+
12+
The Install button runs one package-manager command built from `packages`. With an empty list there is nothing for it to do.
13+
14+
## Example
15+
16+
```ts
17+
import { createOnboarding } from '@devframes/hub-ui-onboard'
18+
19+
createOnboarding({ packages: [] }) // ✗ throws DF9000
20+
```
21+
22+
## Fix
23+
24+
Pass at least one package spec, for example `packages: ['@nuxt/devtools']`.
25+
26+
## Source
27+
28+
- [`packages/hub-ui-onboard/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/hub-ui-onboard/src/index.ts): `createOnboarding` throws this before it reads any other option.

‎docs/content/6.errors/DF9001.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: 'DF9001: Install Command Failed'
3+
description: 'The package manager exited with a non-zero code.'
4+
---
5+
6+
## Message
7+
8+
> `{command}` exited with code `{exitCode}`.
9+
10+
## Cause
11+
12+
The Install button ran the project's package manager (`npm`, `pnpm`, `yarn`, `bun` or `deno`, detected from the lockfile) and it failed. The last lines of its error output follow the message. Common reasons: no network, a package name that does not exist, or a package manager that is not on `PATH`.
13+
14+
## Fix
15+
16+
Run the command shown in the panel in a terminal to see the full output. Fix the cause, then click Retry. The panel keeps the error until the next attempt.
17+
18+
## Source
19+
20+
- [`packages/hub-ui-onboard/src/install.ts`](https://github.com/devframes/devframe/blob/main/packages/hub-ui-onboard/src/install.ts): `runInstall` throws this when the process exits with a non-zero code. The status route reports it as `{ state: 'error', error: { code, message } }`.

‎docs/content/6.errors/DF9002.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: 'DF9002: Installed Package Not Found'
3+
description: 'The install finished, but a package is not in the project node_modules.'
4+
---
5+
6+
## Message
7+
8+
> The install finished, but "`{name}`" is not in `{cwd}`/node_modules.
9+
10+
## Cause
11+
12+
The package manager exited with code 0, but `<cwd>/node_modules/<name>` does not exist. This happens when `cwd` points at a directory other than the one the package manager installed into, for example the workspace root instead of the package that runs the dev server.
13+
14+
## Fix
15+
16+
Set `cwd` to the project that receives the dependency. In a workspace, that is the package whose dev server mounts the onboarding handler.
17+
18+
## Source
19+
20+
- [`packages/hub-ui-onboard/src/install.ts`](https://github.com/devframes/devframe/blob/main/packages/hub-ui-onboard/src/install.ts): `runInstall` checks each named package after the command succeeds. Path, URL and alias specs are not checked, because their installed name is not in the spec.

‎docs/content/6.errors/DF9003.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: 'DF9003: onInstalled Threw'
3+
description: '`onInstalled` threw after the packages were installed.'
4+
---
5+
6+
## Message
7+
8+
> `onInstalled` threw after the packages were installed.
9+
10+
## Cause
11+
12+
The packages are installed. The host's `onInstalled` callback, which usually imports the new packages and starts the hub, threw. The original error is attached as `cause`.
13+
14+
## Fix
15+
16+
Read the `cause`. A frequent one is a module that cannot be resolved: resolve the new packages from the project root (`createRequire(join(cwd, 'package.json'))`), not from the config file, because under pnpm they are only reachable from there. A restart of the dev server also loads the installed packages.
17+
18+
## Source
19+
20+
- [`packages/hub-ui-onboard/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/hub-ui-onboard/src/index.ts): `install()` reports this and sets `state: 'error'` when `onInstalled` rejects.

0 commit comments

Comments
 (0)