@dimina-kit/devtools 0.3.1 → 0.3.2-dev.20260522085907

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 (190) hide show
  1. package/README.md +297 -133
  2. package/dist/main/api.d.ts +6 -2
  3. package/dist/main/api.js +5 -2
  4. package/dist/main/app/app.d.ts +1 -1
  5. package/dist/main/app/app.js +84 -36
  6. package/dist/main/app/bootstrap.js +2 -1
  7. package/dist/main/app/launch.d.ts +2 -2
  8. package/dist/main/app/launch.js +10 -8
  9. package/dist/main/index.bundle.js +1567 -477
  10. package/dist/main/ipc/app.d.ts +2 -1
  11. package/dist/main/ipc/app.js +5 -4
  12. package/dist/main/ipc/index.d.ts +5 -4
  13. package/dist/main/ipc/index.js +5 -4
  14. package/dist/main/ipc/panels.d.ts +2 -1
  15. package/dist/main/ipc/panels.js +16 -13
  16. package/dist/main/ipc/popover.d.ts +4 -1
  17. package/dist/main/ipc/popover.js +15 -5
  18. package/dist/main/ipc/projects.d.ts +6 -1
  19. package/dist/main/ipc/projects.js +127 -6
  20. package/dist/main/ipc/session.d.ts +4 -1
  21. package/dist/main/ipc/session.js +34 -6
  22. package/dist/main/ipc/settings.d.ts +4 -1
  23. package/dist/main/ipc/settings.js +51 -23
  24. package/dist/main/ipc/simulator-module.d.ts +8 -0
  25. package/dist/main/ipc/simulator-module.js +19 -0
  26. package/dist/main/ipc/simulator.d.ts +2 -1
  27. package/dist/main/ipc/simulator.js +23 -18
  28. package/dist/main/ipc/toolbar.d.ts +2 -1
  29. package/dist/main/ipc/toolbar.js +3 -2
  30. package/dist/main/menu/index.js +11 -0
  31. package/dist/main/services/automation/exec.d.ts +2 -0
  32. package/dist/main/services/automation/exec.js +7 -9
  33. package/dist/main/services/automation/handlers/app.js +37 -51
  34. package/dist/main/services/automation/handlers/element.js +27 -41
  35. package/dist/main/services/automation/index.js +88 -13
  36. package/dist/main/services/default-adapter.js +3 -3
  37. package/dist/main/services/mcp/index.d.ts +1 -0
  38. package/dist/main/services/mcp/index.js +1 -0
  39. package/dist/main/services/mcp/server.d.ts +2 -1
  40. package/dist/main/services/mcp/server.js +10 -0
  41. package/dist/main/services/mcp/status.d.ts +10 -0
  42. package/dist/main/services/mcp/status.js +14 -0
  43. package/dist/main/services/mcp/target-manager.js +2 -1
  44. package/dist/main/services/module.d.ts +18 -0
  45. package/dist/main/services/module.js +2 -0
  46. package/dist/main/services/notifications/renderer-notifier.d.ts +5 -3
  47. package/dist/main/services/notifications/renderer-notifier.js +3 -6
  48. package/dist/main/services/projects/builtin-templates.d.ts +3 -0
  49. package/dist/main/services/projects/builtin-templates.js +24 -0
  50. package/dist/main/services/projects/create-project-service.d.ts +7 -0
  51. package/dist/main/services/projects/create-project-service.js +84 -0
  52. package/dist/main/services/projects/index.d.ts +7 -0
  53. package/dist/main/services/projects/index.js +9 -0
  54. package/dist/main/services/projects/local-provider.d.ts +3 -0
  55. package/dist/main/services/projects/local-provider.js +25 -0
  56. package/dist/main/services/projects/project-repository.d.ts +1 -1
  57. package/dist/main/services/projects/project-repository.js +3 -0
  58. package/dist/main/services/projects/templates.d.ts +21 -0
  59. package/dist/main/services/projects/templates.js +39 -0
  60. package/dist/main/services/projects/thumbnail.d.ts +8 -0
  61. package/dist/main/services/projects/thumbnail.js +37 -0
  62. package/dist/main/services/projects/types.d.ts +118 -0
  63. package/dist/main/services/projects/types.js +19 -0
  64. package/dist/main/services/settings/index.d.ts +11 -0
  65. package/dist/main/services/settings/index.js +18 -2
  66. package/dist/main/services/simulator/custom-apis.d.ts +10 -0
  67. package/dist/main/services/simulator/custom-apis.js +26 -0
  68. package/dist/main/services/simulator-storage/index.d.ts +22 -1
  69. package/dist/main/services/simulator-storage/index.js +334 -97
  70. package/dist/main/services/update/update-manager.d.ts +16 -1
  71. package/dist/main/services/update/update-manager.js +18 -15
  72. package/dist/main/services/views/view-manager.d.ts +7 -7
  73. package/dist/main/services/views/view-manager.js +53 -29
  74. package/dist/main/services/window-service.d.ts +24 -0
  75. package/dist/main/services/window-service.js +26 -0
  76. package/dist/main/services/workbench-context.d.ts +49 -2
  77. package/dist/main/services/workbench-context.js +15 -2
  78. package/dist/main/services/workspace/workspace-service.d.ts +9 -6
  79. package/dist/main/services/workspace/workspace-service.js +71 -11
  80. package/dist/main/simulator-apis.d.ts +23 -0
  81. package/dist/main/simulator-apis.js +24 -0
  82. package/dist/main/utils/disposable.d.ts +13 -0
  83. package/dist/main/utils/disposable.js +60 -0
  84. package/dist/main/utils/ipc-registry.d.ts +39 -0
  85. package/dist/main/utils/ipc-registry.js +109 -0
  86. package/dist/main/utils/ipc-schema.d.ts +28 -0
  87. package/dist/main/utils/ipc-schema.js +38 -0
  88. package/dist/main/utils/paths.d.ts +6 -0
  89. package/dist/main/utils/paths.js +6 -0
  90. package/dist/main/utils/sender-policy.d.ts +25 -0
  91. package/dist/main/utils/sender-policy.js +44 -0
  92. package/dist/main/utils/theme.d.ts +24 -0
  93. package/dist/main/utils/theme.js +38 -1
  94. package/dist/main/windows/main-window/create.d.ts +6 -0
  95. package/dist/main/windows/main-window/create.js +66 -10
  96. package/dist/main/windows/main-window/events.d.ts +2 -1
  97. package/dist/main/windows/main-window/events.js +25 -2
  98. package/dist/main/windows/navigation-hardening.d.ts +24 -0
  99. package/dist/main/windows/navigation-hardening.js +55 -0
  100. package/dist/main/windows/settings-window/create.js +11 -1
  101. package/dist/preload/index.d.ts +22 -0
  102. package/dist/preload/index.js +19 -0
  103. package/dist/preload/instrumentation/app-data.d.ts +32 -2
  104. package/dist/preload/instrumentation/app-data.js +253 -79
  105. package/dist/preload/instrumentation/index.d.ts +2 -2
  106. package/dist/preload/instrumentation/index.js +2 -2
  107. package/dist/preload/instrumentation/wxml.d.ts +16 -4
  108. package/dist/preload/instrumentation/wxml.js +188 -178
  109. package/dist/preload/miniapp-snapshot/host.d.ts +12 -0
  110. package/dist/preload/miniapp-snapshot/host.js +117 -0
  111. package/dist/preload/miniapp-snapshot/types.d.ts +62 -0
  112. package/dist/preload/miniapp-snapshot/types.js +15 -0
  113. package/dist/preload/runtime/bridge.js +5 -24
  114. package/dist/preload/runtime/custom-apis.d.ts +6 -0
  115. package/dist/preload/runtime/custom-apis.js +43 -0
  116. package/dist/preload/runtime/host.d.ts +1 -0
  117. package/dist/preload/runtime/host.js +13 -0
  118. package/dist/preload/shared/expose.d.ts +15 -0
  119. package/dist/preload/shared/expose.js +33 -0
  120. package/dist/preload/shared/page-iframe.d.ts +3 -0
  121. package/dist/preload/shared/page-iframe.js +6 -0
  122. package/dist/preload/shared/types.d.ts +2 -0
  123. package/dist/preload/windows/main.cjs +47 -0
  124. package/dist/preload/windows/main.cjs.map +7 -0
  125. package/dist/preload/windows/simulator.js +501 -272
  126. package/dist/renderer/assets/constants-DI382mTP.js +2 -0
  127. package/dist/renderer/assets/index-D8VFYBh-.js +46 -0
  128. package/dist/renderer/assets/{input-DxfYnzX_.js → input-CoWcfa8u.js} +2 -2
  129. package/dist/renderer/assets/ipc-transport-Cd2xPrsa.js +9 -0
  130. package/dist/renderer/assets/ipc-transport-DqbRdZNO.css +1 -0
  131. package/dist/renderer/assets/popover-Nq5gT9DO.js +2 -0
  132. package/dist/renderer/assets/select-By5KH-UG.js +2 -0
  133. package/dist/renderer/assets/{settings-DteGraEU.js → settings-Dj_N70qR.js} +2 -2
  134. package/dist/renderer/assets/settings-api-Do0_Smq1.js +2 -0
  135. package/dist/renderer/assets/workbenchSettings-CazSLGYt.js +8 -0
  136. package/dist/renderer/entries/main/index.html +13 -5
  137. package/dist/renderer/entries/popover/index.html +8 -5
  138. package/dist/renderer/entries/settings/index.html +7 -5
  139. package/dist/renderer/entries/workbench-settings/index.html +7 -4
  140. package/dist/shared/ipc-channels.d.ts +46 -13
  141. package/dist/shared/ipc-channels.js +60 -14
  142. package/dist/shared/ipc-schemas.d.ts +118 -0
  143. package/dist/shared/ipc-schemas.js +133 -0
  144. package/dist/shared/simulator-route.d.ts +72 -0
  145. package/dist/shared/simulator-route.js +169 -0
  146. package/dist/shared/types.d.ts +82 -8
  147. package/dist/simulator/assets/simulator-DJCozrU5.js +9 -0
  148. package/dist/simulator/simulator.html +6 -43
  149. package/package.json +18 -29
  150. package/templates/blank/app.js +6 -0
  151. package/templates/blank/app.json +11 -0
  152. package/templates/blank/app.wxss +5 -0
  153. package/templates/blank/pages/index/index.js +6 -0
  154. package/templates/blank/pages/index/index.json +3 -0
  155. package/templates/blank/pages/index/index.wxml +3 -0
  156. package/templates/blank/pages/index/index.wxss +11 -0
  157. package/templates/blank/project.config.json +13 -0
  158. package/templates/blank/sitemap.json +9 -0
  159. package/templates/taro-todo/app.js +1 -0
  160. package/templates/taro-todo/app.json +1 -0
  161. package/templates/taro-todo/app.wxss +1 -0
  162. package/templates/taro-todo/base.wxml +851 -0
  163. package/templates/taro-todo/common.js +1 -0
  164. package/templates/taro-todo/comp.js +1 -0
  165. package/templates/taro-todo/comp.json +1 -0
  166. package/templates/taro-todo/comp.wxml +2 -0
  167. package/templates/taro-todo/custom-wrapper.js +1 -0
  168. package/templates/taro-todo/custom-wrapper.json +1 -0
  169. package/templates/taro-todo/custom-wrapper.wxml +4 -0
  170. package/templates/taro-todo/pages/index/index.js +2 -0
  171. package/templates/taro-todo/pages/index/index.json +1 -0
  172. package/templates/taro-todo/pages/index/index.wxml +2 -0
  173. package/templates/taro-todo/pages/index/index.wxss +1 -0
  174. package/templates/taro-todo/project.config.json +44 -0
  175. package/templates/taro-todo/runtime.js +1 -0
  176. package/templates/taro-todo/taro.js +2 -0
  177. package/templates/taro-todo/utils.wxs +39 -0
  178. package/templates/taro-todo/vendors.js +2 -0
  179. package/dist/preload/instrumentation/disposable.d.ts +0 -11
  180. package/dist/preload/instrumentation/disposable.js +0 -15
  181. package/dist/preload/windows/main.d.ts +0 -6
  182. package/dist/preload/windows/main.js +0 -6
  183. package/dist/renderer/assets/index-D1p6crtf.js +0 -46
  184. package/dist/renderer/assets/ipc-transport-ClIvDcYY.js +0 -9
  185. package/dist/renderer/assets/ipc-transport-EiWxu_hG.css +0 -1
  186. package/dist/renderer/assets/popover-CtXcBJtM.js +0 -2
  187. package/dist/renderer/assets/select-DMfp49f1.js +0 -2
  188. package/dist/renderer/assets/settings-api-CY84UMpN.js +0 -2
  189. package/dist/renderer/assets/workbenchSettings-DjELLnSJ.js +0 -8
  190. package/dist/simulator/assets/simulator-C1eHXnuq.js +0 -9
