@devframes/hub 0.5.4 → 0.6.0-beta.1

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.
@@ -1,5 +1,5 @@
1
- import { E as RemoteConnectionInfo, R as DevframeMessagesClient, d as DevframeDockEntriesGrouped, f as DevframeDockEntry, g as DevframeDockUserEntry } from "../context-Dox-G9vt.mjs";
2
- import { i as DevframeCommandEntry, n as DevframeClientCommand, o as DevframeCommandKeybinding, t as DevframeDocksUserSettings } from "../settings-Bp5Ax6Os.mjs";
1
+ import { b as DevframeMessagesClient, h as DevframeMessageEntryInput } from "../context-DVh4gk8f.mjs";
2
+ import { O as RemoteConnectionInfo, i as DevframeCommandEntry, m as DevframeDockEntry, n as DevframeClientCommand, o as DevframeCommandKeybinding, p as DevframeDockEntriesGrouped, t as DevframeDocksUserSettings, v as DevframeDockUserEntry } from "../settings-B9YjSEcl.mjs";
3
3
  import { DevframeClientRpcHost, DevframeRpcClient, DevframeRpcClientOptions, DevframeRpcContext, RpcClientEvents } from "devframe/client";
4
4
  import { EventEmitter } from "devframe/types";
5
5
  import { SharedState } from "devframe/utils/shared-state";
@@ -138,7 +138,8 @@ interface DockClientScriptContext extends DocksContext {
138
138
  */
139
139
  current: DockEntryState;
140
140
  /**
141
- * Messages client scoped to this dock entry's source
141
+ * Messages client scoped to this dock entry — messages it adds default
142
+ * their `category` to the entry id, so the feed can attribute them.
142
143
  */
143
144
  messages: DevframeMessagesClient;
144
145
  }
@@ -149,6 +150,69 @@ declare const CLIENT_CONTEXT_KEY = "__DEVFRAME_HUB_CLIENT_CONTEXT__";
149
150
  * Get the global Devframe client context, or `undefined` if not yet initialized.
150
151
  */
151
152
  declare function getDevframeClientContext(): DevframeClientContext | undefined;
153
+ /**
154
+ * Publish the global Devframe client context (or clear it with `undefined`).
155
+ * Called by {@link import('./host').createDevframeClientHost}; a dock client
156
+ * script or a viewer reads it back with {@link getDevframeClientContext}.
157
+ */
158
+ declare function setDevframeClientContext(ctx: DevframeClientContext | undefined): void;
159
+ //#endregion
160
+ //#region src/client/host.d.ts
161
+ interface DevframeClientHostOptions {
162
+ /**
163
+ * An already-connected RPC client. When omitted, one is created via
164
+ * `connectDevframe(connect)` — pass `connect.baseURL` to point at the hub's
165
+ * connection-meta mount (e.g. `/__hub/`).
166
+ */
167
+ rpc?: DevframeRpcClient;
168
+ /** Options forwarded to `connectDevframe` when `rpc` is not supplied. */
169
+ connect?: DevframeRpcClientOptions;
170
+ /**
171
+ * Environment the host runs in.
172
+ * - `'standalone'` (default) — the runtime owns the whole page (a hub UI).
173
+ * - `'embedded'` — the runtime lives inside a user app alongside a panel.
174
+ */
175
+ clientType?: DockClientType;
176
+ /**
177
+ * Import and run the client scripts declared on dock entries (`action`,
178
+ * `custom-render`, and iframe `clientScript`). Default `true`.
179
+ */
180
+ loadClientScripts?: boolean;
181
+ }
182
+ interface DevframeClientHost {
183
+ /** The assembled, globally-registered client host context. */
184
+ context: DevframeClientContext;
185
+ /** Tear down listeners and stop tracking newly-registered client scripts. */
186
+ dispose: () => void;
187
+ }
188
+ /**
189
+ * Boot the framework-level client host: connect RPC, assemble the full
190
+ * {@link DevframeClientContext} (panel, docks, commands, when) from the hub's
191
+ * shared state, publish it at `__DEVFRAME_HUB_CLIENT_CONTEXT__`, and load every
192
+ * dock entry's client script into this page — the devframe equivalent of the
193
+ * runtime `@vitejs/devtools-kit` injects into a host app.
194
+ *
195
+ * A viewer keeps rendering its own dock UI (reading the same shared state); this
196
+ * runtime is what gives plugin client scripts a live host context to run in.
197
+ */
198
+ declare function createDevframeClientHost(options?: DevframeClientHostOptions): Promise<DevframeClientHost>;
199
+ //#endregion
200
+ //#region src/client/messages.d.ts
201
+ interface MessagesClientOptions {
202
+ /**
203
+ * Default fields merged beneath every `add()` input — the client host passes
204
+ * `{ category: entry.id }` to scope a dock client script's messages to its
205
+ * entry. Fields set on the input itself win.
206
+ */
207
+ defaults?: Partial<DevframeMessageEntryInput>;
208
+ }
209
+ /**
210
+ * Build a browser-side {@link DevframeMessagesClient} that writes into the
211
+ * hub's messages subsystem over the `hub:messages:*` built-in RPCs. The handle
212
+ * returned by `add` proxies `update`/`dismiss` back through the same RPCs, so a
213
+ * dock client script reports into the very feed the server writes to.
214
+ */
215
+ declare function createMessagesClient(rpc: DevframeRpcClient, options?: MessagesClientOptions): DevframeMessagesClient;
152
216
  //#endregion
