@devframes/hub 0.9.0-beta.9 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.mts +113 -2
- package/dist/client/index.mjs +102 -4
- package/package.json +3 -3
package/dist/client/index.d.mts
CHANGED
|
@@ -134,9 +134,30 @@ interface DockPanelStorage {
|
|
|
134
134
|
top: number;
|
|
135
135
|
left: number;
|
|
136
136
|
position: 'left' | 'right' | 'bottom' | 'top';
|
|
137
|
-
open: boolean;
|
|
138
137
|
inactiveTimeout: number;
|
|
139
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Per-tab UI state of the dock panel, distinct from the browser-shared geometry
|
|
141
|
+
* in {@link DockPanelStorage}. A viewer persists this to `sessionStorage` so a
|
|
142
|
+
* reload (or the RPC auth handshake that follows one) restores the panel to
|
|
143
|
+
* exactly where the developer left it — which dock was open, and the
|
|
144
|
+
* address-bar route of that dock's iframe. Kept `sessionStorage`-scoped (not
|
|
145
|
+
* `localStorage`) because it is per-tab navigation state: two tabs open against
|
|
146
|
+
* the same dev server each keep their own selection rather than fighting over a
|
|
147
|
+
* shared one. Every field is serializable.
|
|
148
|
+
*/
|
|
149
|
+
interface DockSessionStorage {
|
|
150
|
+
/** Whether the dock panel is open. */
|
|
151
|
+
open: boolean;
|
|
152
|
+
/** The currently selected dock entry id, or `null` when nothing is open. */
|
|
153
|
+
selectedDockId: string | null;
|
|
154
|
+
/**
|
|
155
|
+
* The live address-bar URL of the selected iframe dock, restored as that
|
|
156
|
+
* iframe's boot `src` on the next load so the deep-linked route survives a
|
|
157
|
+
* reload. `null` when the selected dock has no iframe route to remember.
|
|
158
|
+
*/
|
|
159
|
+
selectedDockRoute: string | null;
|
|
160
|
+
}
|
|
140
161
|
type DockClientType = 'embedded' | 'standalone';
|
|
141
162
|
interface DocksContext extends DevframeRpcContext {
|
|
142
163
|
/**
|
|
@@ -196,12 +217,29 @@ interface WhenClauseContext {
|
|
|
196
217
|
type DevframeClientContext = DocksContext;
|
|
197
218
|
interface DocksPanelContext {
|
|
198
219
|
store: DockPanelStorage;
|
|
220
|
+
/**
|
|
221
|
+
* Per-tab session UI state — whether the panel is open, which dock is
|
|
222
|
+
* selected, and that dock's iframe route. Restored across reloads (and the
|
|
223
|
+
* auth handshake that follows one) by a viewer that persists it to
|
|
224
|
+
* `sessionStorage`.
|
|
225
|
+
*/
|
|
226
|
+
session: DockSessionStorage;
|
|
199
227
|
isDragging: boolean;
|
|
200
228
|
isResizing: boolean;
|
|
201
229
|
readonly isVertical: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Claim the persisted {@link DockSessionStorage.selectedDockRoute boot route} for a dock
|
|
232
|
+
* id, once. Returns the saved address-bar URL only for the dock that was
|
|
233
|
+
* selected before the last reload — and only on the first call for it — so a
|
|
234
|
+
* restored iframe boots deep-linked while a later switch to a different dock
|
|
235
|
+
* (whose live route isn't reflected in {@link DockSessionStorage.selectedDockRoute} yet)
|
|
236
|
+
* doesn't reuse the stale value. Returns `null` otherwise.
|
|
237
|
+
*/
|
|
238
|
+
consumeBootRoute?: (id: string) => string | null;
|
|
202
239
|
}
|
|
203
240
|
interface DocksEntriesContext {
|
|
204
241
|
selectedId: string | null;
|
|
242
|
+
selectedRoute: string | null;
|
|
205
243
|
readonly selected: DevframeDockEntry | null;
|
|
206
244
|
entries: DevframeDockEntry[];
|
|
207
245
|
entryToStateMap: Map<string, DockEntryState>;
|
|
@@ -346,6 +384,79 @@ declare function resolveDockUrl(url: string, connection: DevframeConnection): st
|
|
|
346
384
|
/** Resolve URL-backed dock icons while preserving Iconify names and data URLs. */
|
|
347
385
|
declare function resolveDockIcon(icon: DevframeDockEntryIcon, connection: DevframeConnection): DevframeDockEntryIcon;
|
|
348
386
|
//#endregion
|
|
387
|
+
//#region src/client/frame-location.d.ts
|
|
388
|
+
/**
|
|
389
|
+
* Live location tracking for an iframe dock.
|
|
390
|
+
*
|
|
391
|
+
* An iframe dock's address bar — and the route persisted from it into
|
|
392
|
+
* `DockSessionStorage.selectedDockRoute` — has to follow wherever the embedded
|
|
393
|
+
* app actually goes. The `load` event only covers whole-document navigations, so
|
|
394
|
+
* an SPA router moving between routes with `history.pushState()` leaves both
|
|
395
|
+
* showing the URL the frame booted with.
|
|
396
|
+
*
|
|
397
|
+
* {@link watchFrameLocation} closes that gap for a same-origin frame, reporting
|
|
398
|
+
* `location.href` on every navigation it can observe:
|
|
399
|
+
*
|
|
400
|
+
* - `popstate` / `hashchange` — back/forward and hash routing;
|
|
401
|
+
* - `history.pushState`/`replaceState`, wrapped in place (and restored on
|
|
402
|
+
* dispose) because those two fire no event of their own;
|
|
403
|
+
* - the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API)'s
|
|
404
|
+
* `currententrychange` where it exists, which reports post-commit and so also
|
|
405
|
+
* catches what a wrapper structurally cannot — a router holding a reference to
|
|
406
|
+
* `pushState` captured before this watch attached;
|
|
407
|
+
* - `load`, which also re-subscribes: a document navigation swaps the frame's
|
|
408
|
+
* `history`/`navigation` objects, so the previous subscription dies with them.
|
|
409
|
+
*
|
|
410
|
+
* These sources overlap deliberately rather than being chosen between, since a
|
|
411
|
+
* report is deduped against the last href — the cost of hearing the same
|
|
412
|
+
* navigation twice is nothing, and the cost of missing one is a stale route.
|
|
413
|
+
*
|
|
414
|
+
* A cross-origin frame reports nothing — its location is unreadable by design,
|
|
415
|
+
* so the caller keeps the last URL it knew.
|
|
416
|
+
*/
|
|
417
|
+
/** The minimal `history` surface the watcher wraps. Injectable for tests. */
|
|
418
|
+
interface FrameLocationHistory {
|
|
419
|
+
pushState: (...args: any[]) => void;
|
|
420
|
+
replaceState: (...args: any[]) => void;
|
|
421
|
+
}
|
|
422
|
+
/** The minimal frame-window surface the watcher reads and subscribes to. */
|
|
423
|
+
interface FrameLocationWindow {
|
|
424
|
+
readonly location: {
|
|
425
|
+
readonly href: string;
|
|
426
|
+
};
|
|
427
|
+
readonly history?: FrameLocationHistory;
|
|
428
|
+
/** Present only where the Navigation API ships. */
|
|
429
|
+
readonly navigation?: {
|
|
430
|
+
addEventListener: (type: 'currententrychange', listener: () => void) => void;
|
|
431
|
+
removeEventListener: (type: 'currententrychange', listener: () => void) => void;
|
|
432
|
+
};
|
|
433
|
+
addEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
|
|
434
|
+
removeEventListener: (type: 'popstate' | 'hashchange', listener: () => void) => void;
|
|
435
|
+
}
|
|
436
|
+
/** The minimal iframe surface the watcher needs. Injectable for tests. */
|
|
437
|
+
interface FrameLocationTarget {
|
|
438
|
+
readonly contentWindow: FrameLocationWindow | null;
|
|
439
|
+
addEventListener: (type: 'load', listener: () => void) => void;
|
|
440
|
+
removeEventListener: (type: 'load', listener: () => void) => void;
|
|
441
|
+
}
|
|
442
|
+
interface WatchFrameLocationOptions {
|
|
443
|
+
/** The live iframe whose location is tracked. */
|
|
444
|
+
iframe: FrameLocationTarget;
|
|
445
|
+
/** Called with the frame's `location.href` on each observed change. */
|
|
446
|
+
onChange: (href: string) => void;
|
|
447
|
+
/**
|
|
448
|
+
* The href the caller already shows, so an initial report is only made when
|
|
449
|
+
* the frame is somewhere else (a frame re-attached mid-session, say).
|
|
450
|
+
*/
|
|
451
|
+
initial?: string;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Track a frame's location until the returned disposer is called. Disposing
|
|
455
|
+
* detaches every listener and restores any wrapped `history` method, leaving the
|
|
456
|
+
* embedded page as it was found.
|
|
457
|
+
*/
|
|
458
|
+
declare function watchFrameLocation(options: WatchFrameLocationOptions): () => void;
|
|
459
|
+
//#endregion
|
|
349
460
|
//#region src/client/frame-nav.d.ts
|
|
350
461
|
/**
|
|
351
462
|
* Shared-iframe soft navigation — the viewer-side half of a host↔iframe
|
|
@@ -587,4 +698,4 @@ declare function parseRemoteConnection(input?: string): RemoteConnectionInfo | n
|
|
|
587
698
|
*/
|
|
588
699
|
declare function connectRemoteDevframe(options?: ConnectRemoteDevframeOptions): Promise<DevframeRpcClient>;
|
|
589
700
|
//#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 };
|
|
701
|
+
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, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation };
|
package/dist/client/index.mjs
CHANGED
|
@@ -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
|
|
@@ -462,7 +544,7 @@ async function createDevframeClientHost(options = {}) {
|
|
|
462
544
|
when: { get context() {
|
|
463
545
|
return {
|
|
464
546
|
clientType,
|
|
465
|
-
dockOpen: panel.
|
|
547
|
+
dockOpen: panel.session.open,
|
|
466
548
|
paletteOpen: commands.paletteOpen,
|
|
467
549
|
dockSelectedId: selectedId ?? ""
|
|
468
550
|
};
|
|
@@ -584,7 +666,11 @@ async function createDevframeClientHost(options = {}) {
|
|
|
584
666
|
}
|
|
585
667
|
docks.entries = entries;
|
|
586
668
|
docks.groupedEntries = groupByCategory(entries, categoryOrder);
|
|
587
|
-
if (selectedId && !entryToStateMap.has(selectedId))
|
|
669
|
+
if (selectedId && !entryToStateMap.has(selectedId)) {
|
|
670
|
+
selectedId = null;
|
|
671
|
+
panel.session.selectedDockId = null;
|
|
672
|
+
panel.session.selectedDockRoute = null;
|
|
673
|
+
}
|
|
588
674
|
}
|
|
589
675
|
function createDocksContext() {
|
|
590
676
|
return {
|
|
@@ -594,6 +680,12 @@ async function createDevframeClientHost(options = {}) {
|
|
|
594
680
|
set selectedId(id) {
|
|
595
681
|
switchEntry(id);
|
|
596
682
|
},
|
|
683
|
+
get selectedRoute() {
|
|
684
|
+
return panel.session.selectedDockRoute;
|
|
685
|
+
},
|
|
686
|
+
set selectedRoute(route) {
|
|
687
|
+
panel.session.selectedDockRoute = route;
|
|
688
|
+
},
|
|
597
689
|
get selected() {
|
|
598
690
|
return selectedId && entryToStateMap.get(selectedId)?.entryMeta || null;
|
|
599
691
|
},
|
|
@@ -638,6 +730,8 @@ async function createDevframeClientHost(options = {}) {
|
|
|
638
730
|
if (next !== null && !entryToStateMap.has(next)) return false;
|
|
639
731
|
const previous = selectedId;
|
|
640
732
|
selectedId = next;
|
|
733
|
+
panel.session.selectedDockId = next;
|
|
734
|
+
if (next === null || entryToStateMap.get(next)?.entryMeta.type !== "iframe") panel.session.selectedDockRoute = null;
|
|
641
735
|
if (previous) entryToStateMap.get(previous)?.events.emit("entry:deactivated");
|
|
642
736
|
if (next) entryToStateMap.get(next)?.events.emit("entry:activated");
|
|
643
737
|
return true;
|
|
@@ -734,11 +828,15 @@ function createPanelContext(clientType) {
|
|
|
734
828
|
top: 0,
|
|
735
829
|
left: 0,
|
|
736
830
|
position: "right",
|
|
737
|
-
open: clientType === "standalone",
|
|
738
831
|
inactiveTimeout: 0
|
|
739
832
|
};
|
|
740
833
|
return {
|
|
741
834
|
store,
|
|
835
|
+
session: {
|
|
836
|
+
open: clientType === "standalone",
|
|
837
|
+
selectedDockId: null,
|
|
838
|
+
selectedDockRoute: null
|
|
839
|
+
},
|
|
742
840
|
isDragging: false,
|
|
743
841
|
isResizing: false,
|
|
744
842
|
get isVertical() {
|
|
@@ -869,4 +967,4 @@ async function connectRemoteDevframe(options = {}) {
|
|
|
869
967
|
});
|
|
870
968
|
}
|
|
871
969
|
//#endregion
|
|
872
|
-
export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl };
|
|
970
|
+
export { CLIENT_CONTEXT_KEY, FRAME_NAV_CHANNEL, FRAME_NAV_VERSION, attachFrameNavClient, buildRemoteDevframeUrl, connectRemoteDevframe, createDevframeClientHost, createDockRenderersContext, createMessagesClient, getDevframeClientContext, parseRemoteConnection, resolveDockIcon, resolveDockUrl, setDevframeClientContext, stripRemoteConnectionFromUrl, watchFrameLocation };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devframes/hub",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.9.0
|
|
4
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"devframe": "0.9.0
|
|
37
|
+
"devframe": "0.9.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@standard-schema/spec": "^1.1.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"mlly": "^1.8.2",
|
|
53
53
|
"tsdown": "^0.22.14",
|
|
54
54
|
"valibot": "^1.4.2",
|
|
55
|
-
"devframe": "0.9.0
|
|
55
|
+
"devframe": "0.9.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsdown",
|