diff --git a/docs/content/1.guide/17.client-context.md b/docs/content/1.guide/17.client-context.md index be8ac3e8..b54292ba 100644 --- a/docs/content/1.guide/17.client-context.md +++ b/docs/content/1.guide/17.client-context.md @@ -76,12 +76,13 @@ A failed import retries on the next dock update. ### Shipping a client script -`importFrom` accepts two shapes: +`importFrom` accepts three shapes: - **A URL served by the host framework** — a self-contained ES module; works on every host framework. - **A bare npm specifier** (`'vite-plugin-vue-tracer/client/vite-devtools'`) — resolved through the host framework. +- **An absolute filesystem path**, declared on the definition's `dock.clientScript`. The hub serves its directory under `__page-script/` and rewrites `importFrom` to that URL, so mounting by package name needs no host wiring. -For a URL, attach it via `ctx.install(myDevframe, { dock: { clientScript: { importFrom } } })`. Under Vite `/@fs/` serves it; other host frameworks mount the directory statically. +Per-mount, attach a URL via `ctx.install(myDevframe, { dock: { clientScript: { importFrom } } })`; under Vite `/@fs/` serves it, and other host frameworks mount the directory statically. ### Bare npm specifiers diff --git a/docs/content/5.plugins/4.a11y.md b/docs/content/5.plugins/4.a11y.md index ad5dc17a..0875e925 100644 --- a/docs/content/5.plugins/4.a11y.md +++ b/docs/content/5.plugins/4.a11y.md @@ -31,7 +31,15 @@ The page script and the panel talk over the [in-page channel](/guide/in-page-cha ## In a hub -The page script is the a11y dock's [client script](/guide/client-context): attach `a11yPageScriptBundlePath` as the dock's `clientScript` and the hub imports it into the page. It also mirrors each scan into the hub's messages feed — a summary plus one per rule: +The definition declares the page script as its dock [client script](/guide/client-context), so mounting by package name just works: + +```ts +initHub({ devframes: ['@devframes/plugin-a11y'] }) +``` + +The hub serves the bundle same-origin and a client runtime imports it into the host page. Each scan also mirrors into the hub's messages feed — a summary plus one per rule. + +A host can also mount the module itself — e.g. a Vite host via `/@fs/`: ```ts import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y' diff --git a/examples/a11y-messages-playground/README.md b/examples/a11y-messages-playground/README.md index 39980c55..acab15ef 100644 --- a/examples/a11y-messages-playground/README.md +++ b/examples/a11y-messages-playground/README.md @@ -55,15 +55,12 @@ The window is split in two: ## How it's wired `src/a11y-messages-playground.ts` is the entire host-framework integration - a ~120-line Vite plugin -that runs `@devframes/hub` in the dev server, mounts the two devframes as docks, -and attaches the a11y page script as the a11y dock's `clientScript`: +that runs `@devframes/hub` in the dev server and mounts the two devframes as docks +(the a11y inspector declares its own page script): ```ts a11yMessagesPlayground({ devframes: [a11yDevframe, messagesDevframe], - clientScripts: { - [a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` }, - }, }) ``` @@ -79,7 +76,7 @@ the focused dock - the same path a manual dock click takes. | File | Role | |---|---| | `src/a11y-messages-playground.ts` | The Vite host - hub context, static + connection-meta mounts, side-car WS, instance-registry registration | -| `vite.config.ts` | Mounts a11y + messages; attaches the a11y page script as its dock's `clientScript` | +| `vite.config.ts` | Mounts a11y + messages | | `src/client/main.ts` | Boots the client runtime, renders the dock rail + iframe stage | | `src/client/app-under-test.ts` | The intentionally-broken, multi-route app the page script scans | | `src/client/icons.ts` | Offline Phosphor icons for the dock rail | diff --git a/examples/a11y-messages-playground/src/a11y-messages-playground.ts b/examples/a11y-messages-playground/src/a11y-messages-playground.ts index 115d9e22..d9b431e4 100644 --- a/examples/a11y-messages-playground/src/a11y-messages-playground.ts +++ b/examples/a11y-messages-playground/src/a11y-messages-playground.ts @@ -1,5 +1,4 @@ import type { HubInstance } from '@devframes/hub/initiate' -import type { ClientScriptEntry } from '@devframes/hub/types' import type { DevframeDefinition } from 'devframe' import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite' import { Server as NodeHttpServer } from 'node:http' @@ -14,22 +13,15 @@ export interface A11yMessagesPlaygroundOptions { port?: number /** Devframes to mount as docks (here: a11y + messages). */ devframes?: DevframeDefinition[] - /** - * Per-dock client scripts, keyed by devframe id. Attached to the mounted - * iframe dock so the hub client runtime imports them into the host page - - * this is how the a11y inspector's in-page agent gets into the page it scans. - */ - clientScripts?: Record } /** * A tiny Vite plugin that runs `@devframes/hub` inside the Vite dev server - * the same shape as `examples/hub-vite`, trimmed to the two plugins this * playground pairs (a11y + messages). One `initHub()` call assembles the whole - * hub: it mounts each devframe as a dock (attaching the a11y agent as its - * client script), shares the WebSocket with Vite's own server, serves the - * discovery endpoints, and registers the playground in the global instance - * registry. + * hub: it mounts each devframe as a dock, shares the WebSocket with Vite's own + * server, serves the discovery endpoints, and registers the playground in the + * global instance registry. */ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions = {}): Plugin { const base = normalizeBase(options.base ?? '/__hub/') @@ -70,10 +62,7 @@ export function a11yMessagesPlayground(options: A11yMessagesPlaygroundOptions = return join(cwd, 'node_modules/.a11y-messages-playground') return join(homedir(), '.a11y-messages-playground') }, - devframes: (options.devframes ?? []).map((def) => { - const clientScript = options.clientScripts?.[def.id] - return clientScript ? { devframe: def, dock: { clientScript } } : def - }), + devframes: options.devframes ?? [], // List the playground alongside standalone devframes in discovery // tooling (`devframe connect`, the inspector's Instances tab). register: { diff --git a/examples/a11y-messages-playground/vite.config.ts b/examples/a11y-messages-playground/vite.config.ts index 4733c5b7..50b8c219 100644 --- a/examples/a11y-messages-playground/vite.config.ts +++ b/examples/a11y-messages-playground/vite.config.ts @@ -1,4 +1,4 @@ -import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y' +import createA11yDevframe from '@devframes/plugin-a11y' import createMessagesDevframe from '@devframes/plugin-messages' import UnoCSS from 'unocss/vite' import { defineConfig } from 'vite' @@ -16,12 +16,6 @@ export default defineConfig({ UnoCSS(), a11yMessagesPlayground({ devframes: [a11yDevframe, messagesDevframe], - // Attach the a11y page script as the a11y dock's client script - served - // over Vite's `/@fs/` so it shares this page's origin (the in-page - // channel the page script and panel talk over is same-origin). - clientScripts: { - [a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` }, - }, }), ], }) diff --git a/examples/hub-next/README.md b/examples/hub-next/README.md index eb9123e7..af4b6749 100644 --- a/examples/hub-next/README.md +++ b/examples/hub-next/README.md @@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle - the choice rides a `?transport=` URL param and reconnects the whole client runtime on the pinned transport. -The A11y Inspector shows a live axe-core report of this hub's own page: the hub serves the devframe's page-script module (`a11yPageScriptBundlePath`) same-origin inside the hub namespace and attaches it as the a11y dock's `clientScript` (the `{ devframe, dock }` entry form); the hub client runtime - `createDevframeClientRuntime()` booted in `app/page.tsx` - imports it into the page, so the docked panel and the page script share the origin and tab their in-page channel handshakes across. +The A11y Inspector shows a live axe-core report of this hub's own page: the devframe declares its own page-script module as the a11y dock's `clientScript`, so the hub serves it same-origin with no host wiring; the hub client runtime - `createDevframeClientRuntime()` booted in `app/page.tsx` - imports it into the page, so the docked panel and the page script share the origin and tab their in-page channel handshakes across. The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The hub registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (e.g. `pnpm --filter hub-vite dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA. @@ -57,7 +57,7 @@ The built-in devframes run node-side (child processes, the native `zigpty` PTY b | File | Role | |---|---| -| `src/client/devframe/next-devframe-hub.ts` | The Next host - one `initHub()` call: devframes (incl. the a11y page script as its dock's `clientScript`), hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration | +| `src/client/devframe/next-devframe-hub.ts` | The Next host - one `initHub()` call: devframes, hub RPCs, commands, the json-render dock + renderer manifest, instance-registry registration | | `src/client/devframe/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness | | `../demo-dock-client/` | The shared demo client script, consumed here as a statically-mounted self-contained bundle | | `src/client/app/%5F_devframes/[[...path]]/route.ts` | The one catch-all - delegates every `/__devframes/*` request to the instance's `handler` | diff --git a/examples/hub-next/src/client/devframe/next-devframe-hub.ts b/examples/hub-next/src/client/devframe/next-devframe-hub.ts index d7eb9283..0b8761b5 100644 --- a/examples/hub-next/src/client/devframe/next-devframe-hub.ts +++ b/examples/hub-next/src/client/devframe/next-devframe-hub.ts @@ -88,44 +88,6 @@ async function loadJsonRenderUiRenderer(): Promise { return (mod.jsonRenderUiRenderer as typeof JsonRenderUiRenderer)() } -/** - * URL base the a11y agent module is served under - inside the hub namespace, - * so the one catch-all route reaches it. - */ -const A11Y_AGENT_MOUNT_BASE = `${DEVFRAMES_HUB_BASE}df-a11y-agent/` - -interface A11yAgentMount { - /** The a11y devframe's dock id - the dock the client script attaches to. */ - dockId: string - /** On-disk directory holding the built agent module. */ - dir: string - /** Same-origin URL of the agent module, importable by the hub client runtime. */ - importFrom: string -} - -/** - * Locate the a11y inspector's in-page **agent** module so the hub can serve it - * same-origin and attach it to the a11y dock as its client script - the hub - * client runtime (booted in `app/page.tsx`) imports it into the host page, - * where it scans this hub live. Loaded through the same bundler-ignored dynamic - * `import()` as the plugins, since the package resolves its `dist` via - * `import.meta.url`. Returns `null` if unavailable. - */ -async function loadA11yAgentMount(): Promise { - try { - const mod = await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ '@devframes/plugin-a11y') - const bundle = mod.a11yPageScriptBundlePath as string - return { - dockId: (mod.default as () => DevframeDefinition)().id, - dir: dirname(bundle), - importFrom: `${A11Y_AGENT_MOUNT_BASE}inject.js`, - } - } - catch { - return null - } -} - /** * URL base the demo dock-client bundle is served under - inside the hub * namespace, so the one catch-all route reaches it. @@ -215,13 +177,6 @@ export async function nextDevframeHub( const nextPort = Number(process.env.PORT ?? 3000) const origin = `http://${hostName}:${nextPort}` - // Serve the a11y inspector's in-page agent same-origin (inside the hub - // namespace, via the catch-all route) and attach it to the a11y dock as its - // client script. The hub client runtime booted in `app/page.tsx` imports it - // into the host page, where it scans this hub live; the panel iframe shares - // the origin, so their BroadcastChannel connects. - const a11yAgent = await loadA11yAgentMount() - // The shared demo dock-client script, served as a prebuilt self-contained // bundle (see loadDemoDockClientMount above for why this host uses the // URL shape rather than a bare specifier). @@ -238,11 +193,7 @@ export async function nextDevframeHub( // shim reports - all sharing one iframe. const devframes: (DevframeDefinition | HubDevframeEntry)[] = [ demoDevframe, - ...(await loadBuiltinPlugins()).map(def => - a11yAgent && def.id === a11yAgent.dockId - ? { devframe: def, dock: { clientScript: { importFrom: a11yAgent.importFrom } } } - : def, - ), + ...await loadBuiltinPlugins(), await loadDataInspectorDevframe(), await loadAssetsDevframe(), { @@ -327,9 +278,6 @@ export async function nextDevframeHub( category: '~builtin', }) - if (a11yAgent) - await ctx.host.mountStatic(A11Y_AGENT_MOUNT_BASE, a11yAgent.dir) - // The demo dock-client script - the same package the Vite reference // host loads via a bare specifier - mounted statically and attached as // a momentary `action` dock by its served URL. diff --git a/examples/hub-vite/README.md b/examples/hub-vite/README.md index 7bc106b5..3523bc78 100644 --- a/examples/hub-vite/README.md +++ b/examples/hub-vite/README.md @@ -20,7 +20,7 @@ Open the printed URL. The dock rail on the left lists every mounted tool with it Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`, and a **Transport** section showing which RPC transport the connection runs on (`websocket` or `sse`) with a segmented Auto / WS / SSE toggle - the choice rides a `?transport=` URL param and reconnects the whole client runtime on the pinned transport. -The A11y Inspector shows a live axe-core report of this hub's own page. `vite.config.ts` attaches the devframe's page script as the a11y dock's `clientScript` (served via `/@fs/`), and the hub client runtime - `createDevframeClientRuntime()` booted in `src/client/main.ts` - imports it into the host page. Panel and page script share the Vite origin and tab their in-page channel handshakes across; hover a violation to ring the offending element in the hub UI. +The A11y Inspector shows a live axe-core report of this hub's own page. The devframe declares its own page script as the a11y dock's `clientScript`, so the hub serves it same-origin and the hub client runtime - `createDevframeClientRuntime()` booted in `src/client/main.ts` - imports it into the host page automatically (no wiring in `vite.config.ts`). Panel and page script share the Vite origin and tab their in-page channel handshakes across; hover a violation to ring the offending element in the hub UI. The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The hub registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (`pnpm --filter a11y-messages-playground dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA. @@ -45,7 +45,7 @@ The dock UI is plain DOM in `src/client/`. To skin your own hub UI provider, rea | File | Role | |---|---| | `src/vite-devframe-hub.ts` | The Vite host - one `initHub()` call mounted as connect middleware, plus instance-registry registration | -| `vite.config.ts` | Passes the built-in and demo devframes to the hub's `devframes` option; attaches the a11y page script as its dock's `clientScript`; composes the json-render frontend via `renderers` | +| `vite.config.ts` | Passes the built-in and demo devframes to the hub's `devframes` option; composes the json-render frontend via `renderers` | | `src/unrendered-dock.ts` | A dock type registered with no renderer on purpose - the missing-renderer fallback witness | | `../demo-dock-client/` | The shared demo client script, consumed here via bare specifier (`action: { importFrom: 'demo-dock-client' }`) | | `src/client/main.ts` | The browser UI that consumes the hub protocol, including the interactive-OTP authorization view | diff --git a/examples/hub-vite/vite.config.ts b/examples/hub-vite/vite.config.ts index abbd1b9e..301b9a85 100644 --- a/examples/hub-vite/vite.config.ts +++ b/examples/hub-vite/vite.config.ts @@ -2,7 +2,7 @@ import type { DevframeHubContext } from '@devframes/hub/node' import { defineHubRpcFunction } from '@devframes/hub' import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub' import { toJsonRenderDockEntry } from '@devframes/json-render/hub' -import createA11yDevframe, { a11yPageScriptBundlePath } from '@devframes/plugin-a11y' +import createA11yDevframe from '@devframes/plugin-a11y' import createAssetsDevframe from '@devframes/plugin-assets' import createCodeServerDevframe from '@devframes/plugin-code-server' import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector' @@ -142,13 +142,6 @@ export default defineConfig({ }, }, ], - // Attach the a11y inspector's in-page agent as its dock's client script. - // The hub client runtime (booted in src/client/main.ts) imports it into - // this page so the docked panel scans the host live - no bespoke - // injection plugin needed. `/@fs/` lets Vite serve the built module. - clientScripts: { - [a11yDevframe.id]: { importFrom: `/@fs/${a11yPageScriptBundlePath}` }, - }, // Serve the reference json-render frontend as a prebuilt renderer // module: the hub publishes it in the renderer manifest and the client // (src/client/main.ts) imports it lazily the first time a diff --git a/knip.jsonc b/knip.jsonc index b51714cf..d8f926c6 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -66,6 +66,7 @@ // deliberately invisible to bundlers (and knip) so Next never inlines // their node-only code. They're genuinely used at runtime. "ignoreDependencies": [ + "@devframes/plugin-a11y", "@devframes/plugin-code-server", "@devframes/plugin-git", "@devframes/plugin-inspect", diff --git a/packages/devframe/src/types/devframe.ts b/packages/devframe/src/types/devframe.ts index 44c5ff7f..95fed918 100644 --- a/packages/devframe/src/types/devframe.ts +++ b/packages/devframe/src/types/devframe.ts @@ -263,6 +263,21 @@ export interface DevframeDockDefaults { badge?: string /** Id of the dock group this entry collapses under, if any. */ groupId?: string + /** + * A client script the hub imports into the host page (this devframe's **page + * script**). An absolute-path `importFrom` is served by the hub under the + * mount base and rewritten to that URL, so mounting by package name needs no + * host wiring; a URL or bare specifier passes through untouched. + */ + clientScript?: { + /** An absolute filesystem path, a served URL, or a bare npm specifier. */ + importFrom: string + /** + * The name to import the module as. + * @default 'default' + */ + importName?: string + } } /** diff --git a/packages/hub/src/node/__tests__/install-devframe.test.ts b/packages/hub/src/node/__tests__/install-devframe.test.ts index a6cf7122..da4762e2 100644 --- a/packages/hub/src/node/__tests__/install-devframe.test.ts +++ b/packages/hub/src/node/__tests__/install-devframe.test.ts @@ -1,9 +1,9 @@ import type { DevframeDefinition, DevframeDuplicationStrategy } from 'devframe/types' import type { DevframeHubContext } from '../context' -import { mkdtempSync } from 'node:fs' +import { mkdtempSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { join } from 'node:path' import { defineDevframe } from 'devframe' +import { dirname, join } from 'pathe' import { afterEach, describe, expect, it, vi } from 'vitest' import { DevframeDocksHost } from '../host-docks' import { installDevframe } from '../install-devframe' @@ -12,7 +12,7 @@ function createContext(): DevframeHubContext { const storageDir = mkdtempSync(join(tmpdir(), 'devframe-hub-install-')) const context = { host: { - mountStatic: () => {}, + mountStatic: vi.fn(), resolveOrigin: () => 'http://localhost:5173', getStorageDir: () => storageDir, }, @@ -163,6 +163,58 @@ describe('ctx.install', () => { expect(warn.mock.calls[0].join(' ')).toContain('DF8106') }) + it('serves an absolute-path page script under the mount base and rewrites it to the served URL', async () => { + const ctx = createContext() + const scriptPath = join(mkdtempSync(join(tmpdir(), 'devframe-page-script-')), 'inject.js') + writeFileSync(scriptPath, 'export default () => {}') + + await ctx.install(makeDevframe({ + dock: { clientScript: { importFrom: scriptPath } }, + })) + + expect(ctx.host.mountStatic).toHaveBeenCalledWith('/__demo/__page-script/', dirname(scriptPath)) + expect(ctx.docks.views.get('demo')).toMatchObject({ + clientScript: { importFrom: '/__demo/__page-script/inject.js' }, + }) + }) + + it('leaves a URL or bare-specifier page script untouched (no mount)', async () => { + const ctx = createContext() + + await ctx.install(makeDevframe({ + dock: { clientScript: { importFrom: '/@fs/abs/inject.js' } }, + })) + await ctx.install(makeDevframe({ + id: 'bare', + dock: { clientScript: { importFrom: 'some-pkg/client' } }, + })) + + expect(ctx.host.mountStatic).not.toHaveBeenCalled() + expect(ctx.docks.views.get('demo')).toMatchObject({ + clientScript: { importFrom: '/@fs/abs/inject.js' }, + }) + expect(ctx.docks.views.get('bare')).toMatchObject({ + clientScript: { importFrom: 'some-pkg/client' }, + }) + }) + + it('lets a per-mount clientScript override the definition default before serving', async () => { + const ctx = createContext() + const dir = mkdtempSync(join(tmpdir(), 'devframe-page-script-')) + const scriptPath = join(dir, 'inject.js') + writeFileSync(scriptPath, 'export default () => {}') + + await ctx.install( + makeDevframe({ dock: { clientScript: { importFrom: scriptPath } } }), + { dock: { clientScript: { importFrom: '/@fs/override/inject.js' } } }, + ) + + expect(ctx.host.mountStatic).not.toHaveBeenCalled() + expect(ctx.docks.views.get('demo')).toMatchObject({ + clientScript: { importFrom: '/@fs/override/inject.js' }, + }) + }) + it('lets instances coexist under disambiguated ids when "duplicate"', async () => { const ctx = createContext() const setup = vi.fn() diff --git a/packages/hub/src/node/install-devframe.ts b/packages/hub/src/node/install-devframe.ts index 7b4fadf6..72fdd0d2 100644 --- a/packages/hub/src/node/install-devframe.ts +++ b/packages/hub/src/node/install-devframe.ts @@ -1,9 +1,11 @@ import type { DevframeDefinition } from 'devframe/types' -import type { DevframeViewIframe } from '../types/docks' +import type { ClientScriptEntry, DevframeViewIframe } from '../types/docks' import type { DevframeHubContext } from './context' +import { existsSync } from 'node:fs' import { resolveClientAssets } from 'devframe' import { resolveBasePath } from 'devframe/node/hub-internals' -import { resolve } from 'pathe' +import { basename, dirname, isAbsolute, resolve } from 'pathe' +import { joinURL, withTrailingSlash } from 'ufo' import { diagnostics } from './diagnostics' export interface InstallDevframeOptions { @@ -37,6 +39,26 @@ function nextAvailableDockId(views: DevframeHubContext['docks']['views'], baseId return `${baseId}-${n}` } +/** + * When a dock's `clientScript.importFrom` is an absolute filesystem path, serve + * its directory under `__page-script/` and rewrite `importFrom` to that + * URL. A URL or bare specifier (not existing on disk) passes through untouched. + */ +async function resolvePageScriptClientScript( + ctx: DevframeHubContext, + clientScript: ClientScriptEntry | undefined, + base: string, +): Promise { + if (!clientScript?.importFrom) + return clientScript + const { importFrom } = clientScript + if (!isAbsolute(importFrom) || !existsSync(importFrom)) + return clientScript + const scriptBase = withTrailingSlash(joinURL(base, '__page-script')) + await ctx.host.mountStatic(scriptBase, dirname(importFrom)) + return { ...clientScript, importFrom: joinURL(scriptBase, basename(importFrom)) } +} + /** * Framework-neutral primitive backing {@link DevframeHubContext.install} — * installs a {@link DevframeDefinition} as a dock inside a hub-aware context: @@ -87,6 +109,13 @@ export async function prepareDevframe( ? resolveBasePath(d, 'hosted') : resolveBasePath({ ...d, id, basePath: undefined }, 'hosted')) + // Definition `dock` beneath per-mount `options.dock`. Resolved before the SPA + // mount so an absolute-path page script is served ahead of the SPA catch-all. + const dockDefaults = { ...d.dock, ...options.dock } + const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base) + if (clientScript) + dockDefaults.clientScript = clientScript + const clientAssets = resolveClientAssets(d) if (clientAssets) { // Serve the hub's connection meta under the devframe's base so its SPA @@ -114,11 +143,9 @@ export async function prepareDevframe( id, title: d.name, icon: d.icon, - // Definition-level `dock` defaults sit above the name/icon-derived - // defaults; per-mount `options.dock` overrides them; `type`/`url` - // (and `id`) stay locked, derived from the definition. - ...d.dock, - ...options.dock, + // `dockDefaults` sits above the name/icon defaults; `type`/`url`/`id` stay + // locked, derived from the definition. + ...dockDefaults, type: 'iframe', url: base, } as DevframeViewIframe) diff --git a/plugins/a11y/README.md b/plugins/a11y/README.md index fa8ef654..48697c3f 100644 --- a/plugins/a11y/README.md +++ b/plugins/a11y/README.md @@ -56,11 +56,13 @@ page script itself free of any RPC dependency. devframe deliberately provides no access to the user app's DOM, so the page script is the author-provided bridge into the user app's page. In a hub, the -page script is the a11y dock's **client script**: attach `a11yPageScriptBundlePath` as the -dock's `clientScript` (resolved to an importable URL — `/@fs/…` under Vite, or a -statically-served path) and the hub's client runtime (`createDevframeClientRuntime` -from `@devframes/hub/client`) imports it into the host page and calls its -default export with the client-script context. Booted that way, the page script also +page script is the a11y dock's **client script**, and the definition declares it by +path, so the hub serves it same-origin and `devframes: ['@devframes/plugin-a11y']` +works with no host wiring. The hub's client runtime (`createDevframeClientRuntime` +from `@devframes/hub/client`) then imports it into the host page and calls its default +export with the client-script context. A host can also serve the module itself (e.g. +via `/@fs/…` under Vite) by attaching `a11yPageScriptBundlePath` as a per-mount +`clientScript`. Booted that way, the page script also mirrors the active route's scan into the hub's **messages feed** — a summary entry driven through the loading → idle lifecycle plus one entry per violated rule, carrying the impact-mapped level, WCAG tags as labels, and the first offending @@ -117,7 +119,7 @@ pnpm -C plugins/a11y dev # from source: same, at /__devframes_plugin_a11 | Path | Export | Purpose | |------|--------|---------| -| `src/index.ts` | `.` | `createA11yDevframe()` (also the default export); `a11yPageScriptBundlePath` — the page-script module a hub attaches as this dock's client script | +| `src/index.ts` | `.` | `createA11yDevframe()` (also the default export), declaring the page script as its dock's client script; `a11yPageScriptBundlePath` — that module, for hosts that serve it themselves | | `src/node/index.ts` | `/node` | `setupA11y(ctx, options?)` — registers the RPC functions with the runtime config | | `src/cli.ts` | `/cli` | `createA11yCli()` — backs the `devframes_plugin_a11y` bin | | `src/client/index.ts` | `/client` | `connectA11y()` — typed browser RPC client wrapper | diff --git a/plugins/a11y/src/index.ts b/plugins/a11y/src/index.ts index db0bcfc4..9cf836fa 100644 --- a/plugins/a11y/src/index.ts +++ b/plugins/a11y/src/index.ts @@ -23,11 +23,10 @@ const distDir: RemoteAssets = { * — the dock **client script** the client runtime imports into the host page to * scan it (its default export boots the page script; importing it does too). * - * A hub attaches this as the a11y dock's `clientScript`, resolved to a URL the - * page can import: `/@fs/${a11yPageScriptBundlePath}` for a Vite host, or a - * statically-served path for others (see the minimal hub examples). Resolves - * under `/dist/inject/inject.js` from both the source and the published - * entry. Requires the built bundle (`pnpm -C plugins/a11y build`). + * The definition already declares this as its dock `clientScript`, so a hub + * serves it with no host wiring. Exported for hosts that mount the module + * themselves (e.g. via `/@fs/` under Vite). Requires the built bundle + * (`pnpm -C plugins/a11y build`). */ export const a11yPageScriptBundlePath: string = fileURLToPath(new URL('../dist/inject/inject.js', import.meta.url)) @@ -91,6 +90,11 @@ export function createA11yDevframe(options: A11yDevframeOptions = {}): DevframeD description: pkg.description, icon: options.icon ?? 'ph:person-simple-circle-duotone', basePath: options.basePath ?? BASE_PATH, + // Declare the page script by path; the hub serves it with no host wiring. + dock: { + category: '~builtin', + clientScript: { importFrom: a11yPageScriptBundlePath }, + }, cli: { command: id, port: options.port ?? 9899, diff --git a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts index 7cab9b7e..97830b1b 100644 --- a/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts @@ -169,6 +169,10 @@ export interface DevframeDockDefaults { visibility?: string; badge?: string; groupId?: string; + clientScript?: { + importFrom: string; + importName?: string; + }; } export interface DevframeHost { mountStatic: (_: string, _: string | RemoteAssetsStore) => void | Promise;