153
217
  //#region src/client/remote.d.ts
154
218
  type ConnectRemoteDevframeOptions = Omit<DevframeRpcClientOptions, 'connectionMeta' | 'authToken'>;
@@ -171,4 +235,4 @@ declare function parseRemoteConnection(input?: string): RemoteConnectionInfo | n
171
235
  */
172
236
  declare function connectRemoteDevframe(options?: ConnectRemoteDevframeOptions): Promise<DevframeRpcClient>;
173
237
  //#endregion
174
- export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, DevframeClientContext, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DocksContext, DocksEntriesContext, DocksPanelContext, type RpcClientEvents, WhenClauseContext, connectRemoteDevframe, getDevframeClientContext, parseRemoteConnection };
238
+ export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DocksContext, DocksEntriesContext, DocksPanelContext, MessagesClientOptions, type RpcClientEvents, WhenClauseContext, connectRemoteDevframe, createDevframeClientHost, createMessagesClient, getDevframeClientContext, parseRemoteConnection, setDevframeClientContext };
@@ -1,5 +1,8 @@
1
+ import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS } from "../constants.mjs";
1
2
  import { REMOTE_CONNECTION_KEY } from "devframe/constants";
2
- import { getDevframeRpcClient } from "devframe/client";
3
+ import { connectDevframe, getDevframeRpcClient } from "devframe/client";
4
+ import { createEventEmitter } from "devframe/utils/events";
5
+ import { destr } from "destr";
3
6
  export * from "devframe/client";
4
7
  //#region src/client/context.ts
5
8
  const CLIENT_CONTEXT_KEY = "__DEVFRAME_HUB_CLIENT_CONTEXT__";
@@ -9,6 +12,282 @@ const CLIENT_CONTEXT_KEY = "__DEVFRAME_HUB_CLIENT_CONTEXT__";
9
12
  function getDevframeClientContext() {
10
13
  return globalThis[CLIENT_CONTEXT_KEY];
11
14
  }
