@devframes/hub 0.9.0-beta.9 → 0.9.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,11 +1,35 @@
1
- import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-D5TJIHwp.mjs";
2
- import { N as NavTarget, P as RemoteConnectionInfo, S as DevframeDockUserEntry, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, g as DevframeDockEntriesGrouped, k as DevframeViewIframe, n as DevframeClientCommand, p as ClientScriptEntry, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings } from "../settings-DfrRa9Wf.mjs";
3
- import "../index-coDmWngQ.mjs";
1
+ import { E as DevframeMessagesClient, x as DevframeMessageEntryInput } from "../context-BVkxwz5k.mjs";
2
+ import { N as NavTarget, P as RemoteConnectionInfo, S as DevframeDockUserEntry, _ as DevframeDockEntry, a as DevframeCommandEntry, b as DevframeDockEntryIcon, g as DevframeDockEntriesGrouped, k as DevframeViewIframe, n as DevframeClientCommand, p as ClientScriptEntry, s as DevframeCommandKeybinding, t as DevframeDocksUserSettings } from "../settings-D7whfcx2.mjs";
3
+ import "../index-CyEyZHB3.mjs";
4
4
  import { DevframeClientRpcHost, DevframeConnection, DevframeConnectionStatus, DevframeRpcClient, DevframeRpcClientOptions, DevframeRpcContext, RpcClientEvents, RpcClientEvents as RpcClientEvents$1 } from "devframe/client";
5
5
  import { EventEmitter } from "devframe/types";
6
6
  import { SharedState } from "devframe/utils/shared-state";
7
7
  import { WhenContext } from "devframe/utils/when";
8
8
  export * from "devframe/client";
9
+ //#region src/client-modules.d.ts
10
+ /**
11
+ * Resolve a dock client script's `importFrom` to the URL to import. URL
12
+ * specifiers pass through untouched; a **bare** specifier resolves through
13
+ * the explicit `resolveClientModule` callback (a viewer's own policy), then
14
+ * the host-advertised `template` (`ConnectionMeta.configs.dock.clientModuleResolution`,
15
+ * its `{specifier}` token replaced — e.g. Vite's `'/@id/{specifier}'`),
16
+ * resolved against `metaBaseUrl` so it targets the host's origin from any
17
+ * viewer. With neither, it stays unchanged — a page import map may still
18
+ * cover it.
19
+ */
20
+ declare function resolveClientModuleSpecifier(specifier: string, options?: {
21
+ resolveClientModule?: (specifier: string) => string | undefined;
22
+ template?: string;
23
+ metaBaseUrl?: string;
24
+ }): string;
25
+ /**
26
+ * Diagnose a failed client-script import: name the likely cause instead of
27
+ * leaving only the browser's opaque `TypeError`. `importFrom` is the entry's
28
+ * declared specifier, `specifier` what was actually imported (after
29
+ * {@link resolveClientModuleSpecifier}).
30
+ */
31
+ declare function clientScriptFailureHint(importFrom: string, specifier: string): string;
32
+ //#endregion
9
33
  //#region src/client/renderers.d.ts
10
34
  /**
11
35
  * Options handed to a dock renderer when the client host mounts a dock entry.
@@ -134,9 +158,30 @@ interface DockPanelStorage {
134
158
  top: number;
135
159
  left: number;
136
160
  position: 'left' | 'right' | 'bottom' | 'top';
137
- open: boolean;
138
161
  inactiveTimeout: number;
139
162
  }
163
+ /**
164
+ * Per-tab UI state of the dock panel, distinct from the browser-shared geometry
165
+ * in {@link DockPanelStorage}. A viewer persists this to `sessionStorage` so a
166
+ * reload (or the RPC auth handshake that follows one) restores the panel to
167
+ * exactly where the developer left it — which dock was open, and the
168
+ * address-bar route of that dock's iframe. Kept `sessionStorage`-scoped (not
169
+ * `localStorage`) because it is per-tab navigation state: two tabs open against
170
+ * the same dev server each keep their own selection rather than fighting over a
171
+ * shared one. Every field is serializable.
172
+ */
173
+ interface DockSessionStorage {
174
+ /** Whether the dock panel is open. */
175
+ open: boolean;
176
+ /** The currently selected dock entry id, or `null` when nothing is open. */
177
+ selectedDockId: string | null;
178
+ /**
179
+ * The live address-bar URL of the selected iframe dock, restored as that
180
+ * iframe's boot `src` on the next load so the deep-linked route survives a
181
+ * reload. `null` when the selected dock has no iframe route to remember.
182
+ */
183
+ selectedDockRoute: string | null;
184
+ }
140
185
  type DockClientType = 'embedded' | 'standalone';
141
186
  interface DocksContext extends DevframeRpcContext {
142
187
  /**
@@ -196,12 +241,29 @@ interface WhenClauseContext {
196
241
  type DevframeClientContext = DocksContext;
197
242
  interface DocksPanelContext {
198
243
  store: DockPanelStorage;
244
+ /**
245
+ * Per-tab session UI state — whether the panel is open, which dock is
246
+ * selected, and that dock's iframe route. Restored across reloads (and the
247
+ * auth handshake that follows one) by a viewer that persists it to
248
+ * `sessionStorage`.
249
+ */
250
+ session: DockSessionStorage;
199
251
  isDragging: boolean;
200
252
  isResizing: boolean;
201
253
  readonly isVertical: boolean;
254
+ /**
255
+ * Claim the persisted {@link DockSessionStorage.selectedDockRoute boot route} for a dock
256
+ * id, once. Returns the saved address-bar URL only for the dock that was
257
+ * selected before the last reload — and only on the first call for it — so a
258
+ * restored iframe boots deep-linked while a later switch to a different dock
259
+ * (whose live route isn't reflected in {@link DockSessionStorage.selectedDockRoute} yet)
260
+ * doesn't reuse the stale value. Returns `null` otherwise.
261
+ */
262
+ consumeBootRoute?: (id: string) => string | null;
202
263
  }
203
264
  interface DocksEntriesContext {
204
265
  selectedId: string | null;
266
+ selectedRoute: string | null;
205
267
  readonly selected: DevframeDockEntry | null;
206
268
  entries: DevframeDockEntry[];
207
269
  entryToStateMap: Map<string, DockEntryState>;
@@ -346,6 +408,79 @@ declare function resolveDockUrl(url: string, connection: DevframeConnection): st
346
408
  /** Resolve URL-backed dock icons while preserving Iconify names and data URLs. */
347
409
  declare function resolveDockIcon(icon: DevframeDockEntryIcon, connection: DevframeConnection): DevframeDockEntryIcon;
348
410
  //#endregion
411
+ //#region src/client/frame-location.d.ts
412
+ /**
413
+ * Live location tracking for an iframe dock.
414
+ *
415
+ * An iframe dock's address bar — and the route persisted from it into
416
+ * `DockSessionStorage.selectedDockRoute` — has to follow wherever the embedded
417
+ * app actually goes. The `load` event only covers whole-document navigations, so
418
+ * an SPA router moving between routes with `history.pushState()` leaves both
419
+ * showing the URL the frame booted with.
420
+ *
421
+ * {@link watchFrameLocation} closes that gap for a same-origin frame, reporting
422
+ * `location.href` on every navigation it can observe:
423
+ *
424
+ * - `popstate` / `hashchange` — back/forward and hash routing;
425
+ * - `history.pushState`/`replaceState`, wrapped in place (and restored on
426
+ * dispose) because those two fire no event of their own;
427
+ * - the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API)'s
428
+ * `currententrychange` where it exists, which reports post-commit and so also
429
+ * catches what a wrapper structurally cannot — a router holding a reference to
430
+ * `pushState` captured before this watch attached;
431
+ * - `load`, which also re-subscribes: a document navigation swaps the frame's
432
+ * `history`/`navigation` objects, so the previous subscription dies with them.
433
+ *
434
+ * These sources overlap deliberately rather than being chosen between, since a
435
+ * report is deduped against the last href — the cost of hearing the same
436
+ * navigation twice is nothing, and the cost of missing one is a stale route.
437
+ *
438
+ * A cross-origin frame reports nothing — its location is unreadable by design,
439
+ * so the caller keeps the last URL it knew.
440
+ */
441
+ /** The minimal `history` surface the watcher wraps. Injectable for tests. */
442
+ interface FrameLocationHistory {
443
+ pushState: (...args: any[]) => void;
444
+ replaceState: (...args: any[]) => void;
445
+ }
446
+ /** The minimal frame-window surface the watcher reads and subscribes to. */
447
+ interface FrameLocationWindow {
448
+ readonly location: {
449
+ readonly href: string;
450
+ };
451
+ readonly history?: FrameLocationHistory;
452
+ /** Present only where the Navigation API ships. */
453
+ readonly navigation?: {
454
+ addEventListener: (type: 'currententrychange', listener: () => void) => void;
455
+ removeEventListener: (type: 'currententrychange', listener: () => void) => void;
456
+ };
457
+ addEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
458
+ removeEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
459
+ }
460
+ /** The minimal iframe surface the watcher needs. Injectable for tests. */
461
+ interface FrameLocationTarget {
462
+ readonly contentWindow: FrameLocationWindow | null;
463
+ addEventListener: (type: 'load', listener: () => void) => void;
464
+ removeEventListener: (type: 'load', listener: () => void) => void;
465
+ }
466
+ interface WatchFrameLocationOptions {
467
+ /** The live iframe whose location is tracked. */
468
+ iframe: FrameLocationTarget;
469
+ /** Called with the frame's `location.href` on each observed change. */
470
+ onChange: (href: string) => void;
471
+ /**
472
+ * The href the caller already shows, so an initial report is only made when
473
+ * the frame is somewhere else (a frame re-attached mid-session, say).
474
+ */
475
+ initial?: string;
476
+ }
477
+ /**
478
+ * Track a frame's location until the returned disposer is called. Disposing
479
+ * detaches every listener and restores any wrapped `history` method, leaving the
480
+ * embedded page as it was found.
481
+ */
482
+ declare function watchFrameLocation(options: WatchFrameLocationOptions): () => void;
483
+ //#endregion
349
484
  //#region src/client/frame-nav.d.ts
350
485
  /**
351
486
  * Shared-iframe soft navigation — the viewer-side half of a host↔iframe
@@ -367,7 +502,7 @@ declare function resolveDockIcon(icon: DevframeDockEntryIcon, connection: Devfra
367
502
  * decoupled — the embedded app takes no hub/RPC dependency, only the shim.
368
503
  */
369
504
  /** `postMessage` channel tag shared by both halves of the protocol. */
370
- declare const FRAME_NAV_CHANNEL = "devframe:frame-nav";
505
+ declare const FRAME_NAV_CHANNEL: string;
371
506
  /** Protocol version. */
372
507
  declare const FRAME_NAV_VERSION = 1;
373
508
  /** Envelope every frame-nav message carries. */
@@ -488,6 +623,15 @@ interface DevframeClientHostOptions {
488
623
  * `custom-render`, and iframe `clientScript`). Default `true`.
489
624
  */
490
625
  loadClientScripts?: boolean;
626
+ /**
627
+ * Resolve a **bare-specifier** client script (`importFrom` naming an npm
628
+ * module) to a URL this page can import — a viewer's own policy, winning
629
+ * over the host-advertised `ConnectionMeta.configs.dock.clientModuleResolution`
630
+ * template (return `undefined` to fall through to it). URL specifiers never
631
+ * reach this hook. E.g. on a Vite-served page:
632
+ * `resolveClientModule: s => `/@id/${s}``.
633
+ */
634
+ resolveClientModule?: (specifier: string) => string | undefined;
491
635
  /**
492
636
  * Dock renderers to register at boot, keyed by dock `type`. The host
493
637
  * application injects the ones it wants (e.g.
@@ -587,4 +731,4 @@ declare function parseRemoteConnection(input?: string): RemoteConnectionInfo | n
587
731
  */
588
732
  declare function connectRemoteDevframe(options?: ConnectRemoteDevframeOptions): Promise<DevframeRpcClient>;
589
733
  //#endregion
590
- export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl };
734
+ export { CLIENT_CONTEXT_KEY, CommandsContext, ConnectRemoteDevframeOptions, CreateDockRenderersContextOptions, DevframeClientContext, DevframeClientHost, DevframeClientHostOptions, type DevframeClientRpcHost, DockClientScriptContext, DockClientType, DockEntryState, DockEntryStateEvents, DockPanelStorage, DockRegistration, DockRenderer, DockRendererInstance, DockRendererManifest, DockRendererMountOptions, DockRendererMountResult, DockRenderersContext, DockSessionStorage, DocksConnectionContext, DocksContext, DocksEntriesContext, DocksPanelContext, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, FrameLocationHistory, FrameLocationTarget, FrameLocationWindow, FrameNavClient, FrameNavClientOptions, FrameNavEnvelope, FrameNavFrameMessage, FrameNavHostMessage, FrameNavHostPayload, FrameNavListenTarget, FrameTab, MessagesClientOptions, type RpcClientEvents, WatchFrameLocationOptions, WhenClauseContext, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation };
@@ -1,5 +1,5 @@
1
- import { DEFAULT_CATEGORIES_ORDER, DEFAULT_STATE_USER_SETTINGS, DOCK_RENDERERS_STATE_KEY } from "../constants.mjs";
2
- import { n as stripRemoteConnectionFromUrl, t as buildRemoteConnectionUrl } from "../remote-url-KVXtKP47.mjs";
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-C2b2cACy.mjs";
2
+ import { a as resolveClientModuleSpecifier, n as stripRemoteConnectionFromUrl, r as clientScriptFailureHint, t as buildRemoteConnectionUrl } from "../remote-url-Bgc7gtsP.mjs";
3
3
  import { REMOTE_CONNECTION_KEY } from "devframe/constants";
