@dimina-kit/devtools 0.4.0-dev.20260618090552 → 0.4.0-dev.20260624084417

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 (57) hide show
  1. package/README.md +5 -1
  2. package/dist/main/app/app.js +14 -2
  3. package/dist/main/index.bundle.js +596 -112
  4. package/dist/main/ipc/bridge-router.js +13 -8
  5. package/dist/main/ipc/projects.js +1 -1
  6. package/dist/main/ipc/simulator.js +1 -1
  7. package/dist/main/runtime/miniapp-runtime.d.ts +3 -3
  8. package/dist/main/services/elements-forward/index.js +42 -8
  9. package/dist/main/services/network-forward/index.d.ts +5 -6
  10. package/dist/main/services/network-forward/index.js +9 -10
  11. package/dist/main/services/notifications/renderer-notifier.d.ts +3 -4
  12. package/dist/main/services/projects/index.js +1 -1
  13. package/dist/main/services/render-inspect/index.js +182 -6
  14. package/dist/main/services/service-console/console-api.d.ts +5 -6
  15. package/dist/main/services/service-console/console-api.js +5 -6
  16. package/dist/main/services/service-host-pool/pool.d.ts +2 -3
  17. package/dist/main/services/simulator-storage/index.d.ts +2 -4
  18. package/dist/main/services/simulator-storage/index.js +2 -4
  19. package/dist/main/services/simulator-temp-files/index.js +2 -2
  20. package/dist/main/services/views/devtools-tabs.d.ts +4 -1
  21. package/dist/main/services/views/devtools-tabs.js +21 -3
  22. package/dist/main/services/views/host-toolbar-session-runtime.d.ts +4 -4
  23. package/dist/main/services/views/host-toolbar-session-runtime.js +4 -4
  24. package/dist/main/services/views/view-manager.d.ts +17 -1
  25. package/dist/main/services/views/view-manager.js +74 -47
  26. package/dist/main/services/workspace/workspace-service.js +11 -0
  27. package/dist/main/windows/service-host-window/create.d.ts +1 -0
  28. package/dist/main/windows/service-host-window/create.js +1 -0
  29. package/dist/preload/instrumentation/wxml-extract.js +53 -7
  30. package/dist/preload/runtime/host-toolbar-port.d.ts +1 -1
  31. package/dist/preload/runtime/host-toolbar-port.js +1 -1
  32. package/dist/preload/runtime/host-toolbar-runtime.d.ts +8 -7
  33. package/dist/preload/runtime/host-toolbar-runtime.js +8 -7
  34. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  35. package/dist/preload/windows/main.cjs.map +2 -2
  36. package/dist/preload/windows/simulator.cjs +1 -1
  37. package/dist/preload/windows/simulator.cjs.map +1 -1
  38. package/dist/preload/windows/simulator.js +1 -1
  39. package/dist/render-host/render-inspect.js +24 -23
  40. package/dist/renderer/assets/index-BMqriQNr.js +50 -0
  41. package/dist/renderer/entries/main/index.html +1 -1
  42. package/dist/shared/appdata-accumulator.d.ts +2 -2
  43. package/dist/shared/appdata-accumulator.js +3 -3
  44. package/dist/shared/constants.d.ts +1 -1
  45. package/dist/shared/constants.js +1 -1
  46. package/dist/shared/ipc-channels.d.ts +3 -3
  47. package/dist/shared/ipc-channels.js +3 -3
  48. package/dist/shared/open-in-editor.d.ts +27 -1
  49. package/dist/shared/open-in-editor.js +369 -3
  50. package/dist/shared/vpath.d.ts +2 -2
  51. package/dist/shared/vpath.js +2 -2
  52. package/dist/simulator/assets/device-shell-CPCnCp1L.js +2 -0
  53. package/dist/simulator/assets/{simulator-sf-D0mhw.js → simulator-gMBWKDeO.js} +3 -3
  54. package/dist/simulator/simulator.html +1 -1
  55. package/package.json +6 -4
  56. package/dist/renderer/assets/index-DmgWoK8N.js +0 -50
  57. package/dist/simulator/assets/device-shell-BWX7Yopg.js +0 -2
@@ -35,9 +35,12 @@
35
35
  */
36
36
  /**
37
37
  * Canonical DevTools panel view ids kept in the DEFAULT tab bar (stable front-end
38
- * ids, unchanged for many Chromium releases): `elements`, `console`, `network`.
38
+ * ids, unchanged for many Chromium releases): `elements`, `console`, `network`,
39
+ * `sources`. Sources stays so a source-link click that isn't routed to Monaco
40
+ * (build/runtime chunks, framework frames) still has a panel to reveal in instead
41
+ * of silently no-op'ing.
39
42
  */
40
- export const DEVTOOLS_KEPT_VIEW_IDS = ['elements', 'console', 'network'];
43
+ export const DEVTOOLS_KEPT_VIEW_IDS = ['elements', 'console', 'network', 'sources'];
41
44
  /**
42
45
  * Build the `executeJavaScript` source injected into the DevTools front-end
43
46
  * WebContents. The kept ids + display names are carried as JSON data literals
@@ -48,13 +51,19 @@ export function buildCustomizeTabsScript(keptIds = DEVTOOLS_KEPT_VIEW_IDS) {
48
51
  // fallback path); the DevTools UI may be EN or ZH, so include both.
49
52
  const NAME_MAP = {
50
53
  elements: ['Elements', '元素'], console: ['Console', '控制台'], network: ['Network', '网络'],
54
+ sources: ['Sources', '来源', '源代码'],
51
55
  };
52
56
  const keepNames = keptIds.flatMap((id) => NAME_MAP[id] ?? [id]);
53
57
  const keepIdsJson = JSON.stringify(JSON.stringify([...keptIds]));
54
58
  const keepNamesJson = JSON.stringify(JSON.stringify(keepNames));
59
+ const srcNamesJson = JSON.stringify(JSON.stringify(NAME_MAP.sources));
60
+ const netNamesJson = JSON.stringify(JSON.stringify(NAME_MAP.network));
55
61
  return `(function(){try{
56
62
  var KEEPID = new Set(JSON.parse(${keepIdsJson}));
57
63
  var KEEPNAME = new Set(JSON.parse(${keepNamesJson}));
64
+ var SRCNAME = new Set(JSON.parse(${srcNamesJson}));
65
+ var NETNAME = new Set(JSON.parse(${netNamesJson}));
66
+ var WANT_SOURCES_LAST = KEEPID.has('sources');
58
67
  // (1) Locale infobar — official host-preference suppression (NOT localStorage).
59
68
  try { var IFH=globalThis.InspectorFrontendHost; if(IFH&&typeof IFH.setPreference==='function'){ IFH.setPreference('disable-locale-info-bar','true'); } }catch(_){}
60
69
  function deepCollect(sel,cap){ var out=[],seen=0,stack=[document]; while(stack.length&&seen<(cap||40000)){ var root=stack.pop(); try{var m=root.querySelectorAll?root.querySelectorAll(sel):[];for(var i=0;i<m.length;i++)out.push(m[i]);}catch(_){} try{var all=root.querySelectorAll?root.querySelectorAll('*'):[];for(var j=0;j<all.length;j++){seen++;if(all[j].shadowRoot)stack.push(all[j].shadowRoot);}}catch(_){} } return out; }
@@ -82,9 +91,18 @@ export function buildCustomizeTabsScript(keptIds = DEVTOOLS_KEPT_VIEW_IDS) {
82
91
  handled=true;
83
92
  }catch(_){} }
84
93
  // fallback: registrations not enumerable -> at least clear the bar by id
85
- if(!handled){ var FALL=['timeline','resources','heap-profiler','sources','security','lighthouse','chrome-recorder','coverage','linear-memory-inspector','sensors','rendering','animations','autofill-view','medias','issues-pane']; for(var fr=0;fr<FALL.length;fr++){ try{ if(!KEEPID.has(FALL[fr])) maybeRemove(FALL[fr]); }catch(_){} } }
94
+ if(!handled){ var FALL=['timeline','resources','heap-profiler','security','lighthouse','chrome-recorder','coverage','linear-memory-inspector','sensors','rendering','animations','autofill-view','medias','issues-pane']; for(var fr=0;fr<FALL.length;fr++){ try{ if(!KEEPID.has(FALL[fr])) maybeRemove(FALL[fr]); }catch(_){} } }
86
95
  } else { domFallback(); }
87
96
  })();
97
+ // Keep Sources LAST in the bar (after Network). Default DevTools order puts
98
+ // Sources between Console and Network; nudge it to just after Network. Runs in
99
+ // BOTH paths (registry removal leaves the kept tabs in default order). Self-
100
+ // stable: once Sources follows Network the condition no longer holds.
101
+ if(WANT_SOURCES_LAST){ var ro=0,rt=setInterval(function(){ ro++; try{
102
+ var tabs=deepCollect('[role="tab"]'),src=null,net=null;
103
+ for(var i=0;i<tabs.length;i++){ var n=txt(tabs[i]); if(SRCNAME.has(n))src=tabs[i]; else if(NETNAME.has(n))net=tabs[i]; }
104
+ if(src&&net&&src.parentElement&&src.parentElement===net.parentElement&&(src.compareDocumentPosition(net)&Node.DOCUMENT_POSITION_FOLLOWING)){ net.parentElement.insertBefore(src,net.nextSibling); }
105
+ }catch(_){} if(ro>120)clearInterval(rt); },60); }
88
106
  // DOM fallback: only if the ESM module couldn't be resolved at all.
89
107
  function domFallback(){ var cachedBar=null;
90
108
  function findBar(){ if(cachedBar&&cachedBar.isConnected)return cachedBar; cachedBar=null; var tabs=deepCollect('[role="tab"]'); var a=null; for(var i=0;i<tabs.length;i++){if(KEEPNAME.has(txt(tabs[i]))){a=tabs[i];break;}} if(!a)return null; var p=a.parentElement; while(p){try{if(p.querySelectorAll('[role="tab"]').length>1){cachedBar=p;return p;}}catch(_){} p=p.parentElement;} cachedBar=a.parentElement; return cachedBar; }
@@ -2,15 +2,15 @@
2
2
  * Ref-counted registration of the host-toolbar framework runtime on
3
3
  * `session.defaultSession`.
4
4
  *
5
- * The toolbar's height advertiser no longer rides the toolbar WCV's
6
- * `webPreferences.preload` (a host's `setPreloadPath` used to replace it and
7
- * the strip height collapsed to 0). Instead the runtime bundle
5
+ * The toolbar's height advertiser does not ride the toolbar WCV's
6
+ * `webPreferences.preload`: a host's `setPreloadPath` would replace it and the
7
+ * strip height would collapse to 0. Instead the runtime bundle
8
8
  * (`hostToolbarRuntimePreloadPath`) is registered once per process as a
9
9
  * session frame preload; its own guard (`--dimina-host-toolbar` marker +
10
10
  * `isMainFrame`) keeps it a zero-footprint no-op in every other defaultSession
11
11
  * renderer.
12
12
  *
13
- * Ref-counting (codex condition 3): multiple ViewManagers can coexist in one
13
+ * Ref-counting: multiple ViewManagers can coexist in one
14
14
  * process and share the ONE defaultSession. Each manager acquires at most one
15
15
  * reference (on first toolbar need) and releases it in `disposeAll`. Only the
16
16
  * first acquire registers; only the last release unregisters (with the id
@@ -2,15 +2,15 @@
2
2
  * Ref-counted registration of the host-toolbar framework runtime on
3
3
  * `session.defaultSession`.
4
4
  *
5
- * The toolbar's height advertiser no longer rides the toolbar WCV's
6
- * `webPreferences.preload` (a host's `setPreloadPath` used to replace it and
7
- * the strip height collapsed to 0). Instead the runtime bundle
5
+ * The toolbar's height advertiser does not ride the toolbar WCV's
6
+ * `webPreferences.preload`: a host's `setPreloadPath` would replace it and the
7
+ * strip height would collapse to 0. Instead the runtime bundle
8
8
  * (`hostToolbarRuntimePreloadPath`) is registered once per process as a
9
9
  * session frame preload; its own guard (`--dimina-host-toolbar` marker +
10
10
  * `isMainFrame`) keeps it a zero-footprint no-op in every other defaultSession
11
11
  * renderer.
12
12
  *
13
- * Ref-counting (codex condition 3): multiple ViewManagers can coexist in one
13
+ * Ref-counting: multiple ViewManagers can coexist in one
14
14
  * process and share the ONE defaultSession. Each manager acquires at most one
15
15
  * reference (on first toolbar need) and releases it in `disposeAll`. Only the
16
16
  * first acquire registers; only the last release unregisters (with the id
@@ -1,5 +1,6 @@
1
1
  import type { WebContents } from 'electron';
2
2
  import type { NativeDeviceInfo } from '../../../shared/ipc-channels.js';
3
+ import { type OpenInEditorRequest } from '../../../shared/open-in-editor.js';
3
4
  import { type WorkbenchContext } from '../workbench-context.js';
4
5
  import { type HostToolbarMessageSubscription } from './host-toolbar-port-channel.js';
5
6
  /**
@@ -17,6 +18,8 @@ export interface ViewManagerContext {
17
18
  * `createWorkbenchContext` always supplies it.
18
19
  */
