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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants