@dimina-kit/devtools 0.3.2-dev.20260601115343 → 0.3.2-dev.20260610114009

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 (189) hide show
  1. package/README.md +49 -59
  2. package/dist/main/api.d.ts +1 -2
  3. package/dist/main/api.js +5 -1
  4. package/dist/main/app/app.d.ts +15 -5
  5. package/dist/main/app/app.js +249 -122
  6. package/dist/main/app/bootstrap.d.ts +16 -0
  7. package/dist/main/app/bootstrap.js +20 -0
  8. package/dist/main/app/launch.d.ts +8 -4
  9. package/dist/main/app/launch.js +9 -4
  10. package/dist/main/index.bundle.js +6071 -1863
  11. package/dist/main/index.js +9 -1
  12. package/dist/main/ipc/app.d.ts +1 -1
  13. package/dist/main/ipc/bridge-router.d.ts +61 -0
  14. package/dist/main/ipc/bridge-router.js +1333 -0
  15. package/dist/main/ipc/panels.d.ts +1 -1
  16. package/dist/main/ipc/panels.js +6 -2
  17. package/dist/main/ipc/popover.d.ts +1 -1
  18. package/dist/main/ipc/project-fs.d.ts +1 -1
  19. package/dist/main/ipc/projects.d.ts +1 -1
  20. package/dist/main/ipc/session.d.ts +1 -1
  21. package/dist/main/ipc/settings.d.ts +1 -1
  22. package/dist/main/ipc/simulator-module.d.ts +6 -0
  23. package/dist/main/ipc/simulator-module.js +9 -1
  24. package/dist/main/ipc/simulator.d.ts +2 -2
  25. package/dist/main/ipc/simulator.js +29 -5
  26. package/dist/main/ipc/toolbar.d.ts +1 -1
  27. package/dist/main/ipc/views.d.ts +1 -1
  28. package/dist/main/ipc/views.js +40 -2
  29. package/dist/main/runtime/devtools-backend.d.ts +17 -0
  30. package/dist/main/runtime/devtools-backend.js +49 -0
  31. package/dist/main/runtime/miniapp-runtime.d.ts +39 -0
  32. package/dist/main/runtime/miniapp-runtime.js +17 -0
  33. package/dist/main/services/automation/exec.d.ts +27 -2
  34. package/dist/main/services/automation/exec.js +37 -3
  35. package/dist/main/services/automation/handlers/app.js +127 -11
  36. package/dist/main/services/automation/handlers/page.js +36 -19
  37. package/dist/main/services/automation/index.js +40 -13
  38. package/dist/main/services/automation/wait-active-page.d.ts +56 -0
  39. package/dist/main/services/automation/wait-active-page.js +71 -0
  40. package/dist/main/services/console-forward/index.d.ts +36 -0
  41. package/dist/main/services/console-forward/index.js +81 -0
  42. package/dist/main/services/dimina-resource-server.d.ts +7 -0
  43. package/dist/main/services/dimina-resource-server.js +99 -0
  44. package/dist/main/services/elements-forward/index.d.ts +66 -0
  45. package/dist/main/services/elements-forward/index.js +583 -0
  46. package/dist/main/services/layout/index.d.ts +22 -0
  47. package/dist/main/services/layout/index.js +25 -0
  48. package/dist/main/services/mcp/index.d.ts +1 -0
  49. package/dist/main/services/mcp/index.js +3 -0
  50. package/dist/main/services/mcp/server.d.ts +1 -1
  51. package/dist/main/services/mcp/server.js +1 -1
  52. package/dist/main/services/mcp/target-manager.d.ts +35 -0
  53. package/dist/main/services/mcp/target-manager.js +63 -1
  54. package/dist/main/services/mcp/tools/context-tools.js +15 -1
  55. package/dist/main/services/module.d.ts +1 -1
  56. package/dist/main/services/network-forward/index.d.ts +190 -0
  57. package/dist/main/services/network-forward/index.js +765 -0
  58. package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
  59. package/dist/main/services/notifications/renderer-notifier.js +7 -1
  60. package/dist/main/services/render-inspect/index.d.ts +41 -0
  61. package/dist/main/services/render-inspect/index.js +88 -0
  62. package/dist/main/services/safe-area/index.d.ts +17 -0
  63. package/dist/main/services/safe-area/index.js +59 -0
  64. package/dist/main/services/service-host-pool/pool.d.ts +197 -0
  65. package/dist/main/services/service-host-pool/pool.js +364 -0
  66. package/dist/main/services/simulator/custom-apis.d.ts +34 -0
  67. package/dist/main/services/simulator/custom-apis.js +22 -0
  68. package/dist/main/services/simulator-appdata/index.d.ts +35 -0
  69. package/dist/main/services/simulator-appdata/index.js +71 -0
  70. package/dist/main/services/simulator-current-page/index.d.ts +24 -0
  71. package/dist/main/services/simulator-current-page/index.js +19 -0
  72. package/dist/main/services/simulator-storage/index.d.ts +37 -2
  73. package/dist/main/services/simulator-storage/index.js +209 -32
  74. package/dist/main/services/simulator-storage/service-storage-ops.d.ts +40 -0
  75. package/dist/main/services/simulator-storage/service-storage-ops.js +67 -0
  76. package/dist/main/services/simulator-temp-files/index.d.ts +1 -1
  77. package/dist/main/services/simulator-temp-files/index.js +39 -17
  78. package/dist/main/services/simulator-wxml/index.d.ts +29 -0
  79. package/dist/main/services/simulator-wxml/index.js +28 -0
  80. package/dist/main/services/toolbar/toolbar-store.d.ts +3 -3
  81. package/dist/main/services/toolbar/toolbar-store.js +1 -18
  82. package/dist/main/services/views/devtools-tabs.d.ts +47 -0
  83. package/dist/main/services/views/devtools-tabs.js +95 -0
  84. package/dist/main/services/views/miniapp-partition.d.ts +80 -0
  85. package/dist/main/services/views/miniapp-partition.js +138 -0
  86. package/dist/main/services/views/simulator-session-policy.d.ts +28 -0
  87. package/dist/main/services/views/simulator-session-policy.js +82 -0
  88. package/dist/main/services/views/view-manager.d.ts +125 -0
  89. package/dist/main/services/views/view-manager.js +852 -4
  90. package/dist/main/services/workbench-context.d.ts +67 -7
  91. package/dist/main/services/workbench-context.js +6 -1
  92. package/dist/main/services/workspace/workspace-service.js +7 -13
  93. package/dist/main/utils/ipc-registry.d.ts +1 -1
  94. package/dist/main/utils/ipc-registry.js +21 -4
  95. package/dist/main/utils/paths.d.ts +15 -0
  96. package/dist/main/utils/paths.js +19 -0
  97. package/dist/main/utils/sender-policy.d.ts +9 -3
  98. package/dist/main/utils/sender-policy.js +18 -4
  99. package/dist/main/utils/theme.d.ts +1 -1
  100. package/dist/main/utils/theme.js +1 -1
  101. package/dist/main/windows/main-window/create.d.ts +0 -6
  102. package/dist/main/windows/main-window/create.js +6 -80
  103. package/dist/main/windows/main-window/events.d.ts +1 -1
  104. package/dist/main/windows/main-window/events.js +1 -1
  105. package/dist/main/windows/service-host-window/create.d.ts +67 -0
  106. package/dist/main/windows/service-host-window/create.js +138 -0
  107. package/dist/native-host/common/common.js +368 -0
  108. package/dist/native-host/container/pageFrame.css +1 -0
  109. package/dist/native-host/render/render.js +9334 -0
  110. package/dist/native-host/service/service.js +6 -0
  111. package/dist/preload/index.d.ts +2 -0
  112. package/dist/preload/index.js +6 -0
  113. package/dist/preload/instrumentation/app-data.d.ts +18 -20
  114. package/dist/preload/instrumentation/app-data.js +23 -160
  115. package/dist/preload/instrumentation/wxml-extract.d.ts +1 -1
  116. package/dist/preload/instrumentation/wxml-extract.js +1 -1
  117. package/dist/preload/runtime/bridge.d.ts +3 -8
  118. package/dist/preload/runtime/bridge.js +2 -32
  119. package/dist/preload/runtime/custom-apis.js +20 -17
  120. package/dist/preload/runtime/host-toolbar-advertiser.d.ts +27 -0
  121. package/dist/preload/runtime/host-toolbar-advertiser.js +55 -0
  122. package/dist/preload/runtime/main-api-runner.d.ts +9 -0
  123. package/dist/preload/runtime/main-api-runner.js +85 -0
  124. package/dist/preload/runtime/native-host.d.ts +43 -0
  125. package/dist/preload/runtime/native-host.js +83 -0
  126. package/dist/preload/shared/expose.d.ts +10 -5
  127. package/dist/preload/shared/expose.js +30 -12
  128. package/dist/preload/shared/sid-registry.d.ts +11 -0
  129. package/dist/preload/shared/sid-registry.js +38 -0
  130. package/dist/preload/windows/host-toolbar.cjs +161 -0
  131. package/dist/preload/windows/host-toolbar.cjs.map +7 -0
  132. package/dist/preload/windows/host-toolbar.d.ts +2 -0
  133. package/dist/preload/windows/host-toolbar.js +12 -0
  134. package/dist/preload/windows/main.cjs +1935 -0
  135. package/dist/preload/windows/main.cjs.map +4 -4
  136. package/dist/preload/windows/simulator.cjs +925 -0
  137. package/dist/preload/windows/simulator.cjs.map +7 -0
  138. package/dist/preload/windows/simulator.js +244 -574
  139. package/dist/render-host/pageFrame.html +29 -0
  140. package/dist/render-host/preload.cjs +154 -0
  141. package/dist/render-host/render-inspect.js +295 -0
  142. package/dist/renderer/assets/index-CcJKzatF.js +50 -0
  143. package/dist/renderer/assets/{input-CCYS8Ave.js → input-QGx6FFX1.js} +2 -2
  144. package/dist/renderer/assets/ipc-transport-BT4QYQqu.css +1 -0
  145. package/dist/renderer/assets/{ipc-transport-Bn4KXvhu.js → ipc-transport-DvZvwK-Z.js} +2 -2
  146. package/dist/renderer/assets/{popover-CH4rwpdt.js → popover-BOH1If1J.js} +2 -2
  147. package/dist/renderer/assets/select-C7JQ8G5N.js +2 -0
  148. package/dist/renderer/assets/{settings-CMTB-t0x.js → settings-DpAbTJyF.js} +2 -2
  149. package/dist/renderer/assets/{settings-api-Csdngf9Z.js → settings-api-RD2IZF53.js} +2 -2
  150. package/dist/renderer/assets/{workbenchSettings-CODFCzOg.js → workbenchSettings-CNJX4Qna.js} +2 -2
  151. package/dist/renderer/entries/main/index.html +5 -5
  152. package/dist/renderer/entries/popover/index.html +5 -5
  153. package/dist/renderer/entries/settings/index.html +5 -5
  154. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  155. package/dist/service-host/preload.cjs +254 -0
  156. package/dist/service-host/service.html +10 -0
  157. package/dist/service-host/sourcemap-rewrite.cjs +58 -0
  158. package/dist/service-host/sync-api-patch.js +20 -0
  159. package/dist/service-host/sync-impls/menu-button-geometry.js +8 -0
  160. package/dist/service-host/sync-impls/menu-button.js +16 -0
  161. package/dist/service-host/sync-impls/storage.js +50 -0
  162. package/dist/service-host/sync-impls/system-info.js +54 -0
  163. package/dist/shared/appdata-accumulator.d.ts +95 -0
  164. package/dist/shared/appdata-accumulator.js +213 -0
  165. package/dist/shared/bridge-channels.d.ts +378 -0
  166. package/dist/shared/bridge-channels.js +81 -0
  167. package/dist/shared/ipc-channels.d.ts +84 -1
  168. package/dist/shared/ipc-channels.js +90 -10
  169. package/dist/shared/ipc-schemas.d.ts +59 -2
  170. package/dist/shared/ipc-schemas.js +64 -3
  171. package/dist/shared/open-in-editor.d.ts +71 -0
  172. package/dist/shared/open-in-editor.js +138 -0
  173. package/dist/shared/simulator-api-metadata.d.ts +21 -0
  174. package/dist/shared/simulator-api-metadata.js +23 -0
  175. package/dist/shared/types.d.ts +2 -39
  176. package/dist/simulator/assets/device-shell-DsdcNEkt.css +1 -0
  177. package/dist/simulator/assets/device-shell-fH2cI-3q.js +2 -0
  178. package/dist/simulator/assets/jsx-runtime-P6ZegaOf.js +2 -0
  179. package/dist/simulator/assets/simulator-DHylZf9Z.js +10 -0
  180. package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +2 -0
  181. package/dist/simulator/device-shell/menu-button-geometry.js +8 -0
  182. package/dist/simulator/simulator.html +2 -112
  183. package/package.json +12 -12
  184. package/dist/main/utils/disposable.d.ts +0 -18
  185. package/dist/main/utils/disposable.js +0 -67
  186. package/dist/renderer/assets/index-BDMxXUfn.js +0 -50
  187. package/dist/renderer/assets/ipc-transport-Cu7tubyg.css +0 -1
  188. package/dist/renderer/assets/select-Ct4-sijm.js +0 -2
  189. package/dist/simulator/assets/simulator-B4e3c6_O.js +0 -9