19
20
  connections: WorkbenchContext['connections'];
21
+ /** Active project root used to validate/open console source locations. */
22
+ workspace?: WorkbenchContext['workspace'];
20
23
  /**
21
24
  * Absolute path to the simulator preload bundle. Only consumed by the
22
25
  * native-host simulator WebContentsView (`attachNativeSimulator`); the
@@ -82,7 +85,7 @@ export interface ViewManager {
82
85
  * `simWidth` rides the wire for schema compatibility but is unused: all
83
86
  * geometry is anchor-published.
84
87
  */
85
- attachNativeSimulator(simulatorUrl: string, simWidth: number): void;
88
+ attachNativeSimulator(simulatorUrl: string, simWidth: number): Promise<void>;
86
89
  /**
87
90
  * Destroy and null out the simulator view (e.g. on simulator detach).
88
91
  * Also destroys the cached settings view and hides the popover —
@@ -106,6 +109,8 @@ export interface ViewManager {
106
109
  getSimulatorWebContentsId(): number | null;
107
110
  /** Return the live webContents of the currently attached simulator, or null. */
108
111
  getSimulatorWebContents(): WebContents | null;
112
+ /** Return the active project path bound to the current native simulator. */
113
+ getSimulatorProjectPath(): string | null;
109
114
  /** Return the settings overlay's WebContents (for renderer-notifier). */
110
115
  getSettingsWebContents(): WebContents | null;
111
116
  /** Return the webContents ID of the settings overlay if alive, else null. */
