@dimina-kit/devtools 0.4.0-dev.20260716214527 → 0.4.0-dev.20260718085557
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 +6 -1
- package/dist/main/index.bundle.js +7658 -7306
- package/dist/main/services/cdp-session/index.d.ts +87 -0
- package/dist/main/services/cdp-session/index.js +173 -0
- package/dist/main/services/elements-forward/index.d.ts +132 -15
- package/dist/main/services/elements-forward/index.js +162 -229
- package/dist/main/services/network-forward/body-cache.d.ts +70 -0
- package/dist/main/services/network-forward/body-cache.js +175 -0
- package/dist/main/services/network-forward/frontend-dispatch.d.ts +27 -0
- package/dist/main/services/network-forward/frontend-dispatch.js +42 -0
- package/dist/main/services/network-forward/index.d.ts +80 -9
- package/dist/main/services/network-forward/index.js +419 -149
- package/dist/main/services/render-inspect/index.d.ts +10 -0
- package/dist/main/services/render-inspect/index.js +17 -98
- package/dist/main/services/safe-area/index.d.ts +4 -1
- package/dist/main/services/safe-area/index.js +62 -34
- package/dist/main/services/simulator-appdata/index.d.ts +6 -0
- package/dist/main/services/simulator-appdata/index.js +20 -1
- package/dist/main/services/simulator-storage/index.d.ts +22 -4
- package/dist/main/services/simulator-storage/index.js +51 -64
- package/dist/main/services/views/native-simulator-devtools-host.js +12 -1
- package/dist/main/services/views/native-simulator-view.js +6 -0
- package/dist/main/services/views/view-manager.d.ts +9 -0
- package/dist/main/services/views/view-manager.js +1 -1
- package/dist/main/services/workbench-context.d.ts +9 -0
- package/dist/main/services/workbench-context.js +3 -0
- package/dist/main/utils/ipc-registry.d.ts +2 -1
- package/dist/main/utils/ipc-registry.js +30 -10
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/renderer/assets/index-U9F8MrtV.js +49 -0
- package/dist/renderer/assets/{input-7P0WbXyg.js → input-c2OcrrZy.js} +2 -2
- package/dist/renderer/assets/ipc-transport-Blac3sOj.css +1 -0
- package/dist/renderer/assets/{ipc-transport-DTEmLpxx.js → ipc-transport-CuDJ07aE.js} +2 -2
- package/dist/renderer/assets/{popover-DsOcyIS6.js → popover-kv2qspWz.js} +2 -2
- package/dist/renderer/assets/{select-Bzbhbg9Z.js → select-DKqzMtTh.js} +2 -2
- package/dist/renderer/assets/{settings-api-B1wVFAPB.js → settings-api-qpXKDRXt.js} +2 -2
- package/dist/renderer/assets/{settings-CR1TyBWu.js → settings-dJNKTGr8.js} +2 -2
- package/dist/renderer/assets/{workbenchSettings-CFbToT4Q.js → workbenchSettings-DQ_VP5-W.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/appdata-set-data.cjs +39 -0
- package/dist/service-host/preload.cjs +21 -0
- package/dist/shared/ipc-channels.d.ts +9 -0
- package/dist/shared/ipc-channels.js +11 -0
- package/package.json +5 -5
- package/dist/renderer/assets/index-CZd3LyY8.js +0 -49
- package/dist/renderer/assets/ipc-transport-BGtDnv6p.css +0 -1
|
@@ -17,6 +17,7 @@ import type { WebContents } from 'electron';
|
|
|
17
17
|
import type { ConnectionRegistry } from '@dimina-kit/electron-deck/main';
|
|
18
18
|
import type { ElementInspection } from '../../../shared/ipc-channels.js';
|
|
19
19
|
import type { WxmlNode } from '@dimina-kit/inspect';
|
|
20
|
+
import { type CdpSessionBroker } from '../cdp-session/index.js';
|
|
20
21
|
export interface RenderInspector {
|
|
21
22
|
/** Inject (once per wc) the inspector IIFE, then read the WXML tree. */
|
|
22
23
|
getWxml(wc: WebContents): Promise<WxmlNode | null>;
|
|
@@ -45,6 +46,15 @@ export interface RenderInspectorOptions {
|
|
|
45
46
|
* back to the direct `once('destroyed')`).
|
|
46
47
|
*/
|
|
47
48
|
connections?: ConnectionRegistry;
|
|
49
|
+
/**
|
|
50
|
+
* Shared CDP session broker (see cdp-session/index.ts) that owns every
|
|
51
|
+
* render-guest debugger session's attach/detach lifecycle — safe-area,
|
|
52
|
+
* elements-forward and network-forward acquire leases from the same
|
|
53
|
+
* instance. Absent → a private broker is created (never explicitly torn
|
|
54
|
+
* down here, matching this module's existing lack of a `dispose()`; the
|
|
55
|
+
* shared instance IS disposed at the app-context level).
|
|
56
|
+
*/
|
|
57
|
+
broker?: CdpSessionBroker;
|
|
48
58
|
}
|
|
49
59
|
export declare function createRenderInspector(options?: RenderInspectorOptions): RenderInspector;
|
|
50
60
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { devtoolsPackageRoot } from '../../utils/paths.js';
|
|
4
|
+
import { createCdpSessionBroker } from '../cdp-session/index.js';
|
|
4
5
|
const DEFAULT_SOURCE_PATH = 'dist/render-host/render-inspect.js';
|
|
5
6
|
/**
|
|
6
7
|
* Chrome DevTools' default Elements-panel highlight palette (content / padding /
|
|
@@ -36,13 +37,9 @@ export function createRenderInspector(options = {}) {
|
|
|
36
37
|
(() => readFileSync(path.join(devtoolsPackageRoot, DEFAULT_SOURCE_PATH), 'utf8'));
|
|
37
38
|
let cachedSource = null;
|
|
38
39
|
const injected = new Set();
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
const selfAttached = new Set();
|
|
43
|
-
// Per-wc `DOM.enable → Overlay.enable` handshake, started once and reused so a
|
|
44
|
-
// burst of hovers shares one enable. Cleared when the guest is destroyed.
|
|
45
|
-
const enablePromises = new Map();
|
|
40
|
+
// Shared CDP session broker — owns attach/detach/enable-domain bookkeeping
|
|
41
|
+
// (see cdp-session/index.ts); this module no longer tracks any of that itself.
|
|
42
|
+
const broker = options.broker ?? createCdpSessionBroker({ connections: options.connections });
|
|
46
43
|
function source() {
|
|
47
44
|
if (cachedSource === null)
|
|
48
45
|
cachedSource = loadSource();
|
|
@@ -75,7 +72,6 @@ export function createRenderInspector(options = {}) {
|
|
|
75
72
|
// bespoke `once('destroyed')` only when omitted (focused unit tests).
|
|
76
73
|
const forget = () => {
|
|
77
74
|
injected.delete(wc.id);
|
|
78
|
-
enablePromises.delete(wc.id);
|
|
79
75
|
};
|
|
80
76
|
if (options.connections) {
|
|
81
77
|
options.connections.acquire(wc).own(forget);
|
|
@@ -122,16 +118,17 @@ export function createRenderInspector(options = {}) {
|
|
|
122
118
|
/**
|
|
123
119
|
* Paint the Chrome-style native highlight over the guest via CDP — the same
|
|
124
120
|
* `Overlay.highlightNode` the embedded Elements panel uses, so the WXML panel's
|
|
125
|
-
* hover box matches it exactly.
|
|
126
|
-
*
|
|
127
|
-
* attached it) and only
|
|
121
|
+
* hover box matches it exactly. Acquires a lease from the shared broker (see
|
|
122
|
+
* cdp-session/index.ts), which reuses the guest's existing debugger session
|
|
123
|
+
* (safe-area / elements-forward has usually already attached it) and only
|
|
124
|
+
* attaches itself when nobody has.
|
|
128
125
|
*
|
|
129
126
|
* `Overlay.highlightNode` paints only while the Overlay domain is enabled, and
|
|
130
127
|
* Chromium rejects `Overlay.enable` with "DOM should be enabled first" unless
|
|
131
128
|
* DOM is enabled first. On a cold/self-attached session (WXML hovered without
|
|
132
129
|
* the Elements panel ever enabling Overlay) the highlight would silently no-op
|
|
133
|
-
* if the command raced ahead of the enable, so we AWAIT the
|
|
134
|
-
*
|
|
130
|
+
* if the command raced ahead of the enable, so we AWAIT the broker's
|
|
131
|
+
* `ensureRenderDomains()` handshake before highlighting — but only up to
|
|
135
132
|
* `ENABLE_HANDSHAKE_TIMEOUT_MS`, so a hung `DOM.enable` degrades to a missed
|
|
136
133
|
* paint rather than a stuck hover.
|
|
137
134
|
*
|
|
@@ -142,14 +139,15 @@ export function createRenderInspector(options = {}) {
|
|
|
142
139
|
async function drawNativeHighlight(wc, sid) {
|
|
143
140
|
if (wc.isDestroyed())
|
|
144
141
|
return;
|
|
145
|
-
|
|
142
|
+
const lease = broker.acquire(wc);
|
|
143
|
+
if (!lease)
|
|
146
144
|
return;
|
|
147
|
-
await withTimeout(
|
|
145
|
+
await withTimeout(lease.ensureRenderDomains(), ENABLE_HANDSHAKE_TIMEOUT_MS);
|
|
148
146
|
if (wc.isDestroyed())
|
|
149
147
|
return;
|
|
150
148
|
const expression = `window.__diminaRenderInspect && window.__diminaRenderInspect.elementFor(${JSON.stringify(sid)})`;
|
|
151
149
|
try {
|
|
152
|
-
const evaluated = await
|
|
150
|
+
const evaluated = await lease.send('Runtime.evaluate', {
|
|
153
151
|
expression,
|
|
154
152
|
returnByValue: false,
|
|
155
153
|
objectGroup: HOVER_OBJECT_GROUP,
|
|
@@ -157,7 +155,7 @@ export function createRenderInspector(options = {}) {
|
|
|
157
155
|
const objectId = evaluated?.result?.objectId;
|
|
158
156
|
if (!objectId)
|
|
159
157
|
return;
|
|
160
|
-
await
|
|
158
|
+
await lease.send('Overlay.highlightNode', {
|
|
161
159
|
objectId,
|
|
162
160
|
highlightConfig: HIGHLIGHT_CONFIG,
|
|
163
161
|
});
|
|
@@ -165,90 +163,11 @@ export function createRenderInspector(options = {}) {
|
|
|
165
163
|
finally {
|
|
166
164
|
// Drop the hover's remote DOM reference so repeated hovers don't accumulate
|
|
167
165
|
// live wrappers in the guest context.
|
|
168
|
-
|
|
169
|
-
.
|
|
166
|
+
lease
|
|
167
|
+
.send('Runtime.releaseObjectGroup', { objectGroup: HOVER_OBJECT_GROUP })
|
|
170
168
|
.catch(() => { });
|
|
171
169
|
}
|
|
172
170
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Ensure the guest's debugger is usable WITHOUT opening a second session: a
|
|
175
|
-
* webContents debugger is single-owner, so reuse the already-attached session
|
|
176
|
-
* (Elements-forward / safe-area) and only `attach('1.3')` when nobody has. The
|
|
177
|
-
* sessions we open ourselves are tracked so guest-destroy detaches only ours.
|
|
178
|
-
* Returns false when the debugger can't be made usable.
|
|
179
|
-
*/
|
|
180
|
-
function ensureGuestDebugger(wc) {
|
|
181
|
-
try {
|
|
182
|
-
if (wc.debugger.isAttached())
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
wc.debugger.attach('1.3');
|
|
190
|
-
trackSelfAttached(wc);
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
// A concurrent attach (race) leaves it usable; anything else is a failure.
|
|
195
|
-
try {
|
|
196
|
-
return wc.debugger.isAttached();
|
|
197
|
-
}
|
|
198
|
-
catch {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
/** Record a self-attached session and detach it when the guest is destroyed. */
|
|
204
|
-
function trackSelfAttached(wc) {
|
|
205
|
-
if (selfAttached.has(wc.id))
|
|
206
|
-
return;
|
|
207
|
-
selfAttached.add(wc.id);
|
|
208
|
-
const detach = () => {
|
|
209
|
-
selfAttached.delete(wc.id);
|
|
210
|
-
try {
|
|
211
|
-
if (!wc.isDestroyed() && wc.debugger.isAttached())
|
|
212
|
-
wc.debugger.detach();
|
|
213
|
-
}
|
|
214
|
-
catch { /* already gone */ }
|
|
215
|
-
};
|
|
216
|
-
if (options.connections)
|
|
217
|
-
options.connections.acquire(wc).own(detach);
|
|
218
|
-
else {
|
|
219
|
-
try {
|
|
220
|
-
wc.once('destroyed', () => selfAttached.delete(wc.id));
|
|
221
|
-
}
|
|
222
|
-
catch { /* fake wc */ }
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Enable the DOM + Overlay render domains in dependency order, ONCE per guest
|
|
227
|
-
* (the promise is cached + reused across hovers). `Overlay.enable` is sent ONLY
|
|
228
|
-
* AFTER `DOM.enable` resolves (Chromium rejects it otherwise). Resolves when
|
|
229
|
-
* Overlay is enabled; a rejection at any step is swallowed (the guest may be
|
|
230
|
-
* mid-teardown) and the cache entry is dropped so a later hover can retry.
|
|
231
|
-
*/
|
|
232
|
-
function ensureDomainsEnabled(wc) {
|
|
233
|
-
const cached = enablePromises.get(wc.id);
|
|
234
|
-
if (cached)
|
|
235
|
-
return cached;
|
|
236
|
-
wc.debugger.sendCommand('CSS.enable').catch(() => { });
|
|
237
|
-
const handshake = wc.debugger
|
|
238
|
-
.sendCommand('DOM.enable')
|
|
239
|
-
.then(() => {
|
|
240
|
-
if (wc.isDestroyed())
|
|
241
|
-
return;
|
|
242
|
-
return wc.debugger.sendCommand('Overlay.enable');
|
|
243
|
-
})
|
|
244
|
-
.then(() => undefined)
|
|
245
|
-
.catch(() => {
|
|
246
|
-
// Allow a retry on the next hover (e.g. the guest was mid-destroy).
|
|
247
|
-
enablePromises.delete(wc.id);
|
|
248
|
-
});
|
|
249
|
-
enablePromises.set(wc.id, handshake);
|
|
250
|
-
return handshake;
|
|
251
|
-
}
|
|
252
171
|
async function unhighlight(wc) {
|
|
253
172
|
if (wc.isDestroyed())
|
|
254
173
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WebContents } from 'electron';
|
|
2
2
|
import type { ConnectionRegistry } from '@dimina-kit/electron-deck/main';
|
|
3
3
|
import type { NativeDeviceInfo } from '../../../shared/ipc-channels.js';
|
|
4
|
+
import { type CdpSessionBroker } from '../cdp-session/index.js';
|
|
4
5
|
export interface SafeAreaController {
|
|
5
6
|
/** Attach the debugger to a freshly-attached render-host guest and push the
|
|
6
7
|
* current device's insets. `isTabPage` selects the bottom-inset policy (0 for
|
|
@@ -10,10 +11,12 @@ export interface SafeAreaController {
|
|
|
10
11
|
/** Re-push insets to every still-attached guest after a device change (each
|
|
11
12
|
* guest keeps the page type it attached with). */
|
|
12
13
|
reapplyAll(device: NativeDeviceInfo | null): void;
|
|
13
|
-
/**
|
|
14
|
+
/** Release this controller's session leases (teardown). Does not itself
|
|
15
|
+
* detach the shared debugger session — see cdp-session/index.ts. */
|
|
14
16
|
dispose(): void;
|
|
15
17
|
}
|
|
16
18
|
export declare function createSafeAreaController(options?: {
|
|
17
19
|
connections?: ConnectionRegistry;
|
|
20
|
+
broker?: CdpSessionBroker;
|
|
18
21
|
}): SafeAreaController;
|
|
19
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createCdpSessionBroker } from '../cdp-session/index.js';
|
|
1
2
|
function guestInsets(device, isTabPage) {
|
|
2
3
|
const top = device?.safeAreaInsets.top ?? 0;
|
|
3
4
|
// A tab page's content sits above the shell-drawn tabBar (which fills the
|
|
@@ -8,57 +9,84 @@ function guestInsets(device, isTabPage) {
|
|
|
8
9
|
return { top, topMax: top, right: 0, rightMax: 0, bottom, bottomMax: bottom, left: 0, leftMax: 0 };
|
|
9
10
|
}
|
|
10
11
|
export function createSafeAreaController(options = {}) {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Own (and dispose on this controller's own dispose()) a private broker
|
|
13
|
+
// only when the caller didn't supply a shared one.
|
|
14
|
+
const ownsBroker = !options.broker;
|
|
15
|
+
// The shared CDP session broker (see cdp-session/index.ts) — reused across
|
|
16
|
+
// safe-area/elements-forward/render-inspect/network-forward when the caller
|
|
17
|
+
// passes one; falls back to a private instance so this module stays
|
|
18
|
+
// independently testable/usable.
|
|
19
|
+
const broker = options.broker ?? createCdpSessionBroker({ connections: options.connections });
|
|
20
|
+
// Each guest's page type, fixed for its life — tracked SEPARATELY from the
|
|
21
|
+
// lease so a lost session (external detach) doesn't lose the policy: a
|
|
22
|
+
// later `override`/`reapplyAll` can reacquire and keep applying the same
|
|
23
|
+
// isTabPage this guest attached with.
|
|
24
|
+
const pageType = new Map();
|
|
25
|
+
// Current lease per guest, if any. Cleared (not just left stale) on
|
|
26
|
+
// `lease.onDetach` — an external detach or a real Chrome DevTools window
|
|
27
|
+
// stealing the session — so the next `override` reacquires instead of
|
|
28
|
+
// sending through a dead lease forever.
|
|
29
|
+
const leases = new Map();
|
|
30
|
+
/** Get-or-reacquire this guest's lease. Null when the session is unavailable. */
|
|
31
|
+
function ensureLease(wc) {
|
|
32
|
+
const existing = leases.get(wc);
|
|
33
|
+
if (existing)
|
|
34
|
+
return existing;
|
|
35
|
+
const lease = broker.acquire(wc);
|
|
36
|
+
if (!lease)
|
|
37
|
+
return null;
|
|
38
|
+
leases.set(wc, lease);
|
|
39
|
+
lease.onDetach(() => { leases.delete(wc); });
|
|
40
|
+
return lease;
|
|
41
|
+
}
|
|
15
42
|
function override(wc, device, isTabPage) {
|
|
16
43
|
if (wc.isDestroyed())
|
|
17
44
|
return;
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
const lease = ensureLease(wc);
|
|
46
|
+
if (!lease) {
|
|
47
|
+
// Exclusively held elsewhere (e.g. a real Chrome DevTools window via
|
|
48
|
+
// --remote-debugging-port). Degrade: leave env at 0 rather than fail
|
|
49
|
+
// the page.
|
|
50
|
+
console.warn('[safe-area] debugger session unavailable; env(safe-area-inset-*) stays 0');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
void lease
|
|
54
|
+
.send('Emulation.setSafeAreaInsetsOverride', { insets: guestInsets(device, isTabPage) })
|
|
20
55
|
.catch((err) => {
|
|
21
56
|
console.warn('[safe-area] setSafeAreaInsetsOverride failed:', err instanceof Error ? err.message : err);
|
|
22
57
|
});
|
|
23
58
|
}
|
|
24
59
|
return {
|
|
25
60
|
applyToGuest: (wc, device, isTabPage) => {
|
|
26
|
-
if (!wc || wc.isDestroyed()
|
|
27
|
-
if (wc && !wc.isDestroyed() && attached.has(wc))
|
|
28
|
-
override(wc, device, isTabPage);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
try {
|
|
32
|
-
wc.debugger.attach('1.3');
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
// Already attached by an external --remote-debugging-port client (or
|
|
36
|
-
// DevTools). Degrade: leave env at 0 rather than fail the page.
|
|
37
|
-
console.warn('[safe-area] debugger.attach failed; env(safe-area-inset-*) stays 0:', err instanceof Error ? err.message : err);
|
|
61
|
+
if (!wc || wc.isDestroyed())
|
|
38
62
|
return;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
const isFirstTime = !pageType.has(wc);
|
|
64
|
+
pageType.set(wc, isTabPage);
|
|
65
|
+
if (isFirstTime) {
|
|
66
|
+
const forget = () => { pageType.delete(wc); leases.delete(wc); };
|
|
67
|
+
if (options.connections) {
|
|
68
|
+
options.connections.acquire(wc).own(forget);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
wc.once('destroyed', forget);
|
|
72
|
+
}
|
|
46
73
|
}
|
|
47
74
|
override(wc, device, isTabPage);
|
|
48
75
|
},
|
|
49
76
|
reapplyAll: (device) => {
|
|
50
|
-
for (const [wc, isTabPage] of
|
|
77
|
+
for (const [wc, isTabPage] of pageType)
|
|
51
78
|
override(wc, device, isTabPage);
|
|
52
79
|
},
|
|
53
80
|
dispose: () => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
// Release our leases only — the shared session's actual detach is the
|
|
82
|
+
// broker's own top-level dispose() to decide (another consumer may
|
|
83
|
+
// still be using it).
|
|
84
|
+
for (const lease of leases.values())
|
|
85
|
+
lease.dispose();
|
|
86
|
+
leases.clear();
|
|
87
|
+
pageType.clear();
|
|
88
|
+
if (ownsBroker)
|
|
89
|
+
broker.dispose();
|
|
62
90
|
},
|
|
63
91
|
};
|
|
64
92
|
}
|
|
@@ -30,6 +30,12 @@ export interface SimulatorAppDataService extends AppDataTap, Disposable {
|
|
|
30
30
|
export interface SimulatorAppDataOptions {
|
|
31
31
|
getActiveAppId: () => string | null;
|
|
32
32
|
senderPolicy?: SenderPolicy;
|
|
33
|
+
/** Resolves the service-host WebContents owning a page bridge — the SetData
|
|
34
|
+
* write-back target. Absent (host without a service bridge) → edits are
|
|
35
|
+
* rejected with `false`. */
|
|
36
|
+
bridge?: {
|
|
37
|
+
getServiceWcForBridge: (bridgeId: string) => WebContents | null;
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
40
|
export declare function setupSimulatorAppData(host: WebContents, options: SimulatorAppDataOptions): SimulatorAppDataService;
|
|
35
41
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SimulatorAppDataChannel } from '../../../shared/ipc-channels.js';
|
|
1
|
+
import { ServiceHostChannel, SimulatorAppDataChannel } from '../../../shared/ipc-channels.js';
|
|
2
2
|
import { AppDataAccumulator, decodeWorkerMessage, decodedToInput, } from '@dimina-kit/inspect';
|
|
3
3
|
import { DisposableRegistry } from '@dimina-kit/electron-deck/main';
|
|
4
4
|
import { IpcRegistry } from '../../utils/ipc-registry.js';
|
|
@@ -54,6 +54,25 @@ export function setupSimulatorAppData(host, options) {
|
|
|
54
54
|
return EMPTY_SNAPSHOT;
|
|
55
55
|
return accumulators.get(appId)?.snapshot() ?? EMPTY_SNAPSHOT;
|
|
56
56
|
});
|
|
57
|
+
/** A plain, non-empty `{key: value}` patch — the only shape page.setData accepts. */
|
|
58
|
+
function isValidSetDataPatch(data) {
|
|
59
|
+
return !!data && typeof data === 'object' && !Array.isArray(data)
|
|
60
|
+
&& Object.keys(data).length > 0;
|
|
61
|
+
}
|
|
62
|
+
ipc.handle(SimulatorAppDataChannel.SetData, (_event, payload) => {
|
|
63
|
+
const p = payload;
|
|
64
|
+
if (!p || typeof p !== 'object')
|
|
65
|
+
return false;
|
|
66
|
+
if (typeof p.bridgeId !== 'string' || p.bridgeId.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
if (!isValidSetDataPatch(p.data))
|
|
69
|
+
return false;
|
|
70
|
+
const wc = options.bridge?.getServiceWcForBridge(p.bridgeId);
|
|
71
|
+
if (!wc || wc.isDestroyed())
|
|
72
|
+
return false;
|
|
73
|
+
wc.send(ServiceHostChannel.AppDataSetData, { bridgeId: p.bridgeId, data: p.data });
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
57
76
|
// disposeAll runs LIFO; add the IPC registry LAST so it is torn down first —
|
|
58
77
|
// its removeHandler then runs synchronously (before the first `await` yields),
|
|
59
78
|
// which callers that `dispose()` without awaiting rely on.
|
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
* SimulatorStorageWatcher
|
|
3
3
|
*
|
|
4
4
|
* Attaches the Chrome DevTools Protocol debugger to the simulator <webview>
|
|
5
|
-
* and forwards `DOMStorage.*` events to the renderer host.
|
|
5
|
+
* and forwards `DOMStorage.*` events to the renderer host. The SAME simulator
|
|
6
|
+
* wc's debugger session is independently captured by network-forward
|
|
7
|
+
* (Network.* events); both acquire a lease from the shared `CdpSessionBroker`
|
|
8
|
+
* (see cdp-session/index.ts) rather than each attaching/detaching directly —
|
|
9
|
+
* releasing our own lease never forces a physical detach that would kill the
|
|
10
|
+
* other's capture.
|
|
6
11
|
*
|
|
7
12
|
* Trade-offs vs a preload-side localStorage.setItem hook:
|
|
8
13
|
* + Uses standard browser protocol; no preload injection
|
|
9
14
|
* + Captures every change including ones bypassing wx (api-compat fallback,
|
|
10
15
|
* direct localStorage.setItem from devtools, etc.)
|
|
11
16
|
* + Decouples panel UI from dimina runtime's wx implementation
|
|
12
|
-
* - debugger.attach is mutually exclusive with Chrome DevTools
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* - debugger.attach is mutually exclusive with a REAL Chrome DevTools window
|
|
18
|
+
* (F12) attaching to the same target from outside the broker. If a user
|
|
19
|
+
* opens one, the shared session detaches externally and events stop
|
|
20
|
+
* flowing until they close it.
|
|
15
21
|
*/
|
|
16
22
|
import { type WebContents } from 'electron';
|
|
17
23
|
import { type ConnectionRegistry, type Disposable } from '@dimina-kit/electron-deck/main';
|
|
@@ -19,6 +25,7 @@ import { type SyncStorageChange } from '../../../shared/ipc-channels.js';
|
|
|
19
25
|
import { type SenderPolicy } from '../../utils/ipc-registry.js';
|
|
20
26
|
import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
|
|
21
27
|
import type { RenderInspector } from '../render-inspect/index.js';
|
|
28
|
+
import { type CdpSessionBroker } from '../cdp-session/index.js';
|
|
22
29
|
/**
|
|
23
30
|
* Runtime async-storage handler (native-host). bridge-router routes async
|
|
24
31
|
* `wx.setStorage`/`getStorage`/… here so they hit the SAME service-host `file://`
|
|
@@ -78,6 +85,17 @@ export interface SimulatorStorageOptions {
|
|
|
78
85
|
* not adopted the connection layer compile and behave unchanged.
|
|
79
86
|
*/
|
|
80
87
|
connections?: ConnectionRegistry;
|
|
88
|
+
/**
|
|
89
|
+
* Shared CDP session broker (see cdp-session/index.ts) that owns the
|
|
90
|
+
* simulator wc's debugger session lifecycle — network-forward independently
|
|
91
|
+
* captures Network.* events on the SAME simulator wc, and an unconditional
|
|
92
|
+
* `debugger.detach()` here (the pre-migration behavior) would kill ITS
|
|
93
|
+
* capture too, exactly as an unconditional detach on its side would kill
|
|
94
|
+
* ours. Absent → a private broker is created and owned for this module's
|
|
95
|
+
* lifetime (torn down on `dispose()`), so existing standalone callers/tests
|
|
96
|
+
* compile and behave unchanged, just without cross-module session sharing.
|
|
97
|
+
*/
|
|
98
|
+
broker?: CdpSessionBroker;
|
|
81
99
|
}
|
|
82
100
|
export declare function setupSimulatorStorage(host: WebContents, options: SimulatorStorageOptions): SimulatorStorageHandle;
|
|
83
101
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,22 +2,29 @@
|
|
|
2
2
|
* SimulatorStorageWatcher
|
|
3
3
|
*
|
|
4
4
|
* Attaches the Chrome DevTools Protocol debugger to the simulator <webview>
|
|
5
|
-
* and forwards `DOMStorage.*` events to the renderer host.
|
|
5
|
+
* and forwards `DOMStorage.*` events to the renderer host. The SAME simulator
|
|
6
|
+
* wc's debugger session is independently captured by network-forward
|
|
7
|
+
* (Network.* events); both acquire a lease from the shared `CdpSessionBroker`
|
|
8
|
+
* (see cdp-session/index.ts) rather than each attaching/detaching directly —
|
|
9
|
+
* releasing our own lease never forces a physical detach that would kill the
|
|
10
|
+
* other's capture.
|
|
6
11
|
*
|
|
7
12
|
* Trade-offs vs a preload-side localStorage.setItem hook:
|
|
8
13
|
* + Uses standard browser protocol; no preload injection
|
|
9
14
|
* + Captures every change including ones bypassing wx (api-compat fallback,
|
|
10
15
|
* direct localStorage.setItem from devtools, etc.)
|
|
11
16
|
* + Decouples panel UI from dimina runtime's wx implementation
|
|
12
|
-
* - debugger.attach is mutually exclusive with Chrome DevTools
|
|
13
|
-
*
|
|
14
|
-
*
|
|
17
|
+
* - debugger.attach is mutually exclusive with a REAL Chrome DevTools window
|
|
18
|
+
* (F12) attaching to the same target from outside the broker. If a user
|
|
19
|
+
* opens one, the shared session detaches externally and events stop
|
|
20
|
+
* flowing until they close it.
|
|
15
21
|
*/
|
|
16
22
|
import { app, webContents as wcStatic } from 'electron';
|
|
17
23
|
import { DisposableRegistry } from '@dimina-kit/electron-deck/main';
|
|
18
24
|
import { SimulatorElementChannel, SimulatorStorageChannel, } from '../../../shared/ipc-channels.js';
|
|
19
25
|
import { IpcRegistry } from '../../utils/ipc-registry.js';
|
|
20
26
|
import { decodeStorageValue, encodeStorageValue, serviceStorage } from './service-storage-ops.js';
|
|
27
|
+
import { createCdpSessionBroker } from '../cdp-session/index.js';
|
|
21
28
|
/** Async wx storage API names routed through the unified service-window store. */
|
|
22
29
|
export const STORAGE_API_NAMES = new Set([
|
|
23
30
|
'setStorage',
|
|
@@ -55,8 +62,12 @@ export function setupSimulatorStorage(host, options) {
|
|
|
55
62
|
return null;
|
|
56
63
|
return options.bridge.getServiceWc(getActiveAppId() ?? undefined);
|
|
57
64
|
}
|
|
65
|
+
// Own (and dispose on this module's own dispose()) a private broker only
|
|
66
|
+
// when the caller didn't supply a shared one.
|
|
67
|
+
const ownsBroker = !options.broker;
|
|
68
|
+
const broker = options.broker ?? createCdpSessionBroker({ connections: options.connections });
|
|
58
69
|
let attachedWc = null;
|
|
59
|
-
let
|
|
70
|
+
let attachedLease = null;
|
|
60
71
|
/**
|
|
61
72
|
* Cached origin discovered via `executeJavaScript('location.origin')` on
|
|
62
73
|
* first use. The simulator <webview> loads `simulator.html` from a stable
|
|
@@ -69,24 +80,21 @@ export function setupSimulatorStorage(host, options) {
|
|
|
69
80
|
const appId = getActiveAppId();
|
|
70
81
|
return appId ? `${appId}_` : null;
|
|
71
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Release OUR OWN lease on the simulator wc's shared session — never a
|
|
85
|
+
* physical `debugger.detach()`. The SAME wc is independently captured by
|
|
86
|
+
* network-forward (Network.* events); an unconditional detach here would
|
|
87
|
+
* kill its capture too. The broker (cdp-session/index.ts) alone decides
|
|
88
|
+
* when an actual detach happens.
|
|
89
|
+
*/
|
|
72
90
|
function detachFromSim() {
|
|
73
91
|
if (!attachedWc)
|
|
74
92
|
return;
|
|
75
|
-
const wc = attachedWc;
|
|
76
93
|
attachedWc = null;
|
|
77
94
|
cachedOrigin = null;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
void ad.disposeAll().catch(() => { });
|
|
82
|
-
try {
|
|
83
|
-
if (!wc.isDestroyed() && wc.debugger.isAttached()) {
|
|
84
|
-
wc.debugger.detach();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// best-effort
|
|
89
|
-
}
|
|
95
|
+
const lease = attachedLease;
|
|
96
|
+
attachedLease = null;
|
|
97
|
+
lease?.dispose();
|
|
90
98
|
}
|
|
91
99
|
async function attachToSim(wc) {
|
|
92
100
|
if (attachedWc === wc)
|
|
@@ -94,57 +102,30 @@ export function setupSimulatorStorage(host, options) {
|
|
|
94
102
|
if (wc.isDestroyed())
|
|
95
103
|
return;
|
|
96
104
|
detachFromSim();
|
|
105
|
+
const lease = broker.acquire(wc);
|
|
106
|
+
if (!lease) {
|
|
107
|
+
console.warn('[storage-watcher] attach failed: debugger session unavailable');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
97
110
|
try {
|
|
98
|
-
|
|
99
|
-
wc.debugger.attach('1.3');
|
|
100
|
-
}
|
|
101
|
-
await wc.debugger.sendCommand('DOMStorage.enable');
|
|
102
|
-
const attach = new DisposableRegistry();
|
|
103
|
-
attachDisposables = attach;
|
|
104
|
-
const onMessage = (_event, method, params) => forwardCdpMessage(method, params);
|
|
105
|
-
wc.debugger.on('message', onMessage);
|
|
106
|
-
attach.add(() => safeOff(wc.debugger, 'message', onMessage));
|
|
107
|
-
const onDetach = () => {
|
|
108
|
-
if (attachedWc === wc)
|
|
109
|
-
attachedWc = null;
|
|
110
|
-
};
|
|
111
|
-
wc.debugger.on('detach', onDetach);
|
|
112
|
-
attach.add(() => safeOff(wc.debugger, 'detach', onDetach));
|
|
113
|
-
const onDestroyed = () => {
|
|
114
|
-
// When the attached wc is destroyed, the lingering attachDisposables
|
|
115
|
-
// hold listener refs to a wc that will never emit again. Dispose the
|
|
116
|
-
// registry here so debugger/wc listeners are removed deterministically
|
|
117
|
-
// and a subsequent attachToSim() starts from a clean slate.
|
|
118
|
-
if (attachedWc === wc) {
|
|
119
|
-
attachedWc = null;
|
|
120
|
-
const ad = attachDisposables;
|
|
121
|
-
attachDisposables = null;
|
|
122
|
-
if (ad)
|
|
123
|
-
void ad.disposeAll().catch(() => { });
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
if (options.connections) {
|
|
127
|
-
// Route through the connection layer: own() disposes onDestroyed
|
|
128
|
-
// deterministically on wc destroy / connection reset, and the returned
|
|
129
|
-
// Disposable lets us release it early when this attach segment tears
|
|
130
|
-
// down (detach / re-attach).
|
|
131
|
-
const owned = options.connections.acquire(wc).own(onDestroyed);
|
|
132
|
-
attach.add(() => owned.dispose());
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
try {
|
|
136
|
-
wc.once('destroyed', onDestroyed);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// fake-wc safety (unit tests pass a wc without a real emitter)
|
|
140
|
-
}
|
|
141
|
-
attach.add(() => safeOff(wc, 'destroyed', onDestroyed));
|
|
142
|
-
}
|
|
143
|
-
attachedWc = wc;
|
|
111
|
+
await lease.send('DOMStorage.enable');
|
|
144
112
|
}
|
|
145
113
|
catch (e) {
|
|
146
114
|
console.warn('[storage-watcher] attach failed:', e.message);
|
|
115
|
+
lease.dispose();
|
|
116
|
+
return;
|
|
147
117
|
}
|
|
118
|
+
lease.onMessage((method, params) => forwardCdpMessage(method, params));
|
|
119
|
+
// Fires on external detach OR the wc being destroyed (see cdp-session's
|
|
120
|
+
// design doc) — either way our tracking of this wc as "attached" is stale.
|
|
121
|
+
lease.onDetach(() => {
|
|
122
|
+
if (attachedWc === wc) {
|
|
123
|
+
attachedWc = null;
|
|
124
|
+
attachedLease = null;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
attachedWc = wc;
|
|
128
|
+
attachedLease = lease;
|
|
148
129
|
}
|
|
149
130
|
function forwardCdpMessage(method, params) {
|
|
150
131
|
if (host.isDestroyed())
|
|
@@ -520,6 +501,12 @@ export function setupSimulatorStorage(host, options) {
|
|
|
520
501
|
registry.add(ipc);
|
|
521
502
|
// Detach active CDP session last
|
|
522
503
|
registry.add(() => detachFromSim());
|
|
504
|
+
// Only detach sessions we self-attached if we own the broker's lifecycle —
|
|
505
|
+
// a shared/injected broker keeps serving other consumers past our dispose().
|
|
506
|
+
registry.add(() => {
|
|
507
|
+
if (ownsBroker)
|
|
508
|
+
broker.dispose();
|
|
509
|
+
});
|
|
523
510
|
// ── Runtime async-storage unification (native-host) ───────────────────────
|
|
524
511
|
// bridge-router routes async wx.setStorage/etc. here so they hit the SAME
|
|
525
512
|
// service-host file:// store as the sync APIs (no more split origin). After a
|
|
@@ -377,7 +377,18 @@ export function createDevtoolsHost(ctx, reconciler, deps) {
|
|
|
377
377
|
// instance (its reconcile loop never gets to install the hook, so Elements
|
|
378
378
|
// falls back to the natively-inspected service host). Stop only this wc's own
|
|
379
379
|
// forward, and clear the module pointer only while it still points here.
|
|
380
|
-
const thisForward = installElementsForward({
|
|
380
|
+
const thisForward = installElementsForward({
|
|
381
|
+
devtoolsWc,
|
|
382
|
+
bridge: ctx.bridge,
|
|
383
|
+
connections: ctx.connections,
|
|
384
|
+
// Shared session broker (see cdp-session/index.ts) — undefined falls
|
|
385
|
+
// back to a private instance owned by this call.
|
|
386
|
+
broker: ctx.cdpSessionBroker,
|
|
387
|
+
// Body/post-data lookups for the virtual requestIds the network
|
|
388
|
+
// forwarder injects — answered from its prefetch cache when the
|
|
389
|
+
// front-end's Response tab round-trips Network.getResponseBody.
|
|
390
|
+
network: ctx.networkForward?.bodies,
|
|
391
|
+
});
|
|
381
392
|
stopElementsForward = thisForward;
|
|
382
393
|
devtoolsWc.once('destroyed', () => {
|
|
383
394
|
try {
|
|
@@ -281,6 +281,12 @@ export function createNativeSimulatorView(ctx, reconciler, deps) {
|
|
|
281
281
|
// render-host URL in will-attach (FIFO).
|
|
282
282
|
const isTabGuest = pendingGuestIsTab.shift() ?? false;
|
|
283
283
|
safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
|
|
284
|
+
// Page-level resource loads (images/fonts/page fetch) run in THIS guest's
|
|
285
|
+
// network stack, never the simulator's — without this, only wx.request
|
|
286
|
+
// (forwarded to the simulator) shows in the Network panel and everything
|
|
287
|
+
// the page itself loads is invisible. Shares the already-attached
|
|
288
|
+
// safe-area debugger session (never a second attach/detach owner).
|
|
289
|
+
ctx.networkForward?.attachRenderGuest(guestWc);
|
|
284
290
|
guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
|
|
285
291
|
guestWc.on('will-navigate', (e, url) => {
|
|
286
292
|
try {
|