Skip to content

Commit 865fb37

Browse files
committed
fix(examples): serve the demo dock client from source; diagnose resolved-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.
1 parent 9cc74b6 commit 865fb37

9 files changed

Lines changed: 71 additions & 42 deletions

File tree

‎examples/demo-dock-client/README.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
The shared dock client script the two reference hubs consume in their two supported shapes — one package, both `importFrom` forms:
44

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 `dist/index.mjs` through Vite's own module graph — its bare `nanoevents` import resolves there too.
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.
66
- **`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.
77

88
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.
99

1010
## Entries
1111

12-
| Entry | Built as | Role |
12+
| Entry | Resolves to | Role |
1313
|---|---|---|
14-
| `demo-dock-client` | `dist/index.mjs` (deps external) | Bare-specifier consumption through a host's module graph |
15-
| — | `dist/bundle.mjs` (self-contained) | URL consumption on hosts without bare-specifier resolution |
16-
| `demo-dock-client/node` | `dist/node.mjs` | Node helper exporting `demoDockClientBundlePath` for static mounting |
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 |
16+
| `demo-dock-client/node` | `dist/node.mjs` (build) | Node helper exporting `demoDockClientBundlePath` for static mounting |

‎examples/demo-dock-client/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"description": "Reference dock client script for the hub examples: bare npm imports and globalThis-anchored shared state.",
77
"homepage": "https://github.com/devframes/devframe/tree/main/examples/demo-dock-client",
88
"exports": {
9-
".": "./dist/index.mjs",
9+
".": "./src/index.ts",
1010
"./node": "./dist/node.mjs",
1111
"./package.json": "./package.json"
1212
},

‎examples/demo-dock-client/tsdown.config.ts‎

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,23 @@ import { defineConfig } from 'tsdown'
22

33
const tsconfig = '../../tsconfig.base.json'
44

5-
// Two builds of the same client script, one per consumption mode the hub
6-
// examples demonstrate:
7-
// 1. `dist/index.mjs` — dependencies stay external (`import 'nanoevents'`
8-
// survives as a bare import), for hosts that resolve bare specifiers
9-
// through their own module graph (hub-vite imports
10-
// `'demo-dock-client'` via the Vite `/@id/{specifier}` template);
11-
// 2. `dist/bundle.mjs` — self-contained (nanoevents inlined), for hosts
5+
// The bare-specifier consumption path needs no build at all: the package's
6+
// `.` export points straight at `src/index.ts`, which a Vite host transforms
7+
// like any linked workspace source (hub-vite imports `'demo-dock-client'`
8+
// via the `/@id/{specifier}` template). What gets built here is only the
9+
// **URL-shape** consumption path:
10+
// 1. `dist/bundle.mjs` — self-contained (nanoevents inlined), for hosts
1211
// without bare-specifier resolution (hub-next mounts it statically and
13-
// passes the served URL as `importFrom`).
14-
// Plus the node-side path helper the Next host uses to locate the bundle.
12+
// passes the served URL as `importFrom`);
13+
// 2. `dist/node.mjs` — the node-side path helper the Next host uses to
14+
// locate the bundle.
1515
export default defineConfig([
1616
{
1717
clean: true,
1818
platform: 'browser',
1919
tsconfig,
2020
dts: false,
2121
outExtensions: () => ({ js: '.mjs' }),
22-
entry: { index: 'src/index.ts' },
23-
},
24-
{
25-
clean: false,
26-
platform: 'browser',
27-
tsconfig,
28-
dts: false,
29-
outExtensions: () => ({ js: '.mjs' }),
3022
entry: { bundle: 'src/index.ts' },
3123
deps: { alwaysBundle: ['nanoevents'] },
3224
},

‎packages/hub-ui/src/client/state/setup-script.ts‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ClientScriptEntry, DevframeDockUserEntry } from '@devframes/hub'
22
import type { DockClientScriptContext } from '@devframes/hub/client'
3-
import { isBareModuleSpecifier, resolveClientModuleSpecifier } from '@devframes/hub/client'
3+
import { clientScriptFailureHint, resolveClientModuleSpecifier } from '@devframes/hub/client'
44

55
/**
66
* Resolve the {@link ClientScriptEntry} a dock entry carries — an `action`'s
@@ -48,14 +48,10 @@ async function _executeSetupScript(
4848
catch (error) {
4949
// TODO: maybe popup a error toast here?
5050
// TODO: A unified logger API
51-
// An unresolved bare specifier is a host-capability gap, not a plugin
52-
// bug — say so instead of surfacing the browser's opaque TypeError.
53-
const hint = specifier === script.importFrom && isBareModuleSpecifier(script.importFrom)
54-
? ` — "${specifier}" is a bare npm specifier and this host advertises no client-module resolution `
55-
+ '(`ConnectionMeta.configs.dock.clientModuleResolution`). Serve the script as a self-contained '
56-
+ 'bundle by URL, or run under a host that resolves bare specifiers (e.g. Vite: `/@id/{specifier}`).'
57-
: ''
58-
console.error(`[@devframes/hub-ui] Error executing client script${hint}`, error)
51+
console.error(
52+
`[@devframes/hub-ui] Error executing client script from ${specifier}${clientScriptFailureHint(script.importFrom, specifier)}`,
53+
error,
54+
)
5955
throw error
6056
}
6157
}

‎packages/hub/src/client-modules.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ConnectionMeta } from 'devframe/types'
22
import { describe, expect, it } from 'vitest'
33
import {
44
applyClientModuleResolutionTemplate,
5+
clientScriptFailureHint,
56
isBareModuleSpecifier,
67
resolveClientModuleSpecifier,
78
} from './client-modules'
@@ -100,3 +101,21 @@ describe('resolveClientModuleSpecifier', () => {
100101
expect(called).toBe(false)
101102
})
102103
})
104+
105+
describe('clientScriptFailureHint', () => {
106+
it('is silent for URL specifiers', () => {
107+
expect(clientScriptFailureHint('/@fs/abs/inject.js', '/@fs/abs/inject.js')).toBe('')
108+
})
109+
110+
it('names the capability gap for an unresolved bare specifier', () => {
111+
const hint = clientScriptFailureHint('foo/bar', 'foo/bar')
112+
expect(hint).toContain('clientModuleResolution')
113+
expect(hint).toContain('bare npm specifier')
114+
})
115+
116+
it('names the serving gap for a resolved-but-failed bare specifier', () => {
117+
const hint = clientScriptFailureHint('foo/bar', 'http://localhost:5173/@id/foo/bar')
118+
expect(hint).toContain('"foo/bar"')
119+
expect(hint).toContain('could not serve the module')
120+
})
121+
})

‎packages/hub/src/client-modules.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,28 @@ export function resolveClientModuleSpecifier(
9191
return applied
9292
}
9393
}
94+
95+
/**
96+
* Diagnose a failed client-script import: name the likely cause instead of
97+
* leaving only the browser's opaque `TypeError`. `importFrom` is the entry's
98+
* declared specifier, `specifier` what was actually imported (after
99+
* {@link resolveClientModuleSpecifier}).
100+
*/
101+
export function clientScriptFailureHint(importFrom: string, specifier: string): string {
102+
if (!isBareModuleSpecifier(importFrom))
103+
return ''
104+
// Bare and nothing resolved it — a host-capability gap, not a plugin bug.
105+
if (specifier === importFrom) {
106+
return ' — the specifier is a bare npm specifier and this host advertises no client-module resolution '
107+
+ '(`ConnectionMeta.configs.dock.clientModuleResolution`). Serve the script as a self-contained '
108+
+ 'bundle by URL, pass `resolveClientModule` to this client host, or run under a host that '
109+
+ 'resolves bare specifiers (e.g. Vite: `/@id/{specifier}`).'
110+
}
111+
// Bare and resolved to a host URL, yet the import still failed — the host
112+
// couldn't actually serve the module. (A dev server typically answers an
113+
// unresolvable module URL with its HTML fallback, which the browser reports
114+
// as "Failed to fetch dynamically imported module".)
115+
return ` — the host resolved the bare specifier "${importFrom}" to this URL but could not serve the module. `
116+
+ 'Check the package is installed and resolvable from the host project\'s root '
117+
+ '(and built, if its exports point at build output).'
118+
}

‎packages/hub/src/client/host.ts‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
import type { DockRenderer, DockRendererManifest, DockRenderersContext } from './renderers'
2727
import { connectDevframe } from 'devframe/client'
2828
import { createEventEmitter } from 'devframe/utils/events'
29-
import { isBareModuleSpecifier, resolveClientModuleSpecifier } from '../client-modules'
29+
import { clientScriptFailureHint, resolveClientModuleSpecifier } from '../client-modules'
3030
import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DOCK_RENDERERS_STATE_KEY } from '../constants'
3131
import { getDevframeClientContext, setDevframeClientContext } from './context'
3232
import { attachFrameNavClient } from './frame-nav'
@@ -546,15 +546,10 @@ export async function createDevframeClientHost(
546546
}
547547
catch (error) {
548548
loadedScripts.delete(entryId)
549-
// An unresolved bare specifier is a host-capability gap, not a plugin
550-
// bug — say so instead of surfacing the browser's opaque TypeError.
551-
const hint = specifier === script.importFrom && isBareModuleSpecifier(script.importFrom)
552-
? ' — the specifier is a bare npm specifier and this host advertises no client-module resolution '
553-
+ '(`ConnectionMeta.configs.dock.clientModuleResolution`). Serve the script as a self-contained '
554-
+ 'bundle by URL, pass `resolveClientModule` to this client host, or run under a host that '
555-
+ 'resolves bare specifiers (e.g. Vite: `/@id/{specifier}`).'
556-
: ''
557-
console.error(`[@devframes/hub] failed to load client script for "${entryId}" from ${specifier}${hint}`, error)
549+
console.error(
550+
`[@devframes/hub] failed to load client script for "${entryId}" from ${specifier}${clientScriptFailureHint(script.importFrom, specifier)}`,
551+
error,
552+
)
558553
}
559554
}
560555
}

‎tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export type FrameNavHostPayload = {
240240
export declare function applyClientModuleResolutionTemplate(_: string, _: string): string;
241241
export declare function attachFrameNavClient(_: FrameNavClientOptions): FrameNavClient;
242242
export declare function buildRemoteDevframeUrl(_: string, _: DevframeConnection): string;
243+
export declare function clientScriptFailureHint(_: string, _: string): string;
243244
export declare function connectRemoteDevframe(_?: ConnectRemoteDevframeOptions): Promise<DevframeRpcClient>;
244245
export declare function createDevframeClientHost(_?: DevframeClientHostOptions): Promise<DevframeClientHost>;
245246
export declare function createDockRenderersContext(_: CreateDockRenderersContextOptions): DockRenderersContext;

‎tests/__snapshots__/tsnapi/@devframes/hub/client.snapshot.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export * from "devframe/client";
2929
// #region Other
3030
export { applyClientModuleResolutionTemplate }
3131
export { CLIENT_MODULE_SPECIFIER_TOKEN }
32+
export { clientScriptFailureHint }
3233
export { isBareModuleSpecifier }
3334
export { resolveClientModuleSpecifier }
3435
export { stripRemoteConnectionFromUrl }

0 commit comments

Comments
 (0)