@@ -277,6 +282,17 @@ export interface HostToolbarControl {
277
282
  */
278
283
  setHeightMode(mode: HostToolbarHeightMode): void;
279
284
  }
285
+ export interface ProjectEditorTarget {
286
+ path: string;
287
+ line?: number;
288
+ column?: number;
289
+ }
290
+ /**
291
+ * Resolve a DevTools source request against the service-host URL that created
292
+ * the inspected app. Its pkgRoot is authoritative; the workspace is only a
293
+ * stale-session consistency guard.
294
+ */
295
+ export declare function resolveProjectEditorTarget(serviceHostUrl: string, activeProjectRoot: string | undefined, req: OpenInEditorRequest, isFile?: (absolutePath: string) => boolean): ProjectEditorTarget | null;
280
296
  /**
281
297
  * Build a ViewManager bound to the given context. The returned object is the
282
298
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -1,10 +1,11 @@
1
1
  import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
2
+ import fs from 'node:fs';
2
3
  import path from 'path';
3
4
  import { cjsSiblingPreloadPath, mainPreloadPath } from '../../utils/paths.js';
4
5
  import { simDeskBg } from '../../utils/theme.js';
5
6
  import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
6
7
  import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
7
- import { OPEN_IN_EDITOR_SCHEME, decodeOpenInEditorUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
8
+ import { buildDevtoolsProjectSourceLinksScript, decodeOpenInEditorUrl, projectSourceContextFromServiceHostUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
8
9
  import { createSafeAreaController } from '../safe-area/index.js';
9
10
  import { buildCustomizeTabsScript } from './devtools-tabs.js';
10
11
  import { installElementsForward } from '../elements-forward/index.js';
@@ -16,6 +17,35 @@ import { acquireHostToolbarSessionRuntime, releaseHostToolbarSessionRuntime, } f
16
17
  import { createHostToolbarPortChannel, } from './host-toolbar-port-channel.js';
17
18
  import { parseRoute } from '../../../shared/simulator-route.js';
18
19
  import { HEADER_H, HOST_TOOLBAR_RUNTIME_MARKER } from '../../../shared/constants.js';
20
+ /**
21
+ * Resolve a DevTools source request against the service-host URL that created
22
+ * the inspected app. Its pkgRoot is authoritative; the workspace is only a
23
+ * stale-session consistency guard.
24
+ */
25
+ export function resolveProjectEditorTarget(serviceHostUrl, activeProjectRoot, req, isFile = (absolutePath) => fs.statSync(absolutePath).isFile()) {
26
+ const sourceContext = projectSourceContextFromServiceHostUrl(serviceHostUrl, activeProjectRoot);
27
+ if (!sourceContext)
28
+ return null;
29
+ const rel = resourceUrlToProjectRelativePath(req.url, sourceContext);
30
+ if (!rel)
31
+ return null;
32
+ const absolute = path.resolve(sourceContext.projectRoot, ...rel.split('/'));
33
+ const fromRoot = path.relative(path.resolve(sourceContext.projectRoot), absolute);
34
+ if (!fromRoot || fromRoot.startsWith('..') || path.isAbsolute(fromRoot))
35
+ return null;
36
+ try {
37
+ if (!isFile(absolute))
38
+ return null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ return {
44
+ path: rel,
45
+ line: typeof req.line === 'number' ? req.line + 1 : undefined,
46
+ column: typeof req.column === 'number' ? req.column + 1 : undefined,
47
+ };
48
+ }
19
49
  /**
20
50
  * Build a ViewManager bound to the given context. The returned object is the
21
51
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -37,6 +67,8 @@ export function createViewManager(ctx) {
37
67
  // while `simulatorView` above hosts its DevTools in the right panel region.
38
68
  let nativeSimulatorView = null;
39
69
  let nativeSimulatorViewAdded = false;
70
+ let nativeSimulatorProjectPath = null;
71
+ let settleNativeSimulatorReady = null;
40
72
  // Model A: the renderer's measured inner-screen rect is the SOLE authority for
41
73
  // the native simulator WCV bounds (see docs/simulator-render-architecture.md).
42
74
  // Cache the last rect so a report that lands before attachNativeSimulator (the
@@ -429,43 +461,18 @@ export function createViewManager(ctx) {
429
461
  // `devtools-open-url` on the inspected (service-host) wc, which we decode,
430
462
  // map to a project-relative path, and broadcast to the renderer's Monaco.
431
463
  const openInEditorWiredWcIds = new Set();
432
- function injectOpenResourceHandler(devtoolsWc) {
433
- // `setOpenResourceHandler` is the official Chromium DevTools hook IDEs use
434
- // to route source-link clicks to an external editor. We poll for the host
435
- // (it appears once the front-end finishes bootstrapping, like `UI` above)
436
- // and register a handler that re-emits an encoded sentinel via
437
- // `openInNewTab` Electron `devtools-open-url`. Best-effort: wrapped in
438
- // try/catch and a bounded poll so a missing API never throws.
439
- const scheme = JSON.stringify(OPEN_IN_EDITOR_SCHEME);
440
- devtoolsWc.executeJavaScript(`
441
- (function() {
442
- try {
443
- let tries = 0
444
- const timer = setInterval(() => {
445
- tries++
446
- try {
447
- const Host = globalThis.Host
448
- const host = Host && Host.InspectorFrontendHost
449
- if (host && typeof host.setOpenResourceHandler === 'function'
450
- && typeof host.openInNewTab === 'function') {
451
- host.setOpenResourceHandler((url, lineNumber, columnNumber) => {
452
- try {
453
- const p = new URLSearchParams()
454
- p.set('u', String(url))
455
- if (typeof lineNumber === 'number') p.set('l', String(lineNumber))
456
- if (typeof columnNumber === 'number') p.set('c', String(columnNumber))
457
- host.openInNewTab(${scheme} + ':?' + p.toString())
458
- } catch (_) {}
459
- })
460
- clearInterval(timer)
461
- return
462
- }
463
- } catch (_) {}
464
- if (tries > 80) clearInterval(timer)
465
- }, 50)
466
- } catch (_) {}
467
- })()
468
- `).catch(() => { });
464
+ function injectOpenResourceHandler(serviceWc, devtoolsWc) {
465
+ // Inject the front-end glue that routes a project source-link click to our
466
+ // Monaco editor instead of the DevTools Sources panel: a capture-phase click
467
+ // interceptor re-emits an encoded sentinel via
468
+ // `InspectorFrontendHost.openInNewTab` Electron `devtools-open-url`. (The
469
+ // legacy `setOpenResourceHandler` hook this used to rely on is gone in
470
+ // current Chromium, so the script keeps it only as a fallback.) Best-effort:
471
+ // the script is fully try/catch-wrapped so a missing API never throws.
472
+ const sourceContext = projectSourceContextFromServiceHostUrl(serviceWc.getURL(), ctx.workspace?.getProjectPath?.());
473
+ if (!sourceContext)
474
+ return;
475
+ devtoolsWc.executeJavaScript(buildDevtoolsProjectSourceLinksScript(sourceContext)).catch(() => { });
469
476
  }
470
477
  // ── DevTools tab customization: keep only Elements/Console/Network ──────────
471
478
  // Inject into the same DevTools front-end host wc that the console/network
@@ -502,10 +509,10 @@ export function createViewManager(ctx) {
502
509
  // wc is recreated per simulatorView, so a fresh host needs the handler set.
503
510
  if (!devtoolsWc.isDestroyed()) {
504
511
  if (devtoolsWc.isLoading()) {
505
- devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(devtoolsWc));
512
+ devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(serviceWc, devtoolsWc));
506
513
  }
507
514
  else {
508
- injectOpenResourceHandler(devtoolsWc);
515
+ injectOpenResourceHandler(serviceWc, devtoolsWc);
509
516
  }
510
517
  }
511
518
  // Attach the `devtools-open-url` decoder to the inspected service wc once
@@ -518,13 +525,10 @@ export function createViewManager(ctx) {
518
525
  const req = decodeOpenInEditorUrl(url);
519
526
  if (!req)
520
527
  return; // not our sentinel — leave it to Electron's default path
521
- const rel = resourceUrlToProjectRelativePath(req.url);
522
- if (!rel)
528
+ const target = resolveProjectEditorTarget(serviceWc.getURL(), ctx.workspace?.getProjectPath?.(), req);
529
+ if (!target)
523
530
  return;
524
- // DevTools reports 0-based line/column; Monaco is 1-based.
525
- const line = typeof req.line === 'number' ? req.line + 1 : undefined;
526
- const column = typeof req.column === 'number' ? req.column + 1 : undefined;
527
- ctx.notify.editorOpenFile({ path: rel, line, column });
531
+ ctx.notify.editorOpenFile(target);
528
532
  };
529
533
  serviceWc.on('devtools-open-url', onOpenUrl);
530
534
  // Consolidate teardown onto the connection layer (foundation.md §4 / P2),
@@ -793,8 +797,12 @@ export function createViewManager(ctx) {
793
797
  function attachNativeSimulator(simulatorUrl, _simWidth) {
794
798
  if (!ctx.preloadPath) {
795
799
  console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
796
- return;
800
+ return Promise.resolve();
797
801
  }
802
+ // Unblock a superseded IPC invocation. Its renderer effect cleanup marks
803
+ // that generation cancelled, so this cannot schedule a stale capture.
804
+ settleNativeSimulatorReady?.();
805
+ settleNativeSimulatorReady = null;
798
806
  // Tear down any previous native simulator view (relaunch / re-open).
799
807
  if (nativeSimulatorView) {
800
808
  detachNativeCustomApiBridge();
@@ -813,6 +821,10 @@ export function createViewManager(ctx) {
813
821
  catch { /* ignore */ }
