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`.
The UI mounts as usual — the first request for each file is streamed from a CDN and written to a local cache; subsequent requests are served from disk.
64
64
65
+
The definition's [`importMetaUrl`](./devframe-definition#resolving-against-the-plugins-own-dependencies) supplies the resolution base, so a remote source needs only its `package` and `version`. A per-source `resolveFrom` overrides that base for one source, and an explicit `resolveFrom: null` opts a source out of the installed-copy lookup entirely.
66
+
65
67
### How assets resolve
66
68
67
69
For each request the source resolves in order:
68
70
69
-
1.**Locally installed package** — resolved from `resolveFrom` (`import.meta.url`). If `@acme/my-tool-assets` is installed next to your tool, it's served directly with no network. This is the offline path.
71
+
1.**Locally installed package** — resolved from `resolveFrom`, which defaults to the definition's `importMetaUrl`. If `@acme/my-tool-assets` is installed next to your tool, it's served directly with no network. This is the offline path.
70
72
2.**On-disk cache** — files already fetched, under the project's storage directory.
71
73
3.**CDN back-proxy** — [jsDelivr](https://www.jsdelivr.com/) by default, mirroring npm. Each file streams to the browser and is cached on the way past.
72
74
@@ -78,7 +80,7 @@ Exact-version URLs are immutable, so a cached file never goes stale.
78
80
|-------|---------|
79
81
|`package`| npm package holding the built assets. |
80
82
|`version`| Exact version to serve — usually your tool's own `pkg.version`. |
81
-
|`resolveFrom`|`import.meta.url` of the declaring module; enables the zero-network path from a locally installed copy. Omit to skip straight to cache + CDN. |
83
+
|`resolveFrom`|Resolution base for the zero-network path from a locally installed copy. Defaults to the definition's `importMetaUrl`; set it to override that for one source, or to `null` to skip straight to cache + CDN. |
82
84
|`path`| Subpath inside the package the assets live under. Defaults to `dist`. |
83
85
|`provider`|`'jsdelivr'` (default), `'unpkg'`, or a custom provider for an internal mirror. |
84
86
|`offline`|`true` serves only from a local install or the cache — never the network. |
@@ -109,7 +111,6 @@ A custom provider supplies the file URL, and optionally a file listing (used for
The assets package is an ordinary npm package that ships the built UI under `path` (default `dist`) and exposes its `package.json` so `resolveFrom` can locate it:
123
+
The assets package is an ordinary npm package that ships the built UI under `path` (default `dist`) and exposes its `package.json` so the resolver can locate it:
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.
Copy file name to clipboardExpand all lines: docs/guide/devframe-definition.md
+33Lines changed: 33 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,6 +17,7 @@ export default defineDevframe({
17
17
name: 'My Devframe',
18
18
version: '1.0.0',
19
19
packageName: 'my-devframe',
20
+
importMetaUrl: import.meta.url,
20
21
homepage: 'https://github.com/me/my-devframe',
21
22
description: 'A one-line summary of what the tool does.',
22
23
icon: 'ph:gauge-duotone',
@@ -45,6 +46,7 @@ export default defineDevframe({
45
46
|`name`|`string`|**Required.** Display name shown in the dock and agent manifests. |
46
47
|`version`|`string`|**Required.** Semver of the tool, surfaced in hub UIs and diagnostics. |
47
48
|`packageName`|`string`|**Required.** npm package name the devframe ships in (e.g. `@scope/my-tool`). |
49
+
|`importMetaUrl`|`string`|**Recommended.** Always pass `import.meta.url`. The resolution base for the tool's own dependency graph: it becomes the default `resolveFrom` for any [remote assets](./client-assets) the devframe hosts, and the base the host resolves declared [services](./services#wire-services) from — so a plugin ships an assets or service package as its own dependency instead of asking users to install it. See [Resolving against the plugin's own dependencies](#resolving-against-the-plugins-own-dependencies). |
48
50
|`homepage`|`string`|**Required.** Project homepage or documentation URL. |
49
51
|`description`|`string`|**Required.** One-line summary of what the tool does. |
50
52
|`icon`|`string \| { light, dark }`| Optional Iconify name or URL; supports light/dark pairs. |
The default import with a `with { type: 'json' }` attribute resolves under both bundlers and Node's native TypeScript execution. Bundlers also support the destructured `import { version } from '../package.json'` form when the devframe is always bundled before it runs.
77
80
81
+
### Resolving against the plugin's own dependencies
82
+
83
+
A devframe often ships companion packages — a separate `--assets` package holding its built SPA, or a service package it consumes. `importMetaUrl` lets the host resolve those against the plugin's **own** installed dependencies rather than the consuming app's, so the plugin declares them as its dependencies and users install nothing extra.
For a remote assets source, `importMetaUrl` is the default `resolveFrom`; a per-source `resolveFrom` still wins, and an explicit `resolveFrom: null` opts out of the installed-copy lookup. See [Client Assets](./client-assets) and [Cross-Plugin Services](./services#wire-services) for the full resolution order.
110
+
78
111
### Runtime flags
79
112
80
113
The `ctx.mode` field is either `'dev'` or `'build'`. Use it to gate work that should only run in one runtime:
Copy file name to clipboardExpand all lines: docs/guide/services.md
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -103,11 +103,12 @@ Two declaration merges make it fully typed for consumers: the fully-qualified RP
103
103
104
104
### Declaring
105
105
106
-
Services are **declarative**. A plugin lists what it consumes on its definition; a host lists shared ones on `initHub`. The adapter resolves each package — for a plugin, **against the plugin's own dependencies** — and constructs it:
106
+
Services are **declarative**. A plugin lists what it consumes on its definition; a host lists shared ones on `initHub`. The adapter resolves each package — for a plugin, **against the plugin's own dependencies**via the definition's [`importMetaUrl`](./devframe-definition#resolving-against-the-plugins-own-dependencies), so a plugin ships a service package as its own dependency and users install nothing extra — and constructs it:
107
107
108
108
```ts
109
109
// plugin side — on the definition
110
110
defineDevframe({
111
+
importMetaUrl: import.meta.url, // resolution base for the declared packages
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 |
0 commit comments