You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
# DF8111: Bare-Specifier Client Script Without Host Resolution
6
+
7
+
## Message
8
+
9
+
> 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.
10
+
11
+
## Cause
12
+
13
+
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.
14
+
15
+
## Example
16
+
17
+
```ts
18
+
initHub({
19
+
base: '/__devframes/',
20
+
configure(ctx) {
21
+
ctx.docks.register({
22
+
type: 'action',
23
+
id: 'vue-tracer',
24
+
title: 'Vue Tracer',
25
+
icon: 'ph:crosshair-simple-duotone',
26
+
// ✗ Bare specifier on a host with no `clientModuleResolution`
-**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.
38
+
-**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(...)`.
39
+
-**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.
40
+
41
+
## Source
42
+
43
+
-[`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`.
Under Vite, `/@fs/<absolute path>` serves the built bundle directly; other hosts mount the bundle's directory statically and pass that URL instead.
148
153
154
+
### Bare npm specifiers
155
+
156
+
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:
157
+
158
+
```ts
159
+
// A Vite host resolves bare specifiers through its own module graph.
160
+
// `@devframes/vite/hub` declares this by default.
On a Vite host, `/@id/<specifier>` 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:
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.
177
+
178
+
Two guarantees to design against:
179
+
180
+
-**Client scripts always execute in the inspected page's realm** — the same `window` as the app being inspected.
181
+
-**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.
182
+
149
183
### Dual boots
150
184
151
185
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 `<script type="module">` start the same agent outside a hub. The context-ful call wins because the hub invokes the default export before the deferred self-boot runs.
The shared dock client script the two reference hubs consume in their two supported shapes — one package, both `importFrom` forms:
4
+
5
+
-**`hub-vite`** registers it by **bare specifier** (`action: { importFrom: 'demo-dock-client' }`). The Vite host advertises `clientModuleResolution: '/@id/{specifier}'` (the `@devframes/vite/hub` default), so the client host imports `src/index.ts` through Vite's own module graph — Vite transforms the linked source directly (no build needed on this path) and resolves its bare `nanoevents` import there too.
6
+
-**`hub-next`** mounts the prebuilt **self-contained bundle** (`dist/bundle.mjs`, nanoevents inlined) statically and passes the served URL. Next declares no `clientModuleResolution`, so the URL shape is the supported one there.
7
+
8
+
The script itself demonstrates the state pattern bare-specifier plugins should follow: shared state anchored on `globalThis` (`__devframes_demo_dock_client__`), the same design as `vite-plugin-vue-tracer`'s `__vue_tracer__` store — realm identity is the contract, module identity is best-effort. On each dock activation it bumps the shared counter and reports into the hub's messages feed, naming the URL it was loaded from.
9
+
10
+
## Entries
11
+
12
+
| Entry | Resolves to | Role |
13
+
|---|---|---|
14
+
|`demo-dock-client`|`src/index.ts` (source, deps bare) | Bare-specifier consumption through a host's module graph |
15
+
| — |`dist/bundle.mjs` (self-contained build) | URL consumption on hosts without bare-specifier resolution |
Copy file name to clipboardExpand all lines: examples/hub-next/README.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,6 +47,7 @@ The instance is memoized on `globalThis`, so Next's dev-time module re-evaluatio
47
47
-`createDevframeClientHost()` boots the hub's framework-level client runtime in the host page: it publishes the shared client context and imports each dock's `clientScript` (here, the a11y agent) so plugins run code in the page being inspected
48
48
- The **JSON Render** dock renders through a **local React renderer** (`src/client/json-render/react-renderer.tsx` - a compact React port of the base catalog) registered at `createDevframeClientHost({ renderers })`. The hub *also* publishes the reference Vue frontend through its renderer manifest (`renderers: [jsonRenderUiRenderer()]` on `initHub`), but local registration takes precedence - witnessing that any frontend implementing the `JsonRenderDockRenderer` contract can replace the reference one. Delete the local `renderers` option and the same dock renders via the manifest-served module instead. (The sibling `hub-vite` witness ships no local renderer and consumes the manifest directly - the other side of the swap seam.)
49
49
- The **No Renderer** dock witnesses the missing-renderer path: its type is covered by nothing, so `renderers.mount()` resolves `{ status: 'missing-renderer' }` and the shell shows *No renderer for "demo-unrendered" in the current environment* instead of a dead panel
50
+
- The **Client Script Demo** dock witnesses the **URL shape of client scripts**: this host declares no `clientModuleResolution` (Next's bundler exposes no browser-reachable on-demand module URL, so bare-specifier client scripts are unsupported here), so it mounts `demo-dock-client`'s prebuilt self-contained bundle statically and passes the served URL as `action.importFrom`. The sibling `hub-vite` host consumes the **same package** as a bare npm specifier through its `/@id/{specifier}` template - the two shapes of `importFrom` side by side
50
51
51
52
## Hosting built-in plugins in a bundler
52
53
@@ -58,6 +59,7 @@ The plugins run node-side (child processes, the native `zigpty` PTY backend) and
58
59
|---|---|
59
60
|`src/client/devframe/next-devframe-hub.ts`| The Next host - one `initHub()` call: devframes (incl. the a11y agent's dock `clientScript`), hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration |
60
61
|`src/client/devframe/unrendered-dock.ts`| A dock type registered with no renderer on purpose - the missing-renderer fallback witness |
62
+
|`../demo-dock-client/`| The shared demo client script, consumed here as a statically-mounted self-contained bundle |
61
63
|`src/client/app/%5F_devframes/[[...path]]/route.ts`| The one catch-all - delegates every `/__devframes/*` request to the instance's `handler`|
62
64
|`src/client/app/page.tsx`| The browser UI that consumes the hub protocol, including the interactive-OTP authorization view |
63
65
|`src/client/app/icons.ts`| Offline Phosphor icons for the dock |
0 commit comments