814
822
  nativeSimulatorView = null;
815
823
  }
824
+ nativeSimulatorProjectPath = null;
825
+ const ready = new Promise((resolve) => {
826
+ settleNativeSimulatorReady = resolve;
827
+ });
816
828
  // Derive THIS project's session partition from the simulator URL's appId so
817
829
  // its cookies/localStorage/cache are isolated from every other project (P0
818
830
  // debt). Same project → same partition (storage survives a relaunch);
@@ -840,6 +852,7 @@ export function createViewManager(ctx) {
840
852
  },
841
853
  });
842
854
  nativeSimulatorView = view;
855
+ nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
843
856
  // Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
844
857
  // light #e8e8e8) so a height-resize that grows the region never flashes a
845
858
  // mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
@@ -929,6 +942,15 @@ export function createViewManager(ctx) {
929
942
  e.preventDefault();
930
943
  }
931
944
  });
945
+ // The outer DeviceShell loading is insufficient: slow projects attach a
946
+ // render guest later. Resolve the renderer's attach IPC only after that
947
+ // first mini-app page has completed its own document load.
948
+ guestWc.once('did-finish-load', () => {
949
+ if (nativeSimulatorView !== view || simWc.isDestroyed())
950
+ return;
951
+ settleNativeSimulatorReady?.();
952
+ settleNativeSimulatorReady = null;
953
+ });
932
954
  // A render-host guest only attaches after a spawn, so the service window
