Skip to content

feat(hub): resolve bare-specifier client scripts through the host runtime - #257

Merged
antfu merged 3 commits into
mainfrom
neat-phones-mix
Aug 18, 2026
Merged

feat(hub): resolve bare-specifier client scripts through the host runtime#257
antfu merged 3 commits into
mainfrom
neat-phones-mix

Conversation

@antfubot

@antfubot antfubot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Problem

Since the v0.9 init/middleware refactor, every hub asset is served verbatim by devframe's own middleware, ahead of Vite's transform pipeline — so a dock client script declared with a bare npm specifier (vite-plugin-vue-tracer's action: { importFrom: 'vite-plugin-vue-tracer/client/vite-devtools' }) reaches the browser's native import() unresolved and throws TypeError: Failed to resolve module specifier.

Model: bare specifiers are a host-runtime capability

The hub protocol gains the hook; hosts whose runtime can serve resolvable module URLs declare it; the hub core ships no module serving of its own.

  • ConnectionMeta.configs.dock.clientModuleResolution — a host-declared URL template ({specifier} token), published via initHub({ clientModuleResolution }) and applied by every client-script loader: createDevframeClientHost, hub-ui's setup-script loader, and the generated __client-imports.js module.
  • createDevframeClientHost({ resolveClientModule }) — a viewer-side override that wins over the meta template (how a custom viewer like Vite DevTools can opt in without the meta field).
  • @devframes/vite/hub declares '/@id/{specifier}' by default — the import routes through Vite's own resolution and import-analysis, so the script's transitive bare imports work too and share the inspected app's module graph (vue-tracer needs zero changes).
  • Hosts without such a runtime (Next.js) keep the URL contract: a new DF8111 diagnostic warns at dock registration when a bare specifier can't resolve there, and both browser loaders name the capability gap in their error instead of surfacing the browser's opaque TypeError.

Docs

  • docs/guide/client-context.md — the two importFrom shapes, plus the protocol guarantee: client scripts execute in the inspected page's realm; module identity is best-effort (Vite) — plugins with shared state should anchor it on globalThis, vue-tracer's __vue_tracer__ store being the reference pattern.
  • docs/errors/DF8111.md.

Example parity

One shared demo package (examples/demo-dock-client, nanoevents dep + globalThis-anchored store) consumed in both shapes: hub-vite by bare specifier through /@id/, hub-next as a statically-mounted self-contained bundle — both READMEs state the capability difference explicitly. Both reference clients also learn to render action docks as momentary rail buttons.

Verification

  • Unit tests for the resolver (client-modules.test.ts), the meta advertisement + __client-imports.js rewriting (initiate.test.ts), and DF8111 (host-docks.test.ts); tsnapi API snapshots updated.
  • Verified live in a browser against the hub-vite example: /@id/demo-dock-client served Vite-transformed JS with its bare nanoevents import rewritten into the module graph, and clicking the dock produced the demo script's activation message end to end.

Follow-up (separate repo): @vitejs/devtools-kit passes the resolver / declares the meta field — one-line adoption.


Created with the help of an agent.

…time

A dock client script's importFrom can now name an npm module (e.g.
'vite-plugin-vue-tracer/client/vite-devtools') instead of a served URL.
Bare specifiers are a host-runtime capability: the host advertises a URL
template as ConnectionMeta.configs.dock.clientModuleResolution (declared
via initHub({ clientModuleResolution })), and every client-script loader
applies it before the native import — createDevframeClientHost (which
also gains a resolveClientModule override), hub-ui's setup-script
loader, and the generated __client-imports.js module.

@devframes/vite/hub declares '/@id/{specifier}' by default, so the
import routes through Vite's own resolution and import-analysis: the
script's transitive bare imports work too and share the app's module
graph — the fix for vue-tracer's dock breaking once v0.9's middleware
began serving hub assets verbatim, outside Vite's transform pipeline.

Hosts without such a runtime (Next.js) keep the URL contract; a new
DF8111 diagnostic warns at dock registration when a bare specifier is
doomed there, and both browser loaders name the capability gap in their
error. The reference hubs stay at parity through one shared demo
package (examples/demo-dock-client): hub-vite consumes it by bare
specifier, hub-next as a statically-mounted self-contained bundle, and
its globalThis-anchored store documents the state pattern the docs'
new protocol guarantee prescribes.
@netlify

netlify Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 919cc0c
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a83ef429d929e00086f994b
😎 Deploy Preview https://deploy-preview-257--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

…ved-but-unservable client scripts

The hub-vite demo failed with 'Failed to fetch dynamically imported
module' whenever demo-dock-client's dist was absent (running vite
without the repo build): Vite couldn't resolve the /@id/ request's
exports target and the SPA index.html fallback answered 200 with HTML.
The package's '.' export now points at src/index.ts — a Vite host
transforms the linked source directly, so the bare-specifier path needs
no build at all; tsdown keeps building only the URL-shape artifacts
(dist/bundle.mjs + dist/node.mjs for the Next host).

Both browser loaders also gain a second diagnosis branch via the shared
clientScriptFailureHint(): when a bare specifier WAS resolved through
the host template and the import still failed, the error now points at
the module being unservable on the host (package not installed/built)
instead of leaving only the browser's opaque TypeError.
Public API narrows to what consumers actually touch: the initHub
clientModuleResolution option, the createDevframeClientHost
resolveClientModule override, and — on @devframes/hub/client, for
viewers and hub-ui — resolveClientModuleSpecifier +
clientScriptFailureHint. The template token constant, the standalone
template applier, the options interface, the DockConnectionConfig type
(now inlined in the configs augmentation), and @devframes/vite/hub's
clientModuleResolution option + exported constant all fold away; the
resolver takes the template directly, so the node-side imports-module
rendering reuses it instead of its own helper pair.
@antfu
antfu merged commit b3b4134 into main Aug 18, 2026
12 checks passed
@antfu
antfu deleted the neat-phones-mix branch August 18, 2026 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants