diff --git a/docs/errors/DF8111.md b/docs/errors/DF8111.md new file mode 100644 index 00000000..a209cb57 --- /dev/null +++ b/docs/errors/DF8111.md @@ -0,0 +1,43 @@ +--- +outline: deep +--- + +# DF8111: Bare-Specifier Client Script Without Host Resolution + +## Message + +> Dock "`{id}`" declares the bare-specifier client script "`{specifier}`", but this host advertises no client-module resolution — the browser cannot resolve a bare npm specifier natively, so the script will fail to load. + +## Cause + +A dock entry's client script (`clientScript` on iframe docks, `action`, `renderer`) names an npm module (`'vite-plugin-vue-tracer/client/vite-devtools'`) as its `importFrom`. Client scripts load with a native browser `import()`, and a browser only resolves URL specifiers — bare specifiers work when the **host runtime** resolves them, advertised as `ConnectionMeta.configs.dock.clientModuleResolution` (a URL template whose `{specifier}` token is replaced with the specifier). This host declared none, so every client-script loader will throw `TypeError: Failed to resolve module specifier` for this entry. + +## Example + +```ts +initHub({ + base: '/__devframes/', + configure(ctx) { + ctx.docks.register({ + type: 'action', + id: 'vue-tracer', + title: 'Vue Tracer', + icon: 'ph:crosshair-simple-duotone', + // ✗ Bare specifier on a host with no `clientModuleResolution` + action: { importFrom: 'vite-plugin-vue-tracer/client/vite-devtools' }, + }) + }, +}) +``` + +## Fix + +Pick whichever side you control: + +- **Run under a host that resolves bare specifiers.** A Vite host serves any npm module through its own module graph — declare `initHub({ clientModuleResolution: '/@id/{specifier}' })`. `@devframes/vite/hub` declares this by default, so the example above is fine there; the script's transitive bare imports work too and share the app's module graph. +- **Ship the script as a self-contained bundle** and pass a URL the host serves as `importFrom` (the a11y inspector pattern): `{ importFrom: '/__devframes/my-agent/inject.js' }` after mounting the bundle's directory with `ctx.host.mountStatic(...)`. +- **Resolve it in the viewer.** A custom viewer may pass `createDevframeClientHost({ resolveClientModule })` (or ship a page import map); the warning is then safe to disregard — it fires because the *server* can't know a viewer will cover the gap. + +## Source + +- [`packages/hub/src/node/host-docks.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/host-docks.ts) — `DevframeDocksHost.register()` warns when a bare-specifier client script registers on a host whose `staticConfig.dock` declares no `clientModuleResolution`. diff --git a/docs/guide/client-context.md b/docs/guide/client-context.md index 902acece..2d323f6d 100644 --- a/docs/guide/client-context.md +++ b/docs/guide/client-context.md @@ -136,7 +136,12 @@ A script that fails to import is logged and retried on the next dock update. ### Shipping a client script -Build the script as a single self-contained ES module — it loads outside any chunk graph or import map. Attach it when mounting the devframe: +`importFrom` accepts two shapes: + +- **A URL the host serves** — a single self-contained ES module, loading outside any chunk graph. Works on every host. +- **A bare npm specifier** (`'vite-plugin-vue-tracer/client/vite-devtools'`) — resolved through the host runtime, where supported. + +For the URL shape, attach the built bundle when mounting the devframe: ```ts await ctx.install(myDevframe, { @@ -146,6 +151,35 @@ await ctx.install(myDevframe, { Under Vite, `/@fs/` serves the built bundle directly; other hosts mount the bundle's directory statically and pass that URL instead. +### Bare npm specifiers + +Bare specifiers are a **host-runtime capability**. A host that can serve npm modules to the browser advertises a resolution template as `ConnectionMeta.configs.dock.clientModuleResolution` — the `{specifier}` token is replaced with the specifier, and every client-script loader (the client host, the hub-ui viewers, `__client-imports.js`) applies it before importing: + +```ts +// A Vite host resolves bare specifiers through its own module graph. +// `@devframes/vite/hub` declares this by default. +initHub({ clientModuleResolution: '/@id/{specifier}' }) +``` + +On a Vite host, `/@id/` routes the import through Vite's own resolution and import-analysis, so the script's transitive bare imports work too and resolve in the same module graph as the inspected app — a plugin whose injected app-side code and dock client script import the same modules shares their instances. A plugin can then declare its dock with just the specifier: + +```ts +ctx.docks.register({ + type: 'action', + id: 'vue-tracer', + title: 'Vue Tracer', + icon: 'ph:crosshair-simple-duotone', + action: { importFrom: 'vite-plugin-vue-tracer/client/vite-devtools' }, +}) +``` + +A host that declares no template (Next.js today) supports the URL shape only — registering a bare specifier there warns [`DF8111`](/errors/DF8111). A viewer can also resolve bare specifiers itself with `createDevframeClientHost({ resolveClientModule })`, which wins over the host template. + +Two guarantees to design against: + +- **Client scripts always execute in the inspected page's realm** — the same `window` as the app being inspected. +- **Module identity is best-effort, realm identity is the contract.** On Vite hosts a bare specifier shares the app's module graph; elsewhere a script ships as its own bundle. A plugin keeping shared state between its injected app code and its dock script should anchor that state on `globalThis` (vue-tracer's `__vue_tracer__` store is the reference pattern) rather than rely on both sides importing one module instance. + ### Dual boots The [a11y inspector](/plugins/a11y)'s in-page agent is the canonical client script, and it boots both ways from one bundle: the default export accepts the client-script context (mirroring each scan into the hub's messages feed), while a deferred, globally-guarded self-boot lets a plain `