@dimina-kit/devtools 0.4.0-dev.20260702182435 → 0.4.0-dev.20260703101348
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.
- package/dist/main/app/app.js +5 -25
- package/dist/main/app/native-overview.d.ts +27 -0
- package/dist/main/app/native-overview.js +44 -0
- package/dist/main/index.bundle.js +1217 -735
- package/dist/main/ipc/bridge-router.d.ts +8 -0
- package/dist/main/ipc/bridge-router.js +526 -173
- package/dist/main/ipc/project-fs.js +39 -21
- package/dist/main/services/automation/handlers/app.js +143 -109
- package/dist/main/services/console-forward/index.d.ts +12 -1
- package/dist/main/services/console-forward/index.js +99 -1
- package/dist/main/services/diagnostics/index.d.ts +49 -0
- package/dist/main/services/diagnostics/index.js +61 -0
- package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
- package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
- package/dist/main/services/network-forward/dispatch-batch.js +21 -0
- package/dist/main/services/network-forward/index.js +37 -20
- package/dist/main/services/notifications/renderer-notifier.d.ts +39 -0
- package/dist/main/services/notifications/renderer-notifier.js +4 -1
- package/dist/main/services/projects/create-project-service.js +54 -34
- package/dist/main/services/projects/project-repository.js +2 -1
- package/dist/main/services/projects/types.d.ts +1 -32
- package/dist/main/services/service-console/console-api.js +36 -27
- package/dist/main/services/simulator-temp-files/disk.js +109 -86
- package/dist/main/services/views/simulator-session-policy.js +17 -0
- package/dist/main/services/workbench-coi-server.js +58 -45
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/services/workspace/workspace-service.js +12 -3
- package/dist/main/utils/logger.d.ts +5 -12
- package/dist/main/utils/logger.js +5 -45
- package/dist/main/windows/service-host-window/create.d.ts +15 -1
- package/dist/main/windows/service-host-window/create.js +11 -3
- package/dist/preload/instrumentation/wxml-extract.js +94 -62
- package/dist/preload/runtime/main-api-runner.d.ts +1 -1
- package/dist/preload/shared/api-compat.js +18 -55
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs +284 -565
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +154 -83
- package/dist/preload/windows/simulator.cjs.map +3 -3
- package/dist/preload/windows/simulator.js +154 -83
- package/dist/render-host/render-inspect.js +63 -52
- package/dist/renderer/assets/index-BwzPwCmM.js +49 -0
- package/dist/renderer/assets/{input-Cjk0wSEh.js → input-CTb_le7F.js} +2 -2
- package/dist/renderer/assets/ipc-transport-BPWIV5hA.css +1 -0
- package/dist/renderer/assets/{ipc-transport-OZS-KmOW.js → ipc-transport-Ck5Xa8Tu.js} +2 -2
- package/dist/renderer/assets/popover-B1Zu2UQu.js +2 -0
- package/dist/renderer/assets/{select-BQDXi5ih.js → select-ozSk5Pt6.js} +2 -2
- package/dist/renderer/assets/{settings-tP3Px2KR.js → settings-CNwGOI22.js} +2 -2
- package/dist/renderer/assets/settings-api-DMbMp1Eq.js +2 -0
- package/dist/renderer/assets/workbenchSettings-Cr4EHo5w.js +8 -0
- package/dist/renderer/entries/main/index.html +6 -6
- package/dist/renderer/entries/popover/index.html +5 -5
- package/dist/renderer/entries/settings/index.html +5 -5
- package/dist/renderer/entries/workbench-settings/index.html +4 -4
- package/dist/shared/appdata-accumulator.js +53 -48
- package/dist/shared/bridge-channels.d.ts +22 -0
- package/dist/shared/ipc-channels.d.ts +3 -0
- package/dist/shared/ipc-channels.js +9 -0
- package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
- package/dist/shared/open-in-editor-resource-path.js +133 -0
- package/dist/shared/open-in-editor.d.ts +2 -9
- package/dist/shared/open-in-editor.js +8 -93
- package/dist/shared/request-core.d.ts +84 -0
- package/dist/shared/request-core.js +168 -0
- package/dist/shared/simulator-api-metadata.d.ts +22 -17
- package/dist/shared/simulator-api-metadata.js +36 -0
- package/dist/shared/types.d.ts +20 -4
- package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-C-wcFq3Z.js} +2 -2
- package/dist/simulator/assets/{jsx-runtime-CDK-o-S0.js → jsx-runtime-BDTY6fEq.js} +2 -2
- package/dist/simulator/assets/simulator-DU3-fS3v.js +10 -0
- package/dist/simulator/assets/simulator-mini-app-DzfMfnVM.js +2 -0
- package/dist/simulator/simulator.html +2 -2
- package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
- package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
- package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
- package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
- package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +6 -6
- package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
- package/dist/renderer/assets/ipc-transport-D5dKIti1.css +0 -1
- package/dist/renderer/assets/popover-CQVvOe90.js +0 -2
- package/dist/renderer/assets/settings-api-B6x2mhaD.js +0 -2
- package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
- package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
- package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
- package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
|
@@ -468,6 +468,40 @@ function writeFileSync(root, p, content) {
|
|
|
468
468
|
nodeFs.closeSync(fd);
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Reads one directory's entries. Returns `null` (caller skips the directory)
|
|
473
|
+
* for `EACCES`/`ENOENT`/`ENOTDIR` — a directory can disappear or become
|
|
474
|
+
* unreadable between being queued and being walked. Any other error is
|
|
475
|
+
* rethrown.
|
|
476
|
+
*/
|
|
477
|
+
async function readDirEntriesSafe(dir) {
|
|
478
|
+
try {
|
|
479
|
+
return await fs.readdir(dir, { withFileTypes: true });
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const code = err.code;
|
|
483
|
+
if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
|
|
484
|
+
return null;
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Classifies one readdir entry: a non-`SKIP_DIRS` directory is queued for
|
|
490
|
+
* walking; a plain file is recorded as a `safeRoot`-relative POSIX path.
|
|
491
|
+
* Skipped directories and non-file/non-directory entries (symlinks, sockets,
|
|
492
|
+
* etc.) are a no-op.
|
|
493
|
+
*/
|
|
494
|
+
function collectListEntry(entry, dir, safeRoot, out, queue) {
|
|
495
|
+
const full = path.join(dir, entry.name);
|
|
496
|
+
if (entry.isDirectory()) {
|
|
497
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
498
|
+
queue.push(full);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (entry.isFile()) {
|
|
502
|
+
out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
471
505
|
async function listFiles(root, rootArg) {
|
|
472
506
|
const safeRoot = await resolveWithinProjectRoot(rootArg, root);
|
|
473
507
|
const out = [];
|
|
@@ -476,29 +510,13 @@ async function listFiles(root, rootArg) {
|
|
|
476
510
|
if (out.length >= LIST_FILE_LIMIT)
|
|
477
511
|
break;
|
|
478
512
|
const dir = queue.shift();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
catch (err) {
|
|
484
|
-
const code = err.code;
|
|
485
|
-
if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
|
|
486
|
-
continue;
|
|
487
|
-
throw err;
|
|
488
|
-
}
|
|
489
|
-
for (const e of entries) {
|
|
513
|
+
const entries = await readDirEntriesSafe(dir);
|
|
514
|
+
if (entries === null)
|
|
515
|
+
continue;
|
|
516
|
+
for (const entry of entries) {
|
|
490
517
|
if (out.length >= LIST_FILE_LIMIT)
|
|
491
518
|
break;
|
|
492
|
-
|
|
493
|
-
if (e.isDirectory()) {
|
|
494
|
-
if (SKIP_DIRS.has(e.name))
|
|
495
|
-
continue;
|
|
496
|
-
queue.push(full);
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (!e.isFile())
|
|
500
|
-
continue;
|
|
501
|
-
out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
|
|
519
|
+
collectListEntry(entry, dir, safeRoot, out, queue);
|
|
502
520
|
}
|
|
503
521
|
}
|
|
504
522
|
return out;
|
|
@@ -76,122 +76,141 @@ appHandlers['App.getPageStack'] = async (ctx) => {
|
|
|
76
76
|
}
|
|
77
77
|
return { pageStack };
|
|
78
78
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
/** Nav (incl. navigateBack) on the native-host service-host `wx`, waiting for the page to actually change. */
|
|
80
|
+
async function runNativeHostNav(bridge, serviceWc, method, args) {
|
|
81
|
+
const arg = method === 'navigateBack'
|
|
82
|
+
? (args[0] ?? { delta: 1 })
|
|
83
|
+
: (args[0] ?? {});
|
|
84
|
+
// Capture the active page BEFORE navigating so we can wait for it to
|
|
85
|
+
// actually change, instead of blindly sleeping a fixed duration.
|
|
86
|
+
const since = bridge.getActiveBridgeId();
|
|
87
|
+
await serviceWc.executeJavaScript(`wx.${method}(${JSON.stringify(arg)})`);
|
|
88
|
+
// Wait for the bridge's `activePage` signal (the new page mounted), with a
|
|
89
|
+
// timeout floor = the old blind-wait duration. Resolves as soon as the new
|
|
90
|
+
// page is active — far faster than the fixed sleep for the common case —
|
|
91
|
+
// and still bounded for edges (e.g. switchTab back to an already-mounted
|
|
92
|
+
// tab keeps the same bridgeId, so it falls through to the floor, same as
|
|
93
|
+
// the old behaviour). See wait-active-page.ts.
|
|
94
|
+
const timeoutMs = method === 'navigateBack' ? 1500 : 2000;
|
|
95
|
+
await waitForActivePage(bridge, {
|
|
96
|
+
since,
|
|
97
|
+
timeoutMs,
|
|
98
|
+
onTimeout: () => console.warn(`[automation] ${method}: activePage signal not seen within ${timeoutMs}ms — proceeding on timeout floor`),
|
|
99
|
+
});
|
|
100
|
+
return { result: undefined };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Non-nav `wx.*` on the native-host service-host, returning its (sync) value.
|
|
104
|
+
* Async methods that report via a success callback return undefined here,
|
|
105
|
+
* same as the default-arch generic path (`runSimulatorGeneric`).
|
|
106
|
+
*/
|
|
107
|
+
async function runNativeHostGeneric(serviceWc, method, args) {
|
|
108
|
+
const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
|
|
109
|
+
const result = await serviceWc.executeJavaScript(`
|
|
110
|
+
new Promise((resolve, reject) => {
|
|
111
|
+
try {
|
|
112
|
+
if (typeof wx === 'undefined' || typeof wx[${JSON.stringify(method)}] !== 'function') {
|
|
113
|
+
reject(new Error('wx.' + ${JSON.stringify(method)} + ' is not a function in service host'))
|
|
114
|
+
return
|
|
113
115
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
resolve(wx.${method}(${argsStr}))
|
|
117
|
+
} catch (e) { reject(e && e.message ? e.message : String(e)) }
|
|
118
|
+
})
|
|
119
|
+
`);
|
|
120
|
+
return { result };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* NATIVE-HOST: the authoritative `wx.*` runs in the hidden service-host
|
|
124
|
+
* window (the simulator / render-guest context has no `wx`). Run EVERY method
|
|
125
|
+
* there — nav so the page stack goes through the real runtime path, and
|
|
126
|
+
* non-nav (setNavigationBarTitle / getSystemInfoSync / tabBar APIs / …) so
|
|
127
|
+
* they don't fall through to the default-arch eval-in-simulator path, which
|
|
128
|
+
* throws "wx is not defined" under native-host.
|
|
129
|
+
*/
|
|
130
|
+
async function runNativeHostWxMethod(bridge, method, args) {
|
|
131
|
+
const serviceWc = bridge.getServiceWc();
|
|
132
|
+
if (!serviceWc)
|
|
133
|
+
throw new Error('Service host not connected');
|
|
134
|
+
if (NAV_METHODS.has(method) || method === 'navigateBack') {
|
|
135
|
+
return runNativeHostNav(bridge, serviceWc, method, args);
|
|
136
|
+
}
|
|
137
|
+
return runNativeHostGeneric(serviceWc, method, args);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Try clicking a matching DOM element for a nav target. We can't use
|
|
141
|
+
* `inIframe` here — tabBar mini-apps keep prior tabs' iframes around
|
|
142
|
+
* (display:none) and `iframes[length-1]` ends up being the hidden,
|
|
143
|
+
* freshly-cached tab rather than the visible one. Pick the visible iframe
|
|
144
|
+
* explicitly.
|
|
145
|
+
*/
|
|
146
|
+
async function clickNavTarget(ctx, cleanUrl) {
|
|
147
|
+
return evalInSim(ctx, `(() => {
|
|
148
|
+
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
149
|
+
const visible = iframes.filter((f) => {
|
|
150
|
+
const cs = window.getComputedStyle(f)
|
|
151
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') return false
|
|
152
|
+
const rect = f.getBoundingClientRect()
|
|
153
|
+
return rect.width > 0 && rect.height > 0
|
|
154
|
+
})
|
|
155
|
+
const target = visible[visible.length - 1] || iframes[iframes.length - 1]
|
|
156
|
+
if (!target || !target.contentDocument) return false
|
|
157
|
+
const path = ${JSON.stringify(cleanUrl)}
|
|
158
|
+
const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
|
|
159
|
+
.find((e) => e.getAttribute('data-path') === path)
|
|
160
|
+
if (el) { el.click(); return true }
|
|
161
|
+
return false
|
|
162
|
+
})()`).catch(() => false);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Fallback cascade for navigation methods when no matching DOM element was
|
|
166
|
+
* found to click:
|
|
167
|
+
* 1. iframe wx[method] — page-iframe surface (older arch, where jdimina
|
|
168
|
+
* installed `window.wx` on each page frame).
|
|
169
|
+
* 2. top-window wx[method] — set up by simulator/main.tsx, binds
|
|
170
|
+
* MiniApp.switchTab / navigateTo / etc. Required for switchTab since
|
|
171
|
+
* page-iframe wx (jdimina) doesn't expose it.
|
|
172
|
+
*/
|
|
173
|
+
async function invokeNavViaWx(ctx, method, cleanUrl) {
|
|
174
|
+
const apiName = JSON.stringify(method);
|
|
175
|
+
const urlJson = JSON.stringify(cleanUrl);
|
|
176
|
+
await evalInSim(ctx, `(() => {
|
|
177
|
+
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
178
|
+
for (const f of iframes) {
|
|
120
179
|
try {
|
|
121
|
-
if (
|
|
122
|
-
|
|
180
|
+
if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
|
|
181
|
+
f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
|
|
123
182
|
return
|
|
124
183
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
151
|
-
const path = ${JSON.stringify(cleanUrl)}
|
|
152
|
-
const el = Array.from(target.contentDocument.querySelectorAll('[data-path]'))
|
|
153
|
-
.find((e) => e.getAttribute('data-path') === path)
|
|
154
|
-
if (el) { el.click(); return true }
|
|
155
|
-
return false
|
|
156
|
-
})()`).catch(() => false);
|
|
157
|
-
if (!clicked) {
|
|
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.
|
|
164
|
-
const apiName = JSON.stringify(method);
|
|
165
|
-
const urlJson = JSON.stringify(cleanUrl);
|
|
166
|
-
await evalInSim(ctx, `(() => {
|
|
167
|
-
const iframes = Array.from(document.querySelectorAll('iframe'))
|
|
168
|
-
for (const f of iframes) {
|
|
169
|
-
try {
|
|
170
|
-
if (f.contentWindow && f.contentWindow.wx && typeof f.contentWindow.wx[${apiName}] === 'function') {
|
|
171
|
-
f.contentWindow.wx[${apiName}]({ url: ${urlJson} })
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
} catch (_) {}
|
|
175
|
-
}
|
|
176
|
-
if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
|
|
177
|
-
wx[${apiName}]({ url: ${urlJson} })
|
|
178
|
-
}
|
|
179
|
-
})()`);
|
|
180
|
-
}
|
|
181
|
-
// Wait for navigation
|
|
182
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
183
|
-
}
|
|
184
|
-
return { result: undefined };
|
|
185
|
-
}
|
|
186
|
-
// For navigateBack
|
|
187
|
-
if (method === 'navigateBack') {
|
|
188
|
-
await evalInSim(ctx, 'history.back()');
|
|
189
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
184
|
+
} catch (_) {}
|
|
185
|
+
}
|
|
186
|
+
if (typeof wx !== 'undefined' && wx && typeof wx[${apiName}] === 'function') {
|
|
187
|
+
wx[${apiName}]({ url: ${urlJson} })
|
|
188
|
+
}
|
|
189
|
+
})()`);
|
|
190
|
+
}
|
|
191
|
+
/** Default-arch nav: handled via DOM click / wx.* on the iframe. */
|
|
192
|
+
async function runSimulatorNav(ctx, method, args) {
|
|
193
|
+
const opts = args[0];
|
|
194
|
+
const url = opts?.url;
|
|
195
|
+
if (!url)
|
|
190
196
|
return { result: undefined };
|
|
197
|
+
const cleanUrl = url.startsWith('/') ? url : `/${url}`;
|
|
198
|
+
const clicked = await clickNavTarget(ctx, cleanUrl);
|
|
199
|
+
if (!clicked) {
|
|
200
|
+
await invokeNavViaWx(ctx, method, cleanUrl);
|
|
191
201
|
}
|
|
192
|
-
//
|
|
202
|
+
// Wait for navigation
|
|
203
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
204
|
+
return { result: undefined };
|
|
205
|
+
}
|
|
206
|
+
async function runSimulatorNavigateBack(ctx) {
|
|
207
|
+
await evalInSim(ctx, 'history.back()');
|
|
208
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
209
|
+
return { result: undefined };
|
|
210
|
+
}
|
|
211
|
+
/** General wx method call — async wx methods with success/fail callbacks. */
|
|
212
|
+
async function runSimulatorGeneric(ctx, method, args) {
|
|
193
213
|
const argsStr = args.map((a) => JSON.stringify(a)).join(', ');
|
|
194
|
-
// Async wx methods with success/fail callbacks
|
|
195
214
|
const result = await evalInSim(ctx, `
|
|
196
215
|
new Promise((resolve, reject) => {
|
|
197
216
|
try {
|
|
@@ -203,6 +222,21 @@ appHandlers['App.callWxMethod'] = async (ctx, params) => {
|
|
|
203
222
|
})
|
|
204
223
|
`);
|
|
205
224
|
return { result };
|
|
225
|
+
}
|
|
226
|
+
appHandlers['App.callWxMethod'] = async (ctx, params) => {
|
|
227
|
+
const method = params.method;
|
|
228
|
+
const args = params.args || [];
|
|
229
|
+
const bridge = ctx.bridge;
|
|
230
|
+
if (bridge?.isNativeHost()) {
|
|
231
|
+
return runNativeHostWxMethod(bridge, method, args);
|
|
232
|
+
}
|
|
233
|
+
if (NAV_METHODS.has(method)) {
|
|
234
|
+
return runSimulatorNav(ctx, method, args);
|
|
235
|
+
}
|
|
236
|
+
if (method === 'navigateBack') {
|
|
237
|
+
return runSimulatorNavigateBack(ctx);
|
|
238
|
+
}
|
|
239
|
+
return runSimulatorGeneric(ctx, method, args);
|
|
206
240
|
};
|
|
207
241
|
appHandlers['App.callFunction'] = async (ctx, params) => {
|
|
208
242
|
const fnDecl = params.functionDeclaration;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Disposable } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
|
|
3
|
+
import type { DiagnosticsBus } from '../diagnostics/index.js';
|
|
3
4
|
/**
|
|
4
5
|
* One console entry posted by a guest preload. Shape mirrors
|
|
5
6
|
* render-host/preload.cjs + service-host/preload.cjs `emitConsoleLog`:
|
|
@@ -31,6 +32,16 @@ export interface ConsoleForwarder extends Disposable {
|
|
|
31
32
|
* render→service forward is internal and not exposed as a sink.
|
|
32
33
|
*/
|
|
33
34
|
subscribe(sink: ConsoleSink): Disposable;
|
|
35
|
+
/**
|
|
36
|
+
* Flush any diagnostic queued for `appSessionId` (plus the global,
|
|
37
|
+
* session-less queue) into that session's now-ready service-host wc.
|
|
38
|
+
* Idempotent: entries are removed from their bucket as they flush, so a
|
|
39
|
+
* repeat call for the same session injects nothing further. Call once the
|
|
40
|
+
* session's service-host webContents has actually navigated/loaded (e.g.
|
|
41
|
+
* from `bootServiceHost`, after `did-finish-load`) — calling it earlier just
|
|
42
|
+
* finds nothing resolvable and leaves the queue intact for a later call.
|
|
43
|
+
*/
|
|
44
|
+
notifyServiceHostReady(appSessionId: string): void;
|
|
34
45
|
}
|
|
35
|
-
export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'
|
|
46
|
+
export declare function createConsoleForwarder(bridge: Pick<BridgeRouterHandle, 'getServiceWc' | 'getServiceWcForBridge'>, diagnostics?: DiagnosticsBus): ConsoleForwarder;
|
|
36
47
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -25,9 +25,81 @@ function buildForwardScript(level, args) {
|
|
|
25
25
|
// `consoleAPICalled` whose top frame URL === RENDER_FORWARD_SOURCE_URL.
|
|
26
26
|
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});console[${JSON.stringify(method)}]('[视图]',...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/** `Diagnostic.severity` → the literal `console.<method>` call to emit (dot form, not computed — kept greppable/CDP-attributable like a hand-written call site). */
|
|
29
|
+
const DIAGNOSTIC_CONSOLE_CALL = {
|
|
30
|
+
error: 'console.error',
|
|
31
|
+
warn: 'console.warn',
|
|
32
|
+
info: 'console.info',
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build the `executeJavaScript` source that injects one diagnostic into the
|
|
36
|
+
* service host's console, prefixed `[dimina-kit]`. Args ride as a JSON string
|
|
37
|
+
* (same anti-injection shape as `buildForwardScript`) and the sourceURL is the
|
|
38
|
+
* SAME sentinel the render mirror uses, so this line is exempt from
|
|
39
|
+
* service-console's loop-safety skip too.
|
|
40
|
+
*/
|
|
41
|
+
function buildDiagnosticScript(severity, message) {
|
|
42
|
+
const call = DIAGNOSTIC_CONSOLE_CALL[severity];
|
|
43
|
+
const argsJson = JSON.stringify([`[dimina-kit] ${message}`]);
|
|
44
|
+
return `(()=>{try{const a=JSON.parse(${JSON.stringify(argsJson)});${call}(...a)}catch(_){}})()\n//# sourceURL=${RENDER_FORWARD_SOURCE_URL}`;
|
|
45
|
+
}
|
|
46
|
+
export function createConsoleForwarder(bridge, diagnostics) {
|
|
29
47
|
const sinks = new Set();
|
|
30
48
|
const registry = new DisposableRegistry();
|
|
49
|
+
// Diagnostics queued because no live service-host wc could be resolved yet,
|
|
50
|
+
// bucketed by the owning appSessionId; diagnostics with no appSessionId land
|
|
51
|
+
// in `pendingGlobal` instead. `notifyServiceHostReady` drains both into the
|
|
52
|
+
// session that just became ready.
|
|
53
|
+
const pendingBySession = new Map();
|
|
54
|
+
const pendingGlobal = [];
|
|
55
|
+
// Sessions whose service host has finished loading service.html (the caller
|
|
56
|
+
// signals this via `notifyServiceHostReady`). A resolvable wc alone is NOT
|
|
57
|
+
// readiness: the window exists (and is bound) before its spawn navigation
|
|
58
|
+
// lands, and anything injected into that pre-load document is wiped by the
|
|
59
|
+
// navigation — the diagnostic would silently vanish from the Console panel.
|
|
60
|
+
const readySessions = new Set();
|
|
61
|
+
function injectDiagnostic(wc, d) {
|
|
62
|
+
wc.executeJavaScript(buildDiagnosticScript(d.severity, d.message), true).catch(() => { });
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Inject a diagnostic into its owning session's service-host console, or
|
|
66
|
+
* queue it until that host is READY (not merely constructed — see
|
|
67
|
+
* `readySessions`). Ownership is strict: a session-owned diagnostic NEVER
|
|
68
|
+
* falls back to the currently-active host — while `handleSpawn` is still
|
|
69
|
+
* running, the new session's host doesn't exist yet and the "active" host
|
|
70
|
+
* is the OUTGOING session's window, so a fallback would land the message in
|
|
71
|
+
* a console about to be destroyed instead of the session it explains. Only
|
|
72
|
+
* session-less diagnostics may use the active host. Never throws — a
|
|
73
|
+
* missing/destroyed/not-yet-ready host is a normal state, not an error.
|
|
74
|
+
*/
|
|
75
|
+
function handleDiagnostic(d) {
|
|
76
|
+
if (d.appSessionId) {
|
|
77
|
+
const wc = readySessions.has(d.appSessionId) && bridge.getServiceWcForBridge
|
|
78
|
+
? bridge.getServiceWcForBridge(d.appSessionId)
|
|
79
|
+
: null;
|
|
80
|
+
if (wc && !wc.isDestroyed()) {
|
|
81
|
+
injectDiagnostic(wc, d);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const bucket = pendingBySession.get(d.appSessionId);
|
|
85
|
+
if (bucket)
|
|
86
|
+
bucket.push(d);
|
|
87
|
+
else
|
|
88
|
+
pendingBySession.set(d.appSessionId, [d]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const wc = bridge.getServiceWc();
|
|
92
|
+
if (wc && !wc.isDestroyed()) {
|
|
93
|
+
injectDiagnostic(wc, d);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
pendingGlobal.push(d);
|
|
97
|
+
}
|
|
98
|
+
if (diagnostics) {
|
|
99
|
+
// replay:true so diagnostics reported before this forwarder existed (early
|
|
100
|
+
// boot, buffered on the bus) are queued/injected here too.
|
|
101
|
+
registry.add(diagnostics.subscribe(handleDiagnostic, { replay: true }));
|
|
102
|
+
}
|
|
31
103
|
/**
|
|
32
104
|
* Render→service forward. Resolves the service host for the entry's owning
|
|
33
105
|
* page (multi-app safe), falling back to the active app's service host when
|
|
@@ -79,8 +151,34 @@ export function createConsoleForwarder(bridge) {
|
|
|
79
151
|
sinks.add(sink);
|
|
80
152
|
return registry.add(toDisposable(() => { sinks.delete(sink); }));
|
|
81
153
|
},
|
|
154
|
+
notifyServiceHostReady(appSessionId) {
|
|
155
|
+
// Readiness is sticky per session: from here on, session-owned
|
|
156
|
+
// diagnostics inject directly instead of queueing.
|
|
157
|
+
readySessions.add(appSessionId);
|
|
158
|
+
// Re-resolve fresh rather than trusting whatever `handleDiagnostic` saw at
|
|
159
|
+
// report time — the caller invokes this exactly when it knows the wc just
|
|
160
|
+
// became live, so that resolution wins even if an earlier snapshot was
|
|
161
|
+
// destroyed/missing.
|
|
162
|
+
const wc = bridge.getServiceWcForBridge ? bridge.getServiceWcForBridge(appSessionId) : null;
|
|
163
|
+
if (!wc)
|
|
164
|
+
return;
|
|
165
|
+
const sessionEntries = pendingBySession.get(appSessionId);
|
|
166
|
+
if (sessionEntries) {
|
|
167
|
+
for (const d of sessionEntries)
|
|
168
|
+
injectDiagnostic(wc, d);
|
|
169
|
+
pendingBySession.delete(appSessionId);
|
|
170
|
+
}
|
|
171
|
+
if (pendingGlobal.length) {
|
|
172
|
+
for (const d of pendingGlobal)
|
|
173
|
+
injectDiagnostic(wc, d);
|
|
174
|
+
pendingGlobal.length = 0;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
82
177
|
dispose() {
|
|
83
178
|
sinks.clear();
|
|
179
|
+
pendingBySession.clear();
|
|
180
|
+
pendingGlobal.length = 0;
|
|
181
|
+
readySessions.clear();
|
|
84
182
|
return registry.disposeAll();
|
|
85
183
|
},
|
|
86
184
|
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostics bus: the single authoritative channel for developer-facing
|
|
3
|
+
* diagnostics synthesized by main (e.g. "page not found", "logic bundle
|
|
4
|
+
* unreachable") that do not originate from a real `console.*` call the
|
|
5
|
+
* embedded service-host DevTools' CDP capture could observe on its own.
|
|
6
|
+
*
|
|
7
|
+
* Main-synthesized diagnostics used to reach only `ctx.guestConsole.emit`,
|
|
8
|
+
* which fans out to automation subscribers but — for `source:'service'`
|
|
9
|
+
* entries — is never injected into the service host's own console (see
|
|
10
|
+
* `console-forward`'s render-only forward invariant), so they never appeared
|
|
11
|
+
* in the Console panel. This bus is the fix: `report()` is the one place a
|
|
12
|
+
* diagnostic is born, and `console-forward` subscribes to inject each one
|
|
13
|
+
* into the owning session's service-host console (queuing until that
|
|
14
|
+
* session's window is ready — see `notifyServiceHostReady`).
|
|
15
|
+
*/
|
|
16
|
+
export type DiagnosticSeverity = 'error' | 'warn' | 'info';
|
|
17
|
+
export interface Diagnostic {
|
|
18
|
+
severity: DiagnosticSeverity;
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
appSessionId?: string;
|
|
22
|
+
ts: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DiagnosticsBus {
|
|
25
|
+
/** Record one diagnostic: buffers it, mirrors it to the main-process console, and synchronously notifies every live subscriber. No-op after `dispose()`. */
|
|
26
|
+
report(d: {
|
|
27
|
+
severity: DiagnosticSeverity;
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
appSessionId?: string;
|
|
31
|
+
}): void;
|
|
32
|
+
/**
|
|
33
|
+
* Register a sink. With `replay` (default true) the sink is first called,
|
|
34
|
+
* in order, for every buffered diagnostic still held, then for every
|
|
35
|
+
* diagnostic reported from now on; `replay: false` skips the backlog and
|
|
36
|
+
* only delivers diagnostics reported after this call.
|
|
37
|
+
*/
|
|
38
|
+
subscribe(sink: (d: Diagnostic) => void, opts?: {
|
|
39
|
+
replay?: boolean;
|
|
40
|
+
}): {
|
|
41
|
+
dispose(): void;
|
|
42
|
+
};
|
|
43
|
+
/** Clears subscribers and the buffer. `report()` becomes a no-op afterward (including the console mirror — dispose means fully off, not "log-only"). */
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function createDiagnosticsBus(opts?: {
|
|
47
|
+
bufferCap?: number;
|
|
48
|
+
}): DiagnosticsBus;
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const DEFAULT_BUFFER_CAP = 200;
|
|
2
|
+
const CONSOLE_METHOD = {
|
|
3
|
+
error: 'error',
|
|
4
|
+
warn: 'warn',
|
|
5
|
+
info: 'info',
|
|
6
|
+
};
|
|
7
|
+
export function createDiagnosticsBus(opts) {
|
|
8
|
+
const bufferCap = opts?.bufferCap && opts.bufferCap > 0 ? opts.bufferCap : DEFAULT_BUFFER_CAP;
|
|
9
|
+
const buffer = [];
|
|
10
|
+
const sinks = new Set();
|
|
11
|
+
let disposed = false;
|
|
12
|
+
return {
|
|
13
|
+
report(d) {
|
|
14
|
+
if (disposed)
|
|
15
|
+
return;
|
|
16
|
+
const entry = { ...d, ts: Date.now() };
|
|
17
|
+
buffer.push(entry);
|
|
18
|
+
// Ring buffer: drop the oldest once over cap. Bounded at `bufferCap` so a
|
|
19
|
+
// burst of synthesized diagnostics can never grow this without limit.
|
|
20
|
+
if (buffer.length > bufferCap)
|
|
21
|
+
buffer.shift();
|
|
22
|
+
// The one guaranteed-visible sink: unlike Console-panel injection (which
|
|
23
|
+
// needs a live, ready service-host wc), the main-process terminal is
|
|
24
|
+
// always there.
|
|
25
|
+
console[CONSOLE_METHOD[entry.severity]](`[dimina-kit:${entry.code}] ${entry.message}`);
|
|
26
|
+
for (const sink of sinks) {
|
|
27
|
+
try {
|
|
28
|
+
sink(entry);
|
|
29
|
+
}
|
|
30
|
+
catch { /* a sink must never break the others */ }
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
subscribe(sink, subOpts) {
|
|
34
|
+
const replay = subOpts?.replay ?? true;
|
|
35
|
+
if (replay) {
|
|
36
|
+
for (const entry of buffer) {
|
|
37
|
+
try {
|
|
38
|
+
sink(entry);
|
|
39
|
+
}
|
|
40
|
+
catch { /* isolate a replay throw same as a live one */ }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
sinks.add(sink);
|
|
44
|
+
let released = false;
|
|
45
|
+
return {
|
|
46
|
+
dispose() {
|
|
47
|
+
if (released)
|
|
48
|
+
return;
|
|
49
|
+
released = true;
|
|
50
|
+
sinks.delete(sink);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
dispose() {
|
|
55
|
+
disposed = true;
|
|
56
|
+
sinks.clear();
|
|
57
|
+
buffer.length = 0;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=index.js.map
|