4
4
  import { connectDevframe, getDevframeRpcClient, resolveWsUrl } from "devframe/client";
5
5
  import { createEventEmitter } from "devframe/utils/events";
@@ -69,6 +69,88 @@ function resolveDockIcon(icon, connection) {
69
69
  };
70
70
  }
71
71
  //#endregion
72
+ //#region src/client/frame-location.ts
73
+ /**
74
+ * Track a frame's location until the returned disposer is called. Disposing
75
+ * detaches every listener and restores any wrapped `history` method, leaving the
76
+ * embedded page as it was found.
77
+ */
78
+ function watchFrameLocation(options) {
79
+ const { iframe, onChange } = options;
80
+ let lastReported = options.initial;
81
+ let detach;
82
+ let disposed = false;
83
+ /**
84
+ * The frame's window, or `null` when its location can't be read. Touching
85
+ * `location.href` is the cross-origin probe — it throws for a foreign
86
+ * document, where there is nothing to observe.
87
+ */
88
+ function readableWindow() {
89
+ try {
90
+ const win = iframe.contentWindow;
91
+ return win && typeof win.location.href === "string" ? win : null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ function report() {
97
+ const href = readableWindow()?.location.href;
98
+ if (!href || href === "about:blank" || href === lastReported) return;
99
+ lastReported = href;
100
+ onChange(href);
101
+ }
102
+ function subscribe() {
103
+ const win = readableWindow();
104
+ if (!win) return;
105
+ const listeners = [];
106
+ win.addEventListener("popstate", report);
107
+ win.addEventListener("hashchange", report);
108
+ listeners.push(() => {
109
+ win.removeEventListener("popstate", report);
110
+ win.removeEventListener("hashchange", report);
111
+ });
112
+ const { navigation, history } = win;
113
+ if (navigation) {
114
+ navigation.addEventListener("currententrychange", report);
115
+ listeners.push(() => navigation.removeEventListener("currententrychange", report));
116
+ }
117
+ if (history) {
118
+ const originals = {
119
+ pushState: history.pushState,
120
+ replaceState: history.replaceState
121
+ };
122
+ for (const method of ["pushState", "replaceState"]) history[method] = function(...args) {
123
+ originals[method].apply(this, args);
124
+ report();
125
+ };
126
+ listeners.push(() => {
127
+ history.pushState = originals.pushState;
128
+ history.replaceState = originals.replaceState;
129
+ });
130
+ }
131
+ detach = () => {
132
+ for (const off of listeners) off();
133
+ };
134
+ }
135
+ function onLoad() {
136
+ if (disposed) return;
137
+ detach?.();
138
+ detach = void 0;
139
+ subscribe();
140
+ report();
141
+ }
142
+ iframe.addEventListener("load", onLoad);
143
+ subscribe();
144
+ report();
145
+ return () => {
146
+ if (disposed) return;
147
+ disposed = true;
148
+ iframe.removeEventListener("load", onLoad);
149
+ detach?.();
150
+ detach = void 0;
151
+ };
152
+ }
153
+ //#endregion
72
154
  //#region src/client/frame-nav.ts
73
155
  /**
74
156
  * Shared-iframe soft navigation — the viewer-side half of a host↔iframe
@@ -90,7 +172,7 @@ function resolveDockIcon(icon, connection) {
90
172
  * decoupled — the embedded app takes no hub/RPC dependency, only the shim.
91
173
  */
92
174
  /** `postMessage` channel tag shared by both halves of the protocol. */
93
- const FRAME_NAV_CHANNEL = "devframe:frame-nav";
175
+ const FRAME_NAV_CHANNEL = HUB_EVENTS.postMessage.frameNav;
94
176
  /** Protocol version. */
95
177
  const FRAME_NAV_VERSION = 1;
96
178
  /**
@@ -121,16 +203,14 @@ function attachFrameNavClient(options) {
121
203
  if (disposed) return;
122
204
  if (expectedOrigin !== "*" && ev.origin !== expectedOrigin) return;
123
205
  const data = ev.data;
124
- if (!data || data.channel !== "devframe:frame-nav" || data.v !== 1 || data.frameId !== frameId || data.from !== "frame") return;
206
+ if (!data || data.channel !== FRAME_NAV_CHANNEL || data.v !== 1 || data.frameId !== frameId || data.from !== "frame") return;
125
207
  switch (data.type) {
126
208
  case "ready":
127
209
  case "manifest":
128
210
  markReady();
129
211
  reconcileManifest(data.tabs ?? [], data.current);
130
212
  break;
131
- case "navigated":
132
- onNavigated(data.tabId);
133
- break;
213
+ case "navigated": onNavigated(data.tabId);
134
214
  }
135
215
  }
136
216
  function markReady() {
@@ -416,10 +496,10 @@ function createDockRenderersContext(options) {
416
496
  }
417
497
  //#endregion
418
498
  //#region src/client/host.ts
419
- const DOCKS_STATE_KEY = "devframe:docks";
420
- const COMMANDS_STATE_KEY = "devframe:commands";
421
- const USER_SETTINGS_STATE_KEY = "devframe:user-settings";
422
- const DOCKS_ACTIVATE_EVENT = "devframe:docks:activate";
499
+ const DOCKS_STATE_KEY = HUB_EVENTS.sharedState.docks;
500
+ const COMMANDS_STATE_KEY = HUB_EVENTS.sharedState.commands;
501
+ const USER_SETTINGS_STATE_KEY = HUB_EVENTS.sharedState.userSettings;
502
+ const DOCKS_ACTIVATE_EVENT = HUB_EVENTS.broadcast.docksActivate;
423
503
  /**
424
504
  * Boot the framework-level client host: connect RPC, assemble the full
425
505
  * {@link DevframeClientContext} (panel, docks, commands, when) from the hub's
@@ -462,7 +542,7 @@ async function createDevframeClientHost(options = {}) {
462
542
  when: { get context() {
463
543
  return {
464
544
  clientType,
465
- dockOpen: panel.store.open,
545
+ dockOpen: panel.session.open,
466
546
  paletteOpen: commands.paletteOpen,
467
547
  dockSelectedId: selectedId ?? ""
468
548
  };
@@ -584,7 +664,11 @@ async function createDevframeClientHost(options = {}) {
584
664
  }
585
665
  docks.entries = entries;
586
666
  docks.groupedEntries = groupByCategory(entries, categoryOrder);
587
- if (selectedId && !entryToStateMap.has(selectedId)) selectedId = null;
667
+ if (selectedId && !entryToStateMap.has(selectedId)) {
668
+ selectedId = null;
669
+ panel.session.selectedDockId = null;
670
+ panel.session.selectedDockRoute = null;
671
+ }
588
672
  }
589
673
  function createDocksContext() {
590
674
  return {
@@ -594,6 +678,12 @@ async function createDevframeClientHost(options = {}) {
594
678
  set selectedId(id) {
595
679
  switchEntry(id);
596
680
  },
681
+ get selectedRoute() {
682
+ return panel.session.selectedDockRoute;
683
+ },
684
+ set selectedRoute(route) {
685
+ panel.session.selectedDockRoute = route;
686
+ },
597
687
  get selected() {
598
688
  return selectedId && entryToStateMap.get(selectedId)?.entryMeta || null;
599
689
  },
@@ -638,6 +728,8 @@ async function createDevframeClientHost(options = {}) {
638
728
  if (next !== null && !entryToStateMap.has(next)) return false;
639
729
  const previous = selectedId;
640
730
  selectedId = next;
731
+ panel.session.selectedDockId = next;
732
+ if (next === null || entryToStateMap.get(next)?.entryMeta.type !== "iframe") panel.session.selectedDockRoute = null;
641
733
  if (previous) entryToStateMap.get(previous)?.events.emit("entry:deactivated");
642
734
  if (next) entryToStateMap.get(next)?.events.emit("entry:activated");
643
735
  return true;
@@ -704,12 +796,17 @@ async function createDevframeClientHost(options = {}) {
704
796
  }
705
797
  }
706
798
  async function runClientScript(entryId, script) {
799
+ const specifier = resolveClientModuleSpecifier(script.importFrom, {
800
+ resolveClientModule: options.resolveClientModule,
801
+ template: rpc.connectionMeta?.configs?.dock?.clientModuleResolution,
802
+ metaBaseUrl: rpc.connection?.metaBaseUrl
803
+ });
707
804
  try {
708
805
  const fn = (await import(
709
806
  /* @vite-ignore */
710
807
  /* webpackIgnore: true */
711
808
  /* turbopackIgnore: true */
712
- script.importFrom
809
+ specifier
713
810
  ))[script.importName ?? "default"];
