@dimina-kit/devtools 0.3.0 → 0.3.1-dev.20260513123738

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 (30) hide show
  1. package/dist/main/index.bundle.js +72 -23
  2. package/dist/main/services/default-adapter.js +1 -3
  3. package/dist/main/services/simulator/dir.d.ts +1 -1
  4. package/dist/main/services/simulator/dir.js +1 -4
  5. package/dist/main/services/simulator-storage/index.js +40 -1
  6. package/dist/main/utils/paths.d.ts +2 -0
  7. package/dist/main/utils/paths.js +27 -4
  8. package/dist/preload/instrumentation/wxml-extract.js +134 -3
  9. package/dist/preload/runtime/bridge.d.ts +3 -1
  10. package/dist/preload/runtime/bridge.js +55 -4
  11. package/dist/preload/shared/types.d.ts +2 -1
  12. package/dist/preload/windows/simulator.js +100 -7
  13. package/dist/renderer/assets/index-D1p6crtf.js +46 -0
  14. package/dist/renderer/assets/{input-VLotwer2.js → input-DxfYnzX_.js} +2 -2
  15. package/dist/renderer/assets/{ipc-transport-COKIC6Ny.js → ipc-transport-ClIvDcYY.js} +6 -6
  16. package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +1 -0
  17. package/dist/renderer/assets/{popover-Dn0RwMLI.js → popover-CtXcBJtM.js} +2 -2
  18. package/dist/renderer/assets/{select-DzIsLaKX.js → select-DMfp49f1.js} +2 -2
  19. package/dist/renderer/assets/{settings-CD-dacWB.js → settings-DteGraEU.js} +2 -2
  20. package/dist/renderer/assets/{settings-api-BV9MPckG.js → settings-api-CY84UMpN.js} +2 -2
  21. package/dist/renderer/assets/{workbenchSettings-ClwgsvJL.js → workbenchSettings-DjELLnSJ.js} +2 -2
  22. package/dist/renderer/entries/main/index.html +5 -5
  23. package/dist/renderer/entries/popover/index.html +5 -5
  24. package/dist/renderer/entries/settings/index.html +5 -5
  25. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  26. package/dist/shared/ipc-channels.d.ts +23 -0
  27. package/dist/shared/ipc-channels.js +5 -0
  28. package/package.json +2 -2
  29. package/dist/renderer/assets/index-D_HFRqZA.js +0 -46
  30. package/dist/renderer/assets/ipc-transport-6Gy3FXIL.css +0 -1
@@ -63,15 +63,31 @@ function setupCdpPort() {
63
63
 
64
64
  // src/main/app/app.ts
65
65
  import { app as app11, BrowserWindow as BrowserWindow3, nativeImage } from "electron";
66
- import fs4 from "fs";
66
+ import fs5 from "fs";
67
67
  import path6 from "path";
68
68
 
69
69
  // src/main/utils/paths.ts
70
+ import fs2 from "fs";
70
71
  import path2 from "path";
71
72
  import { fileURLToPath } from "url";
72
- var __dirname = path2.dirname(fileURLToPath(import.meta.url));
73
- var rendererDir = path2.join(__dirname, "../../../dist/renderer");
74
- var defaultPreloadPath = path2.join(__dirname, "../../preload/windows/simulator.js");
73
+ function resolveDevtoolsPackageRoot() {
74
+ let dir = path2.dirname(fileURLToPath(import.meta.url));
75
+ const root = path2.parse(dir).root;
76
+ while (dir !== root) {
77
+ const pkgPath = path2.join(dir, "package.json");
78
+ try {
79
+ const pkg2 = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
80
+ if (pkg2.name === "@dimina-kit/devtools") return dir;
81
+ } catch {
82
+ }
83
+ dir = path2.dirname(dir);
84
+ }
85
+ throw new Error("@dimina-kit/devtools package root not found from " + fileURLToPath(import.meta.url));
86
+ }
87
+ var devtoolsPackageRoot = resolveDevtoolsPackageRoot();
88
+ var rendererDir = path2.join(devtoolsPackageRoot, "dist/renderer");
89
+ var defaultPreloadPath = path2.join(devtoolsPackageRoot, "dist/preload/windows/simulator.js");
90
+ var simulatorDir = path2.join(devtoolsPackageRoot, "dist/simulator");
75
91
 
76
92
  // src/main/app/lifecycle.ts
77
93
  import { app as app3, globalShortcut } from "electron";
@@ -197,10 +213,7 @@ function wireMainWindowEvents(win, state = {}) {
197
213
 
198
214
  // src/main/services/default-adapter.ts
199
215
  import path3 from "path";
200
- import { fileURLToPath as fileURLToPath2 } from "url";
201
216
  import { app as app5 } from "electron";
202
- var __dirname2 = path3.dirname(fileURLToPath2(import.meta.url));
203
- var simulatorDir = path3.resolve(__dirname2, "../../simulator");
204
217
  var defaultAdapter = {
205
218
  async openProject(opts) {
206
219
  const diminaKit = await import("@dimina-kit/devkit");
@@ -228,6 +241,10 @@ var SimulatorStorageChannel = {
228
241
  GetSnapshot: "simulator:storage:snapshot",
229
242
  Event: "simulator:storage:event"
230
243
  };
244
+ var SimulatorElementChannel = {
245
+ Inspect: "simulator:element:inspect",
246
+ Clear: "simulator:element:clear"
247
+ };
231
248
  var WorkbenchChannel = {
232
249
  GetPanelConfig: "workbench:getPanelConfig",
233
250
  GetApiNamespaces: "workbench:getApiNamespaces",
@@ -621,7 +638,7 @@ function createViewManager(ctx) {
621
638
  // src/main/services/projects/project-repository.ts
622
639
  import { app as app6 } from "electron";
623
640
  import path5 from "path";
624
- import fs2 from "fs";
641
+ import fs3 from "fs";
625
642
 
626
643
  // src/shared/constants.ts
627
644
  var DEFAULT_SCENE = 1001;
@@ -671,13 +688,13 @@ function getProjectsFile() {
671
688
  }
672
689
  function load() {
673
690
  try {
674
- return JSON.parse(fs2.readFileSync(getProjectsFile(), "utf-8"));
691
+ return JSON.parse(fs3.readFileSync(getProjectsFile(), "utf-8"));
675
692
  } catch {
676
693
  return [];
677
694
  }
678
695
  }
679
696
  function save(projects) {
680
- fs2.writeFileSync(getProjectsFile(), JSON.stringify(projects, null, 2));
697
+ fs3.writeFileSync(getProjectsFile(), JSON.stringify(projects, null, 2));
681
698
  }
682
699
  function listProjects() {
683
700
  return load();
@@ -686,14 +703,14 @@ function validateProjectDir(dirPath) {
686
703
  if (!dirPath) {
687
704
  return "\u5C0F\u7A0B\u5E8F\u76EE\u5F55\u8DEF\u5F84\u4E3A\u7A7A\uFF0C\u8BF7\u9009\u62E9\u5305\u542B\u5C0F\u7A0B\u5E8F\u6E90\u7801\u7684\u76EE\u5F55";
688
705
  }
689
- if (!fs2.existsSync(dirPath)) {
706
+ if (!fs3.existsSync(dirPath)) {
690
707
  return `\u5C0F\u7A0B\u5E8F\u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${dirPath}`;
691
708
  }
692
- if (!fs2.existsSync(path5.join(dirPath, "app.json"))) {
709
+ if (!fs3.existsSync(path5.join(dirPath, "app.json"))) {
693
710
  const configPath = path5.join(dirPath, "project.config.json");
694
- if (fs2.existsSync(configPath)) {
711
+ if (fs3.existsSync(configPath)) {
695
712
  try {
696
- const cfg = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
713
+ const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
697
714
  if (cfg.miniprogramRoot) {
698
715
  const resolvedRoot = path5.resolve(dirPath, cfg.miniprogramRoot);
699
716
  return `\u8BE5\u76EE\u5F55\u7F3A\u5C11 app.json\uFF0Cproject.config.json \u4E2D\u6307\u5B9A\u4E86 miniprogramRoot: "${cfg.miniprogramRoot}"\uFF0C\u8BF7\u5BFC\u5165 ${resolvedRoot}`;
@@ -711,8 +728,8 @@ function addProject(dirPath) {
711
728
  let name = path5.basename(dirPath);
712
729
  try {
713
730
  const configPath = path5.join(dirPath, "project.config.json");
714
- if (fs2.existsSync(configPath)) {
715
- const cfg = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
731
+ if (fs3.existsSync(configPath)) {
732
+ const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
716
733
  if (cfg.projectname) name = cfg.projectname;
717
734
  }
718
735
  } catch (err) {
@@ -752,7 +769,7 @@ function getProjectPages(dirPath) {
752
769
  const appJsonPath = path5.join(dirPath, "app.json");
753
770
  try {
754
771
  const appJson = JSON.parse(
755
- fs2.readFileSync(appJsonPath, "utf-8")
772
+ fs3.readFileSync(appJsonPath, "utf-8")
756
773
  );
757
774
  return {
758
775
  pages: appJson.pages || [],
@@ -776,7 +793,7 @@ function getProjectSettings(projectPath) {
776
793
  }
777
794
  try {
778
795
  const configPath = path5.join(projectPath, "project.config.json");
779
- const config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
796
+ const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
780
797
  return {
781
798
  uploadWithSourceMap: !!config.setting?.uploadWithSourceMap
782
799
  };
@@ -789,14 +806,14 @@ function updateProjectSettings(projectPath, patch) {
789
806
  const configPath = path5.join(projectPath, "project.config.json");
790
807
  let config = {};
791
808
  try {
792
- config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
809
+ config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
793
810
  } catch {
794
811
  }
795
812
  const nextSetting = {
796
813
  ...config.setting ?? {},
797
814
  ...patch.uploadWithSourceMap === void 0 ? {} : { uploadWithSourceMap: patch.uploadWithSourceMap }
798
815
  };
799
- fs2.writeFileSync(
816
+ fs3.writeFileSync(
800
817
  configPath,
801
818
  JSON.stringify({ ...config, setting: nextSetting }, null, 2)
802
819
  );
@@ -992,10 +1009,10 @@ function installAppMenu(ctx) {
992
1009
 
993
1010
  // src/main/ipc/app.ts
994
1011
  import { ipcMain } from "electron";
995
- import fs3 from "fs";
1012
+ import fs4 from "fs";
996
1013
  function registerAppIpc(ctx) {
997
1014
  ipcMain.handle(AppChannel.GetPreloadPath, () => {
998
- return `file://${fs3.realpathSync(ctx.preloadPath)}`;
1015
+ return `file://${fs4.realpathSync(ctx.preloadPath)}`;
999
1016
  });
1000
1017
  ipcMain.handle(AppChannel.GetBranding, async () => {
1001
1018
  if (ctx.brandingProvider) return ctx.brandingProvider();
@@ -2373,6 +2390,36 @@ async function getSnapshot() {
2373
2390
  return [];
2374
2391
  }
2375
2392
  }
2393
+ function findSimulatorWebContents() {
2394
+ for (const wc of wcStatic.getAllWebContents()) {
2395
+ if (isSimulatorWebview(wc)) return wc;
2396
+ }
2397
+ return null;
2398
+ }
2399
+ async function inspectElement(sid) {
2400
+ if (!sid) return null;
2401
+ const sim = findSimulatorWebContents();
2402
+ if (!sim) return null;
2403
+ try {
2404
+ const result = await sim.executeJavaScript(
2405
+ `window.__simulatorData && window.__simulatorData.highlightElement ? window.__simulatorData.highlightElement(${JSON.stringify(sid)}) : null`
2406
+ );
2407
+ return result ?? null;
2408
+ } catch (e) {
2409
+ console.warn("[simulator-element] inspect failed:", e.message);
2410
+ return null;
2411
+ }
2412
+ }
2413
+ async function clearElementInspection() {
2414
+ const sim = findSimulatorWebContents();
2415
+ if (!sim) return;
2416
+ try {
2417
+ await sim.executeJavaScript(
2418
+ "window.__simulatorData && window.__simulatorData.unhighlightElement && window.__simulatorData.unhighlightElement()"
2419
+ );
2420
+ } catch {
2421
+ }
2422
+ }
2376
2423
  function setupSimulatorStorage(host) {
2377
2424
  hostWc = host;
2378
2425
  for (const wc of wcStatic.getAllWebContents()) {
@@ -2391,6 +2438,8 @@ function setupSimulatorStorage(host) {
2391
2438
  });
2392
2439
  });
2393
2440
  ipcMain10.handle(SimulatorStorageChannel.GetSnapshot, getSnapshot);
2441
+ ipcMain10.handle(SimulatorElementChannel.Inspect, (_event, sid) => inspectElement(sid));
2442
+ ipcMain10.handle(SimulatorElementChannel.Clear, clearElementInspection);
2394
2443
  }
2395
2444
 
2396
2445
  // src/main/services/update/update-manager.ts
@@ -2606,7 +2655,7 @@ function wireAppWindowEvents(config, instance) {
2606
2655
  function enableDevRendererAutoReload(rendererDir2) {
2607
2656
  if (!app11.isPackaged) {
2608
2657
  let reloadTimer = null;
2609
- fs4.watch(rendererDir2, { recursive: true }, () => {
2658
+ fs5.watch(rendererDir2, { recursive: true }, () => {
2610
2659
  if (reloadTimer) clearTimeout(reloadTimer);
2611
2660
  reloadTimer = setTimeout(() => {
2612
2661
  for (const win of BrowserWindow3.getAllWindows()) {
@@ -1,8 +1,6 @@
1
1
  import path from 'path';
2
- import { fileURLToPath } from 'url';
3
2
  import { app } from 'electron';
4
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
- const simulatorDir = path.resolve(__dirname, '../../simulator');
3
+ import { simulatorDir } from '../utils/paths.js';
6
4
  export const defaultAdapter = {
7
5
  async openProject(opts) {
8
6
  const diminaKit = await import('@dimina-kit/devkit');
@@ -1,2 +1,2 @@
1
- export declare const simulatorDir: string;
1
+ export { simulatorDir } from '../../utils/paths.js';
2
2
  //# sourceMappingURL=dir.d.ts.map
@@ -1,5 +1,2 @@
1
- import path from 'path';
2
- import { fileURLToPath } from 'url';
3
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
- export const simulatorDir = path.join(__dirname, '../../../simulator');
1
+ export { simulatorDir } from '../../utils/paths.js';
5
2
  //# sourceMappingURL=dir.js.map
@@ -16,7 +16,7 @@
16
16
  * events stop flowing until they close it.
17
17
  */
18
18
  import { app, ipcMain, webContents as wcStatic } from 'electron';
19
- import { SimulatorStorageChannel, } from '../../../shared/ipc-channels.js';
19
+ import { SimulatorElementChannel, SimulatorStorageChannel, } from '../../../shared/ipc-channels.js';
20
20
  let attachedWc = null;
21
21
  let hostWc = null;
22
22
  function isSimulatorWebview(wc) {
@@ -108,6 +108,43 @@ async function getSnapshot() {
108
108
  return [];
109
109
  }
110
110
  }
111
+ // Element inspection delegates to the simulator preload's __simulatorData
112
+ // bridge via executeJavaScript. Looked up independently from `attachedWc` so
113
+ // it keeps working when the user opens Chrome DevTools and the storage
114
+ // debugger detaches (debugger.attach is mutually exclusive with F12).
115
+ function findSimulatorWebContents() {
116
+ for (const wc of wcStatic.getAllWebContents()) {
117
+ if (isSimulatorWebview(wc))
118
+ return wc;
119
+ }
120
+ return null;
121
+ }
122
+ async function inspectElement(sid) {
123
+ if (!sid)
124
+ return null;
125
+ const sim = findSimulatorWebContents();
126
+ if (!sim)
127
+ return null;
128
+ try {
129
+ const result = (await sim.executeJavaScript(`window.__simulatorData && window.__simulatorData.highlightElement ? window.__simulatorData.highlightElement(${JSON.stringify(sid)}) : null`));
130
+ return result ?? null;
131
+ }
132
+ catch (e) {
133
+ console.warn('[simulator-element] inspect failed:', e.message);
134
+ return null;
135
+ }
136
+ }
137
+ async function clearElementInspection() {
138
+ const sim = findSimulatorWebContents();
139
+ if (!sim)
140
+ return;
141
+ try {
142
+ await sim.executeJavaScript('window.__simulatorData && window.__simulatorData.unhighlightElement && window.__simulatorData.unhighlightElement()');
143
+ }
144
+ catch {
145
+ // best-effort
146
+ }
147
+ }
111
148
  export function setupSimulatorStorage(host) {
112
149
  hostWc = host;
113
150
  // Attach to any simulator webview that already exists (project may have
@@ -132,5 +169,7 @@ export function setupSimulatorStorage(host) {
132
169
  });
133
170
  });
134
171
  ipcMain.handle(SimulatorStorageChannel.GetSnapshot, getSnapshot);
172
+ ipcMain.handle(SimulatorElementChannel.Inspect, (_event, sid) => inspectElement(sid));
173
+ ipcMain.handle(SimulatorElementChannel.Clear, clearElementInspection);
135
174
  }
136
175
  //# sourceMappingURL=index.js.map
@@ -1,5 +1,7 @@
1
+ export declare const devtoolsPackageRoot: string;
1
2
  export declare const rendererDir: string;
2
3
  export declare const defaultPreloadPath: string;
4
+ export declare const simulatorDir: string;
3
5
  export declare function getRendererDir(): string;
4
6
  export declare function getPreloadDir(): string;
5
7
  export declare function getRendererHtml(filename: string): string;
@@ -1,13 +1,36 @@
1
+ import fs from 'fs';
1
2
  import path from 'path';
2
3
  import { fileURLToPath } from 'url';
3
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
- export const rendererDir = path.join(__dirname, '../../../dist/renderer');
5
- export const defaultPreloadPath = path.join(__dirname, '../../preload/windows/simulator.js');
4
+ // Hardcoded relative jumps like '../../dist/renderer' break under the
5
+ // esbuild single-file bundle (dist/main/index.bundle.js): every inlined
6
+ // module ends up at the bundle's depth instead of its tsc location, so
7
+ // the math goes off by a directory. Walk up to this package's own
8
+ // package.json so the resolver works for tsc multi-file, the bundle,
9
+ // npm/pnpm node_modules, and asar packaging alike.
10
+ function resolveDevtoolsPackageRoot() {
11
+ let dir = path.dirname(fileURLToPath(import.meta.url));
12
+ const root = path.parse(dir).root;
13
+ while (dir !== root) {
14
+ const pkgPath = path.join(dir, 'package.json');
15
+ try {
16
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
17
+ if (pkg.name === '@dimina-kit/devtools')
18
+ return dir;
19
+ }
20
+ catch { }
21
+ dir = path.dirname(dir);
22
+ }
23
+ throw new Error('@dimina-kit/devtools package root not found from ' + fileURLToPath(import.meta.url));
24
+ }
25
+ export const devtoolsPackageRoot = resolveDevtoolsPackageRoot();
26
+ export const rendererDir = path.join(devtoolsPackageRoot, 'dist/renderer');
27
+ export const defaultPreloadPath = path.join(devtoolsPackageRoot, 'dist/preload/windows/simulator.js');
28
+ export const simulatorDir = path.join(devtoolsPackageRoot, 'dist/simulator');
6
29
  export function getRendererDir() {
7
30
  return rendererDir;
8
31
  }
9
32
  export function getPreloadDir() {
10
- return path.join(__dirname, '../../preload');
33
+ return path.join(devtoolsPackageRoot, 'dist/preload');
11
34
  }
12
35
  export function getRendererHtml(filename) {
13
36
  return path.join(rendererDir, filename);
@@ -1,3 +1,4 @@
1
+ import { registerSyntheticSid } from '../runtime/bridge.js';
1
2
  const INTERNAL_CLASSES = new Set([
2
3
  'dd-swiper-wrapper', 'dd-swiper-slides', 'dd-swiper-slide-frame', 'dd-swiper-dots',
3
4
  'dd-picker-overlay', 'dd-picker-container', 'dd-picker-header', 'dd-picker-body',
@@ -24,10 +25,44 @@ function resolveTemplateNameFromParent(instance) {
24
25
  }
25
26
  return null;
26
27
  }
28
+ /**
29
+ * Convert a PascalCase / camelCase identifier to kebab-case. Matches the
30
+ * upstream `camelCaseToUnderscore` (dimina/fe/packages/common) so a name
31
+ * already normalized by `withInstall` round-trips identically.
32
+ * `View` -> `view`, `ScrollView` -> `scroll-view`, `CoverImage` -> `cover-image`.
33
+ */
34
+ function pascalToKebab(name) {
35
+ return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
36
+ }
37
+ /**
38
+ * Read the page's source path off Vue's provide/inject. dimina runtime.js 在
39
+ * dd-page 的 setup() 里调用 `provide('path', path)`,Vue 把它存到
40
+ * `instance.provides`。我们利用这个把页面节点的 tagName 从硬编码 `page`
41
+ * 升级为页面全路径(如 `pages/index/index`),对齐微信开发者工具。
42
+ */
43
+ function resolvePagePath(instance) {
44
+ // dimina runtime 在 dd-page 的 setup 里 `instance.proxy.__page__ = true` 并
45
+ // `provide('path', path)`。两个标记同时具备才视为页面层级,避免 dd-page
46
+ // 的子节点继承 provides.path 后被误判(Vue 用 Object.create 链式 provides)。
47
+ const proxy = instance.proxy;
48
+ if (proxy?.__page__ !== true)
49
+ return null;
50
+ const provides = instance.provides;
51
+ const path = provides?.path;
52
+ if (typeof path !== 'string' || !path)
53
+ return null;
54
+ return path.startsWith('/') ? path.slice(1) : path;
55
+ }
27
56
  function resolveTagName(instance) {
28
57
  const type = instance.type;
29
58
  if (!type)
30
59
  return 'unknown';
60
+ // 页面层级优先:dd-page 没有 __tagName/__name,且 home 页等无 usingComponents
61
+ // 的页面 type.components 为 undefined,会落到 resolveTemplateNameFromParent
62
+ // 回到 'page'。在那之前直接用 provide('path') 的路径升级 tag 名。
63
+ const pagePath = resolvePagePath(instance);
64
+ if (pagePath)
65
+ return pagePath;
31
66
  if (typeof type.__tagName === 'string')
32
67
  return type.__tagName;
33
68
  const name = (type.__name || type.name);
@@ -42,8 +77,16 @@ function resolveTagName(instance) {
42
77
  return 'page';
43
78
  if (name.startsWith('dd-'))
44
79
  return name.slice(3);
80
+ // Reverse-map Dimina component names back to their miniprogram tag names.
81
+ // The installer (`withInstall`) sets `__tagName = camelCaseToUnderscore(__name)`,
82
+ // but in dev builds, custom registrations, or when the installer hasn't run,
83
+ // we may only see the raw `__name`/`name` (e.g. `View`, `ScrollView`, `DdButton`).
84
+ // Without this fallback the WXML panel would surface the upstream Vue name
85
+ // verbatim, defeating the panel's purpose of showing source-level tags.
45
86
  if (name.startsWith('Dd') && name.length > 2)
46
- return name.slice(2).toLowerCase();
87
+ return pascalToKebab(name.slice(2));
88
+ if (/^[A-Z]/.test(name))
89
+ return pascalToKebab(name);
47
90
  return name;
48
91
  }
49
92
  function extractProps(instance) {
@@ -79,7 +122,12 @@ function isInternalElement(vnode) {
79
122
  function getElementSid(instance) {
80
123
  const subTree = instance.subTree;
81
124
  const el = subTree?.el;
82
- return el?.getAttribute ? el.getAttribute('data-sid') ?? undefined : undefined;
125
+ if (!el?.getAttribute)
126
+ return undefined;
127
+ const sid = el.getAttribute('data-sid');
128
+ if (sid)
129
+ return sid;
130
+ return registerSyntheticSid(el);
83
131
  }
84
132
  function isTransparentComponent(instance, tagName) {
85
133
  if (tagName === 'unknown')
@@ -97,9 +145,29 @@ function isTransparentComponent(instance, tagName) {
97
145
  }
98
146
  return false;
99
147
  }
148
+ /**
149
+ * 检测 Vue 的 Comment vnode(`v-if`/`v-else`/`v-for` 占位锚点)。
150
+ * Vue 在条件不成立时会留下 `<!-- v-if -->` 这样的注释 vnode 用作 DOM 锚点。
151
+ * 这些 vnode 不是用户内容,必须从 wxml 树里剔除。
152
+ *
153
+ * - dev build:type 是 `Symbol('Comment')`,description = 'Comment' 或 'v-cmt'
154
+ * - prod build:description 可能丢失,但 children 一般是 'v-if'/'v-else' 等 marker
155
+ */
156
+ function isCommentVNode(vnode) {
157
+ const type = vnode.type;
158
+ if (typeof type !== 'symbol')
159
+ return false;
160
+ const desc = type.description;
161
+ if (desc === 'Comment' || desc === 'v-cmt')
162
+ return true;
163
+ const children = typeof vnode.children === 'string' ? vnode.children : '';
164
+ return /^v-(if|else|else-if|for)$/.test(children);
165
+ }
100
166
  function extractChildrenFromVNode(vnode, depth) {
101
167
  if (!vnode || depth > 50)
102
168
  return [];
169
+ if (isCommentVNode(vnode))
170
+ return [];
103
171
  if (vnode.component) {
104
172
  const result = walkInstance(vnode.component, depth + 1);
105
173
  return result ? (Array.isArray(result) ? result : [result]) : [];
@@ -157,6 +225,69 @@ function extractChildrenFromVNode(vnode, depth) {
157
225
  }
158
226
  return result;
159
227
  }
228
+ /**
229
+ * Reverse-map Dimina's `<wrapper name="/components/foo/foo">` (used to host
230
+ * every user-defined component) back to the source-level tag the user wrote
231
+ * in WXML (e.g. `<foo>`).
232
+ *
233
+ * The wrapper carries the registered component path in its `name` Vue prop.
234
+ * By convention (also followed by miniprogram tooling), the registered key is
235
+ * the last path segment, optionally stripping a trailing `/index`. We can't
236
+ * see the page's `usingComponents` map from inside the Vue instance, so this
237
+ * convention-based recovery is best-effort: if the user picked a different
238
+ * registration key than the directory name (e.g. `usingComponents:
239
+ * { myCounter: '/components/counter/counter' }`), the panel will show
240
+ * `counter`, not `myCounter`.
241
+ *
242
+ * The path heuristic also resolves a name-collision risk: a user-written
243
+ * `<counter name="x">` would land in the same `attrs.name` slot as the
244
+ * wrapper-internal path. We only unwrap when the value looks like an absolute
245
+ * component path (leading `/`), which dimina always emits but a user would
246
+ * almost never type as a literal attr.
247
+ */
248
+ function unwrapCustomComponent(node) {
249
+ if (node.tagName !== 'wrapper')
250
+ return node;
251
+ const path = node.attrs?.name;
252
+ if (typeof path !== 'string' || !path.startsWith('/'))
253
+ return node;
254
+ // 去掉前导 `/` 后保留原路径形式(保留所有斜杠,不做 kebab/dash 转换)。
255
+ // 仅当路径以 `/index` 结尾且剥离后仍至少剩一段时才剥(`/index` 单独存在则
256
+ // 退回 wrapper,避免出现空 tagName)。
257
+ const stripped = path.replace(/^\//, '');
258
+ const withoutIndex = stripped.endsWith('/index') ? stripped.slice(0, -'/index'.length) : stripped;
259
+ const recovered = withoutIndex || stripped;
260
+ if (!recovered || recovered === 'index')
261
+ return node;
262
+ const nextAttrs = {};
263
+ for (const [k, v] of Object.entries(node.attrs)) {
264
+ if (k === 'name')
265
+ continue;
266
+ nextAttrs[k] = v;
267
+ }
268
+ return { ...node, tagName: recovered, attrs: nextAttrs };
269
+ }
270
+ /**
271
+ * 把"用户授权"层级(页面 / 自定义组件,tagName 以路径形式呈现,含 `/`)
272
+ * 的 children 包一层合成 `#shadow-root`,对齐微信开发者工具:组件本身
273
+ * 与内部实现之间用 shadow-root 边界视觉分隔。
274
+ *
275
+ * 内置组件(view/text/button 等)和合成节点(#text/#fragment)tagName 都
276
+ * 不含 `/`,自然不会被包裹。children 为空也不插入(避免空壳)。重复调用
277
+ * 是幂等的:若 children[0] 已经是 #shadow-root 就跳过。
278
+ */
279
+ function wrapInShadowRoot(node) {
280
+ if (!node.tagName.includes('/'))
281
+ return node;
282
+ if (node.children.length === 0)
283
+ return node;
284
+ if (node.children[0]?.tagName === '#shadow-root')
285
+ return node;
286
+ return {
287
+ ...node,
288
+ children: [{ tagName: '#shadow-root', attrs: {}, children: node.children }],
289
+ };
290
+ }
160
291
  export function walkInstance(instance, depth) {
161
292
  if (depth > 50)
162
293
  return null;
@@ -168,6 +299,6 @@ export function walkInstance(instance, depth) {
168
299
  const sid = getElementSid(instance);
169
300
  if (sid)
170
301
  node.sid = sid;
171
- return node;
302
+ return wrapInShadowRoot(unwrapCustomComponent(node));
172
303
  }
173
304
  //# sourceMappingURL=wxml-extract.js.map
@@ -1,3 +1,4 @@
1
+ import type { ElementInspection } from '../../shared/ipc-channels.js';
1
2
  export interface WxmlNode {
2
3
  tagName: string;
3
4
  attrs: Record<string, string>;
@@ -20,7 +21,8 @@ interface SimulatorBridgeState {
20
21
  wxml: Snapshot<WxmlNode | WxmlNode[] | null>;
21
22
  refreshHandler: ((type: RefreshTarget) => void) | null;
22
23
  }
23
- declare function highlightElement(sid: string): void;
24
+ export declare function registerSyntheticSid(el: HTMLElement): string;
25
+ declare function highlightElement(sid: string): ElementInspection | null;
24
26
  declare function unhighlightElement(): void;
25
27
  export { highlightElement, unhighlightElement };
26
28
  export declare const simulatorBridge: SimulatorBridgeState;
@@ -7,6 +7,38 @@ const state = {
7
7
  };
8
8
  let highlightOverlay = null;
9
9
  let exposedApi = null;
10
+ // 合成 sid 注册表:用 WeakMap 把元素 ↔ sid 双向绑定,避免在源 DOM 上写
11
+ // `data-*` 属性(提取本应只读,且属性形式会污染用户的快照/选择器)。
12
+ // elBySyntheticSid 为反向查找用 WeakRef,元素被 GC 后下次 lookup 自动清理。
13
+ const SYNTHETIC_SID_PREFIX = 'devtools-';
14
+ const syntheticSidByEl = new WeakMap();
15
+ const elBySyntheticSid = new Map();
16
+ let nextSyntheticSid = 1;
17
+ export function registerSyntheticSid(el) {
18
+ const existing = syntheticSidByEl.get(el);
19
+ if (existing)
20
+ return existing;
21
+ const synthetic = `${SYNTHETIC_SID_PREFIX}${nextSyntheticSid++}`;
22
+ syntheticSidByEl.set(el, synthetic);
23
+ elBySyntheticSid.set(synthetic, new WeakRef(el));
24
+ return synthetic;
25
+ }
26
+ function findElementBySid(doc, sid) {
27
+ if (sid.startsWith(SYNTHETIC_SID_PREFIX)) {
28
+ const ref = elBySyntheticSid.get(sid);
29
+ if (!ref)
30
+ return null;
31
+ const el = ref.deref();
32
+ if (!el || !el.isConnected) {
33
+ elBySyntheticSid.delete(sid);
34
+ return null;
35
+ }
36
+ if (el.ownerDocument !== doc)
37
+ return null;
38
+ return el;
39
+ }
40
+ return doc.querySelector(`[data-sid="${CSS.escape(sid)}"]`);
41
+ }
10
42
  function clone(value) {
11
43
  try {
12
44
  return JSON.parse(JSON.stringify(value));
@@ -27,18 +59,20 @@ function ensureOverlay(doc) {
27
59
  highlightOverlay.style.cssText =
28
60
  'position:fixed;pointer-events:none;z-index:999999;' +
29
61
  'border:2px solid #1a73e8;background:rgba(26,115,232,0.12);' +
30
- 'transition:all 0.1s ease;display:none;border-radius:2px;';
62
+ 'transition:all 0.1s ease;display:none;border-radius:2px;box-sizing:border-box;';
31
63
  doc.body.appendChild(highlightOverlay);
32
64
  return highlightOverlay;
33
65
  }
34
66
  function highlightElement(sid) {
67
+ if (!sid)
68
+ return null;
35
69
  const iframe = getPageIframe();
36
70
  if (!iframe?.contentDocument)
37
- return;
71
+ return null;
38
72
  const doc = iframe.contentDocument;
39
- const el = doc.querySelector(`[data-sid="${sid}"]`);
73
+ const el = findElementBySid(doc, sid);
40
74
  if (!el)
41
- return;
75
+ return null;
42
76
  const rect = el.getBoundingClientRect();
43
77
  const overlay = ensureOverlay(doc);
44
78
  overlay.style.left = `${rect.left}px`;
@@ -46,6 +80,23 @@ function highlightElement(sid) {
46
80
  overlay.style.width = `${rect.width}px`;
47
81
  overlay.style.height = `${rect.height}px`;
48
82
  overlay.style.display = 'block';
83
+ const style = el.ownerDocument.defaultView?.getComputedStyle(el);
84
+ if (!style)
85
+ return null;
86
+ return {
87
+ sid,
88
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
89
+ style: {
90
+ display: style.display,
91
+ position: style.position,
92
+ boxSizing: style.boxSizing,
93
+ margin: style.margin,
94
+ padding: style.padding,
95
+ color: style.color,
96
+ backgroundColor: style.backgroundColor,
97
+ fontSize: style.fontSize,
98
+ },
99
+ };
49
100
  }
50
101
  function unhighlightElement() {
51
102
  if (highlightOverlay)
@@ -5,6 +5,7 @@
5
5
  * window by our preload/instrumentation code.
6
6
  */
7
7
  import type { Snapshot, StorageSnapshot, WxmlNode } from '../runtime/bridge.js';
8
+ import type { ElementInspection } from '../../shared/ipc-channels.js';
8
9
  /** The simulator data bridge exposed via contextBridge. */
9
10
  export interface SimulatorDataBridge {
10
11
  getAppdata: () => Record<string, unknown>;
@@ -16,7 +17,7 @@ export interface SimulatorDataBridge {
16
17
  getWxmlSnapshot: () => Snapshot<WxmlNode | WxmlNode[] | null>;
17
18
  getWxmlGen: () => number;
18
19
  refresh: (type: 'wxml' | 'appdata' | 'storage') => void;
19
- highlightElement: (sid: string) => void;
20
+ highlightElement: (sid: string) => ElementInspection | null;
20
21
  unhighlightElement: () => void;
21
22
  }
22
23
  /** Hook interface for app data interception. */