@@ -1,12 +1,41 @@
1
- import { evalInSim, getSimulator, inIframe } from '../exec.js';
2
- import { parseLocationRoute } from '../../../../shared/simulator-route.js';
1
+ import { evalInSim, getActivePageWc, getSimulator } from '../exec.js';
2
+ import { decodePageSpec, parseLocationRoute } from '../../../../shared/simulator-route.js';
3
+ import { waitForActivePage } from '../wait-active-page.js';
3
4
  export const appHandlers = {};
5
+ /** Nav methods that take a `{ url }` arg (navigateBack is handled separately). */
6
+ const NAV_METHODS = new Set(['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']);
4
7
  // -- App domain --
5
8
  async function readRoute(ctx) {
6
9
  const { search } = await evalInSim(ctx, `({ search: location.search })`);
7
10
  return parseLocationRoute(search);
8
11
  }
12
+ /**
13
+ * Native-host: derive the visible page's `{ pagePath, query }` from the active
14
+ * render guest. The page-stack depth isn't surfaced by the bridge handle, so the
15
+ * native page stack is reported as the single active page (see App.getPageStack).
16
+ * Reads the guest's own `location.search` — the render-host preload encodes
17
+ * `pagePath` (+ per-page query) there.
18
+ */
19
+ async function readNativeActivePage(ctx) {
20
+ const renderWc = getActivePageWc(ctx);
21
+ if (!renderWc)
22
+ return null;
23
+ const search = await renderWc
24
+ .executeJavaScript('location.search')
25
+ .catch(() => '');
26
+ const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
27
+ const pagePath = params.get('pagePath') || '';
28
+ if (!pagePath)
29
+ return null;
30
+ // The guest URL carries the full page spec on `pagePath` (may include `?k=v`).
31
+ const spec = decodePageSpec(pagePath);
32
+ return { pagePath: spec.pagePath, query: spec.query };
33
+ }
9
34
  appHandlers['App.getCurrentPage'] = async (ctx) => {
35
+ if (ctx.bridge?.isNativeHost()) {
36
+ const page = await readNativeActivePage(ctx);
37
+ return { pageId: 1, path: page?.pagePath ?? '', query: page?.query ?? {} };
38
+ }
10
39
  const route = await readRoute(ctx);
11
40
  const iframeCount = await evalInSim(ctx, `document.querySelectorAll('iframe').length`);
12
41
  return {
@@ -16,8 +45,28 @@ appHandlers['App.getCurrentPage'] = async (ctx) => {
16
45
  };
17
46
  };
18
47
  appHandlers['App.getPageStack'] = async (ctx) => {
19
- const route = await readRoute(ctx);
20
48
  const pageStack = [];
49
+ if (ctx.bridge?.isNativeHost()) {
50
+ // DeviceShell reports its full ordered stack (bottom→top) via PAGE_STACK;
51
+ // the bridge stores it. Report that. Before the first signal (or in a mock
52
+ // without the accessor) fall back to the single visible page.
53
+ const stack = ctx.bridge.getPageStack?.();
54
+ if (stack && stack.length > 0) {
55
+ stack.forEach((entry, i) => {
56
+ pageStack.push({
57
+ pageId: i + 1,
58
+ path: entry.pagePath,
59
+ query: Object.fromEntries(Object.entries(entry.query ?? {}).map(([k, v]) => [k, String(v)])),
60
+ });
61
+ });
62
+ return { pageStack };
63
+ }
64
+ const page = await readNativeActivePage(ctx);
65
+ if (page)
66
+ pageStack.push({ pageId: 1, path: page.pagePath, query: page.query });
67
+ return { pageStack };
68
+ }
69
+ const route = await readRoute(ctx);
21
70
  if (!route)
22
71
  return { pageStack };
23
72
  // Mirrors upstream HashRouter.parseSearch: `[entry]` or `[entry, current]`.
@@ -30,24 +79,88 @@ appHandlers['App.getPageStack'] = async (ctx) => {
30
79
  appHandlers['App.callWxMethod'] = async (ctx, params) => {
31
80
  const method = params.method;
32
81
  const args = params.args || [];
82
+ // NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
83
+ // window (the simulator / render-guest context has no `wx`). Run EVERY method
84
+ // there — nav so the page stack goes through the real runtime path, and
85
+ // non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
86
+ // they don't fall through to the default-arch eval-in-simulator path below,
87
+ // which throws "wx is not defined" under native-host.
88
+ if (ctx.bridge?.isNativeHost()) {
89
+ const serviceWc = ctx.bridge.getServiceWc();
90
+ if (!serviceWc)
91
+ throw new Error('Service host not connected');
92
+ if (NAV_METHODS.has(method) || method === 'navigateBack') {
93
+ const arg = method === 'navigateBack'
94
+ ? (args[0] ?? { delta: 1 })
95
+ : (args[0] ?? {});
96
+ // Capture the active page BEFORE navigating so we can wait for it to
97
+ // actually change, instead of blindly sleeping a fixed duration.
98
+ const since = ctx.bridge.getActiveBridgeId();
99
+ await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
100
+ // Wait for the bridge's `activePage` signal (the new page mounted), with a
101
+ // timeout floor = the old blind-wait duration. Resolves as soon as the new
102
+ // page is active — far faster than the fixed sleep for the common case —
103
+ // and still bounded for edges (e.g. switchTab back to an already-mounted
104
+ // tab keeps the same bridgeId, so it falls through to the floor, same as
105
+ // the old behaviour). See wait-active-page.ts.
106
+ const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
107
+ await waitForActivePage(ctx.bridge, {
108
+ since,
109
+ timeoutMs,
110
+ onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
111
+ });
112
+ return { result: undefined };
113
+ }
114
+ // Non-nav: invoke on the service-host wx and return its (sync) value. Async
115
+ // methods that report via a success callback return undefined here, same as
116
+ // the default-arch generic path below.
117
+ const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
118
+ const result = await serviceWc.executeJavaScript(`
119
+ new Promise((resolve, reject) => {
120
+ try {
121
+ if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
122
+ reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
123
+ return
124
+ }
125
+ resolve(wx.${method}(${argsStr}))
126
+ } catch (e) { reject(e && e.message ? e.message : String(e)) }
127
+ })
128
+ `);
129
+ return { result };
130
+ }
33
131
  // For navigation methods, handle specially via DOM click / wx.* on the iframe
34
132
  if (['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].includes(method)) {
35
133
  const opts = args[0];
36
134
  const url = opts?.url;
37
135
  if (url) {
38
136
  const cleanUrl = url.startsWith('/') ? url : `/${url}`;
39
- // Try clicking a matching DOM element first
40
- const clicked = await evalInSim(ctx, inIframe(`
137
+ // Try clicking a matching DOM element first. We can't use `inIframe`
138
+ // here tabBar mini-apps keep prior tabs' iframes around (display:none)
139
+ // and `iframes[length-1]` ends up being the hidden, freshly-cached tab
140
+ // rather than the visible one. Pick the visible iframe explicitly.
141
+ const clicked = await evalInSim(ctx, `(() => {
142
+ const iframes = Array.from(document.querySelectorAll('iframe'))
143
+ const visible = iframes.filter((f) => {
144
+ const cs = window.getComputedStyle(f)
145
+ if (cs.display === 'none' || cs.visibility === 'hidden') return false
146
+ const rect = f.getBoundingClientRect()
147
+ return rect.width > 0 && rect.height > 0
148
+ })
149
+ const target = visible[visible.length - 1] || iframes[iframes.length - 1]
150
+ if (!target || !target.contentDocument) return false
41
151
  const path = ${JSON.stringify(cleanUrl)}
42
- const el = Array.from(_doc.querySelectorAll('[data-path]'))
43
- .find(e => e.getAttribute('data-path') === path)
152
+ const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
153
+ .find((e) => e.getAttribute('data-path') === path)
44
154
  if (el) { el.click(); return true }
45
155
  return false
46
- `)).catch(() => false);
156
+ })()`).catch(() => false);
47
157
  if (!clicked) {
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.
158
+ // Fallback cascade for navigation methods:
159
+ // 1. iframe wx[method] page-iframe surface (older arch, where
160
+ // jdimina installed `window.wx` on each page frame).
161
+ // 2. top-window wx[method] — set up by simulator/main.tsx, binds
162
+ // MiniApp.switchTab / navigateTo / etc. Required for switchTab
163
+ // since page-iframe wx (jdimina) doesn't expose it.
51
164
  const apiName = JSON.stringify(method);
52
165
  const urlJson = JSON.stringify(cleanUrl);
53
166
  await evalInSim(ctx, `(() => {
@@ -60,6 +173,9 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
60
173
  }
61
174
  } catch (_) {}
62
175
  }
176
+ if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
177
+ wx[${apiName}]({ url: ${urlJson} })
178
+ }
63
179
  })()`);
64
180
  }
65
181
  // Wait for navigation
@@ -1,16 +1,35 @@
1
- import { evalInSim, inIframe } from '../exec.js';
1
+ import { evalInActivePage, evalInSim } from '../exec.js';
2
2
  import { registerElement } from '../registry.js';
3
3
  export const pageHandlers = {};
4
+ /**
5
+ * Resolve a dotted/indexed path (`a.b`, `a[0].b`) against an appdata object,
6
+ * or return the whole object when no path is given. Shared by both Page.getData
7
+ * branches so the traversal stays identical.
8
+ */
9
+ function resolveAppDataPath(appdata, pathKey) {
10
+ if (!pathKey)
11
+ return appdata;
12
+ const keys = pathKey.replace(/\[(\d+)\]/g, '.$1').split('.');
13
+ let val = appdata;
14
+ for (const k of keys) {
15
+ if (val == null || typeof val !== 'object') {
16
+ val = undefined;
17
+ break;
18
+ }
19
+ val = val[k];
20
+ }
21
+ return val;
22
+ }
4
23
  // -- Page domain --
5
24
  pageHandlers['Page.getElement'] = async (ctx, params) => {
6
25
  const selector = params.selector;
7
26
  const pageId = params.pageId || 1;
8
27
  const escaped = selector.replace(/'/g, "\\'");
9
- const info = await evalInSim(ctx, inIframe(`
28
+ const info = await evalInActivePage(ctx, `
10
29
  const el = _doc.querySelector('${escaped}')
11
30
  if (!el) return null
12
31
  return { tagName: el.tagName.toLowerCase() }
13
- `));
32
+ `);
14
33
  if (!info)
15
34
  throw new Error(`Element not found: ${selector}`);
16
35
  const elementId = registerElement(selector, 0, pageId);
@@ -20,11 +39,11 @@ pageHandlers['Page.getElements'] = async (ctx, params) => {
20
39
  const selector = params.selector;
21
40
  const pageId = params.pageId || 1;
22
41
  const escaped = selector.replace(/'/g, "\\'");
23
- const items = await evalInSim(ctx, inIframe(`
42
+ const items = await evalInActivePage(ctx, `
24
43
  return Array.from(_doc.querySelectorAll('${escaped}')).map(el => ({
25
44
  tagName: el.tagName.toLowerCase(),
26
45
  }))
27
- `));
46
+ `);
28
47
  return {
29
48
  elements: items.map((item, i) => ({
30
49
  elementId: registerElement(selector, i, pageId),
@@ -34,19 +53,17 @@ pageHandlers['Page.getElements'] = async (ctx, params) => {
34
53
  };
35
54
  pageHandlers['Page.getData'] = async (ctx, params) => {
36
55
  const pathKey = params.path;
37
- const appdata = await evalInSim(ctx, `window.__simulatorData ? window.__simulatorData.getAppdata() : {}`);
38
- if (!pathKey)
39
- return { data: appdata };
40
- const keys = pathKey.replace(/\[(\d+)\]/g, '.$1').split('.');
41
- let val = appdata;
42
- for (const k of keys) {
43
- if (val == null || typeof val !== 'object') {
44
- val = undefined;
45
- break;
46
- }
47
- val = val[k];
56
+ // NATIVE-HOST: under native-host the page AppData lives in the service window's
57
+ // Worker-less runtime (tracked centrally via ctx.appData), NOT in the render
58
+ // guest's DOM. Source the active bridge's reactive state from the central
59
+ // accumulator, then run the SAME path traversal as the default branch.
60
+ if (ctx.bridge?.isNativeHost()) {
61
+ const bridgeId = ctx.bridge.getActiveBridgeId();
62
+ const appdata = bridgeId ? (ctx.appData?.getPageData(bridgeId) ?? {}) : {};
63
+ return { data: resolveAppDataPath(appdata, pathKey) };
48
64
  }
49
- return { data: val };
65
+ const appdata = await evalInSim(ctx, `window.__simulatorData ? window.__simulatorData.getAppdata() : {}`);
66
+ return { data: resolveAppDataPath(appdata, pathKey) };
50
67
  };
51
68
  pageHandlers['Page.setData'] = async () => {
52
69
  throw new Error('Page.setData is not supported (requires AppService layer)');
@@ -56,11 +73,11 @@ pageHandlers['Page.callMethod'] = async (_ctx, params) => {
56
73
  };
57
74
  pageHandlers['Page.getWindowProperties'] = async (ctx, params) => {
58
75
  const names = params.names;
59
- const properties = await evalInSim(ctx, inIframe(`
76
+ const properties = await evalInActivePage(ctx, `
60
77
  return ${JSON.stringify(names)}.map(n => {
61
78
  try { return eval(n) } catch { return undefined }
62
79
  })
63
- `));
80
+ `);
64
81
  return { properties };
65
82
  };
66
83
  //# sourceMappingURL=page.js.map
@@ -54,6 +54,23 @@ export async function startAutomationServer(ctx, port = 0) {
54
54
  ws.send(msg);
55
55
  }
56
56
  }
57
+ // Native-host console forwarding: under native-host the page/service console
58
+ // doesn't flow through the simulator guest's `ipc-message-host` channel (there
59
+ // is no Worker/MiniApp in the guest). Instead the render-host / service-host
60
+ // preloads post each console entry to main, bridge-router routes it to the
61
+ // always-on ConsoleForwarder, and we SUBSCRIBE to rebroadcast every entry
62
+ // (both layers) as an `App.logAdded` event — same shape the default-arch path
63
+ // emits below in `setupConsoleForwarding`. We no longer set `ctx.guestConsole`
64
+ // ourselves (the forwarder owns it); subscribing means render + service both
65
+ // reach automation while the forwarder also mirrors render into the service
66
+ // host's console for the embedded DevTools. Unsubscribed on close().
67
+ const consoleSub = ctx.consoleForwarder?.subscribe((entry) => {
68
+ const e = (entry ?? {});
69
+ broadcast({
70
+ method: 'App.logAdded',
71
+ params: { type: e.level || 'log', args: e.args || [] },
72
+ });
73
+ });
57
74
  // Forward simulator console logs as App.logAdded events.
58
75
  // Track the polling interval + stop timer + currently-attached sim and
59
76
  // the named handler so we can fully detach on close() or sim destruction.
@@ -64,7 +81,11 @@ export async function startAutomationServer(ctx, port = 0) {
64
81
  let pollStopTimer = null;
65
82
  let attachedSim = null;
66
83
  let ipcMessageHostHandler = null;
67
- let simDestroyedHandler = null;
84
+ // Handle for the destroyed-teardown owned by the sim's connection (replaces a
85
+ // bespoke `sim.once('destroyed')` + manual removeListener). Disposing it on
86
+ // graceful close()/detach releases the disposer from the connection segment so
87
+ // it can't accumulate across automation create/dispose cycles on the same sim.
88
+ let simDestroyedDisposer = null;
68
89
  function detachConsoleForwarding() {
69
90
  if (attachedSim) {
70
91
  if (ipcMessageHostHandler) {
@@ -77,18 +98,16 @@ export async function startAutomationServer(ctx, port = 0) {
77
98
  }
78
99
  catch { /* noop */ }
79
100
  }
80
- if (simDestroyedHandler) {
81
- try {
82
- if (!attachedSim.isDestroyed()) {
83
- attachedSim.removeListener('destroyed', simDestroyedHandler);
84
- }
85
- }
86
- catch { /* noop */ }
87
- }
88
101
  }
102
+ // Release the connection-owned destroyed disposer (idempotent; safe even if
103
+ // the sim is already destroyed — the disposer body only nulls local refs).
104
+ try {
105
+ simDestroyedDisposer?.dispose();
106
+ }
107
+ catch { /* noop */ }
89
108
  attachedSim = null;
90
109
  ipcMessageHostHandler = null;
91
- simDestroyedHandler = null;
110
+ simDestroyedDisposer = null;
92
111
  }
93
112
  function setupConsoleForwarding() {
94
113
  if (consoleForwardingSetup)
@@ -116,14 +135,17 @@ export async function startAutomationServer(ctx, port = 0) {
116
135
  // sender would throw) and allow re-attach to a new simulator.
117
136
  attachedSim = null;
118
137
  ipcMessageHostHandler = null;
119
- simDestroyedHandler = null;
138
+ simDestroyedDisposer = null;
120
139
  consoleForwardingSetup = false;
121
140
  };
122
141
  attachedSim = sim;
123
142
  ipcMessageHostHandler = onIpcMessageHost;
124
- simDestroyedHandler = onSimDestroyed;
125
143
  sim.on('ipc-message-host', onIpcMessageHost);
126
- sim.once('destroyed', onSimDestroyed);
144
+ // Route the destroyed-teardown through the per-webContents connection
145
+ // registry: the connection arms a single `wc.once('destroyed')` and runs
146
+ // owned disposers on hard-destroy (or soft reset for pool reuse), so we no
147
+ // longer manage a bespoke 'destroyed' listener here.
148
+ simDestroyedDisposer = ctx.connections.acquire(sim).own(onSimDestroyed);
127
149
  }, 1000);
128
150
  // Stop polling after 30s
129
151
  pollStopTimer = setTimeout(() => {
@@ -176,6 +198,11 @@ export async function startAutomationServer(ctx, port = 0) {
176
198
  pollStopTimer = null;
177
199
  }
178
200
  detachConsoleForwarding();
201
+ // Unsubscribe from the always-on ConsoleForwarder so a late guest console
202
+ // entry can't broadcast against a torn-down server. The forwarder keeps
203
+ // owning `ctx.guestConsole` (render→service mirroring continues without an
204
+ // automation client). No-op if the forwarder was absent.
205
+ consoleSub?.dispose();
179
206
  for (const ws of clients) {
180
207
  try {
181
208
  ws.close();
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Cross-process readiness primitive (shared abstraction).
3
+ *
4
+ * Replaces the blind `setTimeout(1500/2000)` navigation waits in automation
5
+ * handlers with "wait for the bridge's `activePage` signal, with a timeout
6
+ * floor". The bridge (`BridgeRouterHandle`) already fan-outs an `activePage`
7
+ * render event whenever the DeviceShell reports a new top-of-stack page, so we
8
+ * wait for that real signal instead of guessing how long a navigation takes.
9
+ *
10
+ * Design contract:
11
+ * - Subscribe to `bridge.onRenderEvent` and resolve on the first matching
12
+ * `activePage` event. "Matching" = `opts.match(bridgeId, pagePath)` when a
13
+ * predicate is supplied, otherwise `bridgeId !== opts.since` (i.e. the
14
+ * active page actually changed away from where navigation started).
15
+ * - Timeout floor: resolve (NEVER reject, never hang) at `opts.timeoutMs` so a
16
+ * caller awaiting this can always make progress even if the signal is lost.
17
+ * - Race close: navigation can land before we subscribe, so read
18
+ * `getActiveBridgeId()` once right after subscribing; when no `match`
19
+ * predicate is given and it is already `!== since`, resolve immediately.
20
+ * (With a `match` predicate we cannot decide from the bridgeId alone — there
21
+ * is no pagePath in the snapshot — so we wait for an event.)
22
+ * - Resolve exactly once; later events are inert and the subscription is torn
23
+ * down on resolve.
24
+ */
25
+ /** The render-event shape `waitForActivePage` reads (subset of `RenderEvent`). */
26
+ export interface ActivePageEventLike {
27
+ kind: 'domReady' | 'activePage';
28
+ appId: string;
29
+ bridgeId: string;
30
+ pagePath?: string;
31
+ }
32
+ /** Minimal bridge surface this helper is allowed to touch. */
33
+ export interface WaitActivePageBridge {
34
+ onRenderEvent(listener: (event: ActivePageEventLike) => void): () => void;
35
+ getActiveBridgeId(): string | null;
36
+ }
37
+ export interface WaitActivePageOptions {
38
+ /** The active bridgeId before navigation started (or null if unknown). */
39
+ since: string | null;
40
+ /** Timeout floor in ms — resolve unconditionally once this elapses. */
41
+ timeoutMs: number;
42
+ /**
43
+ * Optional predicate deciding which `activePage` event ends the wait. When
44
+ * omitted, any `activePage` whose `bridgeId !== since` ends it.
45
+ */
46
+ match?: (bridgeId: string, pagePath?: string) => boolean;
47
+ /**
48
+ * Fired exactly once if the wait resolves via the timeout floor rather than a
49
+ * matching signal — i.e. the expected `activePage` was never observed. Lets
50
+ * the caller surface "signal not seen, proceeding on timeout" instead of
51
+ * silently masking a lost signal. Never fired on a signal/race-close resolve.
52
+ */
53
+ onTimeout?: () => void;
54
+ }
55
+ export declare function waitForActivePage(bridge: WaitActivePageBridge, opts: WaitActivePageOptions): Promise<void>;
56
+ //# sourceMappingURL=wait-active-page.d.ts.map
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cross-process readiness primitive (shared abstraction).
3
+ *
4
+ * Replaces the blind `setTimeout(1500/2000)` navigation waits in automation
5
+ * handlers with "wait for the bridge's `activePage` signal, with a timeout
6
+ * floor". The bridge (`BridgeRouterHandle`) already fan-outs an `activePage`
7
+ * render event whenever the DeviceShell reports a new top-of-stack page, so we
8
+ * wait for that real signal instead of guessing how long a navigation takes.
9
+ *
10
+ * Design contract:
11
+ * - Subscribe to `bridge.onRenderEvent` and resolve on the first matching
12
+ * `activePage` event. "Matching" = `opts.match(bridgeId, pagePath)` when a
13
+ * predicate is supplied, otherwise `bridgeId !== opts.since` (i.e. the
14
+ * active page actually changed away from where navigation started).
15
+ * - Timeout floor: resolve (NEVER reject, never hang) at `opts.timeoutMs` so a
16
+ * caller awaiting this can always make progress even if the signal is lost.
17
+ * - Race close: navigation can land before we subscribe, so read
18
+ * `getActiveBridgeId()` once right after subscribing; when no `match`
19
+ * predicate is given and it is already `!== since`, resolve immediately.
20
+ * (With a `match` predicate we cannot decide from the bridgeId alone — there
21
+ * is no pagePath in the snapshot — so we wait for an event.)
22
+ * - Resolve exactly once; later events are inert and the subscription is torn
23
+ * down on resolve.
24
+ */
25
+ export function waitForActivePage(bridge, opts) {
26
+ const { since, timeoutMs, match, onTimeout } = opts;
27
+ return new Promise((resolve) => {
28
+ let isSettled = false;
29
+ let unsubscribe = null;
30
+ let timer = null;
31
+ const finish = () => {
32
+ if (isSettled)
33
+ return;
34
+ isSettled = true;
35
+ if (timer !== null) {
36
+ clearTimeout(timer);
37
+ timer = null;
38
+ }
39
+ if (unsubscribe) {
40
+ unsubscribe();
41
+ unsubscribe = null;
42
+ }
43
+ resolve();
44
+ };
45
+ const matches = (bridgeId, pagePath) => match ? match(bridgeId, pagePath) : bridgeId !== since;
46
+ unsubscribe = bridge.onRenderEvent((event) => {
47
+ if (isSettled)
48
+ return;
49
+ if (event.kind !== 'activePage')
50
+ return;
51
+ if (matches(event.bridgeId, event.pagePath))
52
+ finish();
53
+ });
54
+ // Timeout floor — resolve (never reject) so awaiters never hang. Fire the
55
+ // optional onTimeout hook first so a lost signal is visible, not masked.
56
+ timer = setTimeout(() => {
57
+ if (isSettled)
58
+ return;
59
+ onTimeout?.();
60
+ finish();
61
+ }, timeoutMs);
62
+ // Race close: the page may already have advanced before we subscribed.
63
+ // Only decidable without a pagePath when no `match` predicate is supplied.
64
+ if (!match) {
65
+ const current = bridge.getActiveBridgeId();
66
+ if (current !== null && current !== since)
67
+ finish();
68
+ }
69
+ });
70
+ }
71
+ //# sourceMappingURL=wait-active-page.js.map
@@ -0,0 +1,36 @@
1
+ import { type Disposable } from '@dimina-kit/electron-deck/main';
2
+ import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
3
+ /**
4
+ * One console entry posted by a guest preload. Shape mirrors
5
+ * render-host/preload.cjs + service-host/preload.cjs `emitConsoleLog`:
6
+ * - `source` which layer it came from ('render' | 'service'); ONLY 'render'
7
+ * is forwarded into the service host (see loop-safety above).
8
+ * - `bridgeId` present for render entries; identifies the owning page so we
9
+ * can target the matching app's service host (multi-app safe).
10
+ * - `level` console method name ('log' | 'warn' | 'error' | 'info' | 'debug').
11
+ * - `args` already-`safeSerialize`d arguments (structured-cloneable).
12
+ */
13
+ export interface GuestConsoleEntry {
14
+ source?: string;
15
+ bridgeId?: string;
16
+ level?: string;
17
+ args?: unknown[];
18
+ ts?: number;
19
+ }
20
+ /** A consumer of every guest console entry (render AND service). */
21
+ export type ConsoleSink = (entry: GuestConsoleEntry) => void;
22
+ export interface ConsoleForwarder extends Disposable {
23
+ /**
24
+ * The single sink bridge-router invokes (wired onto `ctx.guestConsole.emit`).
25
+ * Fans every entry out to subscribers, then runs the render→service forward.
26
+ */
27
+ emit(entry: unknown): void;
28
+ /**
29
+ * Register an external sink (e.g. automation WS broadcast). Returns a
30
+ * Disposable that unregisters it. Sinks see EVERY entry (both layers) — the
31
+ * render→service forward is internal and not exposed as a sink.
32
+ */
33
+ subscribe(sink: ConsoleSink): Disposable;
34
+ }
35
+ export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'>): ConsoleForwarder;
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,81 @@
1
+ import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
2
+ /** Console levels we re-emit into the service host. Anything else maps to 'log'. */
3
+ const FORWARDABLE_LEVELS = new Set(['log', 'warn', 'error', 'info', 'debug']);
4
+ /**
5
+ * Build the `executeJavaScript` source that re-emits a render entry inside the
6
+ * service host. Args are carried as a JSON string and parsed service-side, so no
7
+ * guest-controlled value is ever interpolated into executable JS — the only
8
+ * thing that reaches the parser is a JSON literal. The `[视图]` prefix lets the
9
+ * DevTools console filter isolate the render layer.
10
+ */
11
+ function buildForwardScript(level, args) {
12
+ const method = FORWARDABLE_LEVELS.has(level) ? level : 'log';
13
+ // JSON.stringify('…') yields a valid JS string literal; embedding it and
14
+ // re-parsing keeps args as data, never code. JSON.stringify can throw on a
15
+ // value that slipped past safeSerialize (e.g. a BigInt) — caller guards.
16
+ const argsJson = JSON.stringify(args ?? []);
17
+ // `console[method]` (not `console.log`) preserves the original level so the
18
+ // DevTools severity filter still works on the forwarded line.
19
+ return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()`;
20
+ }
21
+ export function createConsoleForwarder(bridge) {
22
+ const sinks = new Set();
23
+ const registry = new DisposableRegistry();
24
+ /**
25
+ * Render→service forward. Resolves the service host for the entry's owning
26
+ * page (multi-app safe), falling back to the active app's service host when
27
+ * the bridgeId is unknown — that's the host the embedded DevTools inspects.
28
+ * No-op on a destroyed/missing host so we never write to a torn-down wc (pool
29
+ * reuse / session swap can swap the host out under us).
30
+ */
31
+ function forwardRenderToServiceHost(entry) {
32
+ let wc = null;
33
+ if (entry.bridgeId && bridge.getServiceWcForBridge) {
34
+ wc = bridge.getServiceWcForBridge(entry.bridgeId);
35
+ }
36
+ if (!wc)
37
+ wc = bridge.getServiceWc();
38
+ if (!wc || wc.isDestroyed())
39
+ return;
40
+ let script;
41
+ try {
42
+ script = buildForwardScript(entry.level ?? 'log', entry.args ?? []);
43
+ }
44
+ catch {
45
+ // Args that escaped safeSerialize and aren't JSON-stringifiable — drop
46
+ // rather than break the forward. Console capture must never throw.
47
+ return;
48
+ }
49
+ // executeJavaScript rejects if the host navigates/teardowns mid-call; the
50
+ // injected body is itself try/caught. Swallow either — best-effort mirror.
51
+ wc.executeJavaScript(script, true).catch(() => { });
52
+ }
53
+ return {
54
+ // The single sink bridge-router calls (`ctx.guestConsole.emit`). Fans out
55
+ // to external subscribers (both layers) then runs the render→service forward
56
+ // (render only — see loop-safety invariant in the module header).
57
+ emit(raw) {
58
+ const entry = (raw ?? {});
59
+ for (const sink of sinks) {
60
+ try {
61
+ sink(entry);
62
+ }
63
+ catch { /* a sink must never break the others */ }
64
+ }
65
+ // Forward ONLY the render layer. Service entries are already shown
66
+ // natively in the attached DevTools, and forwarding them would loop
67
+ // (see header).
68
+ if (entry.source === 'render')
69
+ forwardRenderToServiceHost(entry);
70
+ },
71
+ subscribe(sink) {
72
+ sinks.add(sink);
73
+ return registry.add(toDisposable(() => { sinks.delete(sink); }));
74
+ },
75
+ dispose() {
76
+ sinks.clear();
77
+ return registry.disposeAll();
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ export interface DiminaResourceServer {
2
+ baseUrl: string;
3
+ close(): Promise<void>;
4
+ }
5
+ export declare function startDiminaResourceServer(rootDir: string): Promise<DiminaResourceServer>;
6
+ export declare function toSourceUrl(filePath: string): string;
7
+ //# sourceMappingURL=dimina-resource-server.d.ts.map