Skip to content

Commit 2c7979b

Browse files
committed
feat(hub): support eager iframe client scripts
1 parent ed7d846 commit 2c7979b

8 files changed

Lines changed: 79 additions & 1 deletion

File tree

docs/content/1.guide/17.client-context.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ A client-only dock can also carry `type: 'json-render'` with an inline [JSON-ren
6767

6868
## Dock client scripts
6969

70-
A client script is a `ClientScriptEntry`: `{ importFrom, importName? }` (`importName` defaults `'default'`). The field varies by entry kind: an `action` entry's `action` runs when the dock button is activated, a `custom-render` entry's `renderer` renders its panel, and an `iframe` entry's optional `clientScript` runs alongside the iframe panel inside the host page ([Hub API reference](/references/hub-api#dock-client-script-fields)).
70+
A client script is a `ClientScriptEntry`: `{ importFrom, importName?, eager? }` (`importName` defaults `'default'`). The field varies by entry kind: an `action` entry's `action` runs when the dock button is activated, a `custom-render` entry's `renderer` renders its panel, and an `iframe` entry's optional `clientScript` runs alongside the iframe panel inside the host page ([Hub API reference](/references/hub-api#dock-client-script-fields)).
71+
72+
Hub UI normally imports an iframe's client script when its dock is first activated. Set `eager: true` when the script must start observing the host app earlier: the module is imported and its exported function runs once, as soon as both the client context and dock entry are available. This is execution timing, not module preloading; it does not wait for the dock panel to open.
7173

7274
The exported function (`DockClientScriptContext`) receives the client context and two dock-scoped extras:
7375

docs/content/8.references/6.hub-api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ The properties of `DevframeClientContext`: [The client context](/guide/client-co
127127

128128
Which `ClientScriptEntry` field carries an entry's client script, and when it runs: [Dock client scripts](/guide/client-context#dock-client-scripts).
129129

130+
Every client script specifies `importFrom` and may specify `importName` (default: `default`). An iframe `clientScript` may additionally set `eager: true` to import and execute once as soon as the client context and dock entry are available, before activation.
131+
130132
| Entry kind | Field | Runs |
131133
|---|---|---|
132134
| `action` | `action` | when the dock button is activated |

packages/devframe/src/types/devframe.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,13 @@ export interface DevframeDockDefaults {
336336
* @default 'default'
337337
*/
338338
importName?: string
339+
/**
340+
* Import and execute this script as soon as the client context and dock
341+
* entry are available, without waiting for the dock to be activated.
342+
*
343+
* When omitted, the client runtime keeps its normal loading policy.
344+
*/
345+
eager?: boolean
339346
}
340347
}
341348

packages/hub-ui/src/client/state/context.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,45 @@ async function flushRestore(): Promise<void> {
7676
}
7777

7878
describe('createDocksContext', () => {
79+
it('runs eager iframe client scripts before activation', async () => {
80+
const { rpc, sharedStates } = createStubRpc()
81+
const executeSetupScriptMock = vi.mocked(executeSetupScript)
82+
executeSetupScriptMock.mockClear()
83+
const context = await createDocksContext('embedded', rpc)
84+
const eagerEntry = {
85+
id: 'eager',
86+
type: 'iframe',
87+
title: 'Eager',
88+
icon: 'ph:play',
89+
url: '/eager',
90+
clientScript: { importFrom: '/eager-client.js', eager: true },
91+
} satisfies DevframeDockEntry
92+
const activationEntry = {
93+
id: 'activation',
94+
type: 'iframe',
95+
title: 'Activation',
96+
icon: 'ph:play',
97+
url: '/activation',
98+
clientScript: { importFrom: '/activation-client.js' },
99+
} satisfies DevframeDockEntry
100+
101+
sharedStates.get('devframe:docks')!.push([eagerEntry, activationEntry])
102+
await flushRestore()
103+
104+
expect(executeSetupScriptMock).toHaveBeenCalledOnce()
105+
expect(executeSetupScriptMock).toHaveBeenLastCalledWith(
106+
eagerEntry,
107+
expect.objectContaining({ current: expect.objectContaining({ entryMeta: eagerEntry }) }),
108+
)
109+
110+
await context.docks.switchEntry('activation')
111+
expect(executeSetupScriptMock).toHaveBeenCalledTimes(2)
112+
expect(executeSetupScriptMock).toHaveBeenLastCalledWith(
113+
activationEntry,
114+
expect.objectContaining({ current: expect.objectContaining({ entryMeta: activationEntry }) }),
115+
)
116+
})
117+
79118
it('exposes restored panel state and emits selected, hidden, and closed changes', async () => {
80119
expect.assertions(9)
81120

packages/hub-ui/src/client/state/context.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,25 @@ export async function createDocksContext(
641641
clientType,
642642
})
643643

644+
// Hub UI normally loads an iframe client script on first activation. Some
645+
// page integrations need to observe the host app sooner, so start explicitly
646+
// eager scripts once the entry and complete client context are available.
647+
// executeSetupScript caches non-action scripts, preventing activation from
648+
// executing the same script again.
649+
watch(
650+
entries,
651+
(list) => {
652+
for (const entry of list) {
653+
if (entry.type !== 'iframe' || !entry.clientScript?.eager)
654+
continue
655+
runDockSetupScript(entry).catch(() => {
656+
// executeSetupScript already reports import and execution failures.
657+
})
658+
}
659+
},
660+
{ immediate: true, flush: 'post' },
661+
)
662+
644663
registerMainFrameDockActionHandler(clientType, async (id) => {
645664
const entry = entries.value.find(e => e.id === id)
646665
if (!entry || entry.type !== 'action')

packages/hub/src/types/docks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ export interface ClientScriptEntry {
188188
* @default 'default'
189189
*/
190190
importName?: string
191+
/**
192+
* Import and execute this script as soon as the client context and dock entry
193+
* are available, without waiting for the dock to be activated.
194+
*
195+
* When omitted, the client runtime keeps its normal loading policy.
196+
*/
197+
eager?: boolean
191198
}
192199

193200
declare module 'devframe/types' {

tests/__snapshots__/tsnapi/@devframes/hub/index.snapshot.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export interface ClientScriptEntry {
66
importFrom: string;
77
importName?: string;
8+
eager?: boolean;
89
}
910
export interface CreateHubContextOptions extends CreateHostContextOptions {}
1011
export interface DevframeChildProcessExecuteOptions {

tests/__snapshots__/tsnapi/devframe/index.snapshot.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export interface DevframeDockDefaults {
173173
clientScript?: {
174174
importFrom: string;
175175
importName?: string;
176+
eager?: boolean;
176177
};
177178
}
178179
export interface DevframeHost {

0 commit comments

Comments
 (0)