@devframes/hub 0.9.9 → 0.9.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/assemble-Hj36CCzH.mjs +103 -0
- package/dist/client/index.mjs +59 -1
- package/dist/{constants-Coo1p3ja.d.mts → constants-Bz4yclHo.d.mts} +9 -0
- package/dist/{constants-BUd6oMci.mjs → constants-Da23JxCZ.mjs} +8 -1
- package/dist/constants.d.mts +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/{context-D4L966cN.mjs → context-BWaW6N53.mjs} +142 -129
- package/dist/node/build.d.mts +74 -0
- package/dist/node/build.mjs +167 -0
- package/dist/node/index.mjs +3 -3
- package/dist/node/initiate.d.mts +11 -9
- package/dist/node/initiate.mjs +19 -108
- package/package.json +4 -3
- package/dist/{dist-R0T2g9Vw.mjs → dist-BP3CGpgZ.mjs} +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { a as resolveClientModuleSpecifier } from "./remote-url-UClKLQ5_.mjs";
|
|
2
|
+
import { b as diagnostics, v as prepareDevframe, y as skippedInStaticBuild } from "./context-BWaW6N53.mjs";
|
|
3
|
+
import { joinURL, withTrailingSlash } from "ufo";
|
|
4
|
+
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE, DEVFRAME_WS_ROUTE } from "devframe/constants";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { resolve } from "pathe";
|
|
7
|
+
//#region src/node/assemble.ts
|
|
8
|
+
/** Reserved filenames directly under the hub base; a frame id can't shadow them. */
|
|
9
|
+
const RESERVED_HUB_PATHS = [
|
|
10
|
+
DEVFRAME_CONNECTION_META_FILENAME,
|
|
11
|
+
DEVFRAME_DOCK_IMPORTS_FILENAME,
|
|
12
|
+
DEVFRAME_WS_ROUTE,
|
|
13
|
+
DEVFRAME_MCP_ROUTE,
|
|
14
|
+
"__index.json",
|
|
15
|
+
"__renderers",
|
|
16
|
+
"embedded.js"
|
|
17
|
+
];
|
|
18
|
+
function normalizeDevframeEntry(entry) {
|
|
19
|
+
return "devframe" in entry ? entry : { devframe: entry };
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Flatten the `devframes` input: await every thenable, call every factory,
|
|
23
|
+
* spread the arrays, and drop the empty slots, so a host can build the list
|
|
24
|
+
* conditionally (`isDev && loadInspect()`) without filtering it first.
|
|
25
|
+
*/
|
|
26
|
+
function resolveDevframesInput(input) {
|
|
27
|
+
return Promise.all(input.map(async (entry) => {
|
|
28
|
+
const resolved = await (typeof entry === "function" ? entry() : entry);
|
|
29
|
+
return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
|
|
30
|
+
})).then((arrays) => arrays.flat());
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate the renderer-module registrations fail-fast: route-safe types
|
|
34
|
+
* (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
|
|
35
|
+
* per type, and an existing bundle file (renderer packages are prebuilt).
|
|
36
|
+
*/
|
|
37
|
+
function resolveRendererRegistrations(registrations) {
|
|
38
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39
|
+
return registrations.map((registration) => {
|
|
40
|
+
if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
|
|
41
|
+
if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
|
|
42
|
+
seen.add(registration.type);
|
|
43
|
+
const file = resolve(registration.file);
|
|
44
|
+
if (!existsSync(file)) throw diagnostics.DF8109({
|
|
45
|
+
type: registration.type,
|
|
46
|
+
file
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
...registration,
|
|
50
|
+
file
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render the dock client-script import map as an ES module: one dynamic
|
|
56
|
+
* import thunk per dock that carries a client script (`clientScript` on
|
|
57
|
+
* iframe docks, `action`, `renderer`). External viewers import this module
|
|
58
|
+
* from `<base>__client-imports.js` to load per-dock client code into the
|
|
59
|
+
* host page; `importFrom` values are URL paths the host serves, or bare npm
|
|
60
|
+
* specifiers resolved through the host's `clientModuleResolution` template.
|
|
61
|
+
*/
|
|
62
|
+
function renderClientImportsModule(ctx) {
|
|
63
|
+
const template = ctx.staticConfig.dock?.clientModuleResolution;
|
|
64
|
+
const entries = [];
|
|
65
|
+
for (const [id, view] of ctx.docks.views) {
|
|
66
|
+
const scripts = [];
|
|
67
|
+
const anyView = view;
|
|
68
|
+
if (anyView.clientScript) scripts.push(anyView.clientScript);
|
|
69
|
+
if (anyView.action) scripts.push(anyView.action);
|
|
70
|
+
if (anyView.renderer) scripts.push(anyView.renderer);
|
|
71
|
+
if (scripts.length === 0) continue;
|
|
72
|
+
const thunks = scripts.map((script) => `() => import(${JSON.stringify(resolveClientModuleSpecifier(script.importFrom, { template }))})`);
|
|
73
|
+
entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
|
|
74
|
+
}
|
|
75
|
+
return `// Generated by @devframes/hub: dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Pass 1: mount each devframe under `<base><id>/` (SPA, meta, iframe dock)
|
|
79
|
+
* and queue its declared services, guarding the id against reserved hub
|
|
80
|
+
* filenames and route-pattern characters. Returns the deferred setup thunks.
|
|
81
|
+
*/
|
|
82
|
+
async function mountDevframes(ctx, devframes, base, frames, hubMcpEnabled) {
|
|
83
|
+
const setups = [];
|
|
84
|
+
for (const { devframe: def, dock } of devframes) {
|
|
85
|
+
if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
|
|
86
|
+
if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
|
|
87
|
+
if (!hubMcpEnabled && def.cli?.mcp) diagnostics.DF8005({ id: def.id });
|
|
88
|
+
const frameBase = withTrailingSlash(joinURL(base, def.id));
|
|
89
|
+
const run = await prepareDevframe(ctx, def, {
|
|
90
|
+
base: frameBase,
|
|
91
|
+
...dock ? { dock } : {}
|
|
92
|
+
});
|
|
93
|
+
if (run) setups.push(run);
|
|
94
|
+
if (!skippedInStaticBuild(ctx, def)) frames.push({
|
|
95
|
+
id: def.id,
|
|
96
|
+
base: frameBase,
|
|
97
|
+
title: def.name
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return setups;
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { resolveRendererRegistrations as i, renderClientImportsModule as n, resolveDevframesInput as r, mountDevframes as t };
|
package/dist/client/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-
|
|
1
|
+
import { i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, t as DEFAULT_CATEGORIES_ORDER } from "../constants-Da23JxCZ.mjs";
|
|
2
2
|
import { a as resolveClientModuleSpecifier, n as stripRemoteConnectionFromUrl, r as clientScriptFailureHint, t as buildRemoteConnectionUrl } from "../remote-url-UClKLQ5_.mjs";
|
|
3
3
|
import { REMOTE_CONNECTION_KEY } from "devframe/constants";
|
|
4
4
|
import { connectDevframe, getDevframeRpcClient, resolveWsUrl } from "devframe/client";
|
|
@@ -362,6 +362,7 @@ function serializeNavQuery(query) {
|
|
|
362
362
|
* dock client script reports into the very feed the server writes to.
|
|
363
363
|
*/
|
|
364
364
|
function createMessagesClient(rpc, options = {}) {
|
|
365
|
+
if (rpc.connectionMeta?.backend === "static") return createLocalMessagesClient(options);
|
|
365
366
|
const { call } = rpc;
|
|
366
367
|
function makeHandle(entry) {
|
|
367
368
|
let current = entry;
|
|
@@ -404,6 +405,58 @@ function createMessagesClient(rpc, options = {}) {
|
|
|
404
405
|
debug: levelShortcut("debug")
|
|
405
406
|
};
|
|
406
407
|
}
|
|
408
|
+
/** In-page-only writes for a static backend: same surface, no wire. */
|
|
409
|
+
function createLocalMessagesClient(options) {
|
|
410
|
+
let nextId = 0;
|
|
411
|
+
function makeEntry(input) {
|
|
412
|
+
return {
|
|
413
|
+
timestamp: Date.now(),
|
|
414
|
+
...options.defaults,
|
|
415
|
+
...input,
|
|
416
|
+
id: input.id ?? `local-${++nextId}`,
|
|
417
|
+
from: "browser"
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function makeHandle(entry) {
|
|
421
|
+
let current = entry;
|
|
422
|
+
return {
|
|
423
|
+
get entry() {
|
|
424
|
+
return current;
|
|
425
|
+
},
|
|
426
|
+
get id() {
|
|
427
|
+
return current.id;
|
|
428
|
+
},
|
|
429
|
+
async update(patch) {
|
|
430
|
+
current = {
|
|
431
|
+
...current,
|
|
432
|
+
...patch,
|
|
433
|
+
id: current.id,
|
|
434
|
+
from: current.from
|
|
435
|
+
};
|
|
436
|
+
return current;
|
|
437
|
+
},
|
|
438
|
+
dismiss: async () => {}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const add = async (input) => makeHandle(makeEntry(input));
|
|
442
|
+
function levelShortcut(level) {
|
|
443
|
+
return (message, extra) => add({
|
|
444
|
+
...extra,
|
|
445
|
+
message,
|
|
446
|
+
level
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
add,
|
|
451
|
+
remove: async () => {},
|
|
452
|
+
clear: async () => {},
|
|
453
|
+
info: levelShortcut("info"),
|
|
454
|
+
warn: levelShortcut("warn"),
|
|
455
|
+
error: levelShortcut("error"),
|
|
456
|
+
success: levelShortcut("success"),
|
|
457
|
+
debug: levelShortcut("debug")
|
|
458
|
+
};
|
|
459
|
+
}
|
|
407
460
|
//#endregion
|
|
408
461
|
//#region src/client/renderers.ts
|
|
409
462
|
/**
|
|
@@ -576,6 +629,11 @@ async function createDevframeClientRuntime(options = {}) {
|
|
|
576
629
|
type: "action",
|
|
577
630
|
handler: (activation) => activateHandler(activation)
|
|
578
631
|
});
|
|
632
|
+
if (rpc.connectionMeta?.backend === "static" && typeof BroadcastChannel !== "undefined") {
|
|
633
|
+
const activateChannel = new BroadcastChannel(HUB_EVENTS.broadcastChannel.docksActivate);
|
|
634
|
+
activateChannel.onmessage = (event) => activateHandler(event.data);
|
|
635
|
+
disposers.push(() => activateChannel.close());
|
|
636
|
+
}
|
|
579
637
|
if (getDevframeClientContext()) console.warn("[@devframes/hub] A client context is already published on this page; replacing it. Boot createDevframeClientRuntime() once per page (e.g. HTML injection combined with a manual import boots it twice).");
|
|
580
638
|
setDevframeClientContext(context);
|
|
581
639
|
const loadedScripts = /* @__PURE__ */ new Set();
|
|
@@ -72,6 +72,15 @@ declare const HUB_EVENTS: {
|
|
|
72
72
|
readonly postMessage: {
|
|
73
73
|
readonly frameNav: "devframe:frame-nav";
|
|
74
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Same-origin `BroadcastChannel` names, `devframe:` prefix. Used on a
|
|
77
|
+
* `static` backend, where no live server can relay a client's request to
|
|
78
|
+
* its sibling browsing contexts (e.g. a panel iframe steering the host
|
|
79
|
+
* page's dock selection).
|
|
80
|
+
*/
|
|
81
|
+
readonly broadcastChannel: {
|
|
82
|
+
readonly docksActivate: "devframe:docks:activate";
|
|
83
|
+
};
|
|
75
84
|
};
|
|
76
85
|
//#endregion
|
|
77
86
|
//#region src/constants.d.ts
|
|
@@ -65,7 +65,14 @@ const HUB_EVENTS = {
|
|
|
65
65
|
/** Streaming channel ids (server → client), `devframe:` prefix. */
|
|
66
66
|
stream: { terminals: "devframe:terminals" },
|
|
67
67
|
/** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */
|
|
68
|
-
postMessage: { frameNav: "devframe:frame-nav" }
|
|
68
|
+
postMessage: { frameNav: "devframe:frame-nav" },
|
|
69
|
+
/**
|
|
70
|
+
* Same-origin `BroadcastChannel` names, `devframe:` prefix. Used on a
|
|
71
|
+
* `static` backend, where no live server can relay a client's request to
|
|
72
|
+
* its sibling browsing contexts (e.g. a panel iframe steering the host
|
|
73
|
+
* page's dock selection).
|
|
74
|
+
*/
|
|
75
|
+
broadcastChannel: { docksActivate: "devframe:docks:activate" }
|
|
69
76
|
};
|
|
70
77
|
//#endregion
|
|
71
78
|
//#region src/constants.ts
|
package/dist/constants.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-
|
|
1
|
+
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-Bz4yclHo.mjs";
|
|
2
2
|
export * from "devframe/constants";
|
|
3
3
|
export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase };
|
package/dist/constants.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-
|
|
1
|
+
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS, r as DEVFRAMES_HUB_BASE, t as DEFAULT_CATEGORIES_ORDER } from "./constants-Da23JxCZ.mjs";
|
|
2
2
|
export * from "devframe/constants";
|
|
3
3
|
export { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DEVFRAMES_HUB_BASE, DOCK_RENDERERS_STATE_KEY, HUB_EVENTS, normalizeHubBase };
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { r as defineHubRpcFunction } from "./define-Ceekw2EO.mjs";
|
|
2
|
-
import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-
|
|
2
|
+
import { n as DEFAULT_STATE_USER_SETTINGS, o as HUB_EVENTS } from "./constants-Da23JxCZ.mjs";
|
|
3
3
|
import { i as isBareModuleSpecifier, t as buildRemoteConnectionUrl } from "./remote-url-UClKLQ5_.mjs";
|
|
4
4
|
import { joinURL, withTrailingSlash } from "ufo";
|
|
5
5
|
import { createEventEmitter } from "devframe/utils/events";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import { debounce } from "perfect-debounce";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import process from "node:process";
|
|
9
8
|
import { coerceAgentPositionalArgs, resolveClientAssets } from "devframe/internal";
|
|
10
|
-
import { defineDiagnostics } from "devframe/utils/nostics";
|
|
11
9
|
import { basename, dirname, isAbsolute, join, resolve } from "pathe";
|
|
10
|
+
import { defineDiagnostics } from "devframe/utils/nostics";
|
|
11
|
+
import { getInternalContext, resolveBasePath } from "devframe/node/hub-internals";
|
|
12
|
+
import { createHostContext, createStorage } from "devframe/node";
|
|
13
|
+
import { debounce } from "perfect-debounce";
|
|
12
14
|
import { nanoid } from "devframe/utils/nanoid";
|
|
13
|
-
import process from "node:process";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
15
|
//#region src/node/diagnostics.ts
|
|
16
16
|
/**
|
|
17
17
|
* Hub-side diagnostics for docks, terminals, messages, and commands.
|
|
@@ -44,8 +44,12 @@ const diagnostics = defineDiagnostics({
|
|
|
44
44
|
fix: "Ids become route segments, so they may only contain letters, digits, `_`, `-`, and `.`; `:` and `*` are route-pattern markers to the underlying router, and `/` would escape the segment. Set a route-safe `id` on the definition (e.g. `my_plugin` instead of `my:plugin`)."
|
|
45
45
|
},
|
|
46
46
|
DF8005: {
|
|
47
|
-
why: (p) => `Devframe "${p.id}" requests an MCP route, but the hub's aggregate MCP is off, so its tools are not exposed over MCP.`,
|
|
48
|
-
fix: "A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored.
|
|
47
|
+
why: (p) => `Devframe "${p.id}" requests an MCP route, but the hub's aggregate MCP is off (\`mcp: false\`), so its tools are not exposed over MCP.`,
|
|
48
|
+
fix: "A hub exposes one aggregate MCP endpoint over every mounted devframe, so per-devframe `mcp` settings are ignored. Drop `mcp: false` from `initHub` (the `'auto'` default mounts the aggregate route once agent tools exist) to surface this devframe's tools, or drop `mcp` from the devframe to silence this warning."
|
|
49
|
+
},
|
|
50
|
+
DF8006: {
|
|
51
|
+
why: (p) => `A static hub build can only write mounts under its own base: "${p.urlBase}" escapes "${p.base}".`,
|
|
52
|
+
fix: "buildHub maps each mount base to a directory under its `outDir`, so every mount must live under the hub base. Drop the `basePath` override (or the `ctx.install` base) that points outside it, or move the hub `base` up so it contains the mount."
|
|
49
53
|
},
|
|
50
54
|
DF8100: {
|
|
51
55
|
why: (p) => `Dock with id "${p.id}" is already registered`,
|
|
@@ -128,6 +132,134 @@ const diagnostics = defineDiagnostics({
|
|
|
128
132
|
}
|
|
129
133
|
});
|
|
130
134
|
//#endregion
|
|
135
|
+
//#region src/node/install-devframe.ts
|
|
136
|
+
/**
|
|
137
|
+
* Find the next free dock id derived from `baseId`. Returns `baseId`
|
|
138
|
+
* when it is unused, otherwise appends `-2`, `-3`, … until a free slot
|
|
139
|
+
* is found. Used by the `'duplicate'` strategy so co-existing instances
|
|
140
|
+
* never collide in the dock registry.
|
|
141
|
+
*/
|
|
142
|
+
function nextAvailableDockId(views, baseId) {
|
|
143
|
+
if (!views.has(baseId)) return baseId;
|
|
144
|
+
let n = 2;
|
|
145
|
+
while (views.has(`${baseId}-${n}`)) n++;
|
|
146
|
+
return `${baseId}-${n}`;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* When a dock's `clientScript.importFrom` is an absolute filesystem path, serve
|
|
150
|
+
* its directory under `<base>__page-script/` and rewrite `importFrom` to that
|
|
151
|
+
* URL. A URL or bare specifier (not existing on disk) passes through untouched.
|
|
152
|
+
*/
|
|
153
|
+
async function resolvePageScriptClientScript(ctx, clientScript, base) {
|
|
154
|
+
if (!clientScript?.importFrom) return clientScript;
|
|
155
|
+
const { importFrom } = clientScript;
|
|
156
|
+
if (!isAbsolute(importFrom) || !existsSync(importFrom)) return clientScript;
|
|
157
|
+
const scriptBase = withTrailingSlash(joinURL(base, "__page-script"));
|
|
158
|
+
await ctx.host.mountStatic(scriptBase, dirname(importFrom));
|
|
159
|
+
return {
|
|
160
|
+
...clientScript,
|
|
161
|
+
importFrom: joinURL(scriptBase, basename(importFrom))
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Framework-neutral primitive backing {@link DevframeHubContext.install} -
|
|
166
|
+
* installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
|
|
167
|
+
* serves the devframe's SPA at the resolved base path, synthesizes an iframe
|
|
168
|
+
* dock entry from the definition's metadata, and runs the definition's
|
|
169
|
+
* `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than
|
|
170
|
+
* calling it directly.
|
|
171
|
+
*
|
|
172
|
+
* Framework kits wrap `ctx.install` with their own plugin/middleware
|
|
173
|
+
* machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
|
|
174
|
+
* returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
|
|
175
|
+
*/
|
|
176
|
+
/**
|
|
177
|
+
* Phase one of an install: run the duplication guard, serve the SPA + meta,
|
|
178
|
+
* register the iframe dock, and queue the definition's declarative wire
|
|
179
|
+
* services, everything up to (but not including) `setup(ctx)`. Returns a
|
|
180
|
+
* deferred setup thunk, or `null` when the devframe was deduplicated.
|
|
181
|
+
*
|
|
182
|
+
* The hub's initial batch uses this to collect every devframe's services
|
|
183
|
+
* across the whole hub, `ready()` them once, and only then run the setups,
|
|
184
|
+
* so services are ready before any setup, and a plugin can consume a service
|
|
185
|
+
* another plugin declared regardless of mount order.
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* Serve a devframe's SPA (and the hub's connection meta) under `base`, if the
|
|
189
|
+
* definition ships client assets.
|
|
190
|
+
*/
|
|
191
|
+
async function serveDevframeAssets(ctx, d, id, base) {
|
|
192
|
+
const clientAssets = resolveClientAssets(d);
|
|
193
|
+
if (!clientAssets) return;
|
|
194
|
+
if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
|
|
195
|
+
else diagnostics.DF8106({
|
|
196
|
+
id,
|
|
197
|
+
name: d.name,
|
|
198
|
+
base
|
|
199
|
+
});
|
|
200
|
+
ctx.views.hostStatic(base, typeof clientAssets === "string" ? resolve(clientAssets) : clientAssets, d.importMetaUrl);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether a devframe stays out of a static hub build: `capabilities.build:
|
|
204
|
+
* false` declares its value inherently live (a terminal, a process proxy),
|
|
205
|
+
* so `buildHub` never mounts it, registers its dock, or bakes its RPCs.
|
|
206
|
+
*/
|
|
207
|
+
function skippedInStaticBuild(ctx, d) {
|
|
208
|
+
return ctx.mode === "build" && d.capabilities?.build === false;
|
|
209
|
+
}
|
|
210
|
+
async function prepareDevframe(ctx, d, options = {}) {
|
|
211
|
+
if (skippedInStaticBuild(ctx, d)) return null;
|
|
212
|
+
const strategy = d.duplicationStrategy ?? "warn";
|
|
213
|
+
const isDuplicate = ctx.docks.views.has(d.id);
|
|
214
|
+
if (isDuplicate && strategy !== "duplicate") {
|
|
215
|
+
if (strategy === "throw") throw diagnostics.DF8105({
|
|
216
|
+
id: d.id,
|
|
217
|
+
name: d.name
|
|
218
|
+
});
|
|
219
|
+
if (strategy === "warn") diagnostics.DF8105({
|
|
220
|
+
id: d.id,
|
|
221
|
+
name: d.name
|
|
222
|
+
});
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id;
|
|
226
|
+
const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({
|
|
227
|
+
...d,
|
|
228
|
+
id,
|
|
229
|
+
basePath: void 0
|
|
230
|
+
}, "hosted"));
|
|
231
|
+
const dockDefaults = {
|
|
232
|
+
...d.dock,
|
|
233
|
+
...options.dock
|
|
234
|
+
};
|
|
235
|
+
const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
|
|
236
|
+
if (clientScript) dockDefaults.clientScript = clientScript;
|
|
237
|
+
await serveDevframeAssets(ctx, d, id, base);
|
|
238
|
+
ctx.docks.register({
|
|
239
|
+
id,
|
|
240
|
+
title: d.name,
|
|
241
|
+
icon: d.icon,
|
|
242
|
+
...dockDefaults,
|
|
243
|
+
type: "iframe",
|
|
244
|
+
url: base
|
|
245
|
+
});
|
|
246
|
+
for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl });
|
|
247
|
+
return () => Promise.resolve(d.setup(ctx));
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
|
|
251
|
+
* register its dock, ready its services, and run `setup(ctx)`. The imperative
|
|
252
|
+
* counterpart to the hub's declarative `devframes` list (which batches the
|
|
253
|
+
* phases via {@link prepareDevframe}); use it from `configure(ctx)` or
|
|
254
|
+
* wherever you hold the context to plug in an extra devframe after startup.
|
|
255
|
+
*/
|
|
256
|
+
async function installDevframe(ctx, d, options = {}) {
|
|
257
|
+
const run = await prepareDevframe(ctx, d, options);
|
|
258
|
+
if (!run) return;
|
|
259
|
+
await ctx.services.ready();
|
|
260
|
+
await run();
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
131
263
|
//#region src/node/host-commands.ts
|
|
132
264
|
function findChildCommand(command, id) {
|
|
133
265
|
for (const child of command.children ?? []) {
|
|
@@ -952,125 +1084,6 @@ var DevframeTerminalsHost = class {
|
|
|
952
1084
|
}
|
|
953
1085
|
};
|
|
954
1086
|
//#endregion
|
|
955
|
-
//#region src/node/install-devframe.ts
|
|
956
|
-
/**
|
|
957
|
-
* Find the next free dock id derived from `baseId`. Returns `baseId`
|
|
958
|
-
* when it is unused, otherwise appends `-2`, `-3`, … until a free slot
|
|
959
|
-
* is found. Used by the `'duplicate'` strategy so co-existing instances
|
|
960
|
-
* never collide in the dock registry.
|
|
961
|
-
*/
|
|
962
|
-
function nextAvailableDockId(views, baseId) {
|
|
963
|
-
if (!views.has(baseId)) return baseId;
|
|
964
|
-
let n = 2;
|
|
965
|
-
while (views.has(`${baseId}-${n}`)) n++;
|
|
966
|
-
return `${baseId}-${n}`;
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* When a dock's `clientScript.importFrom` is an absolute filesystem path, serve
|
|
970
|
-
* its directory under `<base>__page-script/` and rewrite `importFrom` to that
|
|
971
|
-
* URL. A URL or bare specifier (not existing on disk) passes through untouched.
|
|
972
|
-
*/
|
|
973
|
-
async function resolvePageScriptClientScript(ctx, clientScript, base) {
|
|
974
|
-
if (!clientScript?.importFrom) return clientScript;
|
|
975
|
-
const { importFrom } = clientScript;
|
|
976
|
-
if (!isAbsolute(importFrom) || !existsSync(importFrom)) return clientScript;
|
|
977
|
-
const scriptBase = withTrailingSlash(joinURL(base, "__page-script"));
|
|
978
|
-
await ctx.host.mountStatic(scriptBase, dirname(importFrom));
|
|
979
|
-
return {
|
|
980
|
-
...clientScript,
|
|
981
|
-
importFrom: joinURL(scriptBase, basename(importFrom))
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Framework-neutral primitive backing {@link DevframeHubContext.install} -
|
|
986
|
-
* installs a {@link DevframeDefinition} as a dock inside a hub-aware context:
|
|
987
|
-
* serves the devframe's SPA at the resolved base path, synthesizes an iframe
|
|
988
|
-
* dock entry from the definition's metadata, and runs the definition's
|
|
989
|
-
* `setup(ctx)`. Reach for it through `ctx.install(devframe)` rather than
|
|
990
|
-
* calling it directly.
|
|
991
|
-
*
|
|
992
|
-
* Framework kits wrap `ctx.install` with their own plugin/middleware
|
|
993
|
-
* machinery, e.g. `@vitejs/devtools-kit`'s `createPluginFromDevframe`
|
|
994
|
-
* returns a Vite `Plugin` whose `devtools.setup` ultimately delegates here.
|
|
995
|
-
*/
|
|
996
|
-
/**
|
|
997
|
-
* Phase one of an install: run the duplication guard, serve the SPA + meta,
|
|
998
|
-
* register the iframe dock, and queue the definition's declarative wire
|
|
999
|
-
* services, everything up to (but not including) `setup(ctx)`. Returns a
|
|
1000
|
-
* deferred setup thunk, or `null` when the devframe was deduplicated.
|
|
1001
|
-
*
|
|
1002
|
-
* The hub's initial batch uses this to collect every devframe's services
|
|
1003
|
-
* across the whole hub, `ready()` them once, and only then run the setups,
|
|
1004
|
-
* so services are ready before any setup, and a plugin can consume a service
|
|
1005
|
-
* another plugin declared regardless of mount order.
|
|
1006
|
-
*/
|
|
1007
|
-
/**
|
|
1008
|
-
* Serve a devframe's SPA (and the hub's connection meta) under `base`, if the
|
|
1009
|
-
* definition ships client assets.
|
|
1010
|
-
*/
|
|
1011
|
-
async function serveDevframeAssets(ctx, d, id, base) {
|
|
1012
|
-
const clientAssets = resolveClientAssets(d);
|
|
1013
|
-
if (!clientAssets) return;
|
|
1014
|
-
if (ctx.host.mountConnectionMeta) await ctx.host.mountConnectionMeta(base);
|
|
1015
|
-
else diagnostics.DF8106({
|
|
1016
|
-
id,
|
|
1017
|
-
name: d.name,
|
|
1018
|
-
base
|
|
1019
|
-
});
|
|
1020
|
-
ctx.views.hostStatic(base, typeof clientAssets === "string" ? resolve(clientAssets) : clientAssets, d.importMetaUrl);
|
|
1021
|
-
}
|
|
1022
|
-
async function prepareDevframe(ctx, d, options = {}) {
|
|
1023
|
-
const strategy = d.duplicationStrategy ?? "warn";
|
|
1024
|
-
const isDuplicate = ctx.docks.views.has(d.id);
|
|
1025
|
-
if (isDuplicate && strategy !== "duplicate") {
|
|
1026
|
-
if (strategy === "throw") throw diagnostics.DF8105({
|
|
1027
|
-
id: d.id,
|
|
1028
|
-
name: d.name
|
|
1029
|
-
});
|
|
1030
|
-
if (strategy === "warn") diagnostics.DF8105({
|
|
1031
|
-
id: d.id,
|
|
1032
|
-
name: d.name
|
|
1033
|
-
});
|
|
1034
|
-
return null;
|
|
1035
|
-
}
|
|
1036
|
-
const id = isDuplicate ? nextAvailableDockId(ctx.docks.views, d.id) : d.id;
|
|
1037
|
-
const base = options.base ?? (id === d.id ? resolveBasePath(d, "hosted") : resolveBasePath({
|
|
1038
|
-
...d,
|
|
1039
|
-
id,
|
|
1040
|
-
basePath: void 0
|
|
1041
|
-
}, "hosted"));
|
|
1042
|
-
const dockDefaults = {
|
|
1043
|
-
...d.dock,
|
|
1044
|
-
...options.dock
|
|
1045
|
-
};
|
|
1046
|
-
const clientScript = await resolvePageScriptClientScript(ctx, dockDefaults.clientScript, base);
|
|
1047
|
-
if (clientScript) dockDefaults.clientScript = clientScript;
|
|
1048
|
-
await serveDevframeAssets(ctx, d, id, base);
|
|
1049
|
-
ctx.docks.register({
|
|
1050
|
-
id,
|
|
1051
|
-
title: d.name,
|
|
1052
|
-
icon: d.icon,
|
|
1053
|
-
...dockDefaults,
|
|
1054
|
-
type: "iframe",
|
|
1055
|
-
url: base
|
|
1056
|
-
});
|
|
1057
|
-
for (const input of d.services ?? []) ctx.services.install(input, { resolveFrom: d.importMetaUrl });
|
|
1058
|
-
return () => Promise.resolve(d.setup(ctx));
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* Install a {@link DevframeDefinition} into a hub in one call: serve its SPA,
|
|
1062
|
-
* register its dock, ready its services, and run `setup(ctx)`. The imperative
|
|
1063
|
-
* counterpart to the hub's declarative `devframes` list (which batches the
|
|
1064
|
-
* phases via {@link prepareDevframe}); use it from `configure(ctx)` or
|
|
1065
|
-
* wherever you hold the context to plug in an extra devframe after startup.
|
|
1066
|
-
*/
|
|
1067
|
-
async function installDevframe(ctx, d, options = {}) {
|
|
1068
|
-
const run = await prepareDevframe(ctx, d, options);
|
|
1069
|
-
if (!run) return;
|
|
1070
|
-
await ctx.services.ready();
|
|
1071
|
-
await run();
|
|
1072
|
-
}
|
|
1073
|
-
//#endregion
|
|
1074
1087
|
//#region src/node/rpc-builtins.ts
|
|
1075
1088
|
/**
|
|
1076
1089
|
* Resolve an interactive (PTY) terminal session by id, or throw. Sessions
|
|
@@ -1324,4 +1337,4 @@ async function createHubContext(options) {
|
|
|
1324
1337
|
return context;
|
|
1325
1338
|
}
|
|
1326
1339
|
//#endregion
|
|
1327
|
-
export {
|
|
1340
|
+
export { DevframeCommandsHost as _, hubMessagesAdd as a, diagnostics as b, hubMessagesUpdate as c, hubTerminalsRestart as d, hubTerminalsTerminate as f, DevframeDocksHost as g, DevframeMessagesHost as h, hubDocksActivate as i, hubTerminalsRemove as l, DevframeTerminalsHost as m, builtinHubRpcDeclarations as n, hubMessagesClear as o, hubTerminalsWrite as p, hubCommandsExecute as r, hubMessagesRemove as s, createHubContext as t, hubTerminalsResize as u, prepareDevframe as v, skippedInStaticBuild as y };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Di-fhMKP.mjs";
|
|
2
|
+
import { DevframeHubUi, DevframesInput, DockRendererRegistration } from "./initiate.mjs";
|
|
3
|
+
import { DevframeServiceInput, DevframeStorageScope } from "devframe/types";
|
|
4
|
+
//#region src/node/build.d.ts
|
|
5
|
+
interface BuildHubOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Output directory the hub subtree is written into. It corresponds to the
|
|
8
|
+
* hub {@link BuildHubOptions.base} at serve time: building with
|
|
9
|
+
* `base: '/__devframes/'` into `dist/__devframes` makes the deployed app's
|
|
10
|
+
* `dist/` servable as-is by any static file server.
|
|
11
|
+
*/
|
|
12
|
+
outDir: string;
|
|
13
|
+
/**
|
|
14
|
+
* Mount base the deployed hub answers under (baked into every absolute URL
|
|
15
|
+
* the build emits: page-script rewrites, renderer modules, per-frame meta
|
|
16
|
+
* pointers). Default: `/__devframes/`.
|
|
17
|
+
*/
|
|
18
|
+
base?: string;
|
|
19
|
+
/** Devframes to bake as docks, same input as `initHub({ devframes })`. */
|
|
20
|
+
devframes?: DevframesInput;
|
|
21
|
+
/** Host-level wire services, same contract as `initHub({ services })`. */
|
|
22
|
+
services?: DevframeServiceInput[];
|
|
23
|
+
/** Extra RPC declarations registered at context creation. */
|
|
24
|
+
rpcDeclarations?: CreateHubContextOptions['builtinRpcDeclarations'];
|
|
25
|
+
/**
|
|
26
|
+
* Runs once the context exists and every `devframes` entry is mounted;
|
|
27
|
+
* register docks, commands, and messages surfaces here, exactly like
|
|
28
|
+
* `initHub({ configure })`. Everything it registers is baked into the
|
|
29
|
+
* static shared-state snapshot.
|
|
30
|
+
*/
|
|
31
|
+
configure?: (ctx: DevframeHubContext) => void | Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* The hub's UI slot: the viewer SPA is copied to the hub base, the
|
|
34
|
+
* `embedded.js` bootstrap next to it, and `setup(ctx)` runs so its static
|
|
35
|
+
* config (branding, dock preferences) is baked into the connection meta.
|
|
36
|
+
*/
|
|
37
|
+
ui?: DevframeHubUi;
|
|
38
|
+
/** Prebuilt dock-renderer modules, copied to `<base>__renderers/<type>.mjs`. */
|
|
39
|
+
renderers?: readonly DockRendererRegistration[];
|
|
40
|
+
/** Name written into `<base>__index.json`. */
|
|
41
|
+
name?: string;
|
|
42
|
+
/** Version written into `<base>__index.json`. */
|
|
43
|
+
version?: string;
|
|
44
|
+
/** Working directory for the hub context. Default: `process.cwd()`. */
|
|
45
|
+
cwd?: string;
|
|
46
|
+
/** Override where persisted devframe state lives during the build. */
|
|
47
|
+
getStorageDir?: (scope: DevframeStorageScope) => string;
|
|
48
|
+
/** Pretty-print RPC dump JSON files. Default: `false` (minified shards). */
|
|
49
|
+
pretty?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Produce a self-contained static deploy of a whole hub, the multi-devframe
|
|
53
|
+
* counterpart of devframe's `createBuild`:
|
|
54
|
+
*
|
|
55
|
+
* - Build a `mode: 'build'` hub context and mount every devframe: each
|
|
56
|
+
* SPA is copied to `<outDir>/<id>/`, an absolute-path page script to
|
|
57
|
+
* `<outDir>/<id>/__page-script/`, and its `setup(ctx)` runs.
|
|
58
|
+
* - Copy the UI slot's viewer SPA, `embedded.js`, and the renderer
|
|
59
|
+
* modules, and write the discovery documents (`__index.json`,
|
|
60
|
+
* `__client-imports.js`).
|
|
61
|
+
* - Write `__connection.json` (`{ backend: 'static' }`, carrying
|
|
62
|
+
* `ctx.staticConfig` as `configs`) at the hub base and at every frame
|
|
63
|
+
* base (each pointing back at the hub's own meta via `baseUrl`).
|
|
64
|
+
* - Bake the shared RPC dump under `<outDir>/__rpc-dump/`: every
|
|
65
|
+
* `static`/`snapshot` RPC plus a snapshot of each shared-state key
|
|
66
|
+
* (docks, commands, renderer manifest), so `createDevframeClientRuntime`
|
|
67
|
+
* and every frame SPA boot from the dump with no live server.
|
|
68
|
+
*
|
|
69
|
+
* Reads work from the baked dump; live writes (messages, terminals,
|
|
70
|
+
* commands execution) have no server and degrade to no-ops in the clients.
|
|
71
|
+
*/
|
|
72
|
+
declare function buildHub(options: BuildHubOptions): Promise<void>;
|
|
73
|
+
//#endregion
|
|
74
|
+
export { BuildHubOptions, buildHub };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY } from "../constants-Da23JxCZ.mjs";
|
|
2
|
+
import { b as diagnostics, t as createHubContext } from "../context-BWaW6N53.mjs";
|
|
3
|
+
import { i as resolveRendererRegistrations, n as renderClientImportsModule, r as resolveDevframesInput, t as mountDevframes } from "../assemble-Hj36CCzH.mjs";
|
|
4
|
+
import { joinURL } from "ufo";
|
|
5
|
+
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME } from "devframe/constants";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import process from "node:process";
|
|
9
|
+
import { createH3DevframeHost } from "devframe/internal";
|
|
10
|
+
import { collectStaticRpcDump, writeStaticRpcDump } from "devframe/rpc/dump";
|
|
11
|
+
import { colors } from "devframe/utils/colors";
|
|
12
|
+
import { dirname, resolve } from "pathe";
|
|
13
|
+
//#region src/node/build.ts
|
|
14
|
+
/**
|
|
15
|
+
* Produce a self-contained static deploy of a whole hub, the multi-devframe
|
|
16
|
+
* counterpart of devframe's `createBuild`:
|
|
17
|
+
*
|
|
18
|
+
* - Build a `mode: 'build'` hub context and mount every devframe: each
|
|
19
|
+
* SPA is copied to `<outDir>/<id>/`, an absolute-path page script to
|
|
20
|
+
* `<outDir>/<id>/__page-script/`, and its `setup(ctx)` runs.
|
|
21
|
+
* - Copy the UI slot's viewer SPA, `embedded.js`, and the renderer
|
|
22
|
+
* modules, and write the discovery documents (`__index.json`,
|
|
23
|
+
* `__client-imports.js`).
|
|
24
|
+
* - Write `__connection.json` (`{ backend: 'static' }`, carrying
|
|
25
|
+
* `ctx.staticConfig` as `configs`) at the hub base and at every frame
|
|
26
|
+
* base (each pointing back at the hub's own meta via `baseUrl`).
|
|
27
|
+
* - Bake the shared RPC dump under `<outDir>/__rpc-dump/`: every
|
|
28
|
+
* `static`/`snapshot` RPC plus a snapshot of each shared-state key
|
|
29
|
+
* (docks, commands, renderer manifest), so `createDevframeClientRuntime`
|
|
30
|
+
* and every frame SPA boot from the dump with no live server.
|
|
31
|
+
*
|
|
32
|
+
* Reads work from the baked dump; live writes (messages, terminals,
|
|
33
|
+
* commands execution) have no server and degrade to no-ops in the clients.
|
|
34
|
+
*/
|
|
35
|
+
async function buildHub(options) {
|
|
36
|
+
const base = normalizeHubBase(options.base ?? "/__devframes/");
|
|
37
|
+
const cwd = options.cwd ?? process.cwd();
|
|
38
|
+
const outDir = resolve(cwd, options.outDir);
|
|
39
|
+
const rendererRegistrations = resolveRendererRegistrations(options.renderers ?? []);
|
|
40
|
+
if (existsSync(outDir)) await fs.rm(outDir, { recursive: true });
|
|
41
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
42
|
+
/** Map a hub-base-relative URL base to its on-disk location under `outDir`. */
|
|
43
|
+
function resolveOutPath(urlBase) {
|
|
44
|
+
if (!urlBase.startsWith(base)) throw diagnostics.DF8006({
|
|
45
|
+
urlBase,
|
|
46
|
+
base
|
|
47
|
+
});
|
|
48
|
+
return resolve(outDir, urlBase.slice(base.length));
|
|
49
|
+
}
|
|
50
|
+
const frameMetaBases = [];
|
|
51
|
+
const host = {
|
|
52
|
+
...createH3DevframeHost({
|
|
53
|
+
origin: "http://localhost",
|
|
54
|
+
appName: "devframes",
|
|
55
|
+
workspaceRoot: cwd,
|
|
56
|
+
/**
|
|
57
|
+
* A static build "serves" by copying: a local dist verbatim, a remote
|
|
58
|
+
* assets source by materializing every listed file.
|
|
59
|
+
*/
|
|
60
|
+
mount: async (mountBase, source) => {
|
|
61
|
+
const target = resolveOutPath(mountBase);
|
|
62
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
63
|
+
if (typeof source === "string") await fs.cp(source, target, { recursive: true });
|
|
64
|
+
else await source.materialize(target);
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
...options.getStorageDir ? { getStorageDir: options.getStorageDir } : {},
|
|
68
|
+
mountConnectionMeta: (frameBase) => {
|
|
69
|
+
resolveOutPath(frameBase);
|
|
70
|
+
frameMetaBases.push(frameBase);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const ctx = await createHubContext({
|
|
74
|
+
cwd,
|
|
75
|
+
workspaceRoot: cwd,
|
|
76
|
+
mode: "build",
|
|
77
|
+
host,
|
|
78
|
+
...options.rpcDeclarations ? { builtinRpcDeclarations: options.rpcDeclarations } : {}
|
|
79
|
+
});
|
|
80
|
+
const devframes = await resolveDevframesInput(options.devframes ?? []);
|
|
81
|
+
for (const input of options.services ?? []) ctx.services.install(input);
|
|
82
|
+
const frames = [];
|
|
83
|
+
const setups = await mountDevframes(ctx, devframes, base, frames, false);
|
|
84
|
+
await ctx.services.ready();
|
|
85
|
+
for (const run of setups) await run();
|
|
86
|
+
await options.configure?.(ctx);
|
|
87
|
+
await options.ui?.setup?.(ctx);
|
|
88
|
+
await publishRendererManifest(ctx, rendererRegistrations, base, outDir);
|
|
89
|
+
await writeUiArtifacts(options.ui, outDir);
|
|
90
|
+
await fs.writeFile(resolve(outDir, DEVFRAME_DOCK_IMPORTS_FILENAME), renderClientImportsModule(ctx), "utf-8");
|
|
91
|
+
await fs.writeFile(resolve(outDir, "__index.json"), `${JSON.stringify({
|
|
92
|
+
name: options.name,
|
|
93
|
+
version: options.version,
|
|
94
|
+
base,
|
|
95
|
+
frames,
|
|
96
|
+
endpoints: {
|
|
97
|
+
connection: DEVFRAME_CONNECTION_META_FILENAME,
|
|
98
|
+
clientImports: DEVFRAME_DOCK_IMPORTS_FILENAME,
|
|
99
|
+
index: "__index.json",
|
|
100
|
+
...options.ui?.embedded ? { embedded: "embedded.js" } : {}
|
|
101
|
+
}
|
|
102
|
+
}, null, 2)}\n`, "utf-8");
|
|
103
|
+
await writeConnectionMetas(ctx, base, outDir, frameMetaBases.map(resolveOutPath));
|
|
104
|
+
console.log(colors.cyan`[devframes-hub] writing RPC dump to ${resolve(outDir, "__rpc-dump")}`);
|
|
105
|
+
const dump = await collectStaticRpcDump(ctx.rpc.definitions.values(), ctx);
|
|
106
|
+
await writeStaticRpcDump(dump, outDir, { pretty: options.pretty });
|
|
107
|
+
console.log(colors.green`[devframes-hub] built ${frames.length} devframe${frames.length === 1 ? "" : "s"} -> ${outDir}`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Publish the renderer manifest exactly like `initHub` does, so it is baked
|
|
111
|
+
* into the shared-state snapshot, and copy each prebuilt module to the URL
|
|
112
|
+
* the manifest advertises.
|
|
113
|
+
*/
|
|
114
|
+
async function publishRendererManifest(ctx, registrations, base, outDir) {
|
|
115
|
+
const manifest = {};
|
|
116
|
+
for (const registration of registrations) {
|
|
117
|
+
manifest[registration.type] = {
|
|
118
|
+
importFrom: joinURL(base, "__renderers", `${registration.type}.mjs`),
|
|
119
|
+
...registration.importName ? { importName: registration.importName } : {}
|
|
120
|
+
};
|
|
121
|
+
await fs.mkdir(resolve(outDir, "__renderers"), { recursive: true });
|
|
122
|
+
await fs.copyFile(registration.file, resolve(outDir, "__renderers", `${registration.type}.mjs`));
|
|
123
|
+
}
|
|
124
|
+
(await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Copy the UI slot's artifacts: the viewer SPA owns the hub base (copied
|
|
128
|
+
* before the discovery documents, so those win over same-named files it
|
|
129
|
+
* ships), `embedded.js` next to it, plus any produced assets.
|
|
130
|
+
*/
|
|
131
|
+
async function writeUiArtifacts(ui, outDir) {
|
|
132
|
+
if (ui?.viewer) await fs.cp(resolve(ui.viewer.distDir), outDir, { recursive: true });
|
|
133
|
+
if (ui?.embedded) await fs.copyFile(resolve(ui.embedded.entry), resolve(outDir, "embedded.js"));
|
|
134
|
+
for (const [key, produce] of Object.entries(ui?.assets ?? {})) {
|
|
135
|
+
const target = resolve(outDir, key);
|
|
136
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
137
|
+
await fs.writeFile(target, produce());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Write the `backend: 'static'` connection meta at the hub base, and a copy
|
|
142
|
+
* at every frame base whose `baseUrl` points relative resolution (the RPC
|
|
143
|
+
* dump) back at the hub's own meta, so a frame SPA that fetched its
|
|
144
|
+
* per-frame copy (instead of inheriting the host page's connection) still
|
|
145
|
+
* finds the shared dump.
|
|
146
|
+
*/
|
|
147
|
+
async function writeConnectionMetas(ctx, base, outDir, frameDirs) {
|
|
148
|
+
const jsonSerializableMethods = [];
|
|
149
|
+
for (const def of ctx.rpc.definitions.values()) if (def.jsonSerializable === true) jsonSerializableMethods.push(def.name);
|
|
150
|
+
const meta = {
|
|
151
|
+
backend: "static",
|
|
152
|
+
jsonSerializableMethods,
|
|
153
|
+
...Object.keys(ctx.staticConfig).length > 0 ? { configs: ctx.staticConfig } : {}
|
|
154
|
+
};
|
|
155
|
+
await fs.writeFile(resolve(outDir, DEVFRAME_CONNECTION_META_FILENAME), JSON.stringify(meta, null, 2), "utf-8");
|
|
156
|
+
const frameMeta = {
|
|
157
|
+
...meta,
|
|
158
|
+
baseUrl: joinURL(base, DEVFRAME_CONNECTION_META_FILENAME)
|
|
159
|
+
};
|
|
160
|
+
for (const frameDir of frameDirs) {
|
|
161
|
+
const target = resolve(frameDir, DEVFRAME_CONNECTION_META_FILENAME);
|
|
162
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
163
|
+
await fs.writeFile(target, JSON.stringify(frameMeta, null, 2), "utf-8");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { buildHub };
|
package/dist/node/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { _ as DevframeCommandsHost, a as hubMessagesAdd, c as hubMessagesUpdate, d as hubTerminalsRestart, f as hubTerminalsTerminate, g as DevframeDocksHost, h as DevframeMessagesHost, i as hubDocksActivate, l as hubTerminalsRemove, m as DevframeTerminalsHost, n as builtinHubRpcDeclarations, o as hubMessagesClear, p as hubTerminalsWrite, r as hubCommandsExecute, s as hubMessagesRemove, t as createHubContext, u as hubTerminalsResize } from "../context-BWaW6N53.mjs";
|
|
2
2
|
import "ufo";
|
|
3
|
-
import "pathe";
|
|
4
|
-
import "node:process";
|
|
5
3
|
import "node:fs";
|
|
4
|
+
import "node:process";
|
|
5
|
+
import "pathe";
|
|
6
6
|
import { builtinModules } from "node:module";
|
|
7
7
|
import "node:url";
|
|
8
8
|
import assert from "node:assert";
|
package/dist/node/initiate.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as InstallDevframeOptions, n as DevframeHubContext, t as CreateHubContextOptions } from "../context-Di-fhMKP.mjs";
|
|
2
|
-
import { r as DEVFRAMES_HUB_BASE } from "../constants-
|
|
2
|
+
import { r as DEVFRAMES_HUB_BASE } from "../constants-Bz4yclHo.mjs";
|
|
3
3
|
import { DevframeInstanceRecord } from "devframe/internal";
|
|
4
|
-
import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions,
|
|
4
|
+
import { ConnectionMeta, DevframeDefinition, DevframeServiceInput, DevframeSseOptions, DevframeStorageScope, DevframeWsOptions, McpSetting } from "devframe/types";
|
|
5
5
|
import { DevframeAuthHandler } from "devframe/node/auth";
|
|
6
6
|
import { WsOriginRegistry } from "devframe/rpc/transports/ws-server";
|
|
7
7
|
import { Buffer } from "node:buffer";
|
|
@@ -197,14 +197,16 @@ interface InitHubOptions {
|
|
|
197
197
|
/**
|
|
198
198
|
* Expose the **aggregate** MCP endpoint at `<base>__mcp`: one
|
|
199
199
|
* Streamable-HTTP server over the shared context's whole tool registry
|
|
200
|
-
* (ids are already namespaced per plugin).
|
|
201
|
-
* mounts
|
|
202
|
-
* an
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
200
|
+
* (ids are already namespaced per plugin). Defaults to `'auto'`: the route
|
|
201
|
+
* mounts once any mounted devframe (or an agent-flagged hub command)
|
|
202
|
+
* exposes an agent surface. `true` mounts it
|
|
203
|
+
* unconditionally with the loopback origin gate (trusting same-machine
|
|
204
|
+
* callers), an object opts into an {@link McpRouteOptions.authorization}
|
|
205
|
+
* identity check, `false` keeps it off. A mounted devframe's own `mcp`
|
|
206
|
+
* setting is ignored: the hub's aggregate route covers them all (`DF8005`
|
|
207
|
+
* warns when one asks for MCP while this is `false`).
|
|
206
208
|
*/
|
|
207
|
-
mcp?:
|
|
209
|
+
mcp?: McpSetting;
|
|
208
210
|
/**
|
|
209
211
|
* Public origin the host app is reachable at, or a getter. Derived lazily
|
|
210
212
|
* from the first request when omitted.
|
package/dist/node/initiate.mjs
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { joinURL,
|
|
5
|
-
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE
|
|
6
|
-
import { createH3DevframeHost, createInstanceShell, importRuntimeModule, resolveInstanceRegister, resolveMcpConfig } from "devframe/internal";
|
|
7
|
-
import { resolve } from "pathe";
|
|
8
|
-
import process from "node:process";
|
|
9
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { a as normalizeHubBase, i as DOCK_RENDERERS_STATE_KEY, r as DEVFRAMES_HUB_BASE } from "../constants-Da23JxCZ.mjs";
|
|
2
|
+
import { b as diagnostics, t as createHubContext } from "../context-BWaW6N53.mjs";
|
|
3
|
+
import { i as resolveRendererRegistrations, n as renderClientImportsModule, r as resolveDevframesInput, t as mountDevframes } from "../assemble-Hj36CCzH.mjs";
|
|
4
|
+
import { joinURL, withoutLeadingSlash } from "ufo";
|
|
5
|
+
import { DEVFRAME_CONNECTION_META_FILENAME, DEVFRAME_DOCK_IMPORTS_FILENAME, DEVFRAME_MCP_ROUTE } from "devframe/constants";
|
|
10
6
|
import { readFile } from "node:fs/promises";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { createH3DevframeHost, createInstanceShell, importRuntimeModule, loadAutoMcpAdapter, resolveInstanceRegister, resolveMcpConfig } from "devframe/internal";
|
|
9
|
+
import { resolve } from "pathe";
|
|
11
10
|
import { mountStaticHandler } from "devframe/utils/serve-static";
|
|
12
11
|
import { H3 } from "h3";
|
|
13
12
|
//#region src/node/initiate.ts
|
|
14
|
-
function normalizeDevframeEntry(entry) {
|
|
15
|
-
return "devframe" in entry ? entry : { devframe: entry };
|
|
16
|
-
}
|
|
17
13
|
/** Content-type for a UI asset key, inferred from its file extension. */
|
|
18
14
|
function assetContentType(key) {
|
|
19
15
|
if (key.endsWith(".json")) return "application/json; charset=utf-8";
|
|
@@ -23,72 +19,6 @@ function assetContentType(key) {
|
|
|
23
19
|
if (key.endsWith(".html")) return "text/html; charset=utf-8";
|
|
24
20
|
return "application/octet-stream";
|
|
25
21
|
}
|
|
26
|
-
/** Reserved filenames directly under the hub base; a frame id can't shadow them. */
|
|
27
|
-
const RESERVED_HUB_PATHS = [
|
|
28
|
-
DEVFRAME_CONNECTION_META_FILENAME,
|
|
29
|
-
DEVFRAME_DOCK_IMPORTS_FILENAME,
|
|
30
|
-
DEVFRAME_WS_ROUTE,
|
|
31
|
-
DEVFRAME_MCP_ROUTE,
|
|
32
|
-
"__index.json",
|
|
33
|
-
"__renderers",
|
|
34
|
-
"embedded.js"
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* Flatten the `devframes` input: await every thenable, call every factory,
|
|
38
|
-
* spread the arrays, and drop the empty slots, so a host can build the list
|
|
39
|
-
* conditionally (`isDev && loadInspect()`) without filtering it first.
|
|
40
|
-
*/
|
|
41
|
-
function resolveDevframesInput(input) {
|
|
42
|
-
return Promise.all(input.map(async (entry) => {
|
|
43
|
-
const resolved = await (typeof entry === "function" ? entry() : entry);
|
|
44
|
-
return (Array.isArray(resolved) ? resolved : [resolved]).filter((slot) => slot != null).map(normalizeDevframeEntry);
|
|
45
|
-
})).then((arrays) => arrays.flat());
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Validate the renderer-module registrations fail-fast: route-safe types
|
|
49
|
-
* (each becomes the `<base>__renderers/<type>.mjs` URL segment), one module
|
|
50
|
-
* per type, and an existing bundle file (renderer packages are prebuilt).
|
|
51
|
-
*/
|
|
52
|
-
function resolveRendererRegistrations(registrations) {
|
|
53
|
-
const seen = /* @__PURE__ */ new Set();
|
|
54
|
-
return registrations.map((registration) => {
|
|
55
|
-
if (!/^[\w.-]+$/.test(registration.type)) throw diagnostics.DF8110({ type: registration.type });
|
|
56
|
-
if (seen.has(registration.type)) throw diagnostics.DF8108({ type: registration.type });
|
|
57
|
-
seen.add(registration.type);
|
|
58
|
-
const file = resolve(registration.file);
|
|
59
|
-
if (!existsSync(file)) throw diagnostics.DF8109({
|
|
60
|
-
type: registration.type,
|
|
61
|
-
file
|
|
62
|
-
});
|
|
63
|
-
return {
|
|
64
|
-
...registration,
|
|
65
|
-
file
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Render the dock client-script import map as an ES module: one dynamic
|
|
71
|
-
* import thunk per dock that carries a client script (`clientScript` on
|
|
72
|
-
* iframe docks, `action`, `renderer`). External viewers import this module
|
|
73
|
-
* from `<base>__client-imports.js` to load per-dock client code into the
|
|
74
|
-
* host page; `importFrom` values are URL paths the host serves, or bare npm
|
|
75
|
-
* specifiers resolved through the host's `clientModuleResolution` template.
|
|
76
|
-
*/
|
|
77
|
-
function renderClientImportsModule(ctx) {
|
|
78
|
-
const template = ctx.staticConfig.dock?.clientModuleResolution;
|
|
79
|
-
const entries = [];
|
|
80
|
-
for (const [id, view] of ctx.docks.views) {
|
|
81
|
-
const scripts = [];
|
|
82
|
-
const anyView = view;
|
|
83
|
-
if (anyView.clientScript) scripts.push(anyView.clientScript);
|
|
84
|
-
if (anyView.action) scripts.push(anyView.action);
|
|
85
|
-
if (anyView.renderer) scripts.push(anyView.renderer);
|
|
86
|
-
if (scripts.length === 0) continue;
|
|
87
|
-
const thunks = scripts.map((script) => `() => import(${JSON.stringify(resolveClientModuleSpecifier(script.importFrom, { template }))})`);
|
|
88
|
-
entries.push(` ${JSON.stringify(id)}: [${thunks.join(", ")}],`);
|
|
89
|
-
}
|
|
90
|
-
return `// Generated by @devframes/hub: dock client-script import map.\nexport const clientImports = {\n${entries.join("\n")}\n}\nexport default clientImports\n`;
|
|
91
|
-
}
|
|
92
22
|
/**
|
|
93
23
|
* Resolve the shared hub context: reuse a caller-supplied one, or build an
|
|
94
24
|
* h3-backed context that serves the hub's connection meta under every frame.
|
|
@@ -126,31 +56,6 @@ async function resolveHubContext(options, app, cwd, api) {
|
|
|
126
56
|
});
|
|
127
57
|
}
|
|
128
58
|
/**
|
|
129
|
-
* Pass 1: mount each devframe under `<base><id>/` (SPA, meta, iframe dock)
|
|
130
|
-
* and queue its declared services, guarding the id against reserved hub
|
|
131
|
-
* filenames and route-pattern characters. Returns the deferred setup thunks.
|
|
132
|
-
*/
|
|
133
|
-
async function mountDevframes(ctx, devframes, base, frames, hubMcpEnabled) {
|
|
134
|
-
const setups = [];
|
|
135
|
-
for (const { devframe: def, dock } of devframes) {
|
|
136
|
-
if (RESERVED_HUB_PATHS.includes(def.id)) throw diagnostics.DF8000({ id: def.id });
|
|
137
|
-
if (!/^[\w.-]+$/.test(def.id)) throw diagnostics.DF8004({ id: def.id });
|
|
138
|
-
if (!hubMcpEnabled && def.cli?.mcp) diagnostics.DF8005({ id: def.id });
|
|
139
|
-
const frameBase = withTrailingSlash(joinURL(base, def.id));
|
|
140
|
-
const run = await prepareDevframe(ctx, def, {
|
|
141
|
-
base: frameBase,
|
|
142
|
-
...dock ? { dock } : {}
|
|
143
|
-
});
|
|
144
|
-
if (run) setups.push(run);
|
|
145
|
-
frames.push({
|
|
146
|
-
id: def.id,
|
|
147
|
-
base: frameBase,
|
|
148
|
-
title: def.name
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
return setups;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
59
|
* Initiate a hub instance: the whole multi-devframe devtools surface
|
|
155
60
|
* behind one framework-agnostic, web-standard handler. Every mounted
|
|
156
61
|
* devframe shares one context (merged RPC registry, shared state, docks /
|
|
@@ -195,7 +100,7 @@ function initHub(options) {
|
|
|
195
100
|
*/
|
|
196
101
|
absoluteWsPath: true,
|
|
197
102
|
resolveSidecarPort: async (sidecarHost) => {
|
|
198
|
-
const { getPort } = await import("../dist-
|
|
103
|
+
const { getPort } = await import("../dist-BP3CGpgZ.mjs");
|
|
199
104
|
return getPort({
|
|
200
105
|
port: 9777,
|
|
201
106
|
portRange: [9777, 9877],
|
|
@@ -213,7 +118,7 @@ function initHub(options) {
|
|
|
213
118
|
};
|
|
214
119
|
const devframes = await resolveDevframesInput(options.devframes ?? []);
|
|
215
120
|
for (const input of options.services ?? []) ctx.services.install(input);
|
|
216
|
-
const setups = await mountDevframes(ctx, devframes, base, frames,
|
|
121
|
+
const setups = await mountDevframes(ctx, devframes, base, frames, options.mcp !== false);
|
|
217
122
|
await ctx.services.ready();
|
|
218
123
|
for (const run of setups) await run();
|
|
219
124
|
await options.configure?.(ctx);
|
|
@@ -224,11 +129,17 @@ function initHub(options) {
|
|
|
224
129
|
...registration.importName ? { importName: registration.importName } : {}
|
|
225
130
|
};
|
|
226
131
|
(await ctx.rpc.sharedState.get(DOCK_RENDERERS_STATE_KEY, { initialValue: {} })).mutate(() => manifest);
|
|
227
|
-
const
|
|
132
|
+
const mcpSetting = options.mcp ?? "auto";
|
|
133
|
+
let mcpModule;
|
|
134
|
+
let mcpConfig;
|
|
135
|
+
if (mcpSetting === "auto") {
|
|
136
|
+
mcpModule = await loadAutoMcpAdapter(ctx.agent);
|
|
137
|
+
if (mcpModule) mcpConfig = { authorization: false };
|
|
138
|
+
} else mcpConfig = resolveMcpConfig(mcpSetting);
|
|
228
139
|
if (!mcpConfig) return { context: ctx };
|
|
229
140
|
const mcpRoute = withoutLeadingSlash(mcpConfig.path ?? DEVFRAME_MCP_ROUTE);
|
|
230
|
-
|
|
231
|
-
const mounted = mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
|
|
141
|
+
mcpModule ??= await importRuntimeModule("devframe/adapters/mcp");
|
|
142
|
+
const mounted = mcpModule.mountMcpHttp(app, ctx, joinURL(base, mcpRoute), {
|
|
232
143
|
serverName: options.name ?? "devframes-hub",
|
|
233
144
|
serverVersion: options.version ?? "0.0.0",
|
|
234
145
|
exposeSharedState: true,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devframes/hub",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.10",
|
|
5
5
|
"description": "Hub layer that orchestrates devframe docks, terminals, messages, and commands on any host.",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"sideEffects": false,
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./dist/index.mjs",
|
|
25
|
+
"./build": "./dist/node/build.mjs",
|
|
25
26
|
"./client": "./dist/client/index.mjs",
|
|
26
27
|
"./constants": "./dist/constants.mjs",
|
|
27
28
|
"./initiate": "./dist/node/initiate.mjs",
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"dist"
|
|
35
36
|
],
|
|
36
37
|
"peerDependencies": {
|
|
37
|
-
"devframe": "0.9.
|
|
38
|
+
"devframe": "0.9.10"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@types/node": "^26.4.0",
|
|
51
|
-
"devframe": "0.9.
|
|
52
|
+
"devframe": "0.9.10",
|
|
52
53
|
"get-port-please": "^3.2.0",
|
|
53
54
|
"mlly": "^1.8.2",
|
|
54
55
|
"tsdown": "^0.22.14",
|