714
811
  if (typeof fn !== "function") return;
715
812
  const current = entryToStateMap.get(entryId);
@@ -722,7 +819,7 @@ async function createDevframeClientHost(options = {}) {
722
819
  });
723
820
  } catch (error) {
724
821
  loadedScripts.delete(entryId);
725
- console.error(`[@devframes/hub] failed to load client script for "${entryId}" from ${script.importFrom}`, error);
822
+ console.error(`[@devframes/hub] failed to load client script for "${entryId}" from ${specifier}${clientScriptFailureHint(script.importFrom, specifier)}`, error);
726
823
  }
727
824
  }
728
825
  }
@@ -734,11 +831,15 @@ function createPanelContext(clientType) {
734
831
  top: 0,
735
832
  left: 0,
736
833
  position: "right",
737
- open: clientType === "standalone",
738
834
  inactiveTimeout: 0
739
835
  };
740
836
  return {
741
837
  store,
838
+ session: {
839
+ open: clientType === "standalone",
840
+ selectedDockId: null,
841
+ selectedDockRoute: null
842
+ },
742
843
  isDragging: false,
743
844
  isResizing: false,
744
845
  get isVertical() {
@@ -785,10 +886,11 @@ function buildRemoteDevframeUrl(url, connection) {
785
886
  } catch {
786
887
  return url;
787
888
  }
889
+ const websocket = resolveWsUrl(connection.connectionMeta.websocket, connection.metaBaseUrl, base);
788
890
  return buildRemoteConnectionUrl(url, {
789
891
  v: 1,
790
892
  backend: "websocket",
791
- websocket: resolveWsUrl(connection.connectionMeta.websocket, connection.metaBaseUrl, base),
893
+ websocket,
792
894
  authToken: connection.authToken,
793
895
  origin: base.origin
794
896
  });
@@ -869,4 +971,4 @@ async function connectRemoteDevframe(options = {}) {
869
971
  });
870
972
  }