@@ -1,13 +1,8 @@
1
- import { webContents } from 'electron';
2
1
  // ── Helpers ───────────────────────────────────────────────────────────
3
2
  export function getSimulator(ctx) {
4
- const simWcId = ctx.views.getSimulatorWebContentsId();
5
- if (!ctx.workspace.hasActiveSession() || simWcId == null)
3
+ if (!ctx.workspace.hasActiveSession())
6
4
  return null;
7
- const sim = webContents.fromId(simWcId);
8
- if (!sim || sim.isDestroyed())
9
- return null;
10
- return sim;
5
+ return ctx.views.getSimulatorWebContents();
11
6
  }
12
7
  export async function evalInSim(ctx, expression) {
13
8
  const sim = getSimulator(ctx);
@@ -17,8 +12,11 @@ export async function evalInSim(ctx, expression) {
17
12
  }
18
13
  /** Build JS that locates an element by selector+index inside the active page iframe. */
19
14
  export function buildElAccess(ref, varName = 'el') {
20
- const escaped = ref.selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
21
- return `const ${varName} = _doc.querySelectorAll('${escaped}')[${ref.index}]`;
15
+ return `const ${varName} = _doc.querySelectorAll(${JSON.stringify(ref.selector)})[${ref.index}]`;
16
+ }
17
+ /** Run `body` inside the active page iframe with `varName` bound to the element at `ref`. */
18
+ export function evalInElement(ctx, ref, body, varName = 'el') {
19
+ return evalInSim(ctx, inIframe(`${buildElAccess(ref, varName)}\n${body}`));
22
20
  }
23
21
  /** Wrap code to run inside the active page iframe (last iframe in the stack). */
24
22
  export function inIframe(code) {
@@ -1,59 +1,36 @@
1
1
  import { evalInSim, getSimulator, inIframe } from '../exec.js';
2
+ import { parseLocationRoute } from '../../../../shared/simulator-route.js';
2
3
  export const appHandlers = {};
3
4
  // -- App domain --
5
+ async function readRoute(ctx) {
6
+ const { search } = await evalInSim(ctx, `({ search: location.search })`);
7
+ return parseLocationRoute(search);
8
+ }
4
9
  appHandlers['App.getCurrentPage'] = async (ctx) => {
5
- const hash = await evalInSim(ctx, 'location.hash');
6
- const clean = hash.replace(/^#/, '');
7
- const qIdx = clean.indexOf('?');
8
- const pathPart = qIdx >= 0 ? clean.substring(0, qIdx) : clean;
9
- const queryPart = qIdx >= 0 ? clean.substring(qIdx + 1) : '';
10
- // New format: #appid|page1|page2 — last segment is current page
11
- let pagePath;
12
- if (pathPart.includes('|')) {
13
- const parts = pathPart.split('|');
14
- pagePath = parts[parts.length - 1] ?? '';
15
- }
16
- else {
17
- // Legacy format: #appid/pagePath
18
- const slashIdx = pathPart.indexOf('/');
19
- pagePath = slashIdx >= 0 ? pathPart.substring(slashIdx + 1) : pathPart;
20
- }
21
- const query = {};
22
- if (queryPart) {
23
- for (const pair of queryPart.split('&')) {
24
- const [k, v] = pair.split('=');
25
- if (k)
26
- query[k] = decodeURIComponent(v || '');
27
- }
28
- }
10
+ const route = await readRoute(ctx);
29
11
  const iframeCount = await evalInSim(ctx, `document.querySelectorAll('iframe').length`);
30
- return { pageId: iframeCount, path: pagePath, query };
12
+ return {
13
+ pageId: iframeCount,
14
+ path: route?.current.pagePath ?? '',
15
+ query: route?.current.query ?? {},
16
+ };
31
17
  };
32
18
  appHandlers['App.getPageStack'] = async (ctx) => {
33
- const hash = await evalInSim(ctx, 'location.hash');
34
- const clean = hash.replace(/^#/, '').replace(/\?.*$/, '');
19
+ const route = await readRoute(ctx);
35
20
  const pageStack = [];
36
- if (clean.includes('|')) {
37
- // New format: #appid|page1|page2 — segments after appid are pages
38
- const parts = clean.split('|');
39
- for (let i = 1; i < parts.length; i++) {
40
- pageStack.push({ pageId: i, path: parts[i] ?? '', query: {} });
41
- }
42
- }
43
- else {
44
- // Legacy format: #appid/pagePath — single page
45
- const slashIdx = clean.indexOf('/');
46
- const currentPath = slashIdx >= 0 ? clean.substring(slashIdx + 1) : clean;
47
- if (currentPath) {
48
- pageStack.push({ pageId: 1, path: currentPath, query: {} });
49
- }
21
+ if (!route)
22
+ return { pageStack };
23
+ // Mirrors upstream HashRouter.parseSearch: `[entry]` or `[entry, current]`.
24
+ pageStack.push({ pageId: 1, path: route.entry.pagePath, query: route.entry.query });
25
+ if (route.current.pagePath !== route.entry.pagePath) {
26
+ pageStack.push({ pageId: 2, path: route.current.pagePath, query: route.current.query });
50
27
  }
51
28
  return { pageStack };
52
29
  };
53
30
  appHandlers['App.callWxMethod'] = async (ctx, params) => {
54
31
  const method = params.method;
55
32
  const args = params.args || [];
56
- // For navigation methods, handle specially via hash change + click
33
+ // For navigation methods, handle specially via DOM click / wx.* on the iframe
57
34
  if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].includes(method)) {
58
35
  const opts = args[0];
59
36
  const url = opts?.url;
@@ -61,20 +38,29 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
61
38
  const cleanUrl = url.startsWith('/') ? url : `/${url}`;
62
39
  // Try clicking a matching DOM element first
63
40
  const clicked = await evalInSim(ctx, inIframe(`
64
- const el = _doc.querySelector('[data-path="${cleanUrl}"]')
41
+ const path = ${JSON.stringify(cleanUrl)}
42
+ const el = Array.from(_doc.querySelectorAll('[data-path]'))
43
+ .find(e => e.getAttribute('data-path') === path)
65
44
  if (el) { el.click(); return true }
66
45
  return false
67
46
  `)).catch(() => false);
68
47
  if (!clicked) {
69
- // Fallback: change hash
70
- const hash = await evalInSim(ctx, 'location.hash');
71
- const appId = hash.replace(/^#/, '').split('/')[0];
72
- if (method === 'reLaunch' || method === 'redirectTo') {
73
- await evalInSim(ctx, `location.href = location.pathname + '#${appId}${cleanUrl}'`);
74
- }
75
- else {
76
- await evalInSim(ctx, `location.hash = '#${appId}${cleanUrl}'`);
48
+ // Fallback: drive navigation via wx.* on the rendered page iframe.
49
+ // Upstream's query router doesn't react to URL mutation; the active
50
+ // page's iframe exposes wx.* with the live dimina runtime bindings.
51
+ const apiName = JSON.stringify(method);
52
+ const urlJson = JSON.stringify(cleanUrl);
53
+ await evalInSim(ctx, `(() => {
54
+ const iframes = Array.from(document.querySelectorAll('iframe'))
55
+ for (const f of iframes) {
56
+ try {
57
+ if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
58
+ f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
59
+ return
77
60
  }
61
+ } catch (_) {}
62
+ }
63
+ })()`);
78
64
  }
79
65
  // Wait for navigation
80
66
  await new Promise((r) => setTimeout(r, 2000));
@@ -1,112 +1,100 @@
1
- import { buildElAccess, evalInSim, inIframe } from '../exec.js';
1
+ import { evalInElement } from '../exec.js';
2
2
  import { getElementRef, registerElement } from '../registry.js';
3
3
  export const elementHandlers = {};
4
4
  // -- Element domain --
5
5
  elementHandlers['Element.tap'] = async (ctx, params) => {
6
6
  const ref = getElementRef(params);
7
- await evalInSim(ctx, inIframe(`
8
- ${buildElAccess(ref)}
7
+ await evalInElement(ctx, ref, `
9
8
  if (!el) throw new Error('Element not found')
10
9
  el.click()
11
- `));
10
+ `);
12
11
  return {};
13
12
  };
14
13
  elementHandlers['Element.triggerEvent'] = async (ctx, params) => {
15
14
  const ref = getElementRef(params);
16
15
  const type = params.type;
17
16
  const detail = params.detail || {};
18
- await evalInSim(ctx, inIframe(`
19
- ${buildElAccess(ref)}
17
+ await evalInElement(ctx, ref, `
20
18
  if (!el) throw new Error('Element not found')
21
19
  el.dispatchEvent(new CustomEvent('${type}', { detail: ${JSON.stringify(detail)}, bubbles: true }))
22
- `));
20
+ `);
23
21
  return {};
24
22
  };
25
23
  elementHandlers['Element.touchstart'] = async (ctx, params) => {
26
24
  const ref = getElementRef(params);
27
- await evalInSim(ctx, inIframe(`
28
- ${buildElAccess(ref)}
25
+ await evalInElement(ctx, ref, `
29
26
  if (el) el.dispatchEvent(new TouchEvent('touchstart', { bubbles: true }))
30
- `));
27
+ `);
31
28
  return {};
32
29
  };
33
30
  elementHandlers['Element.touchmove'] = async (ctx, params) => {
34
31
  const ref = getElementRef(params);
35
- await evalInSim(ctx, inIframe(`
36
- ${buildElAccess(ref)}
32
+ await evalInElement(ctx, ref, `
37
33
  if (el) el.dispatchEvent(new TouchEvent('touchmove', { bubbles: true }))
38
- `));
34
+ `);
39
35
  return {};
40
36
  };
41
37
  elementHandlers['Element.touchend'] = async (ctx, params) => {
42
38
  const ref = getElementRef(params);
43
- await evalInSim(ctx, inIframe(`
44
- ${buildElAccess(ref)}
39
+ await evalInElement(ctx, ref, `
45
40
  if (el) el.dispatchEvent(new TouchEvent('touchend', { bubbles: true }))
46
- `));
41
+ `);
47
42
  return {};
48
43
  };
49
44
  elementHandlers['Element.getDOMProperties'] = async (ctx, params) => {
50
45
  const ref = getElementRef(params);
51
46
  const names = params.names;
52
- const properties = await evalInSim(ctx, inIframe(`
53
- ${buildElAccess(ref)}
47
+ const properties = await evalInElement(ctx, ref, `
54
48
  if (!el) return ${JSON.stringify(names.map(() => null))}
55
49
  return ${JSON.stringify(names)}.map(n => el[n])
56
- `));
50
+ `);
57
51
  return { properties };
58
52
  };
59
53
  elementHandlers['Element.getAttributes'] = async (ctx, params) => {
60
54
  const ref = getElementRef(params);
61
55
  const names = params.names;
62
- const attributes = await evalInSim(ctx, inIframe(`
63
- ${buildElAccess(ref)}
56
+ const attributes = await evalInElement(ctx, ref, `
64
57
  if (!el) return ${JSON.stringify(names.map(() => null))}
65
58
  return ${JSON.stringify(names)}.map(n => el.getAttribute(n))
66
- `));
59
+ `);
67
60
  return { attributes };
68
61
  };
69
62
  elementHandlers['Element.getStyles'] = async (ctx, params) => {
70
63
  const ref = getElementRef(params);
71
64
  const names = params.names;
72
- const styles = await evalInSim(ctx, inIframe(`
73
- ${buildElAccess(ref)}
65
+ const styles = await evalInElement(ctx, ref, `
74
66
  if (!el) return ${JSON.stringify(names.map(() => ''))}
75
67
  const cs = getComputedStyle(el)
76
68
  return ${JSON.stringify(names)}.map(n => cs.getPropertyValue(n))
77
- `));
69
+ `);
78
70
  return { styles };
79
71
  };
80
72
  elementHandlers['Element.getWXML'] = async (ctx, params) => {
81
73
  const ref = getElementRef(params);
82
74
  const type = params.type || 'inner';
83
75
  const prop = type === 'outer' ? 'outerHTML' : 'innerHTML';
84
- const wxml = await evalInSim(ctx, inIframe(`
85
- ${buildElAccess(ref)}
76
+ const wxml = await evalInElement(ctx, ref, `
86
77
  return el ? el.${prop} : ''
87
- `));
78
+ `);
88
79
  return { wxml };
89
80
  };
90
81
  elementHandlers['Element.getOffset'] = async (ctx, params) => {
91
82
  const ref = getElementRef(params);
92
- return evalInSim(ctx, inIframe(`
93
- ${buildElAccess(ref)}
83
+ return evalInElement(ctx, ref, `
94
84
  if (!el) return { left: 0, top: 0 }
95
85
  const r = el.getBoundingClientRect()
96
86
  return { left: r.left, top: r.top }
97
- `));
87
+ `);
98
88
  };
99
89
  elementHandlers['Element.getElement'] = async (ctx, params) => {
100
90
  const parentRef = getElementRef(params);
101
91
  const selector = params.selector;
102
- const escaped = selector.replace(/'/g, "\\'");
103
- const info = await evalInSim(ctx, inIframe(`
104
- ${buildElAccess(parentRef, 'parent')}
92
+ const info = await evalInElement(ctx, parentRef, `
105
93
  if (!parent) return null
106
- const child = parent.querySelector('${escaped}')
94
+ const child = parent.querySelector(${JSON.stringify(selector)})
107
95
  if (!child) return null
108
96
  return { tagName: child.tagName.toLowerCase() }
109
- `));
97
+ `, 'parent');
110
98
  if (!info)
111
99
  throw new Error(`Child element not found: ${selector}`);
112
100
  const combinedSelector = `${parentRef.selector} ${selector}`;
@@ -116,14 +104,12 @@ elementHandlers['Element.getElement'] = async (ctx, params) => {
116
104
  elementHandlers['Element.getElements'] = async (ctx, params) => {
117
105
  const parentRef = getElementRef(params);
118
106
  const selector = params.selector;
119
- const escaped = selector.replace(/'/g, "\\'");
120
- const items = await evalInSim(ctx, inIframe(`
121
- ${buildElAccess(parentRef, 'parent')}
107
+ const items = await evalInElement(ctx, parentRef, `
122
108
  if (!parent) return []
123
- return Array.from(parent.querySelectorAll('${escaped}')).map(el => ({
109
+ return Array.from(parent.querySelectorAll(${JSON.stringify(selector)})).map(el => ({
124
110
  tagName: el.tagName.toLowerCase(),
125
111
  }))
126
- `));
112
+ `, 'parent');
127
113
  const combinedSelector = `${parentRef.selector} ${selector}`;
128
114
  return {
129
115
  elements: items.map((item, i) => ({
@@ -9,9 +9,9 @@
9
9
  * This server handles these messages by delegating to the workbench context
10
10
  * (simulator webContents, IPC handlers, etc.) without modifying dimina upstream.
11
11
  */
12
- import { ipcMain } from 'electron';
13
12
  import { WebSocketServer } from 'ws';
14
13
  import { AutomationChannel, SimulatorChannel } from '../../../shared/ipc-channels.js';
14
+ import { IpcRegistry } from '../../utils/ipc-registry.js';
15
15
  import { getSimulator } from './exec.js';
16
16
  import { toolHandlers } from './handlers/tool.js';
17
17
  import { appHandlers } from './handlers/app.js';
@@ -25,7 +25,6 @@ const handlers = {
25
25
  ...elementHandlers,
26
26
  };
27
27
  let currentPort = null;
28
- let portIpcRegistered = false;
29
28
  export function getAutomationPort() {
30
29
  return currentPort;
31
30
  }
@@ -40,10 +39,14 @@ export async function startAutomationServer(ctx, port = 0) {
40
39
  // ws always binds via an http.Server, so address() is AddressInfo here.
41
40
  const resolvedPort = typeof addr === 'object' && addr ? addr.port : port;
42
41
  currentPort = resolvedPort;
43
- if (!portIpcRegistered) {
44
- portIpcRegistered = true;
45
- ipcMain.handle(AutomationChannel.GetPort, () => currentPort);
46
- }
42
+ // Gate AutomationChannel.GetPort with the workbench sender policy (only
43
+ // the main renderer + workbench settings/popover overlays are allowed to
44
+ // read the port — see createWorkbenchSenderPolicy; the simulator webview
45
+ // is intentionally NOT trusted for IPC invokes).
46
+ const portIpc = new IpcRegistry(ctx.senderPolicy);
47
+ portIpc.handle(AutomationChannel.GetPort, () => {
48
+ return currentPort;
49
+ });
47
50
  function broadcast(event) {
48
51
  const msg = JSON.stringify(event);
49
52
  for (const ws of clients) {
@@ -51,19 +54,55 @@ export async function startAutomationServer(ctx, port = 0) {
51
54
  ws.send(msg);
52
55
  }
53
56
  }
54
- // Forward simulator console logs as App.logAdded events
57
+ // Forward simulator console logs as App.logAdded events.
58
+ // Track the polling interval + stop timer + currently-attached sim and
59
+ // the named handler so we can fully detach on close() or sim destruction.
60
+ // Without this the listener accumulates across create/dispose cycles and
61
+ // across simulator rebuilds.
55
62
  let consoleForwardingSetup = false;
63
+ let pollInterval = null;
64
+ let pollStopTimer = null;
65
+ let attachedSim = null;
66
+ let ipcMessageHostHandler = null;
67
+ let simDestroyedHandler = null;
68
+ function detachConsoleForwarding() {
69
+ if (attachedSim) {
70
+ if (ipcMessageHostHandler) {
71
+ try {
72
+ if (!attachedSim.isDestroyed()) {
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ ;
75
+ attachedSim.removeListener('ipc-message-host', ipcMessageHostHandler);
76
+ }
77
+ }
78
+ catch { /* noop */ }
79
+ }
80
+ if (simDestroyedHandler) {
81
+ try {
82
+ if (!attachedSim.isDestroyed()) {
83
+ attachedSim.removeListener('destroyed', simDestroyedHandler);
84
+ }
85
+ }
86
+ catch { /* noop */ }
87
+ }
88
+ }
89
+ attachedSim = null;
90
+ ipcMessageHostHandler = null;
91
+ simDestroyedHandler = null;
92
+ }
56
93
  function setupConsoleForwarding() {
57
94
  if (consoleForwardingSetup)
58
95
  return;
59
96
  consoleForwardingSetup = true;
60
- // Poll for simulator and set up listener
61
- const check = setInterval(() => {
97
+ pollInterval = setInterval(() => {
62
98
  const sim = getSimulator(ctx);
63
99
  if (!sim)
64
100
  return;
65
- clearInterval(check);
66
- sim.on('ipc-message-host', (_event, channel, data) => {
101
+ if (pollInterval) {
102
+ clearInterval(pollInterval);
103
+ pollInterval = null;
104
+ }
105
+ const onIpcMessageHost = (_event, channel, data) => {
67
106
  if (channel === SimulatorChannel.Console) {
68
107
  const logData = data;
69
108
  broadcast({
@@ -71,10 +110,29 @@ export async function startAutomationServer(ctx, port = 0) {
71
110
  params: { type: logData.level || 'log', args: logData.args || [] },
72
111
  });
73
112
  }
74
- });
113
+ };
114
+ const onSimDestroyed = () => {
115
+ // Sim webContents is dead — drop refs (removeListener on a destroyed
116
+ // sender would throw) and allow re-attach to a new simulator.
117
+ attachedSim = null;
118
+ ipcMessageHostHandler = null;
119
+ simDestroyedHandler = null;
120
+ consoleForwardingSetup = false;
121
+ };
122
+ attachedSim = sim;
123
+ ipcMessageHostHandler = onIpcMessageHost;
124
+ simDestroyedHandler = onSimDestroyed;
125
+ sim.on('ipc-message-host', onIpcMessageHost);
126
+ sim.once('destroyed', onSimDestroyed);
75
127
  }, 1000);
76
128
  // Stop polling after 30s
77
- setTimeout(() => clearInterval(check), 30000);
129
+ pollStopTimer = setTimeout(() => {
130
+ if (pollInterval) {
131
+ clearInterval(pollInterval);
132
+ pollInterval = null;
133
+ }
134
+ pollStopTimer = null;
135
+ }, 30000);
78
136
  }
79
137
  wss.on('connection', (ws) => {
80
138
  clients.add(ws);
@@ -109,8 +167,25 @@ export async function startAutomationServer(ctx, port = 0) {
109
167
  });
110
168
  return {
111
169
  close: () => {
170
+ if (pollInterval) {
171
+ clearInterval(pollInterval);
172
+ pollInterval = null;
173
+ }
174
+ if (pollStopTimer) {
175
+ clearTimeout(pollStopTimer);
176
+ pollStopTimer = null;
177
+ }
178
+ detachConsoleForwarding();
179
+ for (const ws of clients) {
180
+ try {
181
+ ws.close();
182
+ }
183
+ catch { /* noop */ }
184
+ }
185
+ clients.clear();
112
186
  if (currentPort === resolvedPort)
113
187
  currentPort = null;
188
+ void portIpc.dispose();
114
189
  wss.close();
115
190
  },
116
191
  port: resolvedPort,
@@ -4,12 +4,12 @@ import { simulatorDir } from '../utils/paths.js';
4
4
  export const defaultAdapter = {
5
5
  async openProject(opts) {
6
6
  const diminaKit = await import('@dimina-kit/devkit');
7
- const openProject = diminaKit.openProject;
8
- return openProject({
7
+ const openProjectOpts = {
9
8
  outputDir: path.join(app.getPath('userData'), 'dimina-fe-output'),
10
9
  ...opts,
11
10
  simulatorDir,
12
- });
11
+ };
12
+ return diminaKit.openProject(openProjectOpts);
13
13
  },
14
14
  };
15
15
  //# sourceMappingURL=default-adapter.js.map
@@ -12,4 +12,5 @@
12
12
  * - `./target-manager.ts` dual-target CDP connection + reconnect
13
13
  */
14
14
  export { startMcpServer } from './server.js';
15
+ export { getMcpStatus, type McpRuntimeStatus } from './status.js';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -12,4 +12,5 @@
12
12
  * - `./target-manager.ts` dual-target CDP connection + reconnect
13
13
  */
14
14
  export { startMcpServer } from './server.js';
15
+ export { getMcpStatus } from './status.js';
15
16
  //# sourceMappingURL=index.js.map
@@ -6,5 +6,6 @@
6
6
  * Tool registration lives in `./tool-registry` and `./tools/*`; this file
7
7
  * only wires up transports and lifecycle.
8
8
  */
9
- export declare function startMcpServer(resolvedCdpPort: number, mcpPort: number): void;
9
+ import { type Disposable } from '../../utils/disposable.js';
10
+ export declare function startMcpServer(resolvedCdpPort: number, mcpPort: number): Disposable;
10
11
  //# sourceMappingURL=server.d.ts.map
@@ -10,7 +10,9 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
10
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
11
11
  import { createServer } from 'http';
12
12
  import { createRequire } from 'node:module';
13
+ import { toDisposable } from '../../utils/disposable.js';
13
14
  import { connectTarget, setCdpPort } from './target-manager.js';
15
+ import { recordMcpFailed, recordMcpStarted, recordMcpStopped } from './status.js';
14
16
  import { registerCommonTargetTools } from './tool-registry.js';
15
17
  import { registerContextTools } from './tools/context-tools.js';
16
18
  import { registerSimulatorTools } from './tools/simulator-tools.js';
@@ -64,13 +66,21 @@ export function startMcpServer(resolvedCdpPort, mcpPort) {
64
66
  httpServer.on('error', (err) => {
65
67
  if (err.code === 'EADDRINUSE') {
66
68
  console.warn(`[MCP] Port ${mcpPort} already in use — MCP server not started`);
69
+ recordMcpFailed('port-in-use');
67
70
  }
68
71
  else {
69
72
  console.error('[MCP] Server error:', err);
73
+ recordMcpFailed(err.message);
70
74
  }
71
75
  });
72
76
  httpServer.listen(mcpPort, '127.0.0.1', () => {
73
77
  console.log(`[MCP] SSE server listening on http://127.0.0.1:${mcpPort}/sse`);
78
+ recordMcpStarted(mcpPort);
79
+ });
80
+ return toDisposable(() => {
81
+ transports.clear();
82
+ httpServer.close();
83
+ recordMcpStopped();
74
84
  });
75
85
  }
76
86
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,10 @@
1
+ export interface McpRuntimeStatus {
2
+ running: boolean;
3
+ port: number | null;
4
+ error: string | null;
5
+ }
6
+ export declare function getMcpStatus(): McpRuntimeStatus;
7
+ export declare function recordMcpStarted(port: number): void;
8
+ export declare function recordMcpFailed(error: string): void;
9
+ export declare function recordMcpStopped(): void;
10
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1,14 @@
1
+ let status = { running: false, port: null, error: null };
2
+ export function getMcpStatus() {
3
+ return { ...status };
4
+ }
5
+ export function recordMcpStarted(port) {
6
+ status = { running: true, port, error: null };
7
+ }
8
+ export function recordMcpFailed(error) {
9
+ status = { running: false, port: null, error };
10
+ }
11
+ export function recordMcpStopped() {
12
+ status = { running: false, port: null, error: null };
13
+ }
14
+ //# sourceMappingURL=status.js.map
@@ -10,10 +10,11 @@
10
10
  * - rolling buffers of console log and network request events
11
11
  */
12
12
  import CDP from 'chrome-remote-interface';
13
+ import { DEFAULT_CDP_PORT } from '../../../shared/constants.js';
13
14
  const SIMULATOR_URL_PATTERN = 'localhost:7788';
14
15
  const MAX_BUFFER = 500;
15
16
  const RECONNECT_INTERVAL_MS = 3000;
16
- let cdpPort = 9222;
17
+ let cdpPort = DEFAULT_CDP_PORT;
17
18
  const targets = {
18
19
  simulator: { client: null, connected: false, timer: null, consoleLogs: [], networkRequests: [] },
19
20
  workbench: { client: null, connected: false, timer: null, consoleLogs: [], networkRequests: [] },
@@ -0,0 +1,18 @@
1
+ import type { Disposable } from '../utils/disposable.js';
2
+ import type { WorkbenchContext } from './workbench-context.js';
3
+ /**
4
+ * A composable unit of workbench functionality.
5
+ *
6
+ * Built-in modules (projects, session, simulator, popover, settings) are
7
+ * implemented as `WorkbenchModule` values; hosts may also inject extra
8
+ * modules via `WorkbenchAppConfig.extraModules`.
9
+ *
10
+ * `setup` is invoked once during workbench bootstrap. The returned
11
+ * Disposable is added to `ctx.registry` so module teardown is symmetric
12
+ * with the rest of the workbench lifecycle.
13
+ */
14
+ export interface WorkbenchModule {
15
+ /** Set up the module against ctx. Returns a Disposable owned by ctx.registry. */
16
+ setup(ctx: WorkbenchContext): Disposable;
17
+ }
18
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=module.js.map
@@ -8,6 +8,8 @@ import type { ProjectSettings } from '../projects/project-repository.js';
8
8
  export interface ProjectStatusPayload {
9
9
  status: string;
10
10
  message: string;
11
+ /** True when the status update is emitted by the file-watcher rebuild loop. */
12
+ hotReload?: boolean;
11
13
  }
12
14
  /**
13
15
  * Payload for the `settings:init` event sent into the embedded settings overlay
@@ -50,8 +52,6 @@ export interface RendererNotifier {
50
52
  popoverClosed(): void;
51
53
  /** Ask the main renderer to relaunch the simulator with a new config. */
52
54
  popoverRelaunch(config: CompileConfig): void;
53
- /** Ask the main renderer to re-initialise the devtools panels. */
54
- workbenchReset(): void;
55
55
  /** Tell the main renderer the toolbar actions list has changed. */
56
56
  toolbarActionsChanged(): void;
57
57
  /** Initialise the currently shown compile popover overlay. */
@@ -67,7 +67,9 @@ export interface RendererNotifier {
67
67
  * between the notifier module and workbench-context.
68
68
  */
69
69
  export interface NotifierContext {
70
- mainWindow: BrowserWindow;
70
+ windows: {
71
+ readonly mainWindow: BrowserWindow;
72
+ };
71
73
  views: {
72
74
  getSettingsWebContents(): WebContents | null;
73
75
  };
@@ -1,4 +1,4 @@
1
- import { ProjectChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchChannel, ToolbarChannel, WorkbenchSettingsChannel, } from '../../../shared/ipc-channels.js';
1
+ import { ProjectChannel, WindowChannel, SettingsChannel, PopoverChannel, ToolbarChannel, WorkbenchSettingsChannel, } from '../../../shared/ipc-channels.js';
2
2
  /** Safely resolve a WebContents, skipping destroyed / missing targets. */
3
3
  function liveWebContents(wc) {
4
4
  if (!wc)
@@ -13,9 +13,9 @@ function liveWebContents(wc) {
13
13
  */
14
14
  export function createRendererNotifier(ctx) {
15
15
  function sendToMain(channel, ...args) {
16
- if (ctx.mainWindow.isDestroyed())
16
+ if (ctx.windows.mainWindow.isDestroyed())
17
17
  return;
18
- const wc = liveWebContents(ctx.mainWindow.webContents);
18
+ const wc = liveWebContents(ctx.windows.mainWindow.webContents);
19
19
  if (!wc)
20
20
  return;
21
21
  wc.send(channel, ...args);
@@ -39,9 +39,6 @@ export function createRendererNotifier(ctx) {
39
39
  popoverRelaunch(config) {
40
40
  sendToMain(PopoverChannel.Relaunch, config);
41
41
  },
42
- workbenchReset() {
43
- sendToMain(WorkbenchChannel.Reset);
44
- },
45
42
  toolbarActionsChanged() {
46
43
  sendToMain(ToolbarChannel.ActionsChanged);
47
44
  },
@@ -0,0 +1,3 @@
1
+ import type { ProjectTemplate } from './types.js';
2
+ export declare const BUILTIN_TEMPLATES: ProjectTemplate[];
3
+ //# sourceMappingURL=builtin-templates.d.ts.map