@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.
Files changed (87) hide show
  1. package/dist/main/app/app.js +5 -25
  2. package/dist/main/app/native-overview.d.ts +27 -0
  3. package/dist/main/app/native-overview.js +44 -0
  4. package/dist/main/index.bundle.js +1217 -735
  5. package/dist/main/ipc/bridge-router.d.ts +8 -0
  6. package/dist/main/ipc/bridge-router.js +526 -173
  7. package/dist/main/ipc/project-fs.js +39 -21
  8. package/dist/main/services/automation/handlers/app.js +143 -109
  9. package/dist/main/services/console-forward/index.d.ts +12 -1
  10. package/dist/main/services/console-forward/index.js +99 -1
  11. package/dist/main/services/diagnostics/index.d.ts +49 -0
  12. package/dist/main/services/diagnostics/index.js +61 -0
  13. package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
  14. package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
  15. package/dist/main/services/network-forward/dispatch-batch.js +21 -0
  16. package/dist/main/services/network-forward/index.js +37 -20
  17. package/dist/main/services/notifications/renderer-notifier.d.ts +39 -0
  18. package/dist/main/services/notifications/renderer-notifier.js +4 -1
  19. package/dist/main/services/projects/create-project-service.js +54 -34
  20. package/dist/main/services/projects/project-repository.js +2 -1
  21. package/dist/main/services/projects/types.d.ts +1 -32
  22. package/dist/main/services/service-console/console-api.js +36 -27
  23. package/dist/main/services/simulator-temp-files/disk.js +109 -86
  24. package/dist/main/services/views/simulator-session-policy.js +17 -0
  25. package/dist/main/services/workbench-coi-server.js +58 -45
  26. package/dist/main/services/workbench-context.d.ts +10 -0
  27. package/dist/main/services/workspace/workspace-service.js +12 -3
  28. package/dist/main/utils/logger.d.ts +5 -12
  29. package/dist/main/utils/logger.js +5 -45
  30. package/dist/main/windows/service-host-window/create.d.ts +15 -1
  31. package/dist/main/windows/service-host-window/create.js +11 -3
  32. package/dist/preload/instrumentation/wxml-extract.js +94 -62
  33. package/dist/preload/runtime/main-api-runner.d.ts +1 -1
  34. package/dist/preload/shared/api-compat.js +18 -55
  35. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  36. package/dist/preload/windows/main.cjs +284 -565
  37. package/dist/preload/windows/main.cjs.map +2 -2
  38. package/dist/preload/windows/simulator.cjs +154 -83
  39. package/dist/preload/windows/simulator.cjs.map +3 -3
  40. package/dist/preload/windows/simulator.js +154 -83
  41. package/dist/render-host/render-inspect.js +63 -52
  42. package/dist/renderer/assets/index-BwzPwCmM.js +49 -0
  43. package/dist/renderer/assets/{input-Cjk0wSEh.js → input-CTb_le7F.js} +2 -2
  44. package/dist/renderer/assets/ipc-transport-BPWIV5hA.css +1 -0
  45. package/dist/renderer/assets/{ipc-transport-OZS-KmOW.js → ipc-transport-Ck5Xa8Tu.js} +2 -2
  46. package/dist/renderer/assets/popover-B1Zu2UQu.js +2 -0
  47. package/dist/renderer/assets/{select-BQDXi5ih.js → select-ozSk5Pt6.js} +2 -2
  48. package/dist/renderer/assets/{settings-tP3Px2KR.js → settings-CNwGOI22.js} +2 -2
  49. package/dist/renderer/assets/settings-api-DMbMp1Eq.js +2 -0
  50. package/dist/renderer/assets/workbenchSettings-Cr4EHo5w.js +8 -0
  51. package/dist/renderer/entries/main/index.html +6 -6
  52. package/dist/renderer/entries/popover/index.html +5 -5
  53. package/dist/renderer/entries/settings/index.html +5 -5
  54. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  55. package/dist/shared/appdata-accumulator.js +53 -48
  56. package/dist/shared/bridge-channels.d.ts +22 -0
  57. package/dist/shared/ipc-channels.d.ts +3 -0
  58. package/dist/shared/ipc-channels.js +9 -0
  59. package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
  60. package/dist/shared/open-in-editor-resource-path.js +133 -0
  61. package/dist/shared/open-in-editor.d.ts +2 -9
  62. package/dist/shared/open-in-editor.js +8 -93
  63. package/dist/shared/request-core.d.ts +84 -0
  64. package/dist/shared/request-core.js +168 -0
  65. package/dist/shared/simulator-api-metadata.d.ts +22 -17
  66. package/dist/shared/simulator-api-metadata.js +36 -0
  67. package/dist/shared/types.d.ts +20 -4
  68. package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-C-wcFq3Z.js} +2 -2
  69. package/dist/simulator/assets/{jsx-runtime-CDK-o-S0.js → jsx-runtime-BDTY6fEq.js} +2 -2
  70. package/dist/simulator/assets/simulator-DU3-fS3v.js +10 -0
  71. package/dist/simulator/assets/simulator-mini-app-DzfMfnVM.js +2 -0
  72. package/dist/simulator/simulator.html +2 -2
  73. package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
  74. package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
  75. package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
  76. package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
  77. package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
  78. package/dist/vscode-workbench/index.html +1 -1
  79. package/package.json +6 -6
  80. package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
  81. package/dist/renderer/assets/ipc-transport-D5dKIti1.css +0 -1
  82. package/dist/renderer/assets/popover-CQVvOe90.js +0 -2
  83. package/dist/renderer/assets/settings-api-B6x2mhaD.js +0 -2
  84. package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
  85. package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
  86. package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
  87. package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
