@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
|
@@ -11,6 +11,111 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { z } from 'zod';
|
|
13
13
|
import { getClient } from '../target-manager.js';
|
|
14
|
+
function ok(text) {
|
|
15
|
+
return { content: [{ type: 'text', text }] };
|
|
16
|
+
}
|
|
17
|
+
function err(msg) {
|
|
18
|
+
return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
|
|
19
|
+
}
|
|
20
|
+
async function clickAt(c, x, y) {
|
|
21
|
+
await c.Input.dispatchMouseEvent({ type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
|
|
22
|
+
await c.Input.dispatchMouseEvent({ type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
|
|
23
|
+
}
|
|
24
|
+
async function tapCoord(c, x, y) {
|
|
25
|
+
if (typeof x !== 'number' || typeof y !== 'number')
|
|
26
|
+
return err('tap_coord requires x and y');
|
|
27
|
+
await clickAt(c, x, y);
|
|
28
|
+
return ok(`Tapped at (${x}, ${y})`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* In-page expression evaluated by `tap_selector`: locates the nth match,
|
|
32
|
+
* scrolls it into view, and reports its center point (or a reason it can't be
|
|
33
|
+
* tapped) so the caller can dispatch a real mouse click at that point.
|
|
34
|
+
*/
|
|
35
|
+
function buildTapSelectorExpression(selector, index) {
|
|
36
|
+
return `(() => {
|
|
37
|
+
const payload = ${JSON.stringify({ selector, index })}
|
|
38
|
+
try {
|
|
39
|
+
const matches = Array.from(document.querySelectorAll(payload.selector))
|
|
40
|
+
if (matches.length === 0) {
|
|
41
|
+
return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
|
|
42
|
+
}
|
|
43
|
+
if (payload.index < 0 || payload.index >= matches.length) {
|
|
44
|
+
return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
|
|
45
|
+
}
|
|
46
|
+
const element = matches[payload.index]
|
|
47
|
+
element.scrollIntoView({ block: 'center', inline: 'center' })
|
|
48
|
+
const rect = element.getBoundingClientRect()
|
|
49
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
50
|
+
return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
ok: true,
|
|
54
|
+
selector: payload.selector,
|
|
55
|
+
index: payload.index,
|
|
56
|
+
x: rect.left + rect.width / 2,
|
|
57
|
+
y: rect.top + rect.height / 2,
|
|
58
|
+
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
62
|
+
return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
|
|
63
|
+
}
|
|
64
|
+
})()`;
|
|
65
|
+
}
|
|
66
|
+
async function tapSelector(c, selector, nth) {
|
|
67
|
+
if (!selector)
|
|
68
|
+
return err('tap_selector requires selector');
|
|
69
|
+
const index = nth ?? 0;
|
|
70
|
+
const expression = buildTapSelectorExpression(selector, index);
|
|
71
|
+
const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
|
|
72
|
+
const value = result.result?.value;
|
|
73
|
+
if (!value)
|
|
74
|
+
return err(`selector evaluation returned no result: ${selector}[${index}]`);
|
|
75
|
+
if (!value.ok)
|
|
76
|
+
return err(value.message);
|
|
77
|
+
await clickAt(c, value.x, value.y);
|
|
78
|
+
return ok(`Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`);
|
|
79
|
+
}
|
|
80
|
+
async function typeText(c, selector, nth, text) {
|
|
81
|
+
if (!selector || typeof text !== 'string')
|
|
82
|
+
return err('type requires selector and text');
|
|
83
|
+
const { root } = await c.DOM.getDocument({ depth: 0 });
|
|
84
|
+
const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
85
|
+
if (!nodeIds || nodeIds.length === 0)
|
|
86
|
+
return err(`selector matched no elements: ${selector}`);
|
|
87
|
+
const index = nth ?? 0;
|
|
88
|
+
if (index < 0 || index >= nodeIds.length)
|
|
89
|
+
return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
|
|
90
|
+
await c.DOM.focus({ nodeId: nodeIds[index] });
|
|
91
|
+
try {
|
|
92
|
+
await c.Input.insertText({ text });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
for (const ch of text) {
|
|
96
|
+
await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return ok(`Typed ${text.length} char(s) into ${selector}[${index}]`);
|
|
100
|
+
}
|
|
101
|
+
async function scrollAt(c, x, y, deltaX, deltaY) {
|
|
102
|
+
if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
|
|
103
|
+
return err('scroll requires x, y, deltaX, deltaY');
|
|
104
|
+
}
|
|
105
|
+
await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
|
|
106
|
+
return ok(`Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})`);
|
|
107
|
+
}
|
|
108
|
+
async function dispatchKey(c, key) {
|
|
109
|
+
if (!key)
|
|
110
|
+
return err('key requires key');
|
|
111
|
+
const isSingleChar = key.length === 1;
|
|
112
|
+
const down = { type: 'keyDown', key, code: key };
|
|
113
|
+
if (isSingleChar)
|
|
114
|
+
down.text = key;
|
|
115
|
+
await c.Input.dispatchKeyEvent(down);
|
|
116
|
+
await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
|
|
117
|
+
return ok(`Dispatched key ${key}`);
|
|
118
|
+
}
|
|
14
119
|
export function registerSimulatorTools(server) {
|
|
15
120
|
server.tool('simulator_navigate', 'Navigate the simulator to a URL, or reload the current page', {
|
|
16
121
|
url: z.string().optional().describe('URL to navigate to'),
|
|
@@ -40,104 +145,20 @@ export function registerSimulatorTools(server) {
|
|
|
40
145
|
deltaY: z.number().optional().describe('Vertical wheel delta for `scroll`'),
|
|
41
146
|
}, async ({ action, x, y, selector, nth, text, key, deltaX, deltaY }) => {
|
|
42
147
|
const c = getClient('simulator');
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return err('tap_selector requires selector');
|
|
57
|
-
const index = nth ?? 0;
|
|
58
|
-
const expression = `(() => {
|
|
59
|
-
const payload = ${JSON.stringify({ selector, index })}
|
|
60
|
-
try {
|
|
61
|
-
const matches = Array.from(document.querySelectorAll(payload.selector))
|
|
62
|
-
if (matches.length === 0) {
|
|
63
|
-
return { ok: false, reason: 'no_match', message: \`selector matched no elements: \${payload.selector}\` }
|
|
64
|
-
}
|
|
65
|
-
if (payload.index < 0 || payload.index >= matches.length) {
|
|
66
|
-
return { ok: false, reason: 'out_of_range', message: \`nth \${payload.index} out of range (matches: \${matches.length})\` }
|
|
67
|
-
}
|
|
68
|
-
const element = matches[payload.index]
|
|
69
|
-
element.scrollIntoView({ block: 'center', inline: 'center' })
|
|
70
|
-
const rect = element.getBoundingClientRect()
|
|
71
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
72
|
-
return { ok: false, reason: 'not_visible', message: \`selector matched an element, but it is not visible or rendered (zero rect): \${payload.selector}[\${payload.index}]\` }
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
ok: true,
|
|
76
|
-
selector: payload.selector,
|
|
77
|
-
index: payload.index,
|
|
78
|
-
x: rect.left + rect.width / 2,
|
|
79
|
-
y: rect.top + rect.height / 2,
|
|
80
|
-
rect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
84
|
-
return { ok: false, reason: 'selector_error', message: \`invalid selector: \${payload.selector} (\${message})\` }
|
|
85
|
-
}
|
|
86
|
-
})()`;
|
|
87
|
-
const result = await c.Runtime.evaluate({ expression, returnByValue: true, awaitPromise: true });
|
|
88
|
-
const value = result.result?.value;
|
|
89
|
-
if (!value)
|
|
90
|
-
return err(`selector evaluation returned no result: ${selector}[${index}]`);
|
|
91
|
-
if (!value.ok)
|
|
92
|
-
return err(value.message);
|
|
93
|
-
await clickAt(value.x, value.y);
|
|
94
|
-
return {
|
|
95
|
-
content: [{
|
|
96
|
-
type: 'text',
|
|
97
|
-
text: `Tapped selector ${value.selector}[${value.index}] at (${value.x}, ${value.y}) within rect (${value.rect.left}, ${value.rect.top}, ${value.rect.width} x ${value.rect.height})`,
|
|
98
|
-
}],
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
if (action === 'type') {
|
|
102
|
-
if (!selector || typeof text !== 'string')
|
|
103
|
-
return err('type requires selector and text');
|
|
104
|
-
const { root } = await c.DOM.getDocument({ depth: 0 });
|
|
105
|
-
const { nodeIds } = await c.DOM.querySelectorAll({ nodeId: root.nodeId, selector });
|
|
106
|
-
if (!nodeIds || nodeIds.length === 0)
|
|
107
|
-
return err(`selector matched no elements: ${selector}`);
|
|
108
|
-
const index = nth ?? 0;
|
|
109
|
-
if (index < 0 || index >= nodeIds.length)
|
|
110
|
-
return err(`nth ${index} out of range (matches: ${nodeIds.length})`);
|
|
111
|
-
await c.DOM.focus({ nodeId: nodeIds[index] });
|
|
112
|
-
try {
|
|
113
|
-
await c.Input.insertText({ text });
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
for (const ch of text) {
|
|
117
|
-
await c.Input.dispatchKeyEvent({ type: 'char', text: ch });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return { content: [{ type: 'text', text: `Typed ${text.length} char(s) into ${selector}[${index}]` }] };
|
|
121
|
-
}
|
|
122
|
-
if (action === 'scroll') {
|
|
123
|
-
if (typeof x !== 'number' || typeof y !== 'number' || typeof deltaX !== 'number' || typeof deltaY !== 'number') {
|
|
124
|
-
return err('scroll requires x, y, deltaX, deltaY');
|
|
125
|
-
}
|
|
126
|
-
await c.Input.dispatchMouseEvent({ type: 'mouseWheel', x, y, deltaX, deltaY });
|
|
127
|
-
return { content: [{ type: 'text', text: `Scrolled at (${x}, ${y}) by (${deltaX}, ${deltaY})` }] };
|
|
128
|
-
}
|
|
129
|
-
if (action === 'key') {
|
|
130
|
-
if (!key)
|
|
131
|
-
return err('key requires key');
|
|
132
|
-
const isSingleChar = key.length === 1;
|
|
133
|
-
const down = { type: 'keyDown', key, code: key };
|
|
134
|
-
if (isSingleChar)
|
|
135
|
-
down.text = key;
|
|
136
|
-
await c.Input.dispatchKeyEvent(down);
|
|
137
|
-
await c.Input.dispatchKeyEvent({ type: 'keyUp', key, code: key });
|
|
138
|
-
return { content: [{ type: 'text', text: `Dispatched key ${key}` }] };
|
|
148
|
+
switch (action) {
|
|
149
|
+
case 'tap_coord':
|
|
150
|
+
return tapCoord(c, x, y);
|
|
151
|
+
case 'tap_selector':
|
|
152
|
+
return tapSelector(c, selector, nth);
|
|
153
|
+
case 'type':
|
|
154
|
+
return typeText(c, selector, nth, text);
|
|
155
|
+
case 'scroll':
|
|
156
|
+
return scrollAt(c, x, y, deltaX, deltaY);
|
|
157
|
+
case 'key':
|
|
158
|
+
return dispatchKey(c, key);
|
|
159
|
+
default:
|
|
160
|
+
return err(`unknown action: ${String(action)}`);
|
|
139
161
|
}
|
|
140
|
-
return err(`unknown action: ${action}`);
|
|
141
162
|
});
|
|
142
163
|
}
|
|
143
164
|
//# sourceMappingURL=simulator-tools.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure batch-packing for the native dispatch queue: greedily pack queued CDP
|
|
3
|
+
* messages up to `maxBatchChars` so one `executeJavaScript` call stays sized,
|
|
4
|
+
* pulling any oversized message (over `maxSingleChars`) into `chunked` for the
|
|
5
|
+
* separate chunked transport instead.
|
|
6
|
+
*
|
|
7
|
+
* An oversized message reached while `batch` is still empty is queued into
|
|
8
|
+
* `chunked` immediately and packing continues past it, so several oversized
|
|
9
|
+
* messages in a row are all pulled out in one pass. One reached once `batch`
|
|
10
|
+
* already has items stops packing there, leaving it (and everything after) in
|
|
11
|
+
* `remaining` for the next flush — this preserves delivery order across passes.
|
|
12
|
+
*/
|
|
13
|
+
export interface PackedDispatchBatch {
|
|
14
|
+
/** Messages to send in one `executeJavaScript` batch dispatch. */
|
|
15
|
+
batch: string[];
|
|
16
|
+
/** Oversized messages to dispatch individually via the chunked transport. */
|
|
17
|
+
chunked: string[];
|
|
18
|
+
/** Whatever the queue had left after this pass. */
|
|
19
|
+
remaining: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function packDispatchBatch(queue: readonly string[], maxSingleChars: number, maxBatchChars: number): PackedDispatchBatch;
|
|
22
|
+
//# sourceMappingURL=dispatch-batch.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function packDispatchBatch(queue, maxSingleChars, maxBatchChars) {
|
|
2
|
+
const batch = [];
|
|
3
|
+
const chunked = [];
|
|
4
|
+
let batchChars = 0;
|
|
5
|
+
let i = 0;
|
|
6
|
+
for (; i < queue.length; i++) {
|
|
7
|
+
const msg = queue[i];
|
|
8
|
+
if (msg.length > maxSingleChars) {
|
|
9
|
+
if (batch.length > 0)
|
|
10
|
+
break;
|
|
11
|
+
chunked.push(msg);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (batch.length > 0 && batchChars + msg.length > maxBatchChars)
|
|
15
|
+
break;
|
|
16
|
+
batch.push(msg);
|
|
17
|
+
batchChars += msg.length;
|
|
18
|
+
}
|
|
19
|
+
return { batch, chunked, remaining: queue.slice(i) };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=dispatch-batch.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
|
|
2
2
|
import { isFrontendSettled } from '../views/inject-when-ready.js';
|
|
3
|
+
import { packDispatchBatch } from './dispatch-batch.js';
|
|
3
4
|
// ── requestId namespacing (pure, testable) ──────────────────────────────────
|
|
4
5
|
/**
|
|
5
6
|
* CDP events whose `params.requestId` we rewrite into the virtual namespace.
|
|
@@ -252,6 +253,16 @@ export function createNetworkForwarder(bridge) {
|
|
|
252
253
|
// degrade, dropped if we go ready (so a request shows in exactly one sink).
|
|
253
254
|
let probeConsoleBuffer = [];
|
|
254
255
|
let readyTimeoutTimer = null;
|
|
256
|
+
// Wall-clock deadline for the CURRENT probe, set once when 'probing' begins.
|
|
257
|
+
// scheduleReadyRetry() re-checks this on every retry so the retry chain is
|
|
258
|
+
// itself authoritative on giving up — it does not depend on winning a race
|
|
259
|
+
// against readyTimeoutTimer firing first (two timers due at the same virtual
|
|
260
|
+
// instant have no guaranteed firing order under fake timers, which let a
|
|
261
|
+
// never-ready host's retry chain outlive the nominal timeout in CI: an
|
|
262
|
+
// "Aborting after running 10000 timers" abort in `network-forward`'s
|
|
263
|
+
// ready-timeout test). readyTimeoutTimer stays as a backstop for hosts that
|
|
264
|
+
// never retry at all (e.g. the queue drains before the deadline).
|
|
265
|
+
let probeDeadline = null;
|
|
255
266
|
// ── Batched native dispatch into the DevTools front-end ───────────────────
|
|
256
267
|
// Queued raw CDP messages (already namespaced + JSON-stringified) awaiting a
|
|
257
268
|
// microtask flush — coalescing many events into one executeJavaScript avoids
|
|
@@ -268,6 +279,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
268
279
|
if (sink === 'probing')
|
|
269
280
|
return;
|
|
270
281
|
sink = 'probing';
|
|
282
|
+
probeDeadline = Date.now() + DEVTOOLS_READY_TIMEOUT_MS;
|
|
271
283
|
if (readyTimeoutTimer)
|
|
272
284
|
clearTimeout(readyTimeoutTimer);
|
|
273
285
|
readyTimeoutTimer = setTimeout(() => {
|
|
@@ -280,6 +292,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
280
292
|
/** Native path confirmed live: console buffer is moot, drop it. */
|
|
281
293
|
function markReady() {
|
|
282
294
|
sink = 'ready';
|
|
295
|
+
probeDeadline = null;
|
|
283
296
|
if (readyTimeoutTimer) {
|
|
284
297
|
clearTimeout(readyTimeoutTimer);
|
|
285
298
|
readyTimeoutTimer = null;
|
|
@@ -298,6 +311,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
298
311
|
*/
|
|
299
312
|
function degradeToConsole() {
|
|
300
313
|
sink = 'degraded';
|
|
314
|
+
probeDeadline = null;
|
|
301
315
|
dispatchQueue = [];
|
|
302
316
|
if (readyTimeoutTimer) {
|
|
303
317
|
clearTimeout(readyTimeoutTimer);
|
|
@@ -375,26 +389,10 @@ export function createNetworkForwarder(bridge) {
|
|
|
375
389
|
if (sink === 'idle')
|
|
376
390
|
beginProbing();
|
|
377
391
|
// Pack greedily up to MAX_BATCH_CHARS so one executeJavaScript stays sized;
|
|
378
|
-
// oversized single messages go via the chunked transport.
|
|
379
|
-
const batch =
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
for (; i < dispatchQueue.length; i++) {
|
|
383
|
-
const msg = dispatchQueue[i];
|
|
384
|
-
if (msg.length > MAX_SINGLE_DISPATCH_CHARS) {
|
|
385
|
-
// Flush whatever's accumulated first to preserve ordering, then chunk.
|
|
386
|
-
if (batch.length > 0)
|
|
387
|
-
break;
|
|
388
|
-
dispatchChunked(wc, msg);
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
if (batch.length > 0 && batchChars + msg.length > MAX_BATCH_CHARS)
|
|
392
|
-
break;
|
|
393
|
-
batch.push(msg);
|
|
394
|
-
batchChars += msg.length;
|
|
395
|
-
}
|
|
396
|
-
// Everything up to i was either batched or chunk-dispatched; keep the rest.
|
|
397
|
-
const remaining = dispatchQueue.slice(i);
|
|
392
|
+
// oversized single messages go via the chunked transport (see packDispatchBatch).
|
|
393
|
+
const { batch, chunked, remaining } = packDispatchBatch(dispatchQueue, MAX_SINGLE_DISPATCH_CHARS, MAX_BATCH_CHARS);
|
|
394
|
+
for (const msg of chunked)
|
|
395
|
+
dispatchChunked(wc, msg);
|
|
398
396
|
if (batch.length === 0) {
|
|
399
397
|
// Only chunked messages were processed this turn; continue with the rest.
|
|
400
398
|
dispatchQueue = remaining;
|
|
@@ -459,10 +457,21 @@ export function createNetworkForwarder(bridge) {
|
|
|
459
457
|
function scheduleReadyRetry() {
|
|
460
458
|
if (readyRetryTimer || sink === 'ready' || sink === 'degraded')
|
|
461
459
|
return;
|
|
460
|
+
// Self-terminate on the same deadline readyTimeoutTimer enforces, instead of
|
|
461
|
+
// trusting that timer to win a same-instant race against this one (see the
|
|
462
|
+
// comment on `probeDeadline`'s declaration).
|
|
463
|
+
if (probeDeadline !== null && Date.now() >= probeDeadline) {
|
|
464
|
+
degradeToConsole();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
462
467
|
readyRetryTimer = setTimeout(() => {
|
|
463
468
|
readyRetryTimer = null;
|
|
464
469
|
if (sink === 'degraded')
|
|
465
470
|
return;
|
|
471
|
+
if (probeDeadline !== null && Date.now() >= probeDeadline) {
|
|
472
|
+
degradeToConsole();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
466
475
|
if (dispatchQueue.length > 0)
|
|
467
476
|
scheduleFlush();
|
|
468
477
|
}, READY_RETRY_MS);
|
|
@@ -519,6 +528,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
519
528
|
// A new simulator attach restarts the native sink: 'idle' until the next
|
|
520
529
|
// event re-probes the (possibly already-set) host.
|
|
521
530
|
sink = 'idle';
|
|
531
|
+
probeDeadline = null;
|
|
522
532
|
if (readyRetryTimer) {
|
|
523
533
|
clearTimeout(readyRetryTimer);
|
|
524
534
|
readyRetryTimer = null;
|
|
@@ -707,6 +717,7 @@ export function createNetworkForwarder(bridge) {
|
|
|
707
717
|
clearTimeout(readyRetryTimer);
|
|
708
718
|
readyRetryTimer = null;
|
|
709
719
|
}
|
|
720
|
+
probeDeadline = null;
|
|
710
721
|
// Records buffered while probing the OLD host are stale — drop, don't flush
|
|
711
722
|
// (their native copies were already queued; on a host swap we restart clean).
|
|
712
723
|
probeConsoleBuffer = [];
|
|
@@ -747,6 +758,12 @@ export function createNetworkForwarder(bridge) {
|
|
|
747
758
|
catch { /* gone */ }
|
|
748
759
|
});
|
|
749
760
|
}
|
|
761
|
+
// A host swap while still 'probing' the OLD host must not inherit its
|
|
762
|
+
// window: beginProbing()'s `sink === 'probing'` guard would otherwise skip
|
|
763
|
+
// (re-)arming readyTimeoutTimer/probeDeadline for the NEW host, leaving it
|
|
764
|
+
// probing forever with no timeout. Force through 'idle' so beginProbing()
|
|
765
|
+
// always arms a fresh window for whichever host is now current.
|
|
766
|
+
sink = 'idle';
|
|
750
767
|
beginProbing();
|
|
751
768
|
if (dispatchQueue.length > 0)
|
|
752
769
|
scheduleFlush();
|
|
@@ -11,6 +11,43 @@ export interface ProjectStatusPayload {
|
|
|
11
11
|
message: string;
|
|
12
12
|
/** True when the status update is emitted by the file-watcher rebuild loop. */
|
|
13
13
|
hotReload?: boolean;
|
|
14
|
+
/** Freshly-read page list, carried on a hot-reload status so the launch dropdown stays current. Absent when the read failed or wasn't attempted. */
|
|
15
|
+
pages?: string[];
|
|
16
|
+
/** Present (`'dead'`) once the project's file watcher has stopped — saves no longer trigger an automatic rebuild. */
|
|
17
|
+
watcher?: 'dead';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Payload for the `session:runtimeStatus` push — the post-compile SESSION
|
|
21
|
+
* lifecycle main tracks per `appSessionId` in bridge-router (`AppSession`
|
|
22
|
+
* itself is main-private; this is the public projection). A successful
|
|
23
|
+
* compile only means the resource tree exists; it says nothing about whether
|
|
24
|
+
* the simulator actually booted — this closes that gap.
|
|
25
|
+
*/
|
|
26
|
+
export interface SessionRuntimeStatusPayload {
|
|
27
|
+
appId: string;
|
|
28
|
+
phase: 'launching' | 'running' | 'launch-failed' | 'crashed';
|
|
29
|
+
/**
|
|
30
|
+
* Machine-readable cause, present on `'launch-failed'` and `'crashed'`:
|
|
31
|
+
* `'timeout'` — the launch watchdog (`LAUNCH_TIMEOUT_MS`) expired before the
|
|
32
|
+
* root page reported `domReady`; `'logic-bundle-unreachable'` — the
|
|
33
|
+
* compiled `logic.js` could not be fetched/executed;
|
|
34
|
+
* `'service-host-navigation-failed'` — the service-host window failed to
|
|
35
|
+
* navigate to its spawn URL; `'service-host-crashed'` — the service host's
|
|
36
|
+
* renderer process was gone. Plain `string` (not a literal union) so an
|
|
37
|
+
* unrecognized future code degrades gracefully instead of a type error.
|
|
38
|
+
*/
|
|
39
|
+
code?: string;
|
|
40
|
+
/** Human-readable detail, mirroring the short form of the matching `ctx.diagnostics` report. */
|
|
41
|
+
reason?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Present on `'launching'` when `handleSpawn`'s page-mount gate
|
|
44
|
+
* (`resolveRootPagePath`) substituted the resolved page for the request —
|
|
45
|
+
* i.e. `SpawnResult.pageFallbackApplied` was true for this spawn.
|
|
46
|
+
*/
|
|
47
|
+
pageFallback?: {
|
|
48
|
+
requested: string;
|
|
49
|
+
resolved: string;
|
|
50
|
+
};
|
|
14
51
|
}
|
|
15
52
|
/**
|
|
16
53
|
* Payload for the `project:compileLog` push — one filtered dmcc log line.
|
|
@@ -51,6 +88,8 @@ export interface WorkbenchSettingsInitPayload {
|
|
|
51
88
|
export interface RendererNotifier {
|
|
52
89
|
/** Broadcast project compile status transitions to the main renderer. */
|
|
53
90
|
projectStatus(payload: ProjectStatusPayload): void;
|
|
91
|
+
/** Broadcast a session's post-compile runtime lifecycle to the main renderer. */
|
|
92
|
+
sessionRuntimeStatus(payload: SessionRuntimeStatusPayload): void;
|
|
54
93
|
/** Push one per-line dmcc compile-log entry to the main renderer. */
|
|
55
94
|
compileLog(payload: CompileLogPayload): void;
|
|
56
95
|
/** Ask the main renderer to navigate back to its landing screen. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProjectChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
|
|
1
|
+
import { ProjectChannel, SessionChannel, WindowChannel, SettingsChannel, PopoverChannel, WorkbenchSettingsChannel, EditorChannel, ViewChannel, } from '../../../shared/ipc-channels.js';
|
|
2
2
|
/** Safely resolve a WebContents, skipping destroyed / missing targets. */
|
|
3
3
|
function liveWebContents(wc) {
|
|
4
4
|
if (!wc)
|
|
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
|
|
|
24
24
|
projectStatus(payload) {
|
|
25
25
|
sendToMain(ProjectChannel.Status, payload);
|
|
26
26
|
},
|
|
27
|
+
sessionRuntimeStatus(payload) {
|
|
28
|
+
sendToMain(SessionChannel.RuntimeStatus, payload);
|
|
29
|
+
},
|
|
27
30
|
compileLog(payload) {
|
|
28
31
|
sendToMain(ProjectChannel.CompileLog, payload);
|
|
29
32
|
},
|
|
@@ -15,55 +15,67 @@
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
const DEFAULT_TEMPLATE_ID = 'blank';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const name = (
|
|
18
|
+
/** Non-empty-after-trim `input.name`, or throws. */
|
|
19
|
+
function resolveName(rawName) {
|
|
20
|
+
const name = (rawName ?? '').trim();
|
|
21
21
|
if (name.length === 0) {
|
|
22
22
|
throw new Error('Project name cannot be empty');
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
/** `input.path` does-not-exist OR exists+empty, or throws. */
|
|
27
|
+
function validateTargetPath(target) {
|
|
26
28
|
if (!target || typeof target !== 'string') {
|
|
27
29
|
throw new Error('Project path is required');
|
|
28
30
|
}
|
|
29
|
-
if (fs.existsSync(target))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
31
|
+
if (!fs.existsSync(target))
|
|
32
|
+
return;
|
|
33
|
+
const stat = fs.statSync(target);
|
|
34
|
+
if (!stat.isDirectory()) {
|
|
35
|
+
throw new Error(`Project path exists and is not a directory: ${target}`);
|
|
36
|
+
}
|
|
37
|
+
const entries = fs.readdirSync(target);
|
|
38
|
+
if (entries.length > 0) {
|
|
39
|
+
throw new Error(`Project path is not empty (refusing to overwrite): ${target}`);
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
}
|
|
42
|
+
/** Look up `templateId` (default 'blank') and confirm it can materialise a project. */
|
|
43
|
+
function resolveTemplate(templates, templateId) {
|
|
44
|
+
const id = templateId ?? DEFAULT_TEMPLATE_ID;
|
|
45
|
+
const template = templates.find((t) => t.id === id);
|
|
42
46
|
if (!template) {
|
|
43
|
-
throw new Error(`Template not found: ${
|
|
47
|
+
throw new Error(`Template not found: ${id}`);
|
|
44
48
|
}
|
|
45
49
|
if (!template.source && !template.generate) {
|
|
46
|
-
throw new Error(`Template '${
|
|
50
|
+
throw new Error(`Template '${id}' has neither a source directory nor a generate function`);
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
return template;
|
|
53
|
+
}
|
|
54
|
+
/** Ensure `target` exists, then copy the template's `source` tree or run its `generate`. */
|
|
55
|
+
async function materializeTemplate(target, template, name) {
|
|
49
56
|
fs.mkdirSync(target, { recursive: true });
|
|
50
57
|
if (template.generate) {
|
|
51
58
|
await template.generate(target, { name });
|
|
59
|
+
return;
|
|
52
60
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
// `recursive: true` makes cpSync mirror the entire tree (files,
|
|
58
|
-
// subdirs, symlinks). `force: true` mirrors over an empty target.
|
|
59
|
-
fs.cpSync(template.source.path, target, {
|
|
60
|
-
recursive: true,
|
|
61
|
-
force: true,
|
|
62
|
-
});
|
|
61
|
+
if (!template.source)
|
|
62
|
+
return;
|
|
63
|
+
if (!fs.existsSync(template.source.path)) {
|
|
64
|
+
throw new Error(`Template source missing on disk: ${template.source.path}`);
|
|
63
65
|
}
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
66
|
+
// `recursive: true` makes cpSync mirror the entire tree (files,
|
|
67
|
+
// subdirs, symlinks). `force: true` mirrors over an empty target.
|
|
68
|
+
fs.cpSync(template.source.path, target, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
force: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rewrite `<target>/project.config.json`'s `projectname` (best-effort: create
|
|
75
|
+
* the file if the template didn't ship one — gives the user a working
|
|
76
|
+
* starting point even for tiny generators).
|
|
77
|
+
*/
|
|
78
|
+
function writeProjectConfig(target, name) {
|
|
67
79
|
const cfgPath = path.join(target, 'project.config.json');
|
|
68
80
|
let cfg = {};
|
|
69
81
|
if (fs.existsSync(cfgPath)) {
|
|
@@ -76,7 +88,15 @@ export async function createProject(input, ctx) {
|
|
|
76
88
|
}
|
|
77
89
|
cfg.projectname = name;
|
|
78
90
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
79
|
-
|
|
91
|
+
}
|
|
92
|
+
export async function createProject(input, ctx) {
|
|
93
|
+
const name = resolveName(input.name);
|
|
94
|
+
const target = input.path;
|
|
95
|
+
validateTargetPath(target);
|
|
96
|
+
const template = resolveTemplate(ctx.templates, input.templateId);
|
|
97
|
+
await materializeTemplate(target, template, name);
|
|
98
|
+
writeProjectConfig(target, name);
|
|
99
|
+
// register with provider — this is what makes the new project appear
|
|
80
100
|
// in the list immediately after the dialog closes.
|
|
81
101
|
const created = await ctx.projectsProvider.addProject(target);
|
|
82
102
|
return created;
|
|
@@ -103,7 +103,8 @@ export function getProjectPages(dirPath) {
|
|
|
103
103
|
entryPagePath: appJson.entryPagePath || appJson.pages?.[0] || '',
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
|
-
catch {
|
|
106
|
+
catch (err) {
|
|
107
|
+
log.warn(`Failed to read project pages from ${appJsonPath}`, err);
|
|
107
108
|
return { pages: [], entryPagePath: '' };
|
|
108
109
|
}
|
|
109
110
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { CompileConfig } from '../../../shared/types.js';
|
|
8
8
|
import type { Project } from './project-repository.js';
|
|
9
9
|
export type { Project };
|
|
10
|
+
export type { ProjectTemplate, CreateProjectInput } from '../../../shared/types.js';
|
|
10
11
|
/**
|
|
11
12
|
* Default compile config returned by WorkspaceService when the injected
|
|
12
13
|
* provider omits `getCompileConfig`, or when no record exists for a path.
|
|
@@ -81,38 +82,6 @@ export interface ProjectsProvider {
|
|
|
81
82
|
*/
|
|
82
83
|
getThumbnail?(dirPath: string): string | null | Promise<string | null>;
|
|
83
84
|
}
|
|
84
|
-
/**
|
|
85
|
-
* A template the user can pick from in the "新建项目" dialog. Exactly one of
|
|
86
|
-
* `source` or `generate` should be supplied; if both are omitted, the
|
|
87
|
-
* service refuses to materialise the template.
|
|
88
|
-
*/
|
|
89
|
-
export interface ProjectTemplate {
|
|
90
|
-
/** Stable identifier. Used by host whitelists and the `templateId` field of CreateProjectInput. */
|
|
91
|
-
id: string;
|
|
92
|
-
/** Human-readable label shown in the dialog. */
|
|
93
|
-
name: string;
|
|
94
|
-
description?: string;
|
|
95
|
-
icon?: string;
|
|
96
|
-
/** Copy-tree source. `path` must be absolute. */
|
|
97
|
-
source?: {
|
|
98
|
-
type: 'directory';
|
|
99
|
-
path: string;
|
|
100
|
-
};
|
|
101
|
-
/** Programmatic generator. `target` is the absolute destination directory. */
|
|
102
|
-
generate?: (target: string, opts: {
|
|
103
|
-
name: string;
|
|
104
|
-
}) => Promise<void>;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Payload returned from the create-project dialog. The service uses this to
|
|
108
|
-
* scaffold disk content and to register the project with the provider.
|
|
109
|
-
*/
|
|
110
|
-
export interface CreateProjectInput {
|
|
111
|
-
name: string;
|
|
112
|
-
path: string;
|
|
113
|
-
templateId?: string;
|
|
114
|
-
extra?: Record<string, unknown>;
|
|
115
|
-
}
|
|
116
85
|
/** Built-in template policy. */
|
|
117
86
|
export type BuiltinTemplatesMode = 'all' | 'none' | readonly string[];
|
|
118
87
|
//# sourceMappingURL=types.d.ts.map
|