@dimina-kit/devtools 0.4.0-dev.20260630123203 → 0.4.0-dev.20260701155140
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/main/app/app.js +17 -0
- package/dist/main/app/max-listeners-diagnostic.d.ts +32 -0
- package/dist/main/app/max-listeners-diagnostic.js +59 -0
- package/dist/main/index.bundle.js +426 -220
- package/dist/main/ipc/bridge-router.d.ts +5 -4
- package/dist/main/ipc/bridge-router.js +25 -1
- package/dist/main/ipc/simulator.js +1 -5
- package/dist/main/ipc/views.js +8 -19
- package/dist/main/services/automation/wait-active-page.d.ts +4 -2
- package/dist/main/services/render-inspect/index.d.ts +9 -0
- package/dist/main/services/render-inspect/index.js +13 -1
- package/dist/main/services/simulator-storage/index.d.ts +9 -0
- package/dist/main/services/simulator-storage/index.js +26 -1
- package/dist/main/services/simulator-wxml/index.js +117 -11
- package/dist/main/services/views/apply-view-ops.d.ts +11 -0
- package/dist/main/services/views/apply-view-ops.js +22 -0
- package/dist/main/services/views/placement-test-driver.d.ts +25 -0
- package/dist/main/services/views/placement-test-driver.js +48 -0
- package/dist/main/services/views/view-manager.d.ts +9 -55
- package/dist/main/services/views/view-manager.js +264 -233
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/preload/windows/host-toolbar-runtime.cjs +6 -13
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +0 -3
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +0 -3
- package/dist/render-host/render-inspect.js +35 -1
- package/dist/renderer/assets/index-jDrNHghH.js +49 -0
- package/dist/renderer/assets/{input-BbWPVJlM.js → input-Djg2NR4U.js} +2 -2
- package/dist/renderer/assets/{ipc-transport-DTT-j7AD.js → ipc-transport-Be-AzCub.js} +2 -2
- package/dist/renderer/assets/ipc-transport-C6-KPMqw.css +1 -0
- package/dist/renderer/assets/{popover-CjAdKdAo.js → popover-eDUh4NYe.js} +2 -2
- package/dist/renderer/assets/{select-RyHeTVsc.js → select-DesFfkVV.js} +2 -2
- package/dist/renderer/assets/{settings-DaR5vif_.js → settings-CRmO7zUG.js} +2 -2
- package/dist/renderer/assets/{settings-api-DXYCjMQY.js → settings-api-HQ7ZFPWi.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CwW0JEe6.js → workbenchSettings-CZBWsliT.js} +2 -2
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/service-host/sync-api-patch.js +33 -3
- package/dist/shared/ipc-channels.d.ts +28 -14
- package/dist/shared/ipc-channels.js +10 -17
- package/dist/shared/ipc-schemas.d.ts +21 -19
- package/dist/shared/ipc-schemas.js +32 -26
- package/dist/shared/view-ids.d.ts +19 -0
- package/dist/shared/view-ids.js +25 -0
- package/dist/simulator/assets/device-shell-CT3LRnTB.js +2 -0
- package/dist/simulator/assets/{simulator-CYPhufF2.js → simulator-BizhGGE3.js} +3 -3
- package/dist/simulator/simulator.html +1 -1
- package/package.json +5 -5
- package/dist/renderer/assets/index-BMwmmaSp.js +0 -46
- package/dist/renderer/assets/ipc-transport-BZ4gxQ4_.css +0 -1
- package/dist/simulator/assets/device-shell-H3Bq5NgA.js +0 -2
|
@@ -12,12 +12,13 @@ import type { DebugTap } from '@dimina-kit/electron-deck/main';
|
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
14
|
* Render-side activity worth re-reading panel data on: a page's DOM mounted
|
|
15
|
-
* (`domReady`)
|
|
16
|
-
* from the
|
|
17
|
-
*
|
|
15
|
+
* (`domReady`), the visible page changed (`activePage`), or the active page's
|
|
16
|
+
* DOM mutated in place (`domMutated`, from the render-guest MutationObserver).
|
|
17
|
+
* Panels that pull from the active render guest (WXML/element-inspect) subscribe
|
|
18
|
+
* via `BridgeRouterHandle.onRenderEvent` so they can refresh without polling.
|
|
18
19
|
*/
|
|
19
20
|
export interface RenderEvent {
|
|
20
|
-
kind: 'domReady' | 'activePage';
|
|
21
|
+
kind: 'domReady' | 'activePage' | 'domMutated';
|
|
21
22
|
appId: string;
|
|
22
23
|
bridgeId: string;
|
|
23
24
|
/** activePage only: the now-visible page's route (bare pagePath), if known.
|
|
@@ -583,6 +583,7 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
583
583
|
poolEntryId,
|
|
584
584
|
onServiceClosed: null,
|
|
585
585
|
onServiceBoot: null,
|
|
586
|
+
registryHandle: null,
|
|
586
587
|
};
|
|
587
588
|
const rootPage = {
|
|
588
589
|
bridgeId,
|
|
@@ -617,7 +618,7 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
617
618
|
state.wcIdToAppSessionId.delete(appSession.serviceWc.id);
|
|
618
619
|
}
|
|
619
620
|
});
|
|
620
|
-
ctx.registry.add(() => disposeAppSession(state, appSessionId));
|
|
621
|
+
appSession.registryHandle = ctx.registry.add(() => disposeAppSession(state, appSessionId));
|
|
621
622
|
const onServiceClosed = () => {
|
|
622
623
|
void disposeAppSession(state, appSessionId, { serviceAlreadyClosed: true });
|
|
623
624
|
};
|
|
@@ -977,6 +978,20 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
977
978
|
// Guarded + non-throwing: console capture must never break message routing.
|
|
978
979
|
ctx.guestConsole?.emit(msg.body);
|
|
979
980
|
break;
|
|
981
|
+
case 'storageChanged':
|
|
982
|
+
// Native-host SYNC storage liveness: the service-host's `setStorageSync`/etc.
|
|
983
|
+
// write `localStorage` directly (no main round-trip), so they post the change
|
|
984
|
+
// here for the Storage panel to stay live. Trust `ap.appId` (sender-resolved),
|
|
985
|
+
// not any appId in the body. Guarded + non-throwing.
|
|
986
|
+
ctx.onServiceStorageChanged?.(ap.appId, msg.body);
|
|
987
|
+
break;
|
|
988
|
+
case 'wxmlChanged':
|
|
989
|
+
// Native-host WXML liveness: the render-guest MutationObserver posts this when
|
|
990
|
+
// the active page's DOM mutated in place (setData). Surface it as a render
|
|
991
|
+
// event so the WXML panel service re-pulls + pushes — same pipeline as
|
|
992
|
+
// domReady/activePage. Trust `page.bridgeId` (sender-resolved), not the body.
|
|
993
|
+
state.emitRenderEvent({ kind: 'domMutated', appId: ap.appId, bridgeId: page.bridgeId });
|
|
994
|
+
break;
|
|
980
995
|
default:
|
|
981
996
|
break;
|
|
982
997
|
}
|
|
@@ -1428,6 +1443,15 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1428
1443
|
if (!ap)
|
|
1429
1444
|
return;
|
|
1430
1445
|
state.appSessions.delete(appSessionId);
|
|
1446
|
+
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1447
|
+
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1448
|
+
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1449
|
+
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1450
|
+
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1451
|
+
// the fn, so this dispose() is a no-op then.)
|
|
1452
|
+
const registryHandle = ap.registryHandle;
|
|
1453
|
+
ap.registryHandle = null;
|
|
1454
|
+
void registryHandle?.dispose();
|
|
1431
1455
|
state.appLifecycle.dispose(appSessionId);
|
|
1432
1456
|
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1433
1457
|
// page teardown below progressively empties (and finally clears).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ServiceHostChannel, SimulatorChannel, SimulatorCustomApiChannel } from '../../shared/ipc-channels.js';
|
|
2
|
-
import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema,
|
|
2
|
+
import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema, } from '../../shared/ipc-schemas.js';
|
|
3
3
|
import { deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
|
|
4
4
|
import { validate } from '../utils/ipc-schema.js';
|
|
5
5
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
@@ -11,10 +11,6 @@ export function registerSimulatorIpc(ctx) {
|
|
|
11
11
|
})
|
|
12
12
|
.handle(SimulatorChannel.Detach, () => {
|
|
13
13
|
ctx.views.detachSimulator();
|
|
14
|
-
})
|
|
15
|
-
.handle(SimulatorChannel.SetNativeBounds, (_, ...args) => {
|
|
16
|
-
const [p] = validate(SimulatorChannel.SetNativeBounds, SimulatorSetNativeBoundsSchema, args);
|
|
17
|
-
ctx.views.setNativeSimulatorViewBounds(p);
|
|
18
14
|
})
|
|
19
15
|
.handle(SimulatorChannel.SetDeviceInfo, (_, ...args) => {
|
|
20
16
|
const [device] = validate(SimulatorChannel.SetDeviceInfo, SimulatorSetDeviceInfoSchema, args);
|
package/dist/main/ipc/views.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ipcMain } from 'electron';
|
|
2
2
|
import { ViewChannel } from '../../shared/ipc-channels.js';
|
|
3
|
-
import {
|
|
3
|
+
import { PlacementSnapshotSchema, HostToolbarAdvertiseHeightSchema, } from '../../shared/ipc-schemas.js';
|
|
4
4
|
import { validate } from '../utils/ipc-schema.js';
|
|
5
5
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
6
6
|
/**
|
|
@@ -20,24 +20,13 @@ import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export function registerViewsIpc(ctx) {
|
|
22
22
|
const registry = new IpcRegistry(ctx.senderPolicy)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// was never attached (the host did not opt into the workbench editor).
|
|
31
|
-
.handle(ViewChannel.WorkbenchBounds, (_event, ...args) => {
|
|
32
|
-
const [bounds] = validate(ViewChannel.WorkbenchBounds, ViewBoundsSchema, args);
|
|
33
|
-
ctx.views.setWorkbenchBounds(bounds);
|
|
34
|
-
})
|
|
35
|
-
// Host-controllable toolbar: forward anchor (the MAIN renderer measures the
|
|
36
|
-
// placeholder rect → toolbar WCV bounds). invoke, mirroring
|
|
37
|
-
// SimulatorDevtoolsBounds. Sender is the trusted main renderer.
|
|
38
|
-
.handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
|
|
39
|
-
const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
|
|
40
|
-
ctx.views.setHostToolbarBounds(bounds);
|
|
23
|
+
// Window-level placement snapshot: the single source of truth for every
|
|
24
|
+
// managed native view's bounds/visibility/z-order. The renderer's central
|
|
25
|
+
// publisher coalesces one snapshot per frame; the reconciler diffs it
|
|
26
|
+
// against the actual view tree. Supersedes the per-view bounds channels.
|
|
27
|
+
.handle(ViewChannel.PlacementSnapshot, (_event, ...args) => {
|
|
28
|
+
const [snapshot] = validate(ViewChannel.PlacementSnapshot, PlacementSnapshotSchema, args);
|
|
29
|
+
ctx.views.setPlacementSnapshot(snapshot);
|
|
41
30
|
})
|
|
42
31
|
// Height replay pull: a freshly-mounted main-renderer placeholder asks for
|
|
43
32
|
// the last NOTIFIED toolbar height (main retains it — the toolbar's
|
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
* - Resolve exactly once; later events are inert and the subscription is torn
|
|
23
23
|
* down on resolve.
|
|
24
24
|
*/
|
|
25
|
-
/** The render-event shape `waitForActivePage` reads (subset of `RenderEvent`).
|
|
25
|
+
/** The render-event shape `waitForActivePage` reads (subset of `RenderEvent`).
|
|
26
|
+
* Only `activePage` events end the wait; other kinds (`domReady`/`domMutated`)
|
|
27
|
+
* are inert but included so a full `RenderEvent` is assignable here. */
|
|
26
28
|
export interface ActivePageEventLike {
|
|
27
|
-
kind: 'domReady' | 'activePage';
|
|
29
|
+
kind: 'domReady' | 'activePage' | 'domMutated';
|
|
28
30
|
appId: string;
|
|
29
31
|
bridgeId: string;
|
|
30
32
|
pagePath?: string;
|
|
@@ -24,6 +24,15 @@ export interface RenderInspector {
|
|
|
24
24
|
highlight(wc: WebContents, sid: string): Promise<ElementInspection | null>;
|
|
25
25
|
/** Clear any highlight overlay in the guest. */
|
|
26
26
|
unhighlight(wc: WebContents): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Toggle the guest-side WXML MutationObserver. When on, the injected IIFE
|
|
29
|
+
* watches `document.body` and posts a debounced `wxmlChanged` over
|
|
30
|
+
* `DiminaRenderBridge` on every DOM mutation (setData), which bridge-router
|
|
31
|
+
* turns into a `domMutated` render event so the WXML panel re-pulls. Only
|
|
32
|
+
* enabled while the WXML panel is visible, so an unseen panel never drives a
|
|
33
|
+
* full Vue-tree walk. Injects the IIFE first if needed; no-op on a dead guest.
|
|
34
|
+
*/
|
|
35
|
+
setWxmlObserving(wc: WebContents, on: boolean): Promise<void>;
|
|
27
36
|
}
|
|
28
37
|
export interface RenderInspectorOptions {
|
|
29
38
|
/** Override the injected IIFE source (default: read the built bundle). Test seam. */
|
|
@@ -259,6 +259,18 @@ export function createRenderInspector(options = {}) {
|
|
|
259
259
|
// best-effort
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
async function setWxmlObserving(wc, on) {
|
|
263
|
+
if (wc.isDestroyed())
|
|
264
|
+
return;
|
|
265
|
+
if (!(await ensureInjected(wc)))
|
|
266
|
+
return;
|
|
267
|
+
try {
|
|
268
|
+
await wc.executeJavaScript(`window.__diminaRenderInspect && window.__diminaRenderInspect.setWxmlObserving(${on ? 'true' : 'false'})`);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// best-effort: a guest mid-teardown just stops observing on its own.
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { getWxml, highlight, unhighlight, setWxmlObserving };
|
|
263
275
|
}
|
|
264
276
|
//# sourceMappingURL=index.js.map
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { type WebContents } from 'electron';
|
|
17
17
|
import { type ConnectionRegistry, type Disposable } from '@dimina-kit/electron-deck/main';
|
|
18
|
+
import { type SyncStorageChange } from '../../../shared/ipc-channels.js';
|
|
18
19
|
import { type SenderPolicy } from '../../utils/ipc-registry.js';
|
|
19
20
|
import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
|
|
20
21
|
import type { RenderInspector } from '../render-inspect/index.js';
|
|
@@ -34,6 +35,14 @@ export declare const STORAGE_API_NAMES: Set<string>;
|
|
|
34
35
|
export interface SimulatorStorageHandle extends Disposable {
|
|
35
36
|
/** Present only under native-host; wired to ctx.storageApi for bridge-router. */
|
|
36
37
|
storageApi: StorageApi | null;
|
|
38
|
+
/**
|
|
39
|
+
* Native-host SYNC-storage liveness: bridge-router calls this on each
|
|
40
|
+
* `storageChanged` container message the service-host posts after a
|
|
41
|
+
* `setStorageSync`/`removeStorageSync`/`clearStorageSync`. Those write the
|
|
42
|
+
* service-host `localStorage` directly (bypassing main), so they'd otherwise
|
|
43
|
+
* leave the panel stale until reload — this pushes the matching `StorageEvent`.
|
|
44
|
+
*/
|
|
45
|
+
onSyncStorageChange(appId: string, change: SyncStorageChange): void;
|
|
37
46
|
}
|
|
38
47
|
export interface SimulatorStorageOptions {
|
|
39
48
|
/**
|
|
@@ -573,7 +573,32 @@ export function setupSimulatorStorage(host, options) {
|
|
|
573
573
|
const storageApi = options.bridge?.isNativeHost()
|
|
574
574
|
? { invoke: runtimeInvoke }
|
|
575
575
|
: null;
|
|
576
|
-
|
|
576
|
+
/**
|
|
577
|
+
* SYNC wx storage APIs run inside the service-host window and write
|
|
578
|
+
* `localStorage` directly, so they never reach `setItem`/`runtimeInvoke` here.
|
|
579
|
+
* The service-host posts the change over `DiminaServiceBridge` and bridge-router
|
|
580
|
+
* routes it here so the panel stays live. Only the ACTIVE app's changes are
|
|
581
|
+
* surfaced (the panel shows one app); the key already carries the `${appId}_`
|
|
582
|
+
* prefix, matching the CDP / async event shape.
|
|
583
|
+
*/
|
|
584
|
+
function onSyncStorageChange(appId, change) {
|
|
585
|
+
if (appId !== getActiveAppId())
|
|
586
|
+
return;
|
|
587
|
+
if (host.isDestroyed())
|
|
588
|
+
return;
|
|
589
|
+
switch (change.op) {
|
|
590
|
+
case 'set':
|
|
591
|
+
pushSyntheticEvent({ type: 'added', key: change.key, newValue: change.value });
|
|
592
|
+
break;
|
|
593
|
+
case 'remove':
|
|
594
|
+
pushSyntheticEvent({ type: 'removed', key: change.key });
|
|
595
|
+
break;
|
|
596
|
+
case 'clear':
|
|
597
|
+
host.send(SimulatorStorageChannel.Event, { type: 'cleared' });
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { dispose: () => registry.dispose(), storageApi, onSyncStorageChange };
|
|
577
602
|
}
|
|
578
603
|
// Element inspection delegates to the simulator preload's __simulatorData
|
|
579
604
|
// bridge via executeJavaScript. Looked up independently from `attachedWc` so
|
|
@@ -4,25 +4,131 @@ import { IpcRegistry } from '../../utils/ipc-registry.js';
|
|
|
4
4
|
export function setupSimulatorWxml(host, options) {
|
|
5
5
|
const { bridge, inspector, getActiveAppId } = options;
|
|
6
6
|
const registry = new DisposableRegistry();
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// Whether the WXML panel is currently visible (renderer drives this via
|
|
8
|
+
// SetActive). Live tree pushes + the render-guest DOM observer only run while
|
|
9
|
+
// active, so an unseen panel never triggers a full Vue-tree walk.
|
|
10
|
+
let active = false;
|
|
11
|
+
// latest-wins: only the newest pull's result is pushed (a slow walk that
|
|
12
|
+
// resolves after a newer one must not clobber the fresher tree).
|
|
13
|
+
let seq = 0;
|
|
14
|
+
// in-flight coalescing: at most one pull runs at a time; a request that lands
|
|
15
|
+
// mid-pull sets `again` so exactly one follow-up pull runs after it settles.
|
|
16
|
+
let pulling = false;
|
|
17
|
+
let again = false;
|
|
18
|
+
// The render guest currently being observed, so we can stop it on page change
|
|
19
|
+
// / deactivation (single owner).
|
|
20
|
+
let observedWc = null;
|
|
21
|
+
/** The visible page's render WebContents of the active app, or null. */
|
|
22
|
+
function activeWc() {
|
|
23
|
+
return bridge.getActiveRenderWc(getActiveAppId() ?? undefined);
|
|
24
|
+
}
|
|
25
|
+
/** Read the WXML tree of a render guest, or null when there is none. */
|
|
26
|
+
async function pull(wc) {
|
|
10
27
|
if (!wc)
|
|
11
28
|
return null;
|
|
12
29
|
return inspector.getWxml(wc);
|
|
13
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Pull the active tree and push it, gated by `active` and reconciled by
|
|
33
|
+
* latest-wins + in-flight coalescing so a burst of render events (setData)
|
|
34
|
+
* collapses to a single fresh push instead of a pile of overlapping walks.
|
|
35
|
+
*/
|
|
36
|
+
function schedulePull() {
|
|
37
|
+
if (!active)
|
|
38
|
+
return;
|
|
39
|
+
const mySeq = ++seq;
|
|
40
|
+
if (pulling) {
|
|
41
|
+
again = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
pulling = true;
|
|
45
|
+
void pull(activeWc())
|
|
46
|
+
.then((tree) => {
|
|
47
|
+
// Re-check `active` at send time: a pull started while visible must NOT
|
|
48
|
+
// push after the panel was hidden / disposed (deactivate bumps `seq`, so
|
|
49
|
+
// `mySeq === seq` also guards, but the explicit `active` check is the
|
|
50
|
+
// clear invariant).
|
|
51
|
+
if (mySeq === seq && active && !host.isDestroyed()) {
|
|
52
|
+
host.send(SimulatorWxmlChannel.Event, tree);
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.finally(() => {
|
|
56
|
+
pulling = false;
|
|
57
|
+
if (again) {
|
|
58
|
+
again = false;
|
|
59
|
+
schedulePull();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function startObserving(wc) {
|
|
64
|
+
if (!wc)
|
|
65
|
+
return;
|
|
66
|
+
observedWc = wc;
|
|
67
|
+
// The guest IIFE defers observe() until document.body exists, so a wc whose
|
|
68
|
+
// DOM isn't up yet still starts observing once it loads — no retry needed here.
|
|
69
|
+
void inspector.setWxmlObserving(wc, true);
|
|
70
|
+
}
|
|
71
|
+
function stopObserving() {
|
|
72
|
+
const wc = observedWc;
|
|
73
|
+
observedWc = null;
|
|
74
|
+
if (wc)
|
|
75
|
+
void inspector.setWxmlObserving(wc, false);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Point the DOM observer at the CURRENT active render guest. Called on every
|
|
79
|
+
* render event while active, so the observer installs even when SetActive fired
|
|
80
|
+
* before the guest existed (the guest appears on `domReady`) and follows the
|
|
81
|
+
* active page across navigation (`activePage`).
|
|
82
|
+
*/
|
|
83
|
+
function reconcileObserving() {
|
|
84
|
+
if (!active)
|
|
85
|
+
return;
|
|
86
|
+
const wc = activeWc();
|
|
87
|
+
if (wc !== observedWc) {
|
|
88
|
+
stopObserving();
|
|
89
|
+
startObserving(wc);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Invalidate any in-flight pull + queued re-pull so nothing pushes post-hide. */
|
|
93
|
+
function cancelPending() {
|
|
94
|
+
seq++;
|
|
95
|
+
again = false;
|
|
96
|
+
}
|
|
14
97
|
const ipc = new IpcRegistry(options.senderPolicy);
|
|
15
|
-
ipc.handle(SimulatorWxmlChannel.GetSnapshot, () => pull());
|
|
98
|
+
ipc.handle(SimulatorWxmlChannel.GetSnapshot, () => pull(activeWc()));
|
|
99
|
+
ipc.handle(SimulatorWxmlChannel.SetActive, (_event, on) => {
|
|
100
|
+
if (on) {
|
|
101
|
+
active = true;
|
|
102
|
+
reconcileObserving();
|
|
103
|
+
schedulePull(); // seed the now-visible panel
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
active = false;
|
|
107
|
+
cancelPending();
|
|
108
|
+
stopObserving();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
16
111
|
registry.add(ipc);
|
|
17
|
-
// Re-read + push
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
112
|
+
// Re-read + push on render-side activity: a page's DOM mounted (`domReady`),
|
|
113
|
+
// the visible page changed (`activePage`), or the active page's DOM mutated in
|
|
114
|
+
// place (`domMutated`, from the render-guest observer). Gated by `active` and
|
|
115
|
+
// scoped to the ACTIVE app so a background app's / stale bridge's event never
|
|
116
|
+
// drives a spurious full walk of the visible page.
|
|
117
|
+
const unsubscribe = bridge.onRenderEvent((event) => {
|
|
118
|
+
if (!active)
|
|
119
|
+
return;
|
|
120
|
+
const appId = getActiveAppId();
|
|
121
|
+
if (appId && event.appId !== appId)
|
|
122
|
+
return;
|
|
123
|
+
// domReady: the guest may have just appeared → (re)install the observer.
|
|
124
|
+
// activePage: the active guest changed → move the observer.
|
|
125
|
+
// domMutated: same guest → reconcile is a no-op, then pull.
|
|
126
|
+
reconcileObserving();
|
|
127
|
+
schedulePull();
|
|
24
128
|
});
|
|
25
129
|
registry.add(unsubscribe);
|
|
130
|
+
// Stop observing + invalidate in-flight pulls on teardown (best-effort).
|
|
131
|
+
registry.add(() => { active = false; cancelPending(); stopObserving(); });
|
|
26
132
|
return registry;
|
|
27
133
|
}
|
|
28
134
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Bounds, ViewOp } from '@dimina-kit/electron-deck/layout';
|
|
2
|
+
import type { DevtoolsExtra } from '../../../shared/view-ids.js';
|
|
3
|
+
export interface ViewOpTarget {
|
|
4
|
+
attach(viewId: string): void;
|
|
5
|
+
detach(viewId: string): void;
|
|
6
|
+
setBounds(viewId: string, bounds: Bounds, extra?: DevtoolsExtra): void;
|
|
7
|
+
setVisible(viewId: string, visible: boolean): void;
|
|
8
|
+
reorder(order: string[]): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function applyViewOps(ops: ViewOp<DevtoolsExtra>[], target: ViewOpTarget): void;
|
|
11
|
+
//# sourceMappingURL=apply-view-ops.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function applyViewOps(ops, target) {
|
|
2
|
+
for (const op of ops) {
|
|
3
|
+
switch (op.kind) {
|
|
4
|
+
case 'detach':
|
|
5
|
+
target.detach(op.viewId);
|
|
6
|
+
break;
|
|
7
|
+
case 'setVisible':
|
|
8
|
+
target.setVisible(op.viewId, op.visible);
|
|
9
|
+
break;
|
|
10
|
+
case 'setBounds':
|
|
11
|
+
target.setBounds(op.viewId, op.bounds, op.extra);
|
|
12
|
+
break;
|
|
13
|
+
case 'attach':
|
|
14
|
+
target.attach(op.viewId);
|
|
15
|
+
break;
|
|
16
|
+
case 'reorder':
|
|
17
|
+
target.reorder(op.order);
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=apply-view-ops.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DesiredView } from '@dimina-kit/electron-deck/layout';
|
|
2
|
+
import { type DevtoolsExtra } from '../../../shared/view-ids.js';
|
|
3
|
+
interface Rect {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
interface SnapshotSink {
|
|
10
|
+
setPlacementSnapshot(snapshot: {
|
|
11
|
+
generation: number;
|
|
12
|
+
epoch: number;
|
|
13
|
+
views: DesiredView<DevtoolsExtra>[];
|
|
14
|
+
}): void;
|
|
15
|
+
}
|
|
16
|
+
export declare function hostToolbarBounds(mgr: SnapshotSink, rect: Rect): void;
|
|
17
|
+
export declare function simulatorDevtoolsBounds(mgr: SnapshotSink, rect: Rect): void;
|
|
18
|
+
export declare function workbenchBounds(mgr: SnapshotSink, rect: Rect): void;
|
|
19
|
+
export declare function simulatorBounds(mgr: SnapshotSink, params: Rect & {
|
|
20
|
+
zoom: number;
|
|
21
|
+
}): void;
|
|
22
|
+
/** Drop a view from this manager's desired table (reconcile detaches it). */
|
|
23
|
+
export declare function removeView(mgr: SnapshotSink, viewId: string): void;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=placement-test-driver.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Test-only helper: drives the view manager through its real runtime entry
|
|
2
|
+
// point, `setPlacementSnapshot`, the way the renderer's central publisher does
|
|
3
|
+
// — accumulating each view's desired placement (per manager) and publishing the
|
|
4
|
+
// WHOLE table under a monotonic epoch. A single-view update thus keeps the other
|
|
5
|
+
// views' last-declared placement, matching the renderer's level-triggered stream
|
|
6
|
+
// rather than the old per-view setXxxBounds edges. These free functions let the
|
|
7
|
+
// many view-manager tests exercise reconcile behaviour with a drop-in rename
|
|
8
|
+
// (`mgr.setHostToolbarBounds(r)` → `hostToolbarBounds(mgr, r)`).
|
|
9
|
+
import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
|
|
10
|
+
// Monotonic across every push in a test file (vitest isolates module state per
|
|
11
|
+
// file), so a fresh manager (lastEpoch -1) accepts the first push and repeated
|
|
12
|
+
// pushes on one manager never look stale.
|
|
13
|
+
let globalEpoch = 0;
|
|
14
|
+
const desiredByMgr = new WeakMap();
|
|
15
|
+
function toPlacement(r) {
|
|
16
|
+
return r.width > 0 && r.height > 0 ? { visible: true, bounds: r } : { visible: false };
|
|
17
|
+
}
|
|
18
|
+
function pushView(mgr, view, generation = 1) {
|
|
19
|
+
let desired = desiredByMgr.get(mgr);
|
|
20
|
+
if (!desired) {
|
|
21
|
+
desired = new Map();
|
|
22
|
+
desiredByMgr.set(mgr, desired);
|
|
23
|
+
}
|
|
24
|
+
desired.set(view.viewId, view);
|
|
25
|
+
mgr.setPlacementSnapshot({ generation, epoch: globalEpoch++, views: [...desired.values()] });
|
|
26
|
+
}
|
|
27
|
+
export function hostToolbarBounds(mgr, rect) {
|
|
28
|
+
pushView(mgr, { viewId: VIEW_ID.hostToolbar, placement: toPlacement(rect), layer: VIEW_LAYER.hostToolbar });
|
|
29
|
+
}
|
|
30
|
+
export function simulatorDevtoolsBounds(mgr, rect) {
|
|
31
|
+
pushView(mgr, { viewId: VIEW_ID.simulatorDevtools, placement: toPlacement(rect), layer: VIEW_LAYER.base });
|
|
32
|
+
}
|
|
33
|
+
export function workbenchBounds(mgr, rect) {
|
|
34
|
+
pushView(mgr, { viewId: VIEW_ID.workbench, placement: toPlacement(rect), layer: VIEW_LAYER.base });
|
|
35
|
+
}
|
|
36
|
+
export function simulatorBounds(mgr, params) {
|
|
37
|
+
const { zoom, ...rect } = params;
|
|
38
|
+
pushView(mgr, { viewId: VIEW_ID.simulator, placement: toPlacement(rect), layer: VIEW_LAYER.base, extra: { zoom } });
|
|
39
|
+
}
|
|
40
|
+
/** Drop a view from this manager's desired table (reconcile detaches it). */
|
|
41
|
+
export function removeView(mgr, viewId) {
|
|
42
|
+
const desired = desiredByMgr.get(mgr);
|
|
43
|
+
if (!desired)
|
|
44
|
+
return;
|
|
45
|
+
desired.delete(viewId);
|
|
46
|
+
mgr.setPlacementSnapshot({ generation: 1, epoch: globalEpoch++, views: [...desired.values()] });
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=placement-test-driver.js.map
|
|
@@ -3,6 +3,8 @@ import type { NativeDeviceInfo } from '../../../shared/ipc-channels.js';
|
|
|
3
3
|
import { type OpenInEditorRequest } from '../../../shared/open-in-editor.js';
|
|
4
4
|
import { type WorkbenchContext } from '../workbench-context.js';
|
|
5
5
|
import { type HostToolbarMessageSubscription } from './host-toolbar-port-channel.js';
|
|
6
|
+
import type { PlacementSnapshot } from '@dimina-kit/electron-deck/layout';
|
|
7
|
+
import { type DevtoolsExtra } from '../../../shared/view-ids.js';
|
|
6
8
|
/**
|
|
7
9
|
* Context surface used by the ViewManager. We only need a small slice of the
|
|
8
10
|
* full WorkbenchContext here; typing it this way documents the actual dependency.
|
|
@@ -11,10 +13,10 @@ export interface ViewManagerContext {
|
|
|
11
13
|
windows: WorkbenchContext['windows'];
|
|
12
14
|
rendererDir: string;
|
|
13
15
|
/**
|
|
14
|
-
* Per-webContents connection registry
|
|
16
|
+
* Per-webContents connection registry. The native-host
|
|
15
17
|
* simulator WebContentsView is acquired here so the registry tracks that
|
|
16
18
|
* trusted webContents and tears its per-wc resources (the custom-api bridge
|
|
17
|
-
* `ipcMain.on`) down deterministically on destroy
|
|
19
|
+
* `ipcMain.on`) down deterministically on destroy. Required:
|
|
18
20
|
* `createWorkbenchContext` always supplies it.
|
|
19
21
|
*/
|
|
20
22
|
connections: WorkbenchContext['connections'];
|
|
@@ -135,22 +137,6 @@ export interface ViewManager {
|
|
|
135
137
|
* always on close-project → reopen) would otherwise be lost forever.
|
|
136
138
|
*/
|
|
137
139
|
getHostToolbarHeight(): number;
|
|
138
|
-
/**
|
|
139
|
-
* NATIVE-HOST ONLY. Position the simulator content WebContentsView over the
|
|
140
|
-
* renderer-measured simulator panel REGION rect (the flex:1 placeholder slot,
|
|
141
|
-
* CSS px from the main window content top-left, which maps 1:1 to overlay
|
|
142
|
-
* setBounds DIP) and apply the device zoom. The WCV fills the region as a
|
|
143
|
-
* plain rectangle; DeviceShell draws + scrolls the phone inside. No-op in the
|
|
144
|
-
* default `<webview>` path (`nativeSimulatorView` is null). See
|
|
145
|
-
* `computeNativeSimulatorViewParams`.
|
|
146
|
-
*/
|
|
147
|
-
setNativeSimulatorViewBounds(params: {
|
|
148
|
-
x: number;
|
|
149
|
-
y: number;
|
|
150
|
-
width: number;
|
|
151
|
-
height: number;
|
|
152
|
-
zoom: number;
|
|
153
|
-
}): void;
|
|
154
140
|
/**
|
|
155
141
|
* NATIVE-HOST ONLY. Re-push the selected device's CSS `env(safe-area-inset-*)`
|
|
156
142
|
* override to every attached render-host guest (on device change). New guests
|
|
@@ -164,17 +150,12 @@ export interface ViewManager {
|
|
|
164
150
|
*/
|
|
165
151
|
resize(simWidth: number): void;
|
|
166
152
|
/**
|
|
167
|
-
* Apply
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
153
|
+
* Apply the renderer's window-level placement snapshot — the single source of
|
|
154
|
+
* truth for every managed native view's bounds/visibility/z-order. Merged with
|
|
155
|
+
* main-owned settings/popover desired state and reconciled against the actual
|
|
156
|
+
* view tree.
|
|
171
157
|
*/
|
|
172
|
-
|
|
173
|
-
x: number;
|
|
174
|
-
y: number;
|
|
175
|
-
width: number;
|
|
176
|
-
height: number;
|
|
177
|
-
}): void;
|
|
158
|
+
setPlacementSnapshot(snapshot: PlacementSnapshot<DevtoolsExtra>): void;
|
|
178
159
|
/**
|
|
179
160
|
* Lazily create the workbench WebContentsView, load `<url>index.html`, and
|
|
180
161
|
* add it to the contentView. `url` is the COI server's base URL (trailing
|
|
@@ -188,19 +169,6 @@ export interface ViewManager {
|
|
|
188
169
|
* `setWorkbenchBounds`, keeping it off the app boot critical path.
|
|
189
170
|
*/
|
|
190
171
|
setWorkbenchSource(url: string): void;
|
|
191
|
-
/**
|
|
192
|
-
* Apply a renderer-measured rectangle to the workbench editor view. Mirrors
|
|
193
|
-
* `setSimulatorDevtoolsBounds`: `{ width: 0, height: 0 }` is "hide" — the view
|
|
194
|
-
* is removed from the contentView (kept under settings/popover when re-added)
|
|
195
|
-
* but its WebContents stays alive. The first non-zero rect lazily creates +
|
|
196
|
-
* loads the workbench (from the URL set by `setWorkbenchSource`).
|
|
197
|
-
*/
|
|
198
|
-
setWorkbenchBounds(bounds: {
|
|
199
|
-
x: number;
|
|
200
|
-
y: number;
|
|
201
|
-
width: number;
|
|
202
|
-
height: number;
|
|
203
|
-
}): void;
|
|
204
172
|
/** Destroy the workbench editor view (teardown). No-op if never attached. */
|
|
205
173
|
detachWorkbench(): void;
|
|
206
174
|
/**
|
|
@@ -211,20 +179,6 @@ export interface ViewManager {
|
|
|
211
179
|
* request was dispatched to the workbench.
|
|
212
180
|
*/
|
|
213
181
|
openFileInWorkbench(relPath: string, line: number, column: number): boolean;
|
|
214
|
-
/**
|
|
215
|
-
* Apply a renderer-measured rectangle to the host-controllable toolbar
|
|
216
|
-
* WebContentsView (the strip above the devtools header). Forward anchor,
|
|
217
|
-
* mirroring `setSimulatorDevtoolsBounds`. `{ width: 0, height: 0 }` is
|
|
218
|
-
* treated as "hide" — the view is removed from the contentView but its
|
|
219
|
-
* WebContents (and the host's loaded content) stays alive. Lazily creates
|
|
220
|
-
* the view on the first non-empty rect.
|
|
221
|
-
*/
|
|
222
|
-
setHostToolbarBounds(bounds: {
|
|
223
|
-
x: number;
|
|
224
|
-
y: number;
|
|
225
|
-
width: number;
|
|
226
|
-
height: number;
|
|
227
|
-
}): void;
|
|
228
182
|
/**
|
|
229
183
|
* Reverse size-advertiser sink: the toolbar WCV's own renderer advertises
|
|
230
184
|
* its intrinsic content height (block-axis extent); we retain it as the
|