@dimina-kit/devtools 0.4.0-dev.20260624040247 → 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.
@@ -2,11 +2,47 @@ import { readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { devtoolsPackageRoot } from '../../utils/paths.js';
4
4
  const DEFAULT_SOURCE_PATH = 'dist/render-host/render-inspect.js';
5
+ /**
6
+ * Chrome DevTools' default Elements-panel highlight palette (content / padding /
7
+ * border / margin tints + the size tooltip). Mirroring it makes the WXML panel's
8
+ * hover box visually identical to the embedded Elements panel's.
9
+ */
10
+ const HIGHLIGHT_CONFIG = {
11
+ showInfo: true,
12
+ showRulers: false,
13
+ showExtensionLines: false,
14
+ contentColor: { r: 111, g: 168, b: 220, a: 0.66 },
15
+ paddingColor: { r: 147, g: 196, b: 125, a: 0.55 },
16
+ borderColor: { r: 255, g: 229, b: 153, a: 0.66 },
17
+ marginColor: { r: 246, g: 178, b: 107, a: 0.66 },
18
+ };
19
+ /** Object group for the per-hover `Runtime.evaluate` reference, released after the draw. */
20
+ const HOVER_OBJECT_GROUP = 'render-inspect-hover';
21
+ /**
22
+ * Upper bound on awaiting the `DOM.enable → Overlay.enable` handshake before
23
+ * highlighting. A hung `DOM.enable` then degrades to a missed paint instead of a
24
+ * stuck hover.
25
+ */
26
+ const ENABLE_HANDSHAKE_TIMEOUT_MS = 500;
27
+ /** Resolve when `p` settles or `ms` elapses, whichever comes first (never rejects). */
28
+ function withTimeout(p, ms) {
29
+ return new Promise((resolve) => {
30
+ const timer = setTimeout(resolve, ms);
31
+ p.then(() => { clearTimeout(timer); resolve(); }, () => { clearTimeout(timer); resolve(); });
32
+ });
33
+ }
5
34
  export function createRenderInspector(options = {}) {
6
35
  const loadSource = options.loadSource ??
7
36
  (() => readFileSync(path.join(devtoolsPackageRoot, DEFAULT_SOURCE_PATH), 'utf8'));
8
37
  let cachedSource = null;
9
38
  const injected = new Set();
39
+ // Debugger sessions THIS service attached itself (nobody else owned one). Only
40
+ // these are detached on guest-destroy; sessions safe-area / Elements-forward
41
+ // own are never touched (single-owner per wc).
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();
10
46
  function source() {
11
47
  if (cachedSource === null)
12
48
  cachedSource = loadSource();
@@ -37,11 +73,15 @@ export function createRenderInspector(options = {}) {
37
73
  // Consolidate the per-wc bookkeeping teardown onto the connection layer
38
74
  // (foundation.md §4 / P2) when a registry is available; fall back to the
39
75
  // bespoke `once('destroyed')` only when omitted (focused unit tests).
76
+ const forget = () => {
77
+ injected.delete(wc.id);
78
+ enablePromises.delete(wc.id);
79
+ };
40
80
  if (options.connections) {
41
- options.connections.acquire(wc).own(() => injected.delete(wc.id));
81
+ options.connections.acquire(wc).own(forget);
42
82
  }
43
83
  else {
44
- wc.once('destroyed', () => injected.delete(wc.id));
84
+ wc.once('destroyed', forget);
45
85
  }
46
86
  return true;
47
87
  }
@@ -63,21 +103,157 @@ export function createRenderInspector(options = {}) {
63
103
  return null;
64
104
  if (!(await ensureInjected(wc)))
65
105
  return null;
106
+ let inspection;
66
107
  try {
67
108
  const result = await wc.executeJavaScript(`window.__diminaRenderInspect ? window.__diminaRenderInspect.highlightElement(${JSON.stringify(sid)}) : null`);
68
- return result ?? null;
109
+ inspection = result ?? null;
69
110
  }
70
111
  catch {
71
112
  return null;
72
113
  }
114
+ // No geometry → nothing to draw; never touch the debugger.
115
+ if (!inspection)
116
+ return null;
117
+ // Draw the native overlay best-effort: the inspection data must survive even
118
+ // if the CDP draw fails, so a rejected debugger step never propagates.
119
+ await drawNativeHighlight(wc, sid).catch(() => { });
120
+ return inspection;
73
121
  }
74
- async function unhighlight(wc) {
122
+ /**
123
+ * Paint the Chrome-style native highlight over the guest via CDP — the same
124
+ * `Overlay.highlightNode` the embedded Elements panel uses, so the WXML panel's
125
+ * hover box matches it exactly. Reuses the guest's existing debugger session
126
+ * (single-owner per wc; safe-area / Elements-forward has usually already
127
+ * attached it) and only attaches itself when nobody has.
128
+ *
129
+ * `Overlay.highlightNode` paints only while the Overlay domain is enabled, and
130
+ * Chromium rejects `Overlay.enable` with "DOM should be enabled first" unless
131
+ * DOM is enabled first. On a cold/self-attached session (WXML hovered without
132
+ * 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 `DOM.enable →
134
+ * Overlay.enable` handshake before highlighting — but only up to
135
+ * `ENABLE_HANDSHAKE_TIMEOUT_MS`, so a hung `DOM.enable` degrades to a missed
136
+ * paint rather than a stuck hover.
137
+ *
138
+ * The `Runtime.evaluate` runs in a named object group whose remote references
139
+ * are released in `finally`; otherwise every hover would leak a live DOM
140
+ * wrapper into the guest's execution context.
141
+ */
142
+ async function drawNativeHighlight(wc, sid) {
75
143
  if (wc.isDestroyed())
76
144
  return;
77
- if (!(await ensureInjected(wc)))
145
+ if (!ensureGuestDebugger(wc))
146
+ return;
147
+ await withTimeout(ensureDomainsEnabled(wc), ENABLE_HANDSHAKE_TIMEOUT_MS);
148
+ if (wc.isDestroyed())
149
+ return;
150
+ const expression = `window.__diminaRenderInspect && window.__diminaRenderInspect.elementFor(${JSON.stringify(sid)})`;
151
+ try {
152
+ const evaluated = await wc.debugger.sendCommand('Runtime.evaluate', {
153
+ expression,
154
+ returnByValue: false,
155
+ objectGroup: HOVER_OBJECT_GROUP,
156
+ });
157
+ const objectId = evaluated?.result?.objectId;
158
+ if (!objectId)
159
+ return;
160
+ await wc.debugger.sendCommand('Overlay.highlightNode', {
161
+ objectId,
162
+ highlightConfig: HIGHLIGHT_CONFIG,
163
+ });
164
+ }
165
+ finally {
166
+ // Drop the hover's remote DOM reference so repeated hovers don't accumulate
167
+ // live wrappers in the guest context.
168
+ wc.debugger
169
+ .sendCommand('Runtime.releaseObjectGroup', { objectGroup: HOVER_OBJECT_GROUP })
170
+ .catch(() => { });
171
+ }
172
+ }
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
+ async function unhighlight(wc) {
253
+ if (wc.isDestroyed())
78
254
  return;
79
255
  try {
80
- await wc.executeJavaScript('window.__diminaRenderInspect && window.__diminaRenderInspect.unhighlightElement && window.__diminaRenderInspect.unhighlightElement()');
256
+ await wc.debugger.sendCommand('Overlay.hideHighlight');
81
257
  }
82
258
  catch {
83
259
  // best-effort
@@ -35,7 +35,10 @@
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
43
  export declare const DEVTOOLS_KEPT_VIEW_IDS: readonly string[];
41
44
  /**
@@ -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; }
@@ -462,12 +462,13 @@ export function createViewManager(ctx) {
462
462
  // map to a project-relative path, and broadcast to the renderer's Monaco.
463
463
  const openInEditorWiredWcIds = new Set();
464
464
  function injectOpenResourceHandler(serviceWc, devtoolsWc) {
465
- // `setOpenResourceHandler` is the official Chromium DevTools hook IDEs use
466
- // to route source-link clicks to an external editor. We poll for the host
467
- // (it appears once the front-end finishes bootstrapping, like `UI` above)
468
- // and register a handler that re-emits an encoded sentinel via
469
- // `openInNewTab` Electron `devtools-open-url`. Best-effort: wrapped in
470
- // try/catch and a bounded poll so a missing API never throws.
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.
471
472
  const sourceContext = projectSourceContextFromServiceHostUrl(serviceWc.getURL(), ctx.workspace?.getProjectPath?.());
472
473
  if (!sourceContext)
473
474
  return;
@@ -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;
@@ -40,20 +40,23 @@
40
40
  "dd-picker-body"
41
41
  ]);
42
42
  var FRAMEWORK_TEMPLATE_RE = /^(taro_tmpl|tmpl_\d+)/;
43
+ function normalizeRegisteredName(regName) {
44
+ const base = regName.startsWith("dd-") ? regName.slice(3) : regName;
45
+ return base.startsWith("tpl-") ? base.slice(4) : base;
46
+ }
43
47
  function resolveTemplateNameFromParent(instance) {
44
48
  const parent = instance.parent;
45
- if (!parent) return null;
46
- const parentType = parent.type;
49
+ const parentType = parent?.type;
47
50
  const components = parentType?.components;
48
51
  if (components) {
49
52
  for (const [regName, comp] of Object.entries(components)) {
50
- if (comp === instance.type) return regName.startsWith("dd-") ? regName.slice(3) : regName;
53
+ if (comp === instance.type) return normalizeRegisteredName(regName);
51
54
  }
52
55
  }
53
56
  const appComponents = instance.appContext?.components;
54
57
  if (!appComponents) return null;
55
58
  for (const [regName, comp] of Object.entries(appComponents)) {
56
- if (comp === instance.type) return regName.startsWith("dd-") ? regName.slice(3) : regName;
59
+ if (comp === instance.type) return normalizeRegisteredName(regName);
57
60
  }
58
61
  return null;
59
62
  }
@@ -68,15 +71,25 @@
68
71
  if (typeof path !== "string" || !path) return null;
69
72
  return path.startsWith("/") ? path.slice(1) : path;
70
73
  }
74
+ function resolveComponentPath(instance) {
75
+ const provides = instance.provides;
76
+ const path = provides?.path;
77
+ if (typeof path !== "string" || !path) return null;
78
+ const parent = instance.parent;
79
+ const parentProvides = parent?.provides;
80
+ if (path === parentProvides?.path) return null;
81
+ return path.startsWith("/") ? path.slice(1) : path;
82
+ }
71
83
  function resolveTagName(instance) {
72
84
  const type = instance.type;
73
85
  if (!type) return "unknown";
74
86
  const pagePath = resolvePagePath(instance);
75
87
  if (pagePath) return pagePath;
88
+ const componentPath = resolveComponentPath(instance);
89
+ if (componentPath) return componentPath;
76
90
  if (typeof type.__tagName === "string") return type.__tagName;
77
91
  const name = type.__name || type.name;
78
92
  if (!name) {
79
- if (type.__scopeId && type.components) return "page";
80
93
  if (type.__scopeId) return resolveTemplateNameFromParent(instance) || "template";
81
94
  return "unknown";
82
95
  }
@@ -120,6 +133,7 @@
120
133
  }
121
134
  function isTransparentComponent(instance, tagName) {
122
135
  if (tagName === "unknown") return true;
136
+ if (FRAMEWORK_TEMPLATE_RE.test(tagName)) return true;
123
137
  if (tagName === "template") {
124
138
  const props = instance.props;
125
139
  const is = props?.is;
@@ -250,26 +264,14 @@
250
264
  if (!tree) return null;
251
265
  return Array.isArray(tree) ? { tagName: "#fragment", attrs: {}, children: tree } : tree;
252
266
  };
253
- let overlay = null;
254
- const ensureOverlay = () => {
255
- if (overlay && overlay.ownerDocument === document) return overlay;
256
- overlay = document.createElement("div");
257
- overlay.id = "__simulator-highlight";
258
- overlay.style.cssText = "position:fixed;pointer-events:none;z-index:999999;border:2px solid #1a73e8;background:rgba(26,115,232,0.12);transition:all 0.1s ease;display:none;border-radius:2px;box-sizing:border-box;";
259
- document.body.appendChild(overlay);
260
- return overlay;
267
+ const elementFor = (sid) => {
268
+ if (!sid) return null;
269
+ return findElementBySid(document, sid);
261
270
  };
262
271
  const highlightElement = (sid) => {
263
- if (!sid) return null;
264
- const el = findElementBySid(document, sid);
272
+ const el = elementFor(sid);
265
273
  if (!el) return null;
266
274
  const rect = el.getBoundingClientRect();
267
- const box = ensureOverlay();
268
- box.style.left = `${rect.left}px`;
269
- box.style.top = `${rect.top}px`;
270
- box.style.width = `${rect.width}px`;
271
- box.style.height = `${rect.height}px`;
272
- box.style.display = "block";
273
275
  const style = el.ownerDocument.defaultView?.getComputedStyle(el);
274
276
  if (!style) return null;
275
277
  return {
@@ -288,8 +290,7 @@
288
290
  };
289
291
  };
290
292
  const unhighlightElement = () => {
291
- if (overlay) overlay.style.display = "none";
292
293
  };
293
- g.__diminaRenderInspect = { getWxml, highlightElement, unhighlightElement };
294
+ g.__diminaRenderInspect = { getWxml, highlightElement, elementFor, unhighlightElement };
294
295
  }
295
296
  })();