@dimina-kit/devtools 0.4.0-dev.20260718143821 → 0.4.0-dev.20260728063215

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.
Files changed (68) hide show
  1. package/dist/main/app/app.js +67 -1
  2. package/dist/main/app/lifecycle.js +9 -0
  3. package/dist/main/index.bundle.js +928 -162
  4. package/dist/main/ipc/bridge-router.d.ts +36 -0
  5. package/dist/main/ipc/bridge-router.js +56 -0
  6. package/dist/main/ipc/index.d.ts +1 -0
  7. package/dist/main/ipc/index.js +1 -0
  8. package/dist/main/ipc/internal-devtools.d.ts +15 -0
  9. package/dist/main/ipc/internal-devtools.js +9 -0
  10. package/dist/main/services/compile-standby.js +4 -0
  11. package/dist/main/services/console-forward/cdp-inject.d.ts +18 -0
  12. package/dist/main/services/console-forward/cdp-inject.js +30 -0
  13. package/dist/main/services/console-forward/global-console-mirror.d.ts +33 -0
  14. package/dist/main/services/console-forward/global-console-mirror.js +71 -0
  15. package/dist/main/services/console-forward/global-diagnostics-mirror.d.ts +33 -0
  16. package/dist/main/services/console-forward/global-diagnostics-mirror.js +73 -0
  17. package/dist/main/services/console-forward/index.d.ts +15 -1
  18. package/dist/main/services/console-forward/index.js +42 -2
  19. package/dist/main/services/console-forward/internal-log.d.ts +11 -0
  20. package/dist/main/services/console-forward/internal-log.js +23 -0
  21. package/dist/main/services/console-forward/open-gated-relay.d.ts +67 -0
  22. package/dist/main/services/console-forward/open-gated-relay.js +55 -0
  23. package/dist/main/services/diagnostics/index.d.ts +10 -0
  24. package/dist/main/services/elements-forward/index.js +14 -96
  25. package/dist/main/services/network-forward/frontend-dispatch.d.ts +64 -0
  26. package/dist/main/services/network-forward/frontend-dispatch.js +113 -8
  27. package/dist/main/services/network-forward/global-body-gate.d.ts +46 -0
  28. package/dist/main/services/network-forward/global-body-gate.js +93 -0
  29. package/dist/main/services/network-forward/global-mirror-test-fixtures.d.ts +48 -0
  30. package/dist/main/services/network-forward/global-mirror-test-fixtures.js +96 -0
  31. package/dist/main/services/network-forward/index.d.ts +31 -0
  32. package/dist/main/services/network-forward/index.js +256 -8
  33. package/dist/main/services/network-forward/user-facing.d.ts +21 -0
  34. package/dist/main/services/network-forward/user-facing.js +50 -0
  35. package/dist/main/services/views/console-filter.d.ts +145 -0
  36. package/dist/main/services/views/console-filter.js +233 -0
  37. package/dist/main/services/views/frontend-bootstrap-gate.d.ts +59 -0
  38. package/dist/main/services/views/frontend-bootstrap-gate.js +117 -0
  39. package/dist/main/services/views/native-simulator-devtools-host.js +138 -30
  40. package/dist/main/services/views/view-manager-devtools-host-test-fixtures.d.ts +76 -0
  41. package/dist/main/services/views/view-manager-devtools-host-test-fixtures.js +182 -0
  42. package/dist/main/services/workbench-context.d.ts +10 -0
  43. package/dist/main/utils/theme.d.ts +10 -6
  44. package/dist/main/utils/theme.js +10 -6
  45. package/dist/main/windows/internal-devtools-window/index.d.ts +66 -0
  46. package/dist/main/windows/internal-devtools-window/index.js +160 -0
  47. package/dist/native-host/render/render.js +89 -73
  48. package/dist/native-host/service/service.js +2 -2
  49. package/dist/preload/windows/host-toolbar-runtime.cjs.map +1 -1
  50. package/dist/preload/windows/simulator.cjs.map +1 -1
  51. package/dist/renderer/assets/index-M5NDq_vi.js +49 -0
  52. package/dist/renderer/assets/{input-c2OcrrZy.js → input-dsgeTmVX.js} +2 -2
  53. package/dist/renderer/assets/ipc-transport-BgFdT9bT.js +9 -0
  54. package/dist/renderer/assets/{popover-kv2qspWz.js → popover-Disfu1cx.js} +2 -2
  55. package/dist/renderer/assets/{select-DKqzMtTh.js → select-DAw1hzkl.js} +2 -2
  56. package/dist/renderer/assets/{settings-dJNKTGr8.js → settings-M_8GFW2M.js} +2 -2
  57. package/dist/renderer/assets/settings-api-DFR5eDl_.js +2 -0
  58. package/dist/renderer/assets/{workbenchSettings-DQ_VP5-W.js → workbenchSettings-CxdXeoOu.js} +2 -2
  59. package/dist/renderer/entries/main/index.html +5 -5
  60. package/dist/renderer/entries/popover/index.html +4 -4
  61. package/dist/renderer/entries/settings/index.html +4 -4
  62. package/dist/renderer/entries/workbench-settings/index.html +3 -3
  63. package/dist/shared/ipc-channels.d.ts +3 -0
  64. package/dist/shared/ipc-channels.js +6 -0
  65. package/package.json +8 -8
  66. package/dist/renderer/assets/index-DfDIvgvK.js +0 -49
  67. package/dist/renderer/assets/ipc-transport-CuDJ07aE.js +0 -9
  68. package/dist/renderer/assets/settings-api-qpXKDRXt.js +0 -2
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Shared test scaffolding for the Phase 2 "global mirror" test suites
3
+ * (`global-mirror.test.ts` + `global-mirror-bugfixes.test.ts`), split out so
4
+ * neither test file needs to duplicate these fixtures. Pure test scaffolding,
5
+ * not the code under test — see network-forward/index.test.ts for the
6
+ * canonical source these were originally copied from.
7
+ */
8
+ import { vi } from 'vitest';
9
+ export declare function flushMicrotasks(): Promise<void>;
10
+ export declare function makeSimWc(): {
11
+ wc: Electron.WebContents;
12
+ dbg: {
13
+ isAttached: () => boolean;
14
+ attach: import("vitest").Mock<() => void>;
15
+ detach: import("vitest").Mock<() => void>;
16
+ sendCommand: import("vitest").Mock<(_method: string, _params?: object) => Promise<unknown>>;
17
+ on: (ev: string, fn: (...args: unknown[]) => void) => void;
18
+ removeListener: (ev: string, fn: (...args: unknown[]) => void) => void;
19
+ };
20
+ sendCommand: import("vitest").Mock<(_method: string, _params?: object) => Promise<unknown>>;
21
+ emitMessage: (method: string, params: unknown) => void;
22
+ };
23
+ export declare function makeServiceWc(): {
24
+ wc: Electron.WebContents;
25
+ exec: import("vitest").Mock<(_script: string, _userGesture?: boolean) => Promise<undefined>>;
26
+ };
27
+ /**
28
+ * A DevTools FRONT-END host wc. The injected dispatch script returns `true` when
29
+ * `window.DevToolsAPI.dispatchMessage` is "present", so the fake resolves the
30
+ * configured value to simulate the API being ready (true) or still booting
31
+ * (false → forwarder retries / falls back).
32
+ */
33
+ export declare function makeDevtoolsWc(apiReady?: boolean | (() => boolean), isLoading?: () => boolean): {
34
+ wc: Electron.WebContents;
35
+ exec: import("vitest").Mock<(_script: string, _userGesture?: boolean) => Promise<boolean>>;
36
+ emitDestroyed: () => void;
37
+ };
38
+ /** Decode the messages array embedded in a buildDispatchScript() source. */
39
+ export declare function decodeDispatched(script: string): Array<{
40
+ method: string;
41
+ params: unknown;
42
+ }>;
43
+ /** All events mirrored into a fake devtools host wc across every exec() call. */
44
+ export declare function allDispatched(exec: ReturnType<typeof vi.fn>): Array<{
45
+ method: string;
46
+ params: unknown;
47
+ }>;
48
+ //# sourceMappingURL=global-mirror-test-fixtures.d.ts.map
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Shared test scaffolding for the Phase 2 "global mirror" test suites
3
+ * (`global-mirror.test.ts` + `global-mirror-bugfixes.test.ts`), split out so
4
+ * neither test file needs to duplicate these fixtures. Pure test scaffolding,
5
+ * not the code under test — see network-forward/index.test.ts for the
6
+ * canonical source these were originally copied from.
7
+ */
8
+ import { vi } from 'vitest';
9
+ export async function flushMicrotasks() {
10
+ await Promise.resolve();
11
+ await Promise.resolve();
12
+ }
13
+ export function makeSimWc() {
14
+ let attached = false;
15
+ const listeners = new Map();
16
+ const sendCommand = vi.fn((_method, _params) => Promise.resolve({}));
17
+ const dbg = {
18
+ isAttached: () => attached,
19
+ attach: vi.fn(() => { attached = true; }),
20
+ detach: vi.fn(() => { attached = false; }),
21
+ sendCommand,
22
+ on: (ev, fn) => {
23
+ if (!listeners.has(ev))
24
+ listeners.set(ev, new Set());
25
+ listeners.get(ev).add(fn);
26
+ },
27
+ removeListener: (ev, fn) => {
28
+ listeners.get(ev)?.delete(fn);
29
+ },
30
+ };
31
+ const wc = {
32
+ isDestroyed: () => false,
33
+ debugger: dbg,
34
+ once: () => { },
35
+ removeListener: () => { },
36
+ };
37
+ const emitMessage = (method, params) => {
38
+ for (const fn of listeners.get('message') ?? [])
39
+ fn({}, method, params);
40
+ };
41
+ return { wc, dbg, sendCommand, emitMessage };
42
+ }
43
+ export function makeServiceWc() {
44
+ const exec = vi.fn((_script, _userGesture) => Promise.resolve(undefined));
45
+ const wc = { isDestroyed: () => false, executeJavaScript: exec };
46
+ return { wc, exec };
47
+ }
48
+ /**
49
+ * A DevTools FRONT-END host wc. The injected dispatch script returns `true` when
50
+ * `window.DevToolsAPI.dispatchMessage` is "present", so the fake resolves the
51
+ * configured value to simulate the API being ready (true) or still booting
52
+ * (false → forwarder retries / falls back).
53
+ */
54
+ export function makeDevtoolsWc(apiReady = true, isLoading = () => false) {
55
+ const exec = vi.fn((_script, _userGesture) => Promise.resolve(typeof apiReady === 'function' ? apiReady() : apiReady));
56
+ const destroyedListeners = new Set();
57
+ const wc = {
58
+ isDestroyed: () => false,
59
+ isLoading,
60
+ getURL: () => 'devtools://devtools/bundled/devtools_app.html',
61
+ executeJavaScript: exec,
62
+ once: (ev, fn) => { if (ev === 'destroyed')
63
+ destroyedListeners.add(fn); },
64
+ removeListener: (ev, fn) => { if (ev === 'destroyed')
65
+ destroyedListeners.delete(fn); },
66
+ };
67
+ const emitDestroyed = () => { for (const fn of [...destroyedListeners])
68
+ fn(); };
69
+ return { wc, exec, emitDestroyed };
70
+ }
71
+ /** Decode the messages array embedded in a buildDispatchScript() source. */
72
+ export function decodeDispatched(script) {
73
+ const start = script.indexOf('JSON.parse("');
74
+ if (start < 0)
75
+ return [];
76
+ let i = start + 'JSON.parse('.length;
77
+ const open = i;
78
+ i++;
79
+ for (; i < script.length; i++) {
80
+ if (script[i] === '\\') {
81
+ i++;
82
+ continue;
83
+ }
84
+ if (script[i] === '"')
85
+ break;
86
+ }
87
+ const literal = script.slice(open, i + 1);
88
+ const inner = JSON.parse(literal);
89
+ const arr = JSON.parse(inner);
90
+ return arr.map((s) => JSON.parse(s));
91
+ }
92
+ /** All events mirrored into a fake devtools host wc across every exec() call. */
93
+ export function allDispatched(exec) {
94
+ return exec.mock.calls.flatMap((c) => decodeDispatched(String(c[0])));
95
+ }
96
+ //# sourceMappingURL=global-mirror-test-fixtures.js.map
@@ -128,6 +128,26 @@ export interface NetworkForwarderBridge {
128
128
  * `setDevtoolsHost`. Null until the DevTools host view exists.
129
129
  */
130
130
  getDevtoolsWc?(): WebContents | null;
131
+ /**
132
+ * The dimina resource server's baseUrl (e.g. `'http://127.0.0.1:54321/'`),
133
+ * when running — consulted by `isUserFacingRequest` so the framework
134
+ * bundle it serves is classified as host-internal, not business traffic.
135
+ * Absent → that origin check is simply skipped (the scheme rule alone
136
+ * still filters out file:// / difile:// / devtools:// framework loads).
137
+ */
138
+ getResourceServerBaseUrl?(): string | null;
139
+ /**
140
+ * The simulator's own static-asset server baseUrl (serves `simulator.html`
141
+ * + its JS/CSS — a devkit-owned server, independent from the resource
142
+ * server above, on its own dynamically-assigned port), when a project is
143
+ * open — consulted by `isUserFacingRequest` alongside
144
+ * `getResourceServerBaseUrl` so the simulator SHELL's own asset loading
145
+ * (captured because `attachSimulator` CDP-observes that whole
146
+ * WebContentsView, not just the mini-app's own `wx.request` calls) isn't
147
+ * misclassified as the developer's business traffic. Absent → that origin
148
+ * check is simply skipped.
149
+ */
150
+ getSimulatorServerBaseUrl?(): string | null;
131
151
  /**
132
152
  * Optional connection-layer registry (`@dimina-kit/electron-deck/main`). When
133
153
  * present, per-webContents teardowns route through `acquire(wc).own(d)` so the
@@ -179,6 +199,17 @@ export interface NetworkForwarder extends Disposable {
179
199
  * we fall back to the console line. Re-callable across DevTools re-creates.
180
200
  */
181
201
  setDevtoolsHost(wc: WebContents | null): void;
202
+ /**
203
+ * Point the forwarder at the WebContents hosting the standalone internal
204
+ * (app-wide) DevTools window's front-end — the global mirror sink.
205
+ * Unlike `setDevtoolsHost` (the user-facing sink), this has NO probing/
206
+ * ready/degraded state machine and NO console fallback: every
207
+ * FORWARDED_METHODS event is mirrored here raw and UNFILTERED (no
208
+ * `isUserFacingRequest` gating) whenever a host is set, best-effort (a
209
+ * failed dispatch is silently dropped — nobody depends on this path having
210
+ * a fallback). Pass null when the window closes.
211
+ */
212
+ setGlobalDevtoolsHost(wc: WebContents | null): void;
182
213
  /**
183
214
  * Manually surface a request that no `webContents.debugger` can observe
184
215
  * (e.g. a main-process direct send). Uses the console fallback sink.
@@ -4,6 +4,8 @@ import { packDispatchBatch } from './dispatch-batch.js';
4
4
  import { PrefetchCache, DEFAULT_PER_ENTRY_MAX_CHARS } from './body-cache.js';
5
5
  import { PROBE_DEVTOOLS_API, MAX_SINGLE_DISPATCH_CHARS, CHUNK_CHARS, buildChunkedDispatchScript } from './frontend-dispatch.js';
6
6
  import { createCdpSessionBroker } from '../cdp-session/index.js';
7
+ import { isUserFacingRequest } from './user-facing.js';
8
+ import { installGlobalNetworkBodyGate } from './global-body-gate.js';
7
9
  /**
8
10
  * Namespace prefix of every virtual requestId this forwarder injects into the
9
11
  * DevTools front-end. SINGLE SOURCE for the literal: the front-end outbound
@@ -302,6 +304,169 @@ export function createNetworkForwarder(bridge) {
302
304
  let devtoolsWc = null;
303
305
  // Teardown for the wc 'destroyed' watcher on the current host (clears the host).
304
306
  let devtoolsHostDisposable = null;
307
+ // ── Global mirror sink ──────────────────────────────────────────────────────
308
+ // The standalone internal (app-wide) DevTools window's front-end host wc.
309
+ // Deliberately NO probing/ready/degraded state machine and NO console
310
+ // fallback — see setGlobalDevtoolsHost's doc comment.
311
+ let globalDevtoolsWc = null;
312
+ let globalDevtoolsHostDisposable = null;
313
+ // Dedicated network-only outbound gate (body/post-data lookups) for the
314
+ // CURRENT global host — never elements-forward's gate, see
315
+ // global-body-gate.ts's header for why. Stopped whenever the host changes
316
+ // or clears.
317
+ let globalBodyGateStop = null;
318
+ // Bounded in-order hold for global-mirror messages arriving while the
319
+ // global host's front-end has not settled yet (its boot window) — without
320
+ // it those events are silently unrecoverable, breaking the window's
321
+ // full-stream promise. Scoped strictly to the CURRENT host: cleared when
322
+ // the host changes/clears, never replayed into a different host.
323
+ const MAX_GLOBAL_PENDING_QUEUE = 2000;
324
+ let globalPendingQueue = [];
325
+ let globalPendingOverflowWarned = false;
326
+ let globalFlushTimer = null;
327
+ // Wall-clock bound on the settle-poll (not on the queue itself): a
328
+ // front-end that never settles stops being polled after this, and the
329
+ // still-held queue flushes via the next incoming event instead.
330
+ const GLOBAL_FLUSH_POLL_MAX_MS = 60_000;
331
+ function clearGlobalPending() {
332
+ globalPendingQueue = [];
333
+ globalPendingOverflowWarned = false;
334
+ if (globalFlushTimer) {
335
+ clearTimeout(globalFlushTimer);
336
+ globalFlushTimer = null;
337
+ }
338
+ }
339
+ function scheduleGlobalFlush(startedAt = Date.now()) {
340
+ if (globalFlushTimer)
341
+ return;
342
+ if (Date.now() - startedAt >= GLOBAL_FLUSH_POLL_MAX_MS)
343
+ return;
344
+ // unref'd like scheduleRenderGuestRetry's timer: a settle-poll must
345
+ // never be what keeps the process (or a test file) alive.
346
+ globalFlushTimer = setTimeout(() => {
347
+ globalFlushTimer = null;
348
+ if (globalPendingQueue.length === 0)
349
+ return;
350
+ if (!globalDevtoolsWc || globalDevtoolsWc.isDestroyed()) {
351
+ clearGlobalPending();
352
+ return;
353
+ }
354
+ if (!isFrontendSettled(globalDevtoolsWc)) {
355
+ scheduleGlobalFlush(startedAt);
356
+ return;
357
+ }
358
+ flushGlobalPending();
359
+ }, READY_RETRY_MS);
360
+ globalFlushTimer.unref?.();
361
+ }
362
+ /** Drain the pending queue into the (settled) global host, oldest first,
363
+ * packed greedily under the same per-script size bound as the native sink —
364
+ * and through the SAME chunked transport for messages past
365
+ * MAX_SINGLE_DISPATCH_CHARS: a single oversized message stitched into one
366
+ * giant executeJavaScript overflows the IPC/script limit and the whole
367
+ * call rejects into a silent drop. */
368
+ function flushGlobalPending() {
369
+ if (globalPendingQueue.length === 0)
370
+ return;
371
+ const target = globalDevtoolsWc;
372
+ if (!target || target.isDestroyed()) {
373
+ clearGlobalPending();
374
+ return;
375
+ }
376
+ while (globalPendingQueue.length > 0) {
377
+ const { batch, chunked, remaining } = packDispatchBatch(globalPendingQueue, MAX_SINGLE_DISPATCH_CHARS, MAX_BATCH_CHARS);
378
+ globalPendingQueue = remaining;
379
+ for (const msg of chunked)
380
+ dispatchChunked(target, msg);
381
+ if (batch.length > 0) {
382
+ target.executeJavaScript(buildDispatchScript(batch), true).catch(() => { });
383
+ }
384
+ }
385
+ globalPendingOverflowWarned = false;
386
+ }
387
+ function enqueueGlobalPending(json) {
388
+ if (globalPendingQueue.length >= MAX_GLOBAL_PENDING_QUEUE) {
389
+ globalPendingQueue.shift();
390
+ // Not silent: dropping past the cap means the window's full-stream
391
+ // promise is degraded — say so once per overflow episode, not per event.
392
+ if (!globalPendingOverflowWarned) {
393
+ globalPendingOverflowWarned = true;
394
+ console.warn(`[network-forward] global mirror queue overflow (cap ${MAX_GLOBAL_PENDING_QUEUE}): dropping oldest events while the debug window front-end is still loading`);
395
+ }
396
+ }
397
+ globalPendingQueue.push(json);
398
+ scheduleGlobalFlush();
399
+ }
400
+ function applyGlobalDevtoolsHost(host) {
401
+ globalDevtoolsHostDisposable?.dispose();
402
+ globalDevtoolsHostDisposable = null;
403
+ globalBodyGateStop?.();
404
+ globalBodyGateStop = null;
405
+ // Queued events belong to the PREVIOUS host's boot window — never carry
406
+ // them across a host change (or into "no host").
407
+ clearGlobalPending();
408
+ globalDevtoolsWc = host && !host.isDestroyed() ? host : null;
409
+ if (!globalDevtoolsWc)
410
+ return;
411
+ const target = globalDevtoolsWc;
412
+ const onHostDestroyed = () => {
413
+ if (globalDevtoolsWc === target)
414
+ globalDevtoolsWc = null;
415
+ globalBodyGateStop?.();
416
+ globalBodyGateStop = null;
417
+ clearGlobalPending();
418
+ };
419
+ const reg = bridge.connections;
420
+ if (reg && typeof target.once === 'function') {
421
+ const owned = reg.acquire(target).own(onHostDestroyed);
422
+ globalDevtoolsHostDisposable = toDisposable(() => owned.dispose());
423
+ }
424
+ else {
425
+ if (typeof target.once === 'function')
426
+ target.once('destroyed', onHostDestroyed);
427
+ globalDevtoolsHostDisposable = toDisposable(() => {
428
+ try {
429
+ target.removeListener?.('destroyed', onHostDestroyed);
430
+ }
431
+ catch { /* gone */ }
432
+ });
433
+ }
434
+ globalBodyGateStop = installGlobalNetworkBodyGate(target, {
435
+ getResponseBody: (requestId) => bodyCache.lookup(requestId),
436
+ getRequestPostData: (requestId) => postDataCache.lookup(requestId),
437
+ });
438
+ }
439
+ /** Best-effort mirror of one raw CDP message into the global host. */
440
+ function dispatchToGlobal(method, params) {
441
+ if (!globalDevtoolsWc || globalDevtoolsWc.isDestroyed())
442
+ return;
443
+ let json;
444
+ try {
445
+ json = JSON.stringify({ method, params });
446
+ }
447
+ catch {
448
+ return;
449
+ }
450
+ // Same settled gate every other injection point in this file uses: an
451
+ // unsettled front-end wipes its state on load anyway, and executeJavaScript
452
+ // against it queues one did-stop-loading waiter per call — piling toward
453
+ // the MaxListeners ceiling during the global host's boot window. Unlike
454
+ // those injection points, the events here are NOT re-derivable later, so
455
+ // they queue for a post-settle flush instead of dropping.
456
+ if (!isFrontendSettled(globalDevtoolsWc)) {
457
+ enqueueGlobalPending(json);
458
+ return;
459
+ }
460
+ // Anything still queued flushes first so delivery order matches arrival.
461
+ flushGlobalPending();
462
+ // Oversized single messages take the chunked transport, mirroring the
463
+ // native sink's flushDispatch — see flushGlobalPending's doc.
464
+ if (json.length > MAX_SINGLE_DISPATCH_CHARS) {
465
+ dispatchChunked(globalDevtoolsWc, json);
466
+ return;
467
+ }
468
+ globalDevtoolsWc.executeJavaScript(buildDispatchScript([json]), true).catch(() => { });
469
+ }
305
470
  // ── Native-sink state machine ─────────────────────────────────────────────
306
471
  let sink = 'idle';
307
472
  // Buffered completed-request records while 'probing' — flushed to console if we
@@ -625,6 +790,12 @@ export function createNetworkForwarder(bridge) {
625
790
  // Fallback bookkeeping: requestId → in-flight request, for the console line
626
791
  // when the native dispatch path is unusable.
627
792
  const pending = new Map();
793
+ // User-facing classification, decided ONCE at
794
+ // requestWillBeSent (the only event carrying a url) and consulted by every
795
+ // later event on the same rawId — later events (responseReceived/
796
+ // loadingFinished/loadingFailed) carry no url, so they cannot re-derive
797
+ // the verdict; they must remember it.
798
+ const userFacingByRawId = new Map();
628
799
  // Raw ids whose requestWillBeSent announced a post body that was NOT
629
800
  // inlined (`hasPostData` without `postData`) — the only case the front-end
630
801
  // round-trips `Network.getRequestPostData`. Consumed at loadingFinished.
@@ -638,9 +809,15 @@ export function createNetworkForwarder(bridge) {
638
809
  * bodies would only burn memory.
639
810
  */
640
811
  const prefetchBodies = (rawId, encodedDataLength) => {
641
- if (sink === 'degraded')
642
- return;
643
- if (sink === 'idle' && !resolveDevtoolsWc())
812
+ // Skip only when NEITHER consumer can use the body: the user-facing
813
+ // sink is unusable (degraded, or idle with no host) AND the global
814
+ // mirror has no host either. Either alone is enough reason to prefetch
815
+ // — global-body-gate answers Network.getResponseBody/getRequestPostData
816
+ // from this same cache, and it must not 404 just because the
817
+ // user-facing sink happens to be closed/degraded.
818
+ const userSinkUsable = sink !== 'degraded' && !(sink === 'idle' && !resolveDevtoolsWc());
819
+ const globalUsable = globalDevtoolsWc !== null && !globalDevtoolsWc.isDestroyed();
820
+ if (!userSinkUsable && !globalUsable)
644
821
  return;
645
822
  const virtualId = ns.resolve(rawId);
646
823
  // Skip the CDP round-trip entirely for a response already known (from
@@ -693,23 +870,32 @@ export function createNetworkForwarder(bridge) {
693
870
  if (req)
694
871
  req.status = p.response?.status ?? 0;
695
872
  }
873
+ /** Shared "request terminated" bookkeeping for both loadingFinished and
874
+ * loadingFailed: drop the classification + pending record, returning the
875
+ * pending entry (or undefined if it was already gone/never seen). */
876
+ function retirePending(rawId) {
877
+ userFacingByRawId.delete(rawId);
878
+ const req = pending.get(rawId);
879
+ if (!req)
880
+ return undefined;
881
+ pending.delete(rawId);
882
+ return req;
883
+ }
696
884
  function onLoadingFinished(params) {
697
885
  const p = params;
698
886
  if (typeof p?.requestId === 'string')
699
887
  prefetchBodies(p.requestId, p.encodedDataLength);
700
- const req = pending.get(p.requestId);
888
+ const req = retirePending(p?.requestId);
701
889
  if (!req)
702
890
  return;
703
- pending.delete(p.requestId);
704
891
  maybeFallback({ source, url: req.url, method: req.method, status: req.status });
705
892
  }
706
893
  function onLoadingFailed(params) {
707
894
  const p = params;
708
895
  postDataWanted.delete(p?.requestId);
709
- const req = pending.get(p.requestId);
896
+ const req = retirePending(p?.requestId);
710
897
  if (!req)
711
898
  return;
712
- pending.delete(p.requestId);
713
899
  maybeFallback({
714
900
  source,
715
901
  url: req.url,
@@ -724,11 +910,61 @@ export function createNetworkForwarder(bridge) {
724
910
  'Network.loadingFinished': onLoadingFinished,
725
911
  'Network.loadingFailed': onLoadingFailed,
726
912
  };
913
+ /**
914
+ * Resolve (and, on `requestWillBeSent`, record) whether `rawId` is
915
+ * user-facing. MUST be computed eagerly here — not deferred to
916
+ * `onRequestWillBeSent` below, which runs AFTER this in `onMessage` — a
917
+ * `requestWillBeSent` is itself a FORWARDED_METHODS event whose own
918
+ * routing decision depends on the very verdict it establishes. Every
919
+ * later event for the same rawId (no url of its own) just looks up the
920
+ * recorded verdict; an unknown rawId fails open (user-facing) rather
921
+ * than silently hiding it.
922
+ */
923
+ function resolveUserFacing(method, params, rawId) {
924
+ if (method === 'Network.requestWillBeSent') {
925
+ const url = params?.request?.url;
926
+ const verdict = typeof url === 'string'
927
+ ? isUserFacingRequest(url, [bridge.getResourceServerBaseUrl?.(), bridge.getSimulatorServerBaseUrl?.()])
928
+ : true;
929
+ if (rawId) {
930
+ if (userFacingByRawId.size >= MAX_PENDING)
931
+ userFacingByRawId.clear();
932
+ userFacingByRawId.set(rawId, verdict);
933
+ }
934
+ return verdict;
935
+ }
936
+ if (rawId && userFacingByRawId.has(rawId))
937
+ return userFacingByRawId.get(rawId);
938
+ // `Network.requestWillBeSentExtraInfo` can arrive BEFORE its own
939
+ // `requestWillBeSent` (real CDP ordering) — it carries no url, so an
940
+ // unrecorded rawId here means "classification not yet known", not
941
+ // "capture attached mid-flight". Fail CLOSED for this one case: the
942
+ // very next event for the same rawId is (almost always) the real
943
+ // requestWillBeSent, which establishes the true verdict immediately —
944
+ // dropping this one early line is a far smaller cost than leaking an
945
+ // internal resource's ExtraInfo into the user-facing panel before its
946
+ // classification is even known. Every OTHER "rawId never seen" case
947
+ // (e.g. capture attached mid-flight, no requestWillBeSent ever coming)
948
+ // keeps failing open — there is no better signal available there.
949
+ if (method === 'Network.requestWillBeSentExtraInfo')
950
+ return false;
951
+ return true;
952
+ }
727
953
  const onMessage = (_event, method, params) => {
728
954
  // ── Primary sink: forward the raw CDP event into the DevTools front-end ──
729
955
  if (FORWARDED_METHODS.has(method)) {
730
956
  const rewritten = rewriteRequestId(method, params, ns);
731
- enqueueNative(rewritten.method, rewritten.params);
957
+ // Global mirror: full, unfiltered — independent of the
958
+ // user-facing sink's classification and state machine.
959
+ dispatchToGlobal(rewritten.method, rewritten.params);
960
+ // User-facing sink: only requests isUserFacingRequest() judged as the
961
+ // developer's own business traffic — internal/framework resource
962
+ // loads are mirrored to the global window ONLY.
963
+ const rawId = params?.requestId;
964
+ const userFacing = resolveUserFacing(method, params, typeof rawId === 'string' ? rawId : undefined);
965
+ if (userFacing) {
966
+ enqueueNative(rewritten.method, rewritten.params);
967
+ }
732
968
  }
733
969
  else if (REWRITE_REQUEST_ID_METHODS.has(method)) {
734
970
  // Methods we namespace but don't forward (dataReceived): still resolve
@@ -1024,6 +1260,17 @@ export function createNetworkForwarder(bridge) {
1024
1260
  readyRetryTimer = null;
1025
1261
  }
1026
1262
  });
1263
+ registry.add(() => {
1264
+ globalDevtoolsHostDisposable?.dispose();
1265
+ globalDevtoolsHostDisposable = null;
1266
+ globalBodyGateStop?.();
1267
+ globalBodyGateStop = null;
1268
+ globalDevtoolsWc = null;
1269
+ // A live settle-poll timer at dispose time is a real-timer leak (this
1270
+ // suite's flaky-test history: undisposed timers adopted by later tests'
1271
+ // fake clocks).
1272
+ clearGlobalPending();
1273
+ });
1027
1274
  registry.add(() => {
1028
1275
  bodyCache.clear();
1029
1276
  postDataCache.clear();
@@ -1051,6 +1298,7 @@ export function createNetworkForwarder(bridge) {
1051
1298
  detachSimulator,
1052
1299
  attachRenderGuest,
1053
1300
  setDevtoolsHost: (wc) => applyDevtoolsHost(wc),
1301
+ setGlobalDevtoolsHost: (wc) => applyGlobalDevtoolsHost(wc),
1054
1302
  // report() has no observing debugger, so there's no live CDP event to push
1055
1303
  // natively — surface it via the console fallback line.
1056
1304
  report: (record) => forwardToConsole(record),
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Judges whether a captured network request belongs on the user-facing
3
+ * (right-panel) DevTools Network tab, or should be hidden there — a
4
+ * framework/host-internal resource load, visible only in the standalone
5
+ * internal DevTools window's unfiltered global mirror.
6
+ *
7
+ * Single source of truth for the split (network-forward's user-facing sink
8
+ * and the global mirror both key off this): scheme must be one of
9
+ * http/https/ws/wss, AND the request's origin must not match ANY of
10
+ * `internalOrigins` — each is a base URL of a server this app itself runs
11
+ * whose own traffic is host-internal, not the developer's business traffic.
12
+ * There are currently two: the resource server (serves the compiled
13
+ * mini-app bundle) and the simulator's own static-asset server (serves
14
+ * `simulator.html` + its JS/CSS — a THIRD-PARTY-to-the-user-code origin the
15
+ * `attachSimulator` CDP session also observes, since it captures the whole
16
+ * simulator WebContentsView's network activity, not just the mini-app's own
17
+ * `wx.request` calls). `null`/`undefined` entries (a server not currently
18
+ * running, e.g. no project open) are skipped, not treated as a match.
19
+ */
20
+ export declare function isUserFacingRequest(url: string, internalOrigins?: ReadonlyArray<string | null | undefined>): boolean;
21
+ //# sourceMappingURL=user-facing.d.ts.map
@@ -0,0 +1,50 @@
1
+ /** URL schemes that can carry the mini-app author's own business traffic. */
2
+ const USER_FACING_SCHEMES = new Set(['http:', 'https:', 'ws:', 'wss:']);
3
+ /**
4
+ * Judges whether a captured network request belongs on the user-facing
5
+ * (right-panel) DevTools Network tab, or should be hidden there — a
6
+ * framework/host-internal resource load, visible only in the standalone
7
+ * internal DevTools window's unfiltered global mirror.
8
+ *
9
+ * Single source of truth for the split (network-forward's user-facing sink
10
+ * and the global mirror both key off this): scheme must be one of
11
+ * http/https/ws/wss, AND the request's origin must not match ANY of
12
+ * `internalOrigins` — each is a base URL of a server this app itself runs
13
+ * whose own traffic is host-internal, not the developer's business traffic.
14
+ * There are currently two: the resource server (serves the compiled
15
+ * mini-app bundle) and the simulator's own static-asset server (serves
16
+ * `simulator.html` + its JS/CSS — a THIRD-PARTY-to-the-user-code origin the
17
+ * `attachSimulator` CDP session also observes, since it captures the whole
18
+ * simulator WebContentsView's network activity, not just the mini-app's own
19
+ * `wx.request` calls). `null`/`undefined` entries (a server not currently
20
+ * running, e.g. no project open) are skipped, not treated as a match.
21
+ */
22
+ export function isUserFacingRequest(url, internalOrigins) {
23
+ let parsed;
24
+ try {
25
+ parsed = new URL(url);
26
+ }
27
+ catch {
28
+ // Real CDP `Network.requestWillBeSent.request.url` is always a fully
29
+ // qualified URL — this only happens for hand-authored test/placeholder
30
+ // input. Fail OPEN (treat as user-facing): hiding a request the caller
31
+ // couldn't even classify risks silently swallowing real business
32
+ // traffic, a worse outcome than occasionally over-showing something.
33
+ return true;
34
+ }
35
+ if (!USER_FACING_SCHEMES.has(parsed.protocol))
36
+ return false;
37
+ for (const baseUrl of internalOrigins ?? []) {
38
+ if (!baseUrl)
39
+ continue;
40
+ try {
41
+ if (parsed.origin === new URL(baseUrl).origin)
42
+ return false;
43
+ }
44
+ catch {
45
+ // Malformed baseUrl — can't compare, so don't let it hide anything.
46
+ }
47
+ }
48
+ return true;
49
+ }
50
+ //# sourceMappingURL=user-facing.js.map