871
973
  //#endregion
872
- export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl };
974
+ export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, clientScriptFailureHint, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveClientModuleSpecifier, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation };
@@ -0,0 +1,120 @@
1
+ import { cleanDoubleSlashes, withLeadingSlash, withTrailingSlash } from "ufo";
2
+ //#region src/events.ts
3
+ /**
4
+ * Centralized registry of every event, broadcast, RPC method, shared-state
5
+ * key, and channel name the hub uses — the single source of truth that keeps
6
+ * these names out of scattered string literals.
7
+ *
8
+ * **Keep this in sync with [`docs/guide/events.md`](../../../docs/guide/events.md)**
9
+ * (the Hub Events Reference): every name here appears in that page's tables, and
10
+ * every name there resolves to an entry here. Add, rename, or remove a name in
11
+ * both places in the same change, and reference `HUB_EVENTS.*` from call sites
12
+ * instead of re-typing a literal.
13
+ *
14
+ * The `.events` EventEmitter maps in `types/{docks,terminals,messages,commands}.ts`
15
+ * and the RPC augmentation interfaces in `node/context.ts` declare these same
16
+ * names as type-level keys (a literal is unavoidable in a type position); those
17
+ * declarations mirror this map and move with it.
18
+ */
19
+ const HUB_EVENTS = {
20
+ /**
21
+ * Internal node `EventEmitter` events on `ctx.<subsystem>.events`. Emitted
22
+ * and consumed inside the node process (chiefly by `createHubContext`, which
23
+ * fans them out onto the wire); they never cross to the browser.
24
+ */
25
+ bus: {
26
+ docksEntryUpdated: "docks:entry:updated",
27
+ docksActivate: "docks:activate",
28
+ terminalsSessionUpdated: "terminals:session:updated",
29
+ messagesAdded: "messages:added",
30
+ messagesUpdated: "messages:updated",
31
+ messagesRemoved: "messages:removed",
32
+ messagesCleared: "messages:cleared",
33
+ commandsRegistered: "commands:registered",
34
+ commandsUnregistered: "commands:unregistered"
35
+ },
36
+ /** Server RPC methods a connected client calls (client → server), `hub:` prefix. */
37
+ rpc: {
38
+ docksActivate: "hub:docks:activate",
39
+ commandsExecute: "hub:commands:execute",
40
+ messagesAdd: "hub:messages:add",
41
+ messagesUpdate: "hub:messages:update",
42
+ messagesRemove: "hub:messages:remove",
43
+ messagesClear: "hub:messages:clear",
44
+ terminalsWrite: "hub:terminals:write",
45
+ terminalsResize: "hub:terminals:resize",
46
+ terminalsTerminate: "hub:terminals:terminate",
47
+ terminalsRestart: "hub:terminals:restart",
48
+ terminalsRemove: "hub:terminals:remove"
49
+ },
50
+ /** Broadcast notifications the server pushes to clients (server → client), `devframe:` prefix. */
51
+ broadcast: {
52
+ docksActivate: "devframe:docks:activate",
53
+ terminalsUpdated: "devframe:terminals:updated",
54
+ messagesUpdated: "devframe:messages:updated"
55
+ },
56
+ /** Shared-state slot keys a hub-aware client reads (server → client), `devframe:` prefix. */
57
+ sharedState: {
58
+ docks: "devframe:docks",
59
+ docksActive: "devframe:docks:active",
60
+ commands: "devframe:commands",
61
+ userSettings: "devframe:user-settings",
62
+ dockRenderers: "devframe:dock-renderers"
63
+ },
64
+ /** Streaming channel ids (server → client), `devframe:` prefix. */
65
+ stream: { terminals: "devframe:terminals" },
66
+ /** `postMessage` channels for host ↔ iframe protocols, `devframe:` prefix. */
67
+ postMessage: { frameNav: "devframe:frame-nav" }
68
+ };
69
+ //#endregion
70
+ //#region src/constants.ts
71
+ /** Default mount base for a hub instance — one namespace, one catch-all. */
72
+ const DEVFRAMES_HUB_BASE = "/__devframes/";
73
+ /**
74
+ * Normalize a hub mount base to an absolute path with leading and trailing
75
+ * slashes (e.g. `devframes` → `/devframes/`), collapsing any doubled
76
+ * slashes the input introduced. The one implementation every hub-aware
77
+ * host (`@devframes/hub` itself, and the Vite/Nuxt/Next adapters) resolves
78
+ * `options.base` through.
79
+ */
80
+ function normalizeHubBase(base) {
81
+ return cleanDoubleSlashes(withTrailingSlash(withLeadingSlash(base)));
82
+ }
83
+ /**
84
+ * The default ordering weight for each known dock category — lower sorts
85
+ * earlier. Downstream viewers (e.g. `@vitejs/devtools-kit`) import this as the
86
+ * single source of truth so the hub and its viewers agree on category order.
87
+ * `framework` sorts first; `~builtin` (the viewer's own built-in views) last.
88
+ *
89
+ * The buckets read from "closest to your app" → "platform / analysis" →
90
+ * "peripheral". Gaps between the weights are intentional: a kit can interleave
91
+ * its own categories (or override these) without editing this table.
92
+ */
93
+ const DEFAULT_CATEGORIES_ORDER = {
94
+ "framework": -100,
95
+ "default": 0,
96
+ "app": 100,
97
+ "ui": 150,
98
+ "data": 250,
99
+ "web": 300,
100
+ "performance": 350,
101
+ "advanced": 400,
102
+ "docs": 500,
103
+ "~builtin": 1e3
104
+ };
105
+ /**
106
+ * Shared-state slot carrying the hub's renderer manifest — one
107
+ * {@link import('./client/renderers').DockRendererManifest} entry per dock
108
+ * `type`, published by `initHub({ renderers })` and consumed by every
109
+ * hub-aware client (the headless client host and viewers alike).
110
+ */
111
+ const DOCK_RENDERERS_STATE_KEY = HUB_EVENTS.sharedState.dockRenderers;
112
+ const DEFAULT_STATE_USER_SETTINGS = () => ({
113
+ docksHidden: [],
114
+ docksCategoriesHidden: [],
115
+ docksPinned: [],
116
+ docksCustomOrder: {},
117
+ commandShortcuts: {}
118
+ });
119
+ //#endregion
120
+ export { normalizeHubBase as a, DOCK_RENDERERS_STATE_KEY as i, DEFAULT_STATE_USER_SETTINGS as n, HUB_EVENTS as o, DEVFRAMES_HUB_BASE as r, DEFAULT_CATEGORIES_ORDER as t };