@@ -34,35 +34,25 @@ export function mapConsoleApiType(type) {
34
34
  }
35
35
  const BIGINT_LITERAL = /^-?\d+n$/;
36
36
  /**
37
- * A single RemoteObject a JSON-serializable value WITHOUT a CDP round-trip
38
- * (shallow). Objects that need their full contents are flagged by
39
- * {@link needsDeepFetch} and deep-serialized by the caller; this is the
40
- * inline/best-effort fallback.
37
+ * A CDP `unserializableValue` string (specials that can't ride in `value`,
38
+ * e.g. `Infinity`/`NaN`/bigint literals) the JS value it denotes. Unknown
39
+ * literals pass through as-is (e.g. a bigint literal stays a string).
41
40
  */
42
- export function remoteObjectToValue(ro) {
43
- if (!ro)
44
- return ro;
45
- // 1. An inlined value (CDP includes `value` for JSON-serializable primitives
46
- // and small arrays). Use `in` so falsy values (0, '', false) and an
47
- // explicit `null` are honoured rather than falling through.
48
- if ('value' in ro)
49
- return ro.value;
50
- // 2. Specials that can't ride in `value`.
51
- if (typeof ro.unserializableValue === 'string') {
52
- const u = ro.unserializableValue;
53
- if (u === 'Infinity')
54
- return Infinity;
55
- if (u === '-Infinity')
56
- return -Infinity;
57
- if (u === 'NaN')
58
- return NaN;
59
- if (u === '-0')
60
- return -0;
61
- if (BIGINT_LITERAL.test(u))
62
- return u;
41
+ function unserializableToValue(u) {
42
+ if (u === 'Infinity')
43
+ return Infinity;
44
+ if (u === '-Infinity')
45
+ return -Infinity;
46
+ if (u === 'NaN')
47
+ return NaN;
48
+ if (u === '-0')
49
+ return -0;
50
+ if (BIGINT_LITERAL.test(u))
63
51
  return u;
64
- }
65
- // 3. Fall back by type.
52
+ return u;
53
+ }
54
+ /** Fallback serialization by `RemoteObject.type` when neither `value` nor `unserializableValue` is present. */
55
+ function typeFallbackToValue(ro) {
66
56
  switch (ro.type) {
67
57
  case 'undefined':
68
58
  return undefined;
@@ -78,6 +68,25 @@ export function remoteObjectToValue(ro) {
78
68
  return ro.description ?? '[Unknown]';
79
69
  }
80
70
  }
71
+ /**
72
+ * A single RemoteObject → a JSON-serializable value WITHOUT a CDP round-trip
73
+ * (shallow). Objects that need their full contents are flagged by
74
+ * {@link needsDeepFetch} and deep-serialized by the caller; this is the
75
+ * inline/best-effort fallback.
76
+ */
77
+ export function remoteObjectToValue(ro) {
78
+ if (!ro)
79
+ return ro;
80
+ // An inlined value (CDP includes `value` for JSON-serializable primitives
81
+ // and small arrays). Use `in` so falsy values (0, '', false) and an
82
+ // explicit `null` are honoured rather than falling through.
83
+ if ('value' in ro)
84
+ return ro.value;
85
+ if (typeof ro.unserializableValue === 'string') {
86
+ return unserializableToValue(ro.unserializableValue);
87
+ }
88
+ return typeFallbackToValue(ro);
89
+ }
81
90
  /**
82
91
  * Whether this RemoteObject must be deep-fetched via `Runtime.callFunctionOn`
83
92
  * (returnByValue) to be fully serialized — i.e. a real object/array referenced
@@ -62,49 +62,63 @@ const EXT_MIME = {
62
62
  '.pdf': 'application/pdf',
63
63
  '.zip': 'application/zip',
64
64
  };
65
- function sniffMime(head) {
66
- if (head.length >= 8
65
+ function isPng(head) {
66
+ return head.length >= 8
67
67
  && head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47
68
- && head[4] === 0x0d && head[5] === 0x0a && head[6] === 0x1a && head[7] === 0x0a) {
69
- return 'image/png';
70
- }
71
- if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) {
72
- return 'image/jpeg';
73
- }
74
- if (head.length >= 6
68
+ && head[4] === 0x0d && head[5] === 0x0a && head[6] === 0x1a && head[7] === 0x0a;
69
+ }
70
+ function isJpeg(head) {
71
+ return head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff;
72
+ }
73
+ function isGif(head) {
74
+ return head.length >= 6
75
75
  && head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46
76
- && head[3] === 0x38 && (head[4] === 0x37 || head[4] === 0x39) && head[5] === 0x61) {
77
- return 'image/gif';
78
- }
79
- if (head.length >= 12
76
+ && head[3] === 0x38 && (head[4] === 0x37 || head[4] === 0x39) && head[5] === 0x61;
77
+ }
78
+ function isWebp(head) {
79
+ return head.length >= 12
80
80
  && head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46
81
- && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50) {
82
- return 'image/webp';
83
- }
84
- if (head.length >= 12
85
- && head[4] === 0x66 && head[5] === 0x74 && head[6] === 0x79 && head[7] === 0x70) {
86
- // MP4 ftyp box: brand bytes vary, treat all as video/mp4 for now.
87
- return 'video/mp4';
88
- }
89
- if (head.length >= 4
90
- && head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46) {
91
- return 'application/pdf';
92
- }
93
- if (head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d) {
94
- return 'image/bmp';
95
- }
96
- if (head.length >= 4
97
- && head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04) {
98
- return 'application/zip';
99
- }
100
- if (head.length >= 3 && head[0] === 0x49 && head[1] === 0x44 && head[2] === 0x33) {
101
- // ID3-tagged MP3.
102
- return 'audio/mpeg';
103
- }
104
- if (head.length >= 4 && head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53) {
105
- return 'audio/ogg';
106
- }
107
- return null;
81
+ && head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50;
82
+ }
83
+ /** MP4 ftyp box: brand bytes vary, treat all as video/mp4 for now. */
84
+ function isMp4(head) {
85
+ return head.length >= 12
86
+ && head[4] === 0x66 && head[5] === 0x74 && head[6] === 0x79 && head[7] === 0x70;
87
+ }
88
+ function isPdf(head) {
89
+ return head.length >= 4
90
+ && head[0] === 0x25 && head[1] === 0x50 && head[2] === 0x44 && head[3] === 0x46;
91
+ }
92
+ function isBmp(head) {
93
+ return head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d;
94
+ }
95
+ function isZip(head) {
96
+ return head.length >= 4
97
+ && head[0] === 0x50 && head[1] === 0x4b && head[2] === 0x03 && head[3] === 0x04;
98
+ }
99
+ /** ID3-tagged MP3. */
100
+ function isId3Mp3(head) {
101
+ return head.length >= 3 && head[0] === 0x49 && head[1] === 0x44 && head[2] === 0x33;
102
+ }
103
+ function isOgg(head) {
104
+ return head.length >= 4 && head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53;
105
+ }
106
+ /** Magic-byte sniffers in priority order — first match wins. */
107
+ const MAGIC_SNIFFERS = [
108
+ { mime: 'image/png', match: isPng },
109
+ { mime: 'image/jpeg', match: isJpeg },
110
+ { mime: 'image/gif', match: isGif },
111
+ { mime: 'image/webp', match: isWebp },
112
+ { mime: 'video/mp4', match: isMp4 },
113
+ { mime: 'application/pdf', match: isPdf },
114
+ { mime: 'image/bmp', match: isBmp },
115
+ { mime: 'application/zip', match: isZip },
116
+ { mime: 'audio/mpeg', match: isId3Mp3 },
117
+ { mime: 'audio/ogg', match: isOgg },
118
+ ];
119
+ function sniffMime(head) {
120
+ const hit = MAGIC_SNIFFERS.find((sniffer) => sniffer.match(head));
121
+ return hit ? hit.mime : null;
108
122
  }
109
123
  function extMime(realPath) {
110
124
  const ext = path.extname(realPath).toLowerCase();
@@ -126,6 +140,54 @@ function etagOf(mtimeMs, size) {
126
140
  return `W/"${Math.floor(mtimeMs)}-${size}"`;
127
141
  }
128
142
  // -- read -------------------------------------------------------------------
143
+ /** Validate `range` against `totalSize` and resolve it to a byte slice, or throws. */
144
+ function resolveRange(range, totalSize) {
145
+ const { start, end } = range;
146
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
147
+ throw new RangeError(`invalid range: ${start}-${end}`);
148
+ }
149
+ if (start < 0)
150
+ throw new RangeError(`range start out of bounds: ${start}`);
151
+ if (start > end)
152
+ throw new RangeError(`range start > end: ${start} > ${end}`);
153
+ if (start >= totalSize) {
154
+ throw new RangeError(`range start beyond file size: ${start} >= ${totalSize}`);
155
+ }
156
+ const clampedEnd = Math.min(end, totalSize - 1);
157
+ return { sliceStart: start, sliceLen: clampedEnd - start + 1 };
158
+ }
159
+ /**
160
+ * Read at most the first 12 bytes for magic sniffing, regardless of whether
161
+ * the caller asked for a range. The head bytes from position 0 are needed to
162
+ * label Content-Type correctly even on a tail Range request.
163
+ */
164
+ async function readHeadBytes(handle, totalSize) {
165
+ const head = Buffer.alloc(Math.min(12, totalSize));
166
+ if (head.length > 0) {
167
+ await handle.read(head, 0, head.length, 0);
168
+ }
169
+ return head;
170
+ }
171
+ /** Full-file body, re-using `head` (already read) instead of a second read for the leading bytes. */
172
+ async function readFullBody(handle, head, totalSize) {
173
+ const bytes = Buffer.alloc(totalSize);
174
+ if (totalSize === 0)
175
+ return bytes;
176
+ if (totalSize <= head.length) {
177
+ head.copy(bytes, 0, 0, totalSize);
178
+ return bytes;
179
+ }
180
+ head.copy(bytes, 0, 0, head.length);
181
+ await handle.read(bytes, head.length, totalSize - head.length, head.length);
182
+ return bytes;
183
+ }
184
+ async function readRangeBody(handle, sliceStart, sliceLen) {
185
+ const bytes = Buffer.alloc(sliceLen);
186
+ if (sliceLen > 0) {
187
+ await handle.read(bytes, 0, sliceLen, sliceStart);
188
+ }
189
+ return bytes;
190
+ }
129
191
  export async function readDiskFile(realPath, opts) {
130
192
  const handle = await fs.open(realPath, 'r');
131
193
  try {
@@ -133,52 +195,13 @@ export async function readDiskFile(realPath, opts) {
133
195
  const totalSize = st.size;
134
196
  const mtimeMs = st.mtimeMs;
135
197
  const range = opts?.range;
136
- let sliceStart = 0;
137
- let sliceLen = totalSize;
138
- if (range) {
139
- const { start, end } = range;
140
- if (!Number.isFinite(start) || !Number.isFinite(end)) {
141
- throw new RangeError(`invalid range: ${start}-${end}`);
142
- }
143
- if (start < 0)
144
- throw new RangeError(`range start out of bounds: ${start}`);
145
- if (start > end)
146
- throw new RangeError(`range start > end: ${start} > ${end}`);
147
- if (start >= totalSize) {
148
- throw new RangeError(`range start beyond file size: ${start} >= ${totalSize}`);
149
- }
150
- const clampedEnd = Math.min(end, totalSize - 1);
151
- sliceStart = start;
152
- sliceLen = clampedEnd - start + 1;
153
- }
154
- // Read at most the first 12 bytes for magic sniffing, regardless of
155
- // whether the caller asked for a range. We need the head bytes from
156
- // position 0 to label Content-Type correctly even on a tail Range
157
- // request. For full reads the head is part of the body so we re-use
158
- // it without a second read.
159
- const head = Buffer.alloc(Math.min(12, totalSize));
160
- if (head.length > 0) {
161
- await handle.read(head, 0, head.length, 0);
162
- }
163
- let bytes;
164
- if (!range) {
165
- bytes = Buffer.alloc(totalSize);
166
- if (totalSize > 0) {
167
- if (totalSize <= head.length) {
168
- head.copy(bytes, 0, 0, totalSize);
169
- }
170
- else {
171
- head.copy(bytes, 0, 0, head.length);
172
- await handle.read(bytes, head.length, totalSize - head.length, head.length);
173
- }
174
- }
175
- }
176
- else {
177
- bytes = Buffer.alloc(sliceLen);
178
- if (sliceLen > 0) {
179
- await handle.read(bytes, 0, sliceLen, sliceStart);
180
- }
181
- }
198
+ const { sliceStart, sliceLen } = range
199
+ ? resolveRange(range, totalSize)
200
+ : { sliceStart: 0, sliceLen: totalSize };
201
+ const head = await readHeadBytes(handle, totalSize);
202
+ const bytes = range
203
+ ? await readRangeBody(handle, sliceStart, sliceLen)
204
+ : await readFullBody(handle, head, totalSize);
182
205
  return {
183
206
  bytes,
184
207
  mime: detectMime(realPath, head),
@@ -19,6 +19,12 @@ import { session } from 'electron';
19
19
  import { toDisposable } from '@dimina-kit/electron-deck/main';
20
20
  import { getSimulatorServicewechatReferer } from '../simulator/referer.js';
21
21
  import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from './miniapp-partition.js';
22
+ /** The response headers this policy owns outright (compared lowercase). */
23
+ const CORS_OVERRIDE_HEADERS = new Set([
24
+ 'access-control-allow-origin',
25
+ 'access-control-allow-headers',
26
+ 'access-control-allow-methods',
27
+ ]);
22
28
  /** Apply the simulator runtime's referer + CORS webRequest policy to one
23
29
  * session. Each session installs its own listeners (a webRequest listener is
24
30
  * per-session), so this runs once per partition. */
@@ -35,6 +41,17 @@ function applySimulatorWebRequestPolicy(simulatorSession) {
35
41
  // CORS for the native render/service hosts to fetch compiled app resources
36
42
  // cross-origin. (The COOP/COEP cross-origin-isolation headers were only for
37
43
  // the removed default-path SharedArrayBuffer sync Worker — dropped.)
44
+ //
45
+ // This policy is the single authority for these three headers: any value
46
+ // the server itself sent must be REPLACED, not shadowed. responseHeaders
47
+ // keys keep the server's original casing, so assigning the lowercase key
48
+ // beside an existing `Access-Control-Allow-Origin` puts BOTH on the wire —
49
+ // Chromium then rejects the response for carrying multiple ACAO values
50
+ // (`*, *`), breaking every fetch against a CORS-compliant backend.
51
+ for (const key of Object.keys(headers)) {
52
+ if (CORS_OVERRIDE_HEADERS.has(key.toLowerCase()))
53
+ delete headers[key];
54
+ }
38
55
  headers['access-control-allow-origin'] = ['*'];
39
56
  headers['access-control-allow-headers'] = ['*'];
40
57
  headers['access-control-allow-methods'] = ['*'];
@@ -188,6 +188,59 @@ function fsErrorStatus(e) {
188
188
  return 404;
189
189
  return 500;
190
190
  }
191
+ async function fsStat(ctx) {
192
+ const st = await statWithin(ctx.projectRoot, ctx.rel);
193
+ jsonRes(ctx.res, 200, { type: st.isDirectory() ? 2 : 1, size: st.size, ctime: st.ctimeMs, mtime: st.mtimeMs });
194
+ }
195
+ async function fsReaddir(ctx) {
196
+ const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
197
+ jsonRes(ctx.res, 200, entries.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
198
+ }
199
+ async function fsRead(ctx) {
200
+ const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
201
+ ctx.res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': buf.length });
202
+ ctx.res.end(buf);
203
+ }
204
+ async function fsWrite(ctx) {
205
+ let buf;
206
+ try {
207
+ buf = await readBody(ctx.req);
208
+ }
209
+ catch (e) {
210
+ if (e instanceof BodyTooLargeError)
211
+ return jsonRes(ctx.res, 413, { error: e.message });
212
+ throw e;
213
+ }
214
+ await writeFileWithin(ctx.projectRoot, ctx.rel, buf);
215
+ ctx.res.writeHead(204);
216
+ ctx.res.end();
217
+ }
218
+ async function fsMkdir(ctx) {
219
+ await mkdirWithin(ctx.projectRoot, ctx.rel);
220
+ ctx.res.writeHead(204);
221
+ ctx.res.end();
222
+ }
223
+ async function fsDelete(ctx) {
224
+ await deleteWithin(ctx.projectRoot, ctx.rel);
225
+ ctx.res.writeHead(204);
226
+ ctx.res.end();
227
+ }
228
+ async function fsRename(ctx) {
229
+ const toRel = (ctx.url.searchParams.get('to') ?? '').replace(/^\/+/, '');
230
+ await renameWithin(ctx.projectRoot, ctx.rel, toRel);
231
+ ctx.res.writeHead(204);
232
+ ctx.res.end();
233
+ }
234
+ /** One handler per supported `/__fs/<action>` — keyed lookup replaces an if-chain. */
235
+ const FS_ACTIONS = {
236
+ stat: fsStat,
237
+ readdir: fsReaddir,
238
+ read: fsRead,
239
+ write: fsWrite,
240
+ mkdir: fsMkdir,
241
+ delete: fsDelete,
242
+ rename: fsRename,
243
+ };
191
244
  /**
192
245
  * `/__fs/<action>?p=<rel>` bridge onto the live active project root.
193
246
  * `p`/`to` are POSIX paths relative to the project root, resolved through the
@@ -205,54 +258,14 @@ async function handleFsRequest(req, res, projectRoot, url) {
205
258
  return jsonRes(res, rejectStatus, { error: 'mutating fs action requires a non-GET same-origin request' });
206
259
  }
207
260
  }
208
- try {
209
- if (action === 'stat') {
210
- const st = await statWithin(projectRoot, rel);
211
- return jsonRes(res, 200, { type: st.isDirectory() ? 2 : 1, size: st.size, ctime: st.ctimeMs, mtime: st.mtimeMs });
212
- }
213
- if (action === 'readdir') {
214
- const entries = await readdirWithin(projectRoot, rel);
215
- return jsonRes(res, 200, entries.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
216
- }
217
- if (action === 'read') {
218
- const buf = await readFileBufferWithin(projectRoot, rel);
219
- res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': buf.length });
220
- return void res.end(buf);
221
- }
222
- if (action === 'write') {
223
- let buf;
224
- try {
225
- buf = await readBody(req);
226
- }
227
- catch (e) {
228
- if (e instanceof BodyTooLargeError)
229
- return jsonRes(res, 413, { error: e.message });
230
- throw e;
231
- }
232
- await writeFileWithin(projectRoot, rel, buf);
233
- res.writeHead(204);
234
- return void res.end();
235
- }
236
- if (action === 'mkdir') {
237
- await mkdirWithin(projectRoot, rel);
238
- res.writeHead(204);
239
- return void res.end();
240
- }
241
- if (action === 'delete') {
242
- await deleteWithin(projectRoot, rel);
243
- res.writeHead(204);
244
- return void res.end();
245
- }
246
- if (action === 'rename') {
247
- const toRel = (url.searchParams.get('to') ?? '').replace(/^\/+/, '');
248
- await renameWithin(projectRoot, rel, toRel);
249
- res.writeHead(204);
250
- return void res.end();
251
- }
261
+ const handler = FS_ACTIONS[action];
262
+ if (!handler)
252
263
  return jsonRes(res, 404, { error: 'unknown fs action: ' + action });
264
+ try {
265
+ await handler({ req, res, projectRoot, rel, url });
253
266
  }
254
267
  catch (e) {
255
- return jsonRes(res, fsErrorStatus(e), { error: String(e.message), code: e.code });
268
+ jsonRes(res, fsErrorStatus(e), { error: String(e.message), code: e.code });
256
269
  }
257
270
  }
258
271
  /**
@@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron';
2
2
  import type { CompilationAdapter, CustomFileTypes, WorkbenchConfig } from '../../shared/types.js';
3
3
  import type { BridgeRouterHandle } from '../ipc/bridge-router.js';
4
4
  import type { ConsoleForwarder } from './console-forward/index.js';
5
+ import type { DiagnosticsBus } from './diagnostics/index.js';
5
6
  import type { NetworkForwarder } from './network-forward/index.js';
6
7
  import type { AppDataTap } from './simulator-appdata/index.js';
7
8
  import type { StorageApi } from './simulator-storage/index.js';
@@ -167,6 +168,15 @@ export interface WorkbenchContext {
167
168
  * installed.
168
169
  */
169
170
  consoleForwarder?: ConsoleForwarder;
171
+ /**
172
+ * Authoritative diagnostics bus for main-synthesized developer diagnostics
173
+ * (page-not-found, logic-bundle-unreachable, service-host-navigation-failed,
174
+ * …), set by `installBridgeRouter`. `consoleForwarder` subscribes to it so
175
+ * every diagnostic also surfaces in the owning session's embedded DevTools
176
+ * Console panel, not just the main-process log. Undefined until the bridge
177
+ * router is installed.
178
+ */
179
+ diagnostics?: DiagnosticsBus;
170
180
  /**
171
181
  * Native-host network forwarder, set in app bootstrap. Attaches the CDP
172
182
  * debugger to the simulator WCV (where `wx.request`/`downloadFile`/`uploadFile`
@@ -38,8 +38,8 @@ export function createWorkspaceService(ctx) {
38
38
  // wins request tokens, so a close mid-open can't disposeAll() the views the open
39
39
  // is building or desync currentSession vs the bridge appSessions. See op-lock.ts.
40
40
  const opLock = createOpLock();
41
- function sendStatus(status, message, hotReload) {
42
- ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
41
+ function sendStatus(status, message, hotReload, pages, watcherDead) {
42
+ ctx.notify.projectStatus({ status, message, ...(hotReload ? { hotReload: true } : {}), ...(pages ? { pages } : {}), ...(watcherDead ? { watcher: 'dead' } : {}) });
43
43
  }
44
44
  function bestEffort(label, fn) {
45
45
  try {
@@ -127,8 +127,17 @@ export function createWorkspaceService(ctx) {
127
127
  sourcemap: true,
128
128
  fileTypes: ctx.fileTypes,
129
129
  watch: compile.watch,
130
- onRebuild: () => sendStatus('ready', '编译完成,已重启', true),
130
+ onRebuild: () => {
131
+ // getProjectPages never throws — failure yields the empty-pages
132
+ // shape. Empty means "read failed or degenerate project" (a valid
133
+ // app.json names ≥1 page) and is withheld so the renderer keeps its
134
+ // previous dropdown instead of blanking; the push is never blocked.
135
+ const { pages } = repo.getProjectPages(projectPath);
136
+ sendStatus('ready', '编译完成,已重启', true, pages.length ? pages : undefined);
137
+ },
131
138
  onBuildError: (err) => sendStatus('error', String(err)),
139
+ // Watcher died mid-session (EMFILE, permission loss, …): non-fatal, so 'ready' stays but `watcher: 'dead'` flags that saves no longer auto-rebuild.
140
+ onWatcherError: () => sendStatus('ready', '文件监听已停止,保存不再触发自动编译', false, undefined, true),
132
141
  onLog: (entry) => {
133
142
  if (sessionGeneration !== logGeneration)
134
143
  return;
@@ -1,15 +1,8 @@
1
- type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
- export interface Logger {
3
- debug(message: string, ...args: unknown[]): void;
4
- info(message: string, ...args: unknown[]): void;
5
- warn(message: string, ...args: unknown[]): void;
6
- error(message: string, ...args: unknown[]): void;
7
- }
8
1
  /**
9
- * Create a tagged logger for a specific module.
10
- * All output goes to console with structured prefix.
2
+ * Tagged console logger. The implementation lives in
3
+ * `@dimina-kit/electron-deck/main` (single source the two packages share one
4
+ * logger); this module re-exports it so devtools-internal imports keep their
5
+ * stable `utils/logger` path.
11
6
  */
12
- export declare function createLogger(tag: string): Logger;
13
- export declare function setLogLevel(level: LogLevel): void;
14
- export {};
7
+ export { createLogger, setLogLevel, type Logger } from '@dimina-kit/electron-deck/main';
15
8
  //# sourceMappingURL=logger.d.ts.map
@@ -1,48 +1,8 @@
1
- const LEVEL_PRIORITY = {
2
- debug: 0,
3
- info: 1,
4
- warn: 2,
5
- error: 3,
6
- };
7
- const LEVEL_PREFIX = {
8
- debug: '[DEBUG]',
9
- info: '[INFO]',
10
- warn: '[WARN]',
11
- error: '[ERROR]',
12
- };
13
- let currentLevel = 'debug';
14
- function shouldLog(level) {
15
- return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[currentLevel];
16
- }
17
- function formatMessage(level, tag, message) {
18
- const ts = new Date().toISOString();
19
- return `${ts} ${LEVEL_PREFIX[level]} [${tag}] ${message}`;
20
- }
21
1
  /**
22
- * Create a tagged logger for a specific module.
23
- * All output goes to console with structured prefix.
2
+ * Tagged console logger. The implementation lives in
3
+ * `@dimina-kit/electron-deck/main` (single source the two packages share one
4
+ * logger); this module re-exports it so devtools-internal imports keep their
5
+ * stable `utils/logger` path.
24
6
  */
25
- export function createLogger(tag) {
26
- return {
27
- debug(message, ...args) {
28
- if (shouldLog('debug'))
29
- console.debug(formatMessage('debug', tag, message), ...args);
30
- },
31
- info(message, ...args) {
32
- if (shouldLog('info'))
33
- console.info(formatMessage('info', tag, message), ...args);
34
- },
35
- warn(message, ...args) {
36
- if (shouldLog('warn'))
37
- console.warn(formatMessage('warn', tag, message), ...args);
38
- },
39
- error(message, ...args) {
40
- if (shouldLog('error'))
41
- console.error(formatMessage('error', tag, message), ...args);
42
- },
43
- };
44
- }
45
- export function setLogLevel(level) {
46
- currentLevel = level;
47
- }
7
+ export { createLogger, setLogLevel } from '@dimina-kit/electron-deck/main';
48
8
  //# sourceMappingURL=logger.js.map
@@ -28,6 +28,13 @@ export interface ServiceHostWindowOptions {
28
28
  * which page logic referencing `qd.*` throws `ReferenceError: qd is not defined`.
29
29
  */
30
30
  apiNamespaces?: string[];
31
+ /**
32
+ * Observes a spawn-navigation `loadURL` rejection (NOT encoded into the
33
+ * spawn URL). Without it the fresh-window path swallows the failure entirely
34
+ * — `did-finish-load` never fires, the session hangs, and the only signal is
35
+ * the launch watchdog's late timeout instead of the real cause.
36
+ */
37
+ onLoadFailed?: (err: unknown) => void;
31
38
  }
32
39
  /**
33
40
  * Construct an (un-navigated) service-host BrowserWindow. Split out from
@@ -52,8 +59,15 @@ export declare function buildServiceHostSpawnUrl(opts: ServiceHostWindowOptions)
52
59
  * preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
53
60
  * Returns the load promise (resolves on did-finish-load). Attaches the dev-only
54
61
  * detached-DevTools hook, matching the original behavior.
62
+ *
63
+ * A `loadURL` rejection is swallowed — the pooled-window recycle path expects
64
+ * a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
65
+ * given, still observes the original rejection reason (routed by the caller
66
+ * into the diagnostics bus) before this resolves.
55
67
  */
56
- export declare function navigateServiceHost(win: BrowserWindow, url: string): Promise<void>;
68
+ export declare function navigateServiceHost(win: BrowserWindow, url: string, opts?: {
69
+ onLoadFailed?: (err: unknown) => void;
70
+ }): Promise<void>;
57
71
  /**
58
72
  * The spec a `ServiceHostPool` uses to pre-warm service-host windows. Keeps the
59
73
  * pooled window byte-equivalent to `constructServiceHostWindow`'s output, and
@@ -72,9 +72,17 @@ export function buildServiceHostSpawnUrl(opts) {
72
72
  * preload re-evaluates on this navigation, rebuilding `__diminaSpawnContext`.
73
73
  * Returns the load promise (resolves on did-finish-load). Attaches the dev-only
74
74
  * detached-DevTools hook, matching the original behavior.
75
+ *
76
+ * A `loadURL` rejection is swallowed — the pooled-window recycle path expects
77
+ * a failed navigation not to reject the caller — but `opts.onLoadFailed`, when
78
+ * given, still observes the original rejection reason (routed by the caller
79
+ * into the diagnostics bus) before this resolves.
75
80
  */
76
- export function navigateServiceHost(win, url) {
77
- const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, () => undefined);
81
+ export function navigateServiceHost(win, url, opts) {
82
+ const loaded = Promise.resolve(win.loadURL(url)).then(() => undefined, (err) => {
83
+ opts?.onLoadFailed?.(err);
84
+ return undefined;
85
+ });
78
86
  if (!app.isPackaged && process.env.NODE_ENV !== 'test') {
79
87
  // Recycled pooled windows get navigated repeatedly. A `once` hook that never
80
88
  // fires (the spawn was disposed before its service.html settled) stays on the
@@ -141,7 +149,7 @@ export function createServiceHostWindow(opts) {
141
149
  // so its logic-layer localStorage/cookies are shared with the project's render
142
150
  // side only, never with other projects.
143
151
  const win = constructServiceHostWindow({ appId: opts.appId, partition: miniappPartition(opts.appId, opts.projectPath) });
144
- void navigateServiceHost(win, buildServiceHostSpawnUrl(opts));
152
+ void navigateServiceHost(win, buildServiceHostSpawnUrl(opts), { onLoadFailed: opts.onLoadFailed });
145
153
  return win;
146
154
  }
147
155
  //# sourceMappingURL=create.js.map