15
+ /**
16
+ * Publish the global Devframe client context (or clear it with `undefined`).
17
+ * Called by {@link import('./host').createDevframeClientHost}; a dock client
18
+ * script or a viewer reads it back with {@link getDevframeClientContext}.
19
+ */
20
+ function setDevframeClientContext(ctx) {
21
+ globalThis[CLIENT_CONTEXT_KEY] = ctx;
22
+ }
23
+ //#endregion
24
+ //#region src/client/messages.ts
25
+ /**
26
+ * Build a browser-side {@link DevframeMessagesClient} that writes into the
27
+ * hub's messages subsystem over the `hub:messages:*` built-in RPCs. The handle
28
+ * returned by `add` proxies `update`/`dismiss` back through the same RPCs, so a
29
+ * dock client script reports into the very feed the server writes to.
30
+ */
31
+ function createMessagesClient(rpc, options = {}) {
32
+ const call = rpc.call;
33
+ function makeHandle(entry) {
34
+ let current = entry;
35
+ return {
36
+ get entry() {
37
+ return current;
38
+ },
39
+ get id() {
40
+ return current.id;
41
+ },
42
+ async update(patch) {
43
+ const updated = await call("hub:messages:update", current.id, patch);
44
+ if (updated) current = updated;
45
+ return updated;
46
+ },
47
+ dismiss: () => call("hub:messages:remove", current.id)
48
+ };
49
+ }
50
+ async function add(input) {
51
+ return makeHandle(await call("hub:messages:add", {
52
+ ...options.defaults,
53
+ ...input
54
+ }));
55
+ }
56
+ function levelShortcut(level) {
57
+ return (message, extra) => add({
58
+ ...extra,
59
+ message,
60
+ level
61
+ });
62
+ }
63
+ return {
64
+ add,
65
+ remove: (id) => call("hub:messages:remove", id),
66
+ clear: () => call("hub:messages:clear"),
67
+ info: levelShortcut("info"),
68
+ warn: levelShortcut("warn"),
69
+ error: levelShortcut("error"),
70
+ success: levelShortcut("success"),
71
+ debug: levelShortcut("debug")
72
+ };
73
+ }
74
+ //#endregion
75
+ //#region src/client/host.ts
76
+ const DOCKS_STATE_KEY = "devframe:docks";
77
+ const COMMANDS_STATE_KEY = "devframe:commands";
78
+ const USER_SETTINGS_STATE_KEY = "devframe:user-settings";
79
+ /**
80
+ * Boot the framework-level client host: connect RPC, assemble the full
81
+ * {@link DevframeClientContext} (panel, docks, commands, when) from the hub's
82
+ * shared state, publish it at `__DEVFRAME_HUB_CLIENT_CONTEXT__`, and load every
83
+ * dock entry's client script into this page — the devframe equivalent of the
84
+ * runtime `@vitejs/devtools-kit` injects into a host app.
85
+ *
86
+ * A viewer keeps rendering its own dock UI (reading the same shared state); this
87
+ * runtime is what gives plugin client scripts a live host context to run in.
88
+ */
89
+ async function createDevframeClientHost(options = {}) {
90
+ const clientType = options.clientType ?? "standalone";
91
+ const rpc = options.rpc ?? await connectDevframe(options.connect);
92
+ const [docksState, commandsState, settings] = await Promise.all([
93
+ rpc.sharedState.get(DOCKS_STATE_KEY, { initialValue: [] }),
94
+ rpc.sharedState.get(COMMANDS_STATE_KEY, { initialValue: [] }),
95
+ rpc.sharedState.get(USER_SETTINGS_STATE_KEY, { initialValue: DEFAULT_STATE_USER_SETTINGS() })
96
+ ]);
97
+ let selectedId = null;
98
+ const entryToStateMap = /* @__PURE__ */ new Map();
99
+ const panel = createPanelContext(clientType);
100
+ const docks = createDocksContext();
101
+ const commands = createCommandsContext();
102
+ const context = {
103
+ rpc,
104
+ clientType,
105
+ panel,
106
+ docks,
107
+ commands,
108
+ when: { get context() {
109
+ return {
110
+ clientType,
111
+ dockOpen: panel.store.open,
112
+ paletteOpen: commands.paletteOpen,
113
+ dockSelectedId: selectedId ?? ""
114
+ };
115
+ } }
116
+ };
117
+ const disposers = [];
118
+ reconcileEntries();
119
+ disposers.push(docksState.on("updated", reconcileEntries));
120
+ if (getDevframeClientContext()) console.warn("[@devframes/hub] A client host context is already published on this page — replacing it. Boot createDevframeClientHost() once per page (e.g. HTML injection combined with a manual import boots it twice).");
121
+ setDevframeClientContext(context);
122
+ const loadedScripts = /* @__PURE__ */ new Set();
123
+ if (options.loadClientScripts ?? true) {
124
+ loadClientScripts();
125
+ disposers.push(docksState.on("updated", loadClientScripts));
126
+ }
127
+ return {
128
+ context,
129
+ dispose() {
130
+ for (const off of disposers.splice(0)) off();
131
+ if (getDevframeClientContext() === context) setDevframeClientContext(void 0);
132
+ }
133
+ };
134
+ function createDockEntryState(entryMeta) {
135
+ return {
136
+ entryMeta,
137
+ get isActive() {
138
+ return selectedId === entryMeta.id;
139
+ },
140
+ domElements: {},
141
+ events: createEventEmitter()
142
+ };
143
+ }
144
+ function reconcileEntries() {
145
+ const entries = docksState.value();
146
+ const seen = /* @__PURE__ */ new Set();
147
+ for (const meta of entries) {
148
+ seen.add(meta.id);
149
+ const existing = entryToStateMap.get(meta.id);
150
+ if (!existing) entryToStateMap.set(meta.id, createDockEntryState(meta));
151
+ else if (existing.entryMeta !== meta) {
152
+ existing.entryMeta = meta;
153
+ existing.events.emit("entry:updated", meta);
154
+ }
155
+ }
156
+ for (const id of [...entryToStateMap.keys()]) if (!seen.has(id)) entryToStateMap.delete(id);
157
+ docks.entries = entries;
158
+ docks.groupedEntries = groupByCategory(entries);
159
+ if (selectedId && !entryToStateMap.has(selectedId)) selectedId = null;
160
+ }
161
+ function createDocksContext() {
162
+ return {
163
+ get selectedId() {
164
+ return selectedId;
165
+ },
166
+ set selectedId(id) {
167
+ switchEntry(id);
168
+ },
169
+ get selected() {
170
+ return selectedId && entryToStateMap.get(selectedId)?.entryMeta || null;
171
+ },
172
+ entries: [],
173
+ entryToStateMap,
174
+ groupedEntries: [],
175
+ settings,
176
+ getStateById: (id) => entryToStateMap.get(id),
177
+ switchEntry,
178
+ toggleEntry: (id) => selectedId === id ? switchEntry(null) : switchEntry(id)
179
+ };
180
+ }
181
+ async function switchEntry(id) {
182
+ const next = id ?? null;
183
+ if (next === selectedId) return false;
184
+ if (next !== null && !entryToStateMap.has(next)) return false;
185
+ const previous = selectedId;
186
+ selectedId = next;
187
+ if (previous) entryToStateMap.get(previous)?.events.emit("entry:deactivated");
188
+ if (next) entryToStateMap.get(next)?.events.emit("entry:activated");
189
+ return true;
190
+ }
191
+ function createCommandsContext() {
192
+ const clientCommands = /* @__PURE__ */ new Map();
193
+ function allCommands() {
194
+ return [...commandsState.value(), ...clientCommands.values()];
195
+ }
196
+ return {
197
+ get commands() {
198
+ return allCommands();
199
+ },
200
+ get paletteCommands() {
201
+ return allCommands().filter((c) => c.showInPalette !== false);
202
+ },
203
+ register(input) {
204
+ const list = Array.isArray(input) ? input : [input];
205
+ for (const cmd of list) clientCommands.set(cmd.id, cmd);
206
+ return () => {
207
+ for (const cmd of list) clientCommands.delete(cmd.id);
208
+ };
209
+ },
210
+ async execute(id, ...args) {
211
+ const client = clientCommands.get(id);
212
+ if (client?.action) return client.action(...args);
213
+ return rpc.call("hub:commands:execute", id, ...args);
214
+ },
215
+ getKeybindings(id) {
216
+ const override = settings.value().commandShortcuts?.[id];
217
+ if (override) return override;
218
+ return allCommands().find((c) => c.id === id)?.keybindings ?? [];
219
+ },
220
+ settings,
221
+ paletteOpen: false
222
+ };
223
+ }
224
+ function clientScriptOf(entry) {
225
+ return entry.action ?? entry.renderer ?? entry.clientScript;
226
+ }
227
+ function loadClientScripts() {
228
+ for (const entry of docksState.value()) {
229
+ const script = clientScriptOf(entry);
230
+ if (!script?.importFrom || loadedScripts.has(entry.id)) continue;
231
+ loadedScripts.add(entry.id);
232
+ runClientScript(entry.id, script);
233
+ }
234
+ }
235
+ async function runClientScript(entryId, script) {
236
+ try {
237
+ const fn = (await import(
238
+ /* @vite-ignore */
239
+ /* webpackIgnore: true */
240
+ /* turbopackIgnore: true */
241
+ script.importFrom
242
+ ))[script.importName ?? "default"];
243
+ if (typeof fn !== "function") return;
244
+ const current = entryToStateMap.get(entryId);
245
+ if (!current) return;
246
+ const messages = createMessagesClient(rpc, { defaults: { category: entryId } });
247
+ await fn({
248
+ ...context,
249
+ current,
250
+ messages
251
+ });
252
+ } catch (error) {
253
+ loadedScripts.delete(entryId);
254
+ console.error(`[@devframes/hub] failed to load client script for "${entryId}" from ${script.importFrom}`, error);
255
+ }
256
+ }
257
+ }
258
+ function createPanelContext(clientType) {
259
+ const store = {
260
+ mode: "edge",
261
+ width: 480,
262
+ height: 360,
263
+ top: 0,
264
+ left: 0,
265
+ position: "right",
266
+ open: clientType === "standalone",
267
+ inactiveTimeout: 0
268
+ };
269
+ return {
270
+ store,
271
+ isDragging: false,
272
+ isResizing: false,
273
+ get isVertical() {
274
+ return store.position === "left" || store.position === "right";
275
+ }
276
+ };
277
+ }
278
+ function groupByCategory(entries) {
279
+ const groups = /* @__PURE__ */ new Map();
280
+ for (const entry of entries) {
281
+ const category = entry.category ?? "default";
282
+ let list = groups.get(category);
283
+ if (!list) {
284
+ list = [];
285
+ groups.set(category, list);
286
+ }
287
+ list.push(entry);
288
+ }
289
+ return [...groups.entries()].sort(([a], [b]) => (DEFAULT_CATEGORIES_ORDER[a] ?? 0) - (DEFAULT_CATEGORIES_ORDER[b] ?? 0));
290
+ }
12
291
  //#endregion
13
292
  //#region src/client/remote.ts
14
293
  function base64UrlDecode(value) {
@@ -64,7 +343,7 @@ function parseRemoteConnection(input) {
64
343
  if (!encoded) return null;
65
344
  let payload;
66
345
  try {
67
- payload = JSON.parse(base64UrlDecode(encoded));
346
+ payload = destr(base64UrlDecode(encoded), { strict: true });
68
347
  } catch (cause) {
69
348
  throw new Error("[@devframes/hub] Failed to decode remote connection descriptor.", { cause });
70
349
  }
@@ -95,4 +374,4 @@ async function connectRemoteDevframe(options = {}) {
95
374
  });
96
375
  }
97
376
  //#endregion
98
- export { CLIENT_CONTEXT_KEY, connectRemoteDevframe, getDevframeClientContext, parseRemoteConnection };
377
+ export { CLIENT_CONTEXT_KEY, connectRemoteDevframe, createDevframeClientHost, createMessagesClient, getDevframeClientContext, parseRemoteConnection, setDevframeClientContext };
@@ -1,4 +1,4 @@
1
- import { t as DevframeDocksUserSettings } from "./settings-Bp5Ax6Os.mjs";
1
+ import { t as DevframeDocksUserSettings } from "./settings-B9YjSEcl.mjs";
2
2
  export * from "devframe/constants";
3
3
 
4
4
  //#region src/constants.d.ts