933
955
  // exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
934
956
  // with the `onRenderEvent` path in case its emit lost the attach race.
@@ -984,8 +1006,11 @@ export function createViewManager(ctx) {
984
1006
  // (The UI/view layer's Elements equivalent is the native WXML panel +
985
1007
  // render-guest highlight chain, which targets the active render guest.)
986
1008
  attachNativeSimulatorDevtoolsHost();
1009
+ return ready;
987
1010
  }
988
1011
  function detachSimulator() {
1012
+ settleNativeSimulatorReady?.();
1013
+ settleNativeSimulatorReady = null;
989
1014
  stopFollowingNativeServiceHost();
990
1015
  detachNativeCustomApiBridge();
991
1016
  // Stop Elements forwarding (detaches only the debugger sessions IT attached;
@@ -1029,6 +1054,7 @@ export function createViewManager(ctx) {
1029
1054
  simulatorView = null;
1030
1055
  simulatorViewAdded = false;
1031
1056
  simulatorWebContentsId = null;
1057
+ nativeSimulatorProjectPath = null;
1032
1058
  // Drop the renderer-published rect so a stale "hidden" override doesn't
1033
1059
  // suppress the next view before its renderer republishes.
1034
1060
  simulatorBoundsOverride = null;
@@ -1239,6 +1265,7 @@ export function createViewManager(ctx) {
1239
1265
  const wc = webContents.fromId(simulatorWebContentsId);
1240
1266
  return wc && !wc.isDestroyed() ? wc : null;
1241
1267
  },
1268
+ getSimulatorProjectPath: () => nativeSimulatorProjectPath,
1242
1269
  getSettingsWebContents: () => {
1243
1270
  if (!settingsView)
1244
1271
  return null;
@@ -215,11 +215,22 @@ export function createWorkspaceService(ctx) {
215
215
  getLastClosedProjectPath: () => lastClosedProjectPath,
216
216
  hasActiveSession: () => currentSession !== null,
217
217
  async captureThumbnail(projectPath) {
218
+ if (!currentSession || projectPath !== currentProjectPath)
219
+ return null;
218
220
  const wc = ctx.views.getSimulatorWebContents();
219
221
  if (!wc)
220
222
  return null;
223
+ if (ctx.views.getSimulatorProjectPath() !== projectPath)
224
+ return null;
225
+ const session = currentSession;
221
226
  try {
222
227
  const image = await wc.capturePage();
228
+ if (currentSession !== session
229
+ || currentProjectPath !== projectPath
230
+ || ctx.views.getSimulatorWebContents() !== wc
231
+ || ctx.views.getSimulatorProjectPath() !== projectPath) {
232
+ return null;
233
+ }
223
234
  const dataUrl = `data:image/png;base64,${image.toPNG().toString('base64')}`;
224
235
  if (provider.saveThumbnail) {
225
236
  await provider.saveThumbnail(projectPath, dataUrl);
@@ -14,6 +14,7 @@ export interface ServiceHostWindowOptions {
14
14
  appId: string;
15
15
  pagePath: string;
16
16
  pkgRoot: string;
17
+ root?: string;
17
18
  resourceBaseUrl: string;
18
19
  hostEnvSnapshot?: Record<string, unknown>;
19
20
  }
@@ -52,6 +52,7 @@ export function buildServiceHostSpawnUrl(opts) {
52
52
  url.searchParams.set('appId', opts.appId);
53
53
  url.searchParams.set('pagePath', opts.pagePath);
54
54
  url.searchParams.set('pkgRoot', opts.pkgRoot);
55
+ url.searchParams.set('root', opts.root || 'main');
55
56
  url.searchParams.set('resourceBaseUrl', opts.resourceBaseUrl);
56
57
  if (opts.hostEnvSnapshot) {
57
58
  // Encode the resolved host-env so preload can hydrate
@@ -4,16 +4,24 @@ const INTERNAL_CLASSES = new Set([
4
4
  'dd-picker-overlay', 'dd-picker-container', 'dd-picker-header', 'dd-picker-body',
5
5
  ]);
6
6
  const FRAMEWORK_TEMPLATE_RE = /^(taro_tmpl|tmpl_\d+)/;
7
+ /**
8
+ * Strip the `dd-` registration prefix and the `tpl-` prefix dimina adds to
9
+ * compiled template components (`app.component('dd-tpl-taro_tmpl', …)`), so a
10
+ * framework template wrapper normalizes to its bare name (`taro_tmpl`,
11
+ * `tmpl_0_3`) and `FRAMEWORK_TEMPLATE_RE` recognizes it.
12
+ */
13
+ function normalizeRegisteredName(regName) {
14
+ const base = regName.startsWith('dd-') ? regName.slice(3) : regName;
15
+ return base.startsWith('tpl-') ? base.slice(4) : base;
16
+ }
7
17
  function resolveTemplateNameFromParent(instance) {
8
18
  const parent = instance.parent;
9
- if (!parent)
10
- return null;
11
- const parentType = parent.type;
19
+ const parentType = parent?.type;
12
20
  const components = parentType?.components;
13
21
  if (components) {
14
22
  for (const [regName, comp] of Object.entries(components)) {
15
23
  if (comp === instance.type)
16
- return regName.startsWith('dd-') ? regName.slice(3) : regName;
24
+ return normalizeRegisteredName(regName);
17
25
  }
18
26
  }
19
27
  const appComponents = instance.appContext?.components;
@@ -21,7 +29,7 @@ function resolveTemplateNameFromParent(instance) {
21
29
  return null;
22
30
  for (const [regName, comp] of Object.entries(appComponents)) {
23
31
  if (comp === instance.type)
24
- return regName.startsWith('dd-') ? regName.slice(3) : regName;
32
+ return normalizeRegisteredName(regName);
25
33
  }
26
34
  return null;
27
35
  }
@@ -53,6 +61,29 @@ function resolvePagePath(instance) {
53
61
  return null;
54
62
  return path.startsWith('/') ? path.slice(1) : path;
55
63
  }
64
+ /**
65
+ * A NATIVE dimina custom component (a `usingComponents` entry) calls
66
+ * `provide('path', componentPath)` in its setup (render runtime), so its
67
+ * provided `path` DIFFERS from its parent's. A plain descendant inherits the
68
+ * same provides object (identical `path`) and is not a boundary; a Taro template
69
+ * wrapper (`dd-tpl-*`) never provides, so it never differs either. Comparing
70
+ * against the parent's provided path therefore isolates exactly the native
71
+ * component roots — matching WeChat, which shows each custom component as a node
72
+ * tagged with its full registered path (with a synthetic `#shadow-root` added by
73
+ * `wrapInShadowRoot` since the path contains `/`). Pages are handled earlier via
74
+ * their authoritative `__page__` marker, so this only fires for components.
75
+ */
76
+ function resolveComponentPath(instance) {
77
+ const provides = instance.provides;
78
+ const path = provides?.path;
79
+ if (typeof path !== 'string' || !path)
80
+ return null;
81
+ const parent = instance.parent;
82
+ const parentProvides = parent?.provides;
83
+ if (path === parentProvides?.path)
84
+ return null;
85
+ return path.startsWith('/') ? path.slice(1) : path;
86
+ }
56
87
  function resolveTagName(instance) {
57
88
  const type = instance.type;
58
89
  if (!type)
@@ -63,12 +94,22 @@ function resolveTagName(instance) {
63
94
  const pagePath = resolvePagePath(instance);
64
95
  if (pagePath)
65
96
  return pagePath;
97
+ // Native custom-component boundary → tag with its full registered path (WeChat
98
+ // parity). Must precede the __tagName/__name/dd- shortening below, which would
99
+ // otherwise collapse `dd-foo` to the bare `foo` and lose the path.
100
+ const componentPath = resolveComponentPath(instance);
101
+ if (componentPath)
102
+ return componentPath;
66
103
  if (typeof type.__tagName === 'string')
67
104
  return type.__tagName;
68
105
  const name = (type.__name || type.name);
69
106
  if (!name) {
70
- if (type.__scopeId && type.components)
71
- return 'page';
107
+ // A nameless component carrying a `__scopeId` is a compiled page, custom
108
+ // component, or framework template wrapper. The page is already resolved
109
+ // above via its authoritative `__page__` marker, so by here it is NOT a
110
+ // page — recover the registered tag (e.g. a Taro `taro_tmpl`/`tmpl_0_3`
111
+ // wrapper) and fall back to `template`. Defaulting to `page` here would
112
+ // mislabel every such wrapper as a second page root.
72
113
  if (type.__scopeId)
73
114
  return resolveTemplateNameFromParent(instance) || 'template';
74
115
  return 'unknown';
@@ -132,6 +173,11 @@ function getElementSid(instance) {
132
173
  function isTransparentComponent(instance, tagName) {
133
174
  if (tagName === 'unknown')
134
175
  return true;
176
+ // A framework template wrapper (Taro `taro_tmpl` / `tmpl_0_3`, whether named
177
+ // via `props.is` or recovered from its registration) is compiler scaffolding,
178
+ // not user content — pass its children straight through.
179
+ if (FRAMEWORK_TEMPLATE_RE.test(tagName))
180
+ return true;
135
181
  if (tagName === 'template') {
136
182
  const props = instance.props;
137
183
  const is = props?.is;
@@ -19,7 +19,7 @@
19
19
  * envelope stays the single waist (same posture as the main side's
20
20
  * host-toolbar-port-channel.ts).
21
21
  *
22
- * Ordering reality this module absorbs (spike .repro/wave3-spike):
22
+ * Ordering reality this module absorbs:
23
23
  * - The page script runs BEFORE the handshake can complete (the port is
24
24
  * posted on did-finish-load). Page `send()`s issued before the port
25
25
  * arrives go into a PENDING QUEUE and flush in order on handshake —
@@ -19,7 +19,7 @@
19
19
  * envelope stays the single waist (same posture as the main side's
20
20
  * host-toolbar-port-channel.ts).
21
21
  *
22
- * Ordering reality this module absorbs (spike .repro/wave3-spike):
22
+ * Ordering reality this module absorbs:
23
23
  * - The page script runs BEFORE the handshake can complete (the port is
24
24
  * posted on did-finish-load). Page `send()`s issued before the port
25
25
  * arrives go into a PENDING QUEUE and flush in order on handshake —
@@ -10,15 +10,16 @@
10
10
  * - the toolbar WCV's creation injects `HOST_TOOLBAR_RUNTIME_MARKER` into its
11
11
  * process argv via `webPreferences.additionalArguments`;
12
12
  * - the marker is PROCESS-level (subframes of the toolbar window carry it
13
- * too — spike .repro/wave3-spike/RESULTS.md item 3), so the guard needs
14
- * BOTH wings: `isMainFrame` AND `argv.includes(marker)`;
13
+ * too), so the guard needs BOTH wings: `isMainFrame` AND
14
+ * `argv.includes(marker)`;
15
15
  * - a renderer that fails the guard returns immediately with ZERO footprint
16
- * (no advertiser, no listeners, no globals — spike item 4).
16
+ * (no advertiser, no listeners, no globals).
17
17
  *
18
- * Why session-resident at all: the advertiser used to ride the toolbar WCV's
19
- * `webPreferences.preload`, so a host calling `setPreloadPath(<its own
20
- * preload>)` REPLACED it and the strip height collapsed to 0. With the runtime
21
- * on the session layer, the host preload and the framework runtime coexist.
18
+ * Why session-resident at all: riding the toolbar WCV's
19
+ * `webPreferences.preload` lets a host calling `setPreloadPath(<its own
20
+ * preload>)` REPLACE the advertiser, collapsing the strip height to 0. With the
21
+ * runtime on the session layer, the host preload and the framework runtime
22
+ * coexist.
22
23
  */
23
24
  /**
24
25
  * Pure guard predicate: should the toolbar runtime activate in a renderer with
@@ -10,15 +10,16 @@
10
10
  * - the toolbar WCV's creation injects `HOST_TOOLBAR_RUNTIME_MARKER` into its
11
11
  * process argv via `webPreferences.additionalArguments`;
12
12
  * - the marker is PROCESS-level (subframes of the toolbar window carry it
13
- * too — spike .repro/wave3-spike/RESULTS.md item 3), so the guard needs
14
- * BOTH wings: `isMainFrame` AND `argv.includes(marker)`;
13
+ * too), so the guard needs BOTH wings: `isMainFrame` AND
14
+ * `argv.includes(marker)`;
15
15
  * - a renderer that fails the guard returns immediately with ZERO footprint
16
- * (no advertiser, no listeners, no globals — spike item 4).
16
+ * (no advertiser, no listeners, no globals).
17
17
  *
18
- * Why session-resident at all: the advertiser used to ride the toolbar WCV's
19
- * `webPreferences.preload`, so a host calling `setPreloadPath(<its own
20
- * preload>)` REPLACED it and the strip height collapsed to 0. With the runtime
21
- * on the session layer, the host preload and the framework runtime coexist.
18
+ * Why session-resident at all: riding the toolbar WCV's
19
+ * `webPreferences.preload` lets a host calling `setPreloadPath(<its own
20
+ * preload>)` REPLACE the advertiser, collapsing the strip height to 0. With the
21
+ * runtime on the session layer, the host preload and the framework runtime
22
+ * coexist.
22
23
  */
23
24
  import { HOST_TOOLBAR_RUNTIME_MARKER } from '../../shared/constants.js';
24
25
  import { installHostToolbarAdvertiserWhenReady } from './host-toolbar-advertiser.js';