@devframes/hub 0.8.2 → 0.9.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.mts +221 -27
- package/dist/client/index.mjs +213 -41
- package/dist/constants.d.mts +19 -2
- package/dist/constants.mjs +21 -3
- package/dist/{context-BSgzLnsu.d.mts → context-D5TJIHwp.d.mts} +50 -19
- package/dist/context-DzqMQ2mI.mjs +1182 -0
- package/dist/define-Ceekw2EO.mjs +11 -0
- package/dist/dist-R0T2g9Vw.mjs +221 -0
- package/dist/index-coDmWngQ.d.mts +5 -0
- package/dist/index.d.mts +4 -13
- package/dist/index.mjs +2 -3
- package/dist/node/index.d.mts +3 -33
- package/dist/node/index.mjs +5 -1176
- package/dist/node/initiate.d.mts +274 -0
- package/dist/node/initiate.mjs +267 -0
- package/dist/{settings-Byh48aCd.d.mts → settings-DfrRa9Wf.d.mts} +32 -55
- package/dist/types/index.d.mts +4 -4
- package/package.json +9 -6
- package/dist/define-Pbx8dsKz.mjs +0 -21
- package/dist/index-IzywMKfP.d.mts +0 -5
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-D5TJIHwp.mjs";
|
|
2
|
+
import { DEVFRAMES_HUB_BASE } from "../constants.mjs";
|
|
3
|
+
import { DevframeInstanceRecord } from "devframe/internal";
|
|
4
|
+
import { ConnectionMeta, DevframeDefinition, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpRouteOptions } from "devframe/types";
|
|
5
|
+
import { DevframeAuthHandler } from "devframe/node/auth";
|
|
6
|
+
import { WsOriginRegistry } from "devframe/rpc/transports/ws-server";
|
|
7
|
+
import { Buffer } from "node:buffer";
|
|
8
|
+
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
9
|
+
import { Duplex } from "node:stream";
|
|
10
|
+
//#region src/node/initiate.d.ts
|
|
11
|
+
/** A `devframes` entry with per-mount dock customization. */
|
|
12
|
+
interface HubDevframeEntry {
|
|
13
|
+
devframe: DevframeDefinition;
|
|
14
|
+
/** Per-mount overrides for the auto-synthesized iframe dock entry. */
|
|
15
|
+
dock?: InstallDevframeOptions['dock'];
|
|
16
|
+
}
|
|
17
|
+
type Thenable<T> = T | Promise<T>;
|
|
18
|
+
type Arrayable<T> = T | readonly T[];
|
|
19
|
+
/**
|
|
20
|
+
* One dock-type → prebuilt renderer-module registration for
|
|
21
|
+
* {@link InitHubOptions.renderers}. The hub serves the module at
|
|
22
|
+
* `<base>__renderers/<type>.mjs` and publishes it in the renderer manifest
|
|
23
|
+
* (the `devframe:dock-renderers` shared-state slot), so any viewer — the
|
|
24
|
+
* reference UI, a community viewer, a hand-rolled host page — lazily imports
|
|
25
|
+
* it the first time a dock of that `type` needs rendering.
|
|
26
|
+
*
|
|
27
|
+
* The module must be a **self-contained browser ES module** (its framework
|
|
28
|
+
* and styles bundled in) whose {@link DockRendererRegistration.importName}
|
|
29
|
+
* export is a ready `DockRenderer`. Renderer packages ship a node helper
|
|
30
|
+
* returning this shape — e.g. `jsonRenderUiRenderer()` from
|
|
31
|
+
* `@devframes/json-render-ui/hub`.
|
|
32
|
+
*/
|
|
33
|
+
interface DockRendererRegistration {
|
|
34
|
+
/** Dock `type` this renderer handles (e.g. `'json-render'`). */
|
|
35
|
+
type: string;
|
|
36
|
+
/** Absolute path of the prebuilt, self-contained browser ES module. */
|
|
37
|
+
file: string;
|
|
38
|
+
/**
|
|
39
|
+
* Named export carrying the renderer.
|
|
40
|
+
*
|
|
41
|
+
* @default 'default'
|
|
42
|
+
*/
|
|
43
|
+
importName?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The UI slot of a hub instance — pure data, zero policy. The hub itself is
|
|
47
|
+
* headless: whoever fills this slot decides what a viewer looks like.
|
|
48
|
+
* `@devframes/hub-ui` ships the reference implementation (`createUi()`);
|
|
49
|
+
* Vite DevTools or any community viewer supplies its own object to the same
|
|
50
|
+
* slot and reuses all the infrastructure.
|
|
51
|
+
*/
|
|
52
|
+
interface DevframeHubUi {
|
|
53
|
+
/**
|
|
54
|
+
* A standalone viewer SPA (built with relative asset paths) served at the
|
|
55
|
+
* hub base itself — open `<base>` in a tab and the devtools are there.
|
|
56
|
+
*/
|
|
57
|
+
viewer?: {
|
|
58
|
+
/** Directory of the prebuilt viewer SPA. */
|
|
59
|
+
distDir: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* A prebuilt, self-contained script served at `<base>embedded.js` — the
|
|
63
|
+
* floating-devtools bootstrap a host page loads with one
|
|
64
|
+
* `<script type="module" src="<base>embedded.js">` tag. Visibility policy
|
|
65
|
+
* (always-on, keyboard-summoned, …) belongs entirely to this entry.
|
|
66
|
+
*/
|
|
67
|
+
embedded?: {
|
|
68
|
+
/** File path of the prebuilt single-file module. */
|
|
69
|
+
entry: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Extra UI-owned files the hub serves at `<base><key>`, each produced lazily
|
|
73
|
+
* from memory. Keys are base-relative paths; the content-type is inferred
|
|
74
|
+
* from the key's extension. A generic seam a viewer uses to publish
|
|
75
|
+
* arbitrary runtime documents without teaching the hub anything about
|
|
76
|
+
* their meaning.
|
|
77
|
+
*/
|
|
78
|
+
assets?: Record<string, () => string | Uint8Array>;
|
|
79
|
+
/**
|
|
80
|
+
* A setup hook run once during hub init with the hub context — the UI
|
|
81
|
+
* slot's chance to publish its own static, boot-time config through the
|
|
82
|
+
* generic `ctx.staticConfig` (serialized into `ConnectionMeta.configs`).
|
|
83
|
+
* The reference UI's `createUi()` uses it to set
|
|
84
|
+
* `ctx.staticConfig.ui = { branding, … }`, reaching every mounted frame and
|
|
85
|
+
* the standalone viewer through the one connection handshake they perform.
|
|
86
|
+
* The hub stays policy-free about what the UI writes.
|
|
87
|
+
*/
|
|
88
|
+
setup?: (ctx: DevframeHubContext) => void | Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
type DevframesInput = Array<DevframeDefinition | HubDevframeEntry | Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>> | (() => Thenable<Arrayable<DevframeDefinition | HubDevframeEntry | null | undefined>>)>;
|
|
91
|
+
interface InitHubOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Name for the hub instance, used in logs and diagnostics, and mcp server.
|
|
94
|
+
*/
|
|
95
|
+
name?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Version for the hub instance, used in logs and diagnostics, and mcp server.
|
|
98
|
+
*/
|
|
99
|
+
version?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Mount base the hub answers under — required so the mount path is
|
|
102
|
+
* explicit at the call site (pass the exported {@link DEVFRAMES_HUB_BASE}
|
|
103
|
+
* for the conventional `/__devframes/`). Every mounted devframe lives at
|
|
104
|
+
* `<base><id>/`, so the host app needs exactly one catch-all route. The
|
|
105
|
+
* resolved value is echoed back as {@link HubInstance.base} so route and
|
|
106
|
+
* middleware code references it instead of repeating the string.
|
|
107
|
+
*/
|
|
108
|
+
base: string;
|
|
109
|
+
/**
|
|
110
|
+
* Devframes to mount: each runs its `setup()` against the shared hub
|
|
111
|
+
* context (one merged RPC registry, one WebSocket, one auth gate), serves
|
|
112
|
+
* its SPA at `<base><id>/`, and is auto-registered as an iframe dock.
|
|
113
|
+
* Wrap an entry in `{ devframe, dock }` to customize its synthesized dock
|
|
114
|
+
* (category, icon, a `clientScript` to run in the host page, …).
|
|
115
|
+
*/
|
|
116
|
+
devframes?: DevframesInput;
|
|
117
|
+
/**
|
|
118
|
+
* Extra RPC declarations registered at context creation, alongside the
|
|
119
|
+
* hub built-ins — forwarded to `createHubContext`'s
|
|
120
|
+
* `builtinRpcDeclarations`. Declarative mode only (a pre-built `context`
|
|
121
|
+
* already made this choice).
|
|
122
|
+
*/
|
|
123
|
+
rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'];
|
|
124
|
+
/**
|
|
125
|
+
* Bring your own hub context instead of `devframes` — for hosts that
|
|
126
|
+
* assemble `createHubContext` + `ctx.install` themselves (with their own
|
|
127
|
+
* `DevframeHost` serving the frames). The instance then serves only the
|
|
128
|
+
* hub-level endpoints (`__connection.json`, `__index.json`,
|
|
129
|
+
* `__client-imports.js`, the WS transport, MCP, and the `ui` slot);
|
|
130
|
+
* serve each frame's meta yourself from {@link HubInstance.connectionMeta}.
|
|
131
|
+
*/
|
|
132
|
+
context?: DevframeHubContext;
|
|
133
|
+
/**
|
|
134
|
+
* Runs once the context exists and every `devframes` entry is mounted —
|
|
135
|
+
* register docks, commands, terminals, and messages surfaces here.
|
|
136
|
+
*/
|
|
137
|
+
configure?: (ctx: DevframeHubContext) => void | Promise<void>;
|
|
138
|
+
/** See {@link DevframeHubUi} — omitted, the hub stays fully headless. */
|
|
139
|
+
ui?: DevframeHubUi;
|
|
140
|
+
/**
|
|
141
|
+
* Prebuilt dock-renderer modules to serve and advertise — the composition
|
|
142
|
+
* seam that hands a renderer package (e.g. `@devframes/json-render-ui`) to
|
|
143
|
+
* a prebuilt viewer. Each {@link DockRendererRegistration} is served at
|
|
144
|
+
* `<base>__renderers/<type>.mjs` and published in the renderer manifest;
|
|
145
|
+
* clients import a module lazily the first time a dock of its `type`
|
|
146
|
+
* mounts. Renderers registered directly in client code
|
|
147
|
+
* (`createDevframeClientHost({ renderers })`) take precedence.
|
|
148
|
+
*
|
|
149
|
+
* ```ts
|
|
150
|
+
* import { jsonRenderUiRenderer } from '@devframes/json-render-ui/hub'
|
|
151
|
+
*
|
|
152
|
+
* initHub({ ui: createUi(), renderers: [jsonRenderUiRenderer()] })
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
renderers?: readonly DockRendererRegistration[];
|
|
156
|
+
/**
|
|
157
|
+
* Share the host's `node:http` server for the WebSocket RPC endpoint
|
|
158
|
+
* (upgrade bound at `<base>__ws`). Hosts whose handlers never see upgrades
|
|
159
|
+
* (Next.js route handlers, Nitro, Rsbuild) ask for a side-car instead with
|
|
160
|
+
* `ws: { sidecar: true }`; hosts that get their server later wire
|
|
161
|
+
* {@link HubInstance.attach} / {@link HubInstance.handleUpgrade}.
|
|
162
|
+
*/
|
|
163
|
+
server?: Server;
|
|
164
|
+
/**
|
|
165
|
+
* Explicit WebSocket control, same contract as `initDevframe`: the local
|
|
166
|
+
* binding resolves `ws.port` (pinned side-car) > `server` (shared upgrade)
|
|
167
|
+
* > `ws.sidecar` (auto-port side-car) > the host driving upgrades itself,
|
|
168
|
+
* while `url` overrides the advertisement (tunnel pattern) and `route`
|
|
169
|
+
* renames the upgrade segment (default `__ws`). Pass `false` to serve no
|
|
170
|
+
* WebSocket at all — clients connect over SSE instead (`backend: 'sse'`).
|
|
171
|
+
*/
|
|
172
|
+
ws?: DevframeWsOptions | false;
|
|
173
|
+
/**
|
|
174
|
+
* SSE RPC endpoint control, same contract as `initDevframe` — enabled by
|
|
175
|
+
* default at `<base>__sse` as the more portable transport alongside the
|
|
176
|
+
* WebSocket. Pass `false` to disable, or a {@link DevframeSseOptions} to
|
|
177
|
+
* rename the route.
|
|
178
|
+
*/
|
|
179
|
+
sse?: boolean | DevframeSseOptions;
|
|
180
|
+
/** Bind host for a side-car WebSocket server. Default: `localhost`. */
|
|
181
|
+
host?: string;
|
|
182
|
+
/**
|
|
183
|
+
* The hub's **single Auth**: one gate at the one shared transport covers
|
|
184
|
+
* every mounted frame, the MCP route, and the hub built-ins. Gates by
|
|
185
|
+
* default (devframe's interactive OTP); `false` opts out; a
|
|
186
|
+
* {@link DevframeAuthHandler} installs a custom scheme.
|
|
187
|
+
*/
|
|
188
|
+
auth?: boolean | DevframeAuthHandler;
|
|
189
|
+
/**
|
|
190
|
+
* Expose the **aggregate** MCP endpoint at `<base>__mcp` — one
|
|
191
|
+
* Streamable-HTTP server over the shared context's whole tool registry
|
|
192
|
+
* (ids are already namespaced per plugin). Disabled by default.
|
|
193
|
+
*/
|
|
194
|
+
mcp?: boolean | McpRouteOptions;
|
|
195
|
+
/**
|
|
196
|
+
* Public origin the host app is reachable at, or a getter. Derived lazily
|
|
197
|
+
* from the first request when omitted.
|
|
198
|
+
*/
|
|
199
|
+
origin?: string | (() => string);
|
|
200
|
+
/**
|
|
201
|
+
* Publish this hub in the global instance registry
|
|
202
|
+
* (`~/.devframe/instances/`) so discovery tooling (`devframe connect`, the
|
|
203
|
+
* inspect plugin's Instances tab) lists it like any standalone devframe.
|
|
204
|
+
* Registration is a dynamic import that fires once the public origin
|
|
205
|
+
* resolves and is torn down on {@link HubInstance.close}. Defaults to off;
|
|
206
|
+
* pass `true` to enable, or an object to override individual record fields
|
|
207
|
+
* (`id`, `name`, `basePath`, …).
|
|
208
|
+
*/
|
|
209
|
+
register?: boolean | Partial<DevframeInstanceRecord>;
|
|
210
|
+
/** Working directory for the hub context. Default: `process.cwd()`. */
|
|
211
|
+
cwd?: string;
|
|
212
|
+
/** Override where persisted devframe state lives. */
|
|
213
|
+
getStorageDir?: (scope: DevframeStorageScope) => string;
|
|
214
|
+
/** Extra WS-upgrade origins beyond the loopback default; `false` disables the gate. */
|
|
215
|
+
allowedOrigins?: readonly string[] | WsOriginRegistry | false;
|
|
216
|
+
/** Destroy off-route upgrades on a shared `server` devframe's adapter owns outright. */
|
|
217
|
+
destroyUnmatchedUpgrades?: boolean;
|
|
218
|
+
}
|
|
219
|
+
interface HubInstance {
|
|
220
|
+
/**
|
|
221
|
+
* The normalized mount base this hub answers under (leading and trailing
|
|
222
|
+
* slash, e.g. `/__devframes/`). Reference it when wiring the mount — route
|
|
223
|
+
* guards, middleware path checks — instead of repeating the string literal.
|
|
224
|
+
*/
|
|
225
|
+
base: string;
|
|
226
|
+
/**
|
|
227
|
+
* Web-standard request handler for the whole hub — mount it on one
|
|
228
|
+
* catch-all route under {@link HubInstance.base}.
|
|
229
|
+
*/
|
|
230
|
+
handler: (request: Request) => Promise<Response>;
|
|
231
|
+
/** Connect/Express-style middleware over the same surface; `next()`s outside the base. */
|
|
232
|
+
nodeMiddleware: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void;
|
|
233
|
+
/**
|
|
234
|
+
* Route a host server's `upgrade` events to the shared RPC socket,
|
|
235
|
+
* returning a detach function — the manual counterpart to the `server`
|
|
236
|
+
* option, for hosts that get their `node:http` server only after the hub
|
|
237
|
+
* exists. Available on the default tier; a configured transport (`server`,
|
|
238
|
+
* `ws.port`, `ws.sidecar`, `ws.url`) already owns the socket and reports
|
|
239
|
+
* `DF0055` / `DF0056` instead.
|
|
240
|
+
*/
|
|
241
|
+
attach: (server: Server) => () => void;
|
|
242
|
+
/**
|
|
243
|
+
* Complete a single `upgrade` event on the shared RPC socket, for hosts
|
|
244
|
+
* that already own an `upgrade` listener. Same availability as
|
|
245
|
+
* {@link HubInstance.attach}.
|
|
246
|
+
*/
|
|
247
|
+
handleUpgrade: (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
248
|
+
/** Resolves once every frame is mounted and the WebSocket binding is live. */
|
|
249
|
+
ready: Promise<void>;
|
|
250
|
+
/** The shared hub context, once initialized. */
|
|
251
|
+
context: Promise<DevframeHubContext>;
|
|
252
|
+
/** The `ConnectionMeta` served at `<base>__connection.json` (and every frame base). */
|
|
253
|
+
connectionMeta: () => ConnectionMeta;
|
|
254
|
+
/** Tear down: WS transport/side-car, MCP sessions. */
|
|
255
|
+
close: () => Promise<void>;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Initiate a hub instance — the whole multi-devframe devtools surface
|
|
259
|
+
* behind one framework-agnostic, web-standard handler. Every mounted
|
|
260
|
+
* devframe shares one context (merged RPC registry, shared state, docks /
|
|
261
|
+
* terminals / messages / commands), one WebSocket transport, and one Auth;
|
|
262
|
+
* the instance serves each frame's SPA at `<base><id>/`, the discovery
|
|
263
|
+
* endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
|
|
264
|
+
* the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
|
|
265
|
+
* provides — the hub itself stays headless.
|
|
266
|
+
*
|
|
267
|
+
* The factory is synchronous and initializes eagerly, and binds no port of
|
|
268
|
+
* its own: the WebSocket follows `server` / `ws` (see
|
|
269
|
+
* {@link InitHubOptions.ws}), or waits for the host to hand upgrades over
|
|
270
|
+
* through {@link HubInstance.attach}.
|
|
271
|
+
*/
|
|
272
|
+
declare function initHub(options: InitHubOptions): HubInstance;
|
|
273
|
+
//#endregion
|
|
274
|
+
export { DEVFRAMES_HUB_BASE, DevframeHubUi, DevframesInput, DockRendererRegistration, HubDevframeEntry, HubInstance, InitHubOptions, initHub };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, normalizeHubBase } from "../constants.mjs";
|
|
2
|
+
import { t as createHubContext, v as diagnostics } from "../context-DzqMQ2mI.mjs";
|
|
3
|
+
import { joinURL, withTrailingSlash, withoutLeadingSlash } from "ufo";
|
|
4
|
+
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
|
|
5
|
+
import { createH3DevframeHost, createInstanceShell, resolveInstanceRegister } from "devframe/internal";
|
|
6
|
+
import { resolve } from "pathe";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { mountStaticHandler } from "devframe/utils/serve-static";
|
|
11
|
+
import { H3 } from "h3";
|
|
12
|
+
//#region src/node/initiate.ts
|
|
13
|
+
function normalizeDevframeEntry(entry) {
|
|
14
|
+
return "devframe" in entry ? entry : { devframe: entry };
|
|
15
|
+
}
|
|
16
|
+
/** Content-type for a UI asset key, inferred from its file extension. */
|
|
17
|
+
function assetContentType(key) {
|
|
18
|
+
if (key.endsWith(".json")) return "application/json; charset=utf-8";
|
|
19
|
+
if (key.endsWith(".js") || key.endsWith(".mjs")) return "text/javascript; charset=utf-8";
|
|
20
|
+
if (key.endsWith(".css")) return "text/css; charset=utf-8";
|
|
21
|
+
if (key.endsWith(".svg")) return "image/svg+xml";
|
|
22
|
+
if (key.endsWith(".html")) return "text/html; charset=utf-8";
|
|
23
|
+
return "application/octet-stream";
|
|
24
|
+
}
|
|
25
|
+
/** Reserved filenames directly under the hub base — a frame id can't shadow them. */
|
|
26
|
+
const RESERVED_HUB_PATHS = [
|
|
27
|
+
DEVFRAME_CONNECTION_META_FILENAME,
|
|
28
|
+
DEVFRAME_DOCK_IMPORTS_FILENAME,
|
|
29
|
+
DEVFRAME_WS_ROUTE,
|
|
30
|
+
DEVFRAME_MCP_ROUTE,
|
|
31
|
+
"__index.json",
|
|
32
|
+
"__renderers",
|
|
33
|
+
"embedded.js"
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Flatten the `devframes` input: await every thenable, call every factory,
|
|
37
|
+
* spread the arrays, and drop the empty slots — so a host can build the list
|
|
38
|
+
* conditionally (`isDev && loadInspect()`) without filtering it first.
|
|
39
|
+
*/
|
|
40
|
+
function resolveDevframesInput(input) {
|
|
41
|
+
return Promise.all(input.map(async (entry) => {
|
|
42
|
+
const resolved = await (typeof entry === "function" ? entry() : entry);
|
|
43
|
+
return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
|
|
44
|
+
})).then((arrays) => arrays.flat());
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate the renderer-module registrations fail-fast: route-safe types
|
|
48
|
+
* (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
|
|
49
|
+
* per type, and an existing bundle file (renderer packages are prebuilt).
|
|
50
|
+
*/
|
|
51
|
+
function resolveRendererRegistrations(registrations) {
|
|
52
|
+
const seen = /* @__PURE__ */ new Set();
|
|
53
|
+
return registrations.map((registration) => {
|
|
54
|
+
if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
|
|
55
|
+
if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
|
|
56
|
+
seen.add(registration.type);
|
|
57
|
+
const file = resolve(registration.file);
|
|
58
|
+
if (!existsSync(file)) throw diagnostics.DF8109({
|
|
59
|
+
type: registration.type,
|
|
60
|
+
file
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
...registration,
|
|
64
|
+
file
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render the dock client-script import map as an ES module — one dynamic
|
|
70
|
+
* import thunk per dock that carries a client script (`clientScript` on
|
|
71
|
+
* iframe docks, `action`, `renderer`). External viewers import this module
|
|
72
|
+
* from `<base>__client-imports.js` to load per-dock client code into the
|
|
73
|
+
* host page; `importFrom` values must be URL paths the host serves.
|
|
74
|
+
*/
|
|
75
|
+
function renderClientImportsModule(ctx) {
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const [id, view] of ctx.docks.views) {
|
|
78
|
+
const scripts = [];
|
|
79
|
+
const anyView = view;
|
|
80
|
+
if (anyView.clientScript) scripts.push(anyView.clientScript);
|
|
81
|
+
if (anyView.action) scripts.push(anyView.action);
|
|
82
|
+
if (anyView.renderer) scripts.push(anyView.renderer);
|
|
83
|
+
if (scripts.length === 0) continue;
|
|
84
|
+
const thunks = scripts.map((script) => `() => import(${JSON.stringify(script.importFrom)})`);
|
|
85
|
+
entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
|
|
86
|
+
}
|
|
87
|
+
return `// Generated by @devframes/hub — dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Initiate a hub instance — the whole multi-devframe devtools surface
|
|
91
|
+
* behind one framework-agnostic, web-standard handler. Every mounted
|
|
92
|
+
* devframe shares one context (merged RPC registry, shared state, docks /
|
|
93
|
+
* terminals / messages / commands), one WebSocket transport, and one Auth;
|
|
94
|
+
* the instance serves each frame's SPA at `<base><id>/`, the discovery
|
|
95
|
+
* endpoints (`__connection.json`, `__index.json`, `__client-imports.js`),
|
|
96
|
+
* the aggregate MCP route, and whatever the {@link DevframeHubUi} slot
|
|
97
|
+
* provides — the hub itself stays headless.
|
|
98
|
+
*
|
|
99
|
+
* The factory is synchronous and initializes eagerly, and binds no port of
|
|
100
|
+
* its own: the WebSocket follows `server` / `ws` (see
|
|
101
|
+
* {@link InitHubOptions.ws}), or waits for the host to hand upgrades over
|
|
102
|
+
* through {@link HubInstance.attach}.
|
|
103
|
+
*/
|
|
104
|
+
function initHub(options) {
|
|
105
|
+
const base = normalizeHubBase(options.base);
|
|
106
|
+
const baseNoSlash = base.slice(0, -1);
|
|
107
|
+
const app = new H3();
|
|
108
|
+
const cwd = options.cwd ?? process.cwd();
|
|
109
|
+
const frames = [];
|
|
110
|
+
const rendererRegistrations = resolveRendererRegistrations(options.renderers ?? []);
|
|
111
|
+
const shell = createInstanceShell({
|
|
112
|
+
base,
|
|
113
|
+
app,
|
|
114
|
+
host: options.host,
|
|
115
|
+
origin: options.origin,
|
|
116
|
+
auth: options.auth,
|
|
117
|
+
server: options.server,
|
|
118
|
+
ws: options.ws,
|
|
119
|
+
sse: options.sse,
|
|
120
|
+
allowedOrigins: options.allowedOrigins,
|
|
121
|
+
destroyUnmatchedUpgrades: options.destroyUnmatchedUpgrades,
|
|
122
|
+
register: resolveInstanceRegister(options.register, {
|
|
123
|
+
id: options.name ?? "devframes-hub",
|
|
124
|
+
...options.name !== void 0 ? { name: options.name } : {},
|
|
125
|
+
rootDir: cwd
|
|
126
|
+
}),
|
|
127
|
+
absoluteWsPath: true,
|
|
128
|
+
resolveSidecarPort: async (sidecarHost) => {
|
|
129
|
+
const { getPort } = await import("../dist-R0T2g9Vw.mjs");
|
|
130
|
+
return getPort({
|
|
131
|
+
port: 9777,
|
|
132
|
+
portRange: [9777, 9877],
|
|
133
|
+
host: sidecarHost
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
onMetaUnavailable: () => {
|
|
137
|
+
throw diagnostics.DF8003();
|
|
138
|
+
},
|
|
139
|
+
async init(api) {
|
|
140
|
+
if (options.context && options.devframes?.length) throw diagnostics.DF8002();
|
|
141
|
+
let ctx;
|
|
142
|
+
if (options.context) ctx = options.context;
|
|
143
|
+
else {
|
|
144
|
+
const host = {
|
|
145
|
+
...createH3DevframeHost({
|
|
146
|
+
origin: () => api.origin() ?? "http://localhost",
|
|
147
|
+
appName: "devframes",
|
|
148
|
+
workspaceRoot: cwd,
|
|
149
|
+
mount: (mountBase, dir) => {
|
|
150
|
+
mountStaticHandler(app, mountBase, dir);
|
|
151
|
+
}
|
|
152
|
+
}),
|
|
153
|
+
...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
|
|
154
|
+
mountConnectionMeta: (frameBase) => {
|
|
155
|
+
app.use(joinURL(frameBase, DEVFRAME_CONNECTION_META_FILENAME), () => api.connectionMeta());
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
ctx = await createHubContext({
|
|
159
|
+
cwd,
|
|
160
|
+
workspaceRoot: cwd,
|
|
161
|
+
mode: "dev",
|
|
162
|
+
host,
|
|
163
|
+
...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const devframes = await resolveDevframesInput(options.devframes ?? []);
|
|
167
|
+
for (const { devframe: def, dock } of devframes) {
|
|
168
|
+
if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
|
|
169
|
+
if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
|
|
170
|
+
const frameBase = withTrailingSlash(joinURL(base, def.id));
|
|
171
|
+
await ctx.install(def, {
|
|
172
|
+
base: frameBase,
|
|
173
|
+
...dock ? { dock } : {}
|
|
174
|
+
});
|
|
175
|
+
frames.push({
|
|
176
|
+
id: def.id,
|
|
177
|
+
base: frameBase,
|
|
178
|
+
title: def.name
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
await options.configure?.(ctx);
|
|
182
|
+
await options.ui?.setup?.(ctx);
|
|
183
|
+
if (rendererRegistrations.length > 0) {
|
|
184
|
+
const manifest = {};
|
|
185
|
+
for (const registration of rendererRegistrations) manifest[registration.type] = {
|
|
186
|
+
importFrom: joinURL(base, "__renderers", `${registration.type}.mjs`),
|
|
187
|
+
...registration.importName ? { importName: registration.importName } : {}
|
|
188
|
+
};
|
|
189
|
+
(await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
|
|
190
|
+
}
|
|
191
|
+
const mcpConfig = options.mcp === true ? {} : options.mcp;
|
|
192
|
+
if (!mcpConfig) return { context: ctx };
|
|
193
|
+
const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE);
|
|
194
|
+
const { mountMcpHttp } = await import("devframe/adapters/mcp");
|
|
195
|
+
const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
|
|
196
|
+
serverName: options.name ?? "devframes-hub",
|
|
197
|
+
serverVersion: options.version ?? "0.0.0",
|
|
198
|
+
exposeSharedState: true,
|
|
199
|
+
allowedOrigins: mcpConfig.allowedOrigins
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
context: ctx,
|
|
203
|
+
mcp: { path: mcpRoute },
|
|
204
|
+
dispose: mounted.dispose
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
mount(ctx, meta) {
|
|
208
|
+
app.use(joinURL(base, DEVFRAME_CONNECTION_META_FILENAME), () => meta);
|
|
209
|
+
const indexDocument = () => ({
|
|
210
|
+
name: options.name,
|
|
211
|
+
version: options.version,
|
|
212
|
+
base,
|
|
213
|
+
frames,
|
|
214
|
+
endpoints: {
|
|
215
|
+
connection: DEVFRAME_CONNECTION_META_FILENAME,
|
|
216
|
+
clientImports: DEVFRAME_DOCK_IMPORTS_FILENAME,
|
|
217
|
+
index: "__index.json",
|
|
218
|
+
websocket: meta.websocket,
|
|
219
|
+
...meta.mcp ? { mcp: meta.mcp.path } : {},
|
|
220
|
+
...options.ui?.embedded ? { embedded: "embedded.js" } : {}
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
app.use(joinURL(base, "__index.json"), () => indexDocument());
|
|
224
|
+
app.use(joinURL(base, DEVFRAME_DOCK_IMPORTS_FILENAME), (event) => {
|
|
225
|
+
event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
|
|
226
|
+
event.res.headers.set("Cache-Control", "no-store");
|
|
227
|
+
return renderClientImportsModule(ctx);
|
|
228
|
+
});
|
|
229
|
+
if (options.ui?.embedded) {
|
|
230
|
+
const entry = resolve(options.ui.embedded.entry);
|
|
231
|
+
app.use(joinURL(base, "embedded.js"), async (event) => {
|
|
232
|
+
event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
|
|
233
|
+
event.res.headers.set("Cache-Control", "no-store");
|
|
234
|
+
return await readFile(entry);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
for (const registration of rendererRegistrations) app.use(joinURL(base, "__renderers", `${registration.type}.mjs`), async (event) => {
|
|
238
|
+
event.res.headers.set("Content-Type", "text/javascript; charset=utf-8");
|
|
239
|
+
event.res.headers.set("Cache-Control", "no-store");
|
|
240
|
+
return await readFile(registration.file);
|
|
241
|
+
});
|
|
242
|
+
for (const [key, produce] of Object.entries(options.ui?.assets ?? {})) app.use(joinURL(base, key), (event) => {
|
|
243
|
+
event.res.headers.set("Content-Type", assetContentType(key));
|
|
244
|
+
event.res.headers.set("Cache-Control", "no-store");
|
|
245
|
+
return produce();
|
|
246
|
+
});
|
|
247
|
+
if (options.ui?.viewer) mountStaticHandler(app, base, resolve(options.ui.viewer.distDir));
|
|
248
|
+
else {
|
|
249
|
+
app.use(baseNoSlash, () => indexDocument());
|
|
250
|
+
app.use(base, () => indexDocument());
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
base: shell.base,
|
|
256
|
+
handler: shell.handler,
|
|
257
|
+
nodeMiddleware: shell.nodeMiddleware,
|
|
258
|
+
attach: shell.attach,
|
|
259
|
+
handleUpgrade: shell.handleUpgrade,
|
|
260
|
+
ready: shell.ready,
|
|
261
|
+
context: shell.context,
|
|
262
|
+
connectionMeta: shell.connectionMeta,
|
|
263
|
+
close: shell.close
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { DEVFRAMES_HUB_BASE, initHub };
|
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
1
|
import { ConnectionMeta, EventEmitter } from "devframe/types";
|
|
3
|
-
|
|
4
|
-
/** @deprecated Use `DevframeJsonRenderSpec`'s element shape from `@devframes/json-render` instead. Removed in 0.8. */
|
|
5
|
-
interface JsonRenderElement {
|
|
6
|
-
type: string;
|
|
7
|
-
props?: Record<string, unknown>;
|
|
8
|
-
children?: string[];
|
|
9
|
-
/** json-render event bindings (e.g. `{ press: { action: "my:action" } }`) */
|
|
10
|
-
on?: Record<string, unknown>;
|
|
11
|
-
/** json-render visibility condition */
|
|
12
|
-
visible?: unknown;
|
|
13
|
-
/** json-render repeat binding */
|
|
14
|
-
repeat?: unknown;
|
|
15
|
-
/** Allow additional json-render element fields */
|
|
16
|
-
[key: string]: unknown;
|
|
17
|
-
}
|
|
18
|
-
/** @deprecated Use `DevframeJsonRenderSpec` from `@devframes/json-render` instead. Removed in 0.8. */
|
|
19
|
-
interface JsonRenderSpec {
|
|
20
|
-
root: string;
|
|
21
|
-
elements: Record<string, JsonRenderElement>;
|
|
22
|
-
/** Initial client-side state model for $state/$bindState expressions */
|
|
23
|
-
state?: Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
/** @deprecated Use `JsonRenderView` from `@devframes/json-render` instead. Removed in 0.8. */
|
|
26
|
-
interface JsonRenderer {
|
|
27
|
-
/** Replace the entire spec */
|
|
28
|
-
updateSpec: (spec: JsonRenderSpec) => void | Promise<void>;
|
|
29
|
-
/** Update json-render state values (shallow merge into spec.state) */
|
|
30
|
-
updateState: (state: Record<string, unknown>) => void | Promise<void>;
|
|
31
|
-
/** Internal: shared state key used by the client to subscribe */
|
|
32
|
-
readonly _stateKey: string;
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
2
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
35
3
|
//#region src/types/docks.d.ts
|
|
36
4
|
interface DevframeDocksHost {
|
|
37
5
|
readonly views: Map<string, DevframeDockUserEntry>;
|
|
@@ -82,6 +50,8 @@ type DevframeDockEntryIcon = string | {
|
|
|
82
50
|
light: string;
|
|
83
51
|
dark: string;
|
|
84
52
|
};
|
|
53
|
+
/** Color for a dock-bar entry's {@link DevframeDockEntryBase.badge} — mirrors json-render's `TabDescriptor.badgeVariant`. */
|
|
54
|
+
type DevframeDockBadgeVariant = 'default' | 'info' | 'success' | 'warning' | 'danger';
|
|
85
55
|
interface DevframeDockEntryBase {
|
|
86
56
|
id: string;
|
|
87
57
|
title: string;
|
|
@@ -146,6 +116,11 @@ interface DevframeDockEntryBase {
|
|
|
146
116
|
* Badge text to display on the dock icon (e.g., unread count)
|
|
147
117
|
*/
|
|
148
118
|
badge?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Colors {@link badge}. Omitted (or `'default'`) keeps the existing neutral
|
|
121
|
+
* fill, so every pre-existing badge consumer keeps its current look.
|
|
122
|
+
*/
|
|
123
|
+
badgeVariant?: DevframeDockBadgeVariant;
|
|
149
124
|
/**
|
|
150
125
|
* Id of the group this entry belongs to. When set, hosts collapse this entry
|
|
151
126
|
* under the matching group's button instead of showing it directly on the
|
|
@@ -357,20 +332,6 @@ interface DevframeViewBuiltin extends DevframeDockEntryBase {
|
|
|
357
332
|
type: '~builtin';
|
|
358
333
|
id: string;
|
|
359
334
|
}
|
|
360
|
-
/**
|
|
361
|
-
* @deprecated json-render moved out of the hub into the opt-in
|
|
362
|
-
* `@devframes/json-render` integration in 0.7, which contributes its own
|
|
363
|
-
* `'json-render'` entry (carrying a serializable view ref, not a live
|
|
364
|
-
* `JsonRenderer` handle) to {@link DevframeDockEntryRegistry} via declaration
|
|
365
|
-
* merging. This type is kept for compatibility but is no longer a member of
|
|
366
|
-
* {@link DevframeDockUserEntry} — use `@devframes/json-render/hub` instead.
|
|
367
|
-
* Removed in 0.8.
|
|
368
|
-
*/
|
|
369
|
-
interface DevframeViewJsonRender extends DevframeDockEntryBase {
|
|
370
|
-
type: 'json-render';
|
|
371
|
-
/** JsonRenderer handle created by the deprecated ctx.createJsonRenderer() */
|
|
372
|
-
ui: JsonRenderer;
|
|
373
|
-
}
|
|
374
335
|
/**
|
|
375
336
|
* A dock group: a single dock-bar button that collapses every entry whose
|
|
376
337
|
* {@link DevframeDockEntryBase.groupId} matches this group's `id`.
|
|
@@ -506,9 +467,6 @@ interface DevframeCommandBase {
|
|
|
506
467
|
* `when` clauses are evaluated client-side only and are **not** enforced for
|
|
507
468
|
* agent calls — opt in a `when`-gated command only if running it outside its
|
|
508
469
|
* UI context is safe.
|
|
509
|
-
*
|
|
510
|
-
* @experimental The agent-native surface is experimental and may change
|
|
511
|
-
* without a major version bump until it stabilizes.
|
|
512
470
|
*/
|
|
513
471
|
interface DevframeCommandAgentOptions {
|
|
514
472
|
/**
|
|
@@ -545,8 +503,6 @@ interface DevframeServerCommandInput extends DevframeCommandBase {
|
|
|
545
503
|
/**
|
|
546
504
|
* Opt this command in to the agent surface (`ctx.agent` → MCP). Requires a
|
|
547
505
|
* `handler`. See {@link DevframeCommandAgentOptions}.
|
|
548
|
-
*
|
|
549
|
-
* @experimental
|
|
550
506
|
*/
|
|
551
507
|
agent?: DevframeCommandAgentOptions;
|
|
552
508
|
/**
|
|
@@ -619,14 +575,35 @@ interface DevframeCommandShortcutOverrides {
|
|
|
619
575
|
}
|
|
620
576
|
//#endregion
|
|
621
577
|
//#region src/types/settings.d.ts
|
|
578
|
+
/**
|
|
579
|
+
* Persisted per-user dock + command settings, synced over the
|
|
580
|
+
* `devframe:user-settings` shared state. The generic base carries the hub's
|
|
581
|
+
* own dock-registry and command model that every viewer shares; a viewer
|
|
582
|
+
* augments it with its own reference-UI toggles via declaration merging:
|
|
583
|
+
*
|
|
584
|
+
* ```ts
|
|
585
|
+
* declare module '@devframes/hub/types' {
|
|
586
|
+
* interface DevframeDocksUserSettings {
|
|
587
|
+
* myViewerToggle?: boolean
|
|
588
|
+
* }
|
|
589
|
+
* }
|
|
590
|
+
* ```
|
|
591
|
+
*
|
|
592
|
+
* Augmented fields are optional so the hub's `DEFAULT_STATE_USER_SETTINGS()`
|
|
593
|
+
* (which knows nothing about them) stays assignable — an absent value reads
|
|
594
|
+
* as the viewer's documented default.
|
|
595
|
+
*/
|
|
622
596
|
interface DevframeDocksUserSettings {
|
|
597
|
+
/** Ids of dock entries the user has hidden from the bar. */
|
|
623
598
|
docksHidden: string[];
|
|
599
|
+
/** Category ids the user has collapsed/hidden on the bar. */
|
|
624
600
|
docksCategoriesHidden: string[];
|
|
601
|
+
/** Ids of dock entries the user has pinned. */
|
|
625
602
|
docksPinned: string[];
|
|
603
|
+
/** Per-entry sort weight overriding the registry's default order. */
|
|
626
604
|
docksCustomOrder: Record<string, number>;
|
|
627
|
-
|
|
628
|
-
closeOnOutsideClick: boolean;
|
|
605
|
+
/** Per-command keybinding overrides (empty array = shortcut disabled). */
|
|
629
606
|
commandShortcuts: DevframeCommandShortcutOverrides;
|
|
630
607
|
}
|
|
631
608
|
//#endregion
|
|
632
|
-
export { DevframeViewLauncher as A,
|
|
609
|
+
export { DevframeViewLauncher as A, DevframeDocksActiveState as C, DevframeViewCustomRender as D, DevframeViewBuiltin as E, RemoteDockOptions as F, FrameSubTabsConfig as M, NavTarget as N, DevframeViewGroup as O, RemoteConnectionInfo as P, DevframeDockUserEntry as S, DevframeViewAction as T, DevframeDockEntry as _, DevframeCommandEntry as a, DevframeDockEntryIcon as b, DevframeCommandShortcutOverrides as c, DevframeServerCommandEntry as d, DevframeServerCommandInput as f, DevframeDockEntriesGrouped as g, DevframeDockBadgeVariant as h, DevframeCommandBase as i, DevframeViewLauncherStatus as j, DevframeViewIframe as k, DevframeCommandsHost as l, DevframeDockActivation as m, DevframeClientCommand as n, DevframeCommandHandle as o, ClientScriptEntry as p, DevframeCommandAgentOptions as r, DevframeCommandKeybinding as s, DevframeDocksUserSettings as t, DevframeCommandsHostEvents as u, DevframeDockEntryBase as v, DevframeDocksHost as w, DevframeDockEntryRegistry as x, DevframeDockEntryCategory as y };
|