miaoda-expo-devkit 0.1.1-beta.99 → 0.1.1-dev.development-build.2

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 (48) hide show
  1. package/README.md +163 -111
  2. package/dist/babel/plugin-lucide-react-native.js +185 -185
  3. package/dist/cli/lint.js +317 -309
  4. package/dist/index.d.mts +1 -110
  5. package/dist/index.d.ts +1 -110
  6. package/dist/index.js +0 -159
  7. package/dist/index.mjs +0 -132
  8. package/dist/metro.d.mts +25 -5
  9. package/dist/metro.d.ts +25 -5
  10. package/dist/metro.js +49 -18
  11. package/dist/metro.mjs +50 -20
  12. package/dist/rules/no-bare-web-crypto.js +235 -0
  13. package/dist/rules/no-expo-audio-compat.js +90 -90
  14. package/dist/rules/no-expo-file-system-compat.js +2130 -0
  15. package/dist/rules/no-expo-video-compat.js +90 -90
  16. package/dist/rules/no-lucide-star-import.js +78 -0
  17. package/dist/rules/no-sentry-default-import.js +83 -0
  18. package/dist/rules/no-typeof-window-check.js +85 -0
  19. package/dist/stubs/entry-inject.js +0 -2
  20. package/dist/stubs/expo-native-wechat-stub.js +116 -0
  21. package/dist/stubs/expo-router-entry-native-stub.js +4 -0
  22. package/dist/stubs/i18n/en.js +16 -1
  23. package/dist/stubs/i18n/zh.js +16 -1
  24. package/dist/stubs/logbox-control.js +9 -0
  25. package/dist/stubs/screenshot-control.js +1 -1
  26. package/dist/stubs/sentry-react-native-stub.js +15 -180
  27. package/oxlint-config.json +20 -5
  28. package/package.json +65 -5
  29. package/pnpm-patches/@expo__cli@55.0.16-55.0.22.patch +712 -0
  30. package/pnpm-patches/@expo__cli@55.0.16.patch +218 -0
  31. package/pnpm-patches/@expo__cli@55.0.17.patch +218 -0
  32. package/pnpm-patches/@expo__cli@55.0.18.patch +218 -0
  33. package/pnpm-patches/@expo__cli@55.0.19.patch +218 -0
  34. package/pnpm-patches/@expo__cli@55.0.20.patch +218 -0
  35. package/pnpm-patches/@expo__cli@55.0.21.patch +218 -0
  36. package/pnpm-patches/@expo__cli@55.0.22.patch +218 -0
  37. package/pnpm-patches/@shopify__react-native-skia@2.4.18.patch +14 -0
  38. package/pnpm-patches/expo-modules-core@55.0.15.patch +21 -0
  39. package/pnpm-patches/expo@55.0.6.patch +283 -0
  40. package/pnpm-patches/metro-file-map@0.83.3.patch +28 -0
  41. package/pnpm-patches/metro-runtime@0.83.3.patch +17 -0
  42. package/pnpm-patches/react-native-css-interop@0.2.6.patch +12 -0
  43. package/pnpm-patches/react-native-reanimated@4.2.1.patch +53 -0
  44. package/pnpm-patches/react-native-worklets@0.7.2.patch +91 -0
  45. package/pnpm-patches/react-native-worklets@0.7.4.patch +91 -0
  46. package/pnpm-patches/react-native@0.83.2.patch +384 -0
  47. package/pnpmfile.cjs +251 -0
  48. package/dist/stubs/preview-control.js +0 -60
@@ -0,0 +1,712 @@
1
+ diff --git a/build/src/install/index.js b/build/src/install/index.js
2
+ index a374fca..d1229c1 100644
3
+ --- a/build/src/install/index.js
4
+ +++ b/build/src/install/index.js
5
+ @@ -55,10 +55,20 @@ const expoInstall = async (argv)=>{
6
+ }
7
+ // Load modules after the help prompt so `npx expo install -h` shows as fast as possible.
8
+ const { installAsync } = require('./installAsync');
9
+ - const { logCmdError } = require('../utils/errors');
10
+ + const { logCmdError, SilentError } = require('../utils/errors');
11
+ const { resolveArgsAsync } = require('./resolveOptions');
12
+ const { variadic, options, extras } = await resolveArgsAsync(process.argv.slice(3)).catch(logCmdError);
13
+ - return installAsync(variadic, options, extras).catch(logCmdError);
14
+ + return installAsync(variadic, options, extras).catch((error)=>{
15
+ + if (
16
+ + error &&
17
+ + typeof error === 'object' &&
18
+ + typeof error.status === 'number' &&
19
+ + Array.isArray(error.output)
20
+ + ) {
21
+ + return logCmdError(new SilentError(error));
22
+ + }
23
+ + return logCmdError(error);
24
+ + });
25
+ };
26
+
27
+ //# sourceMappingURL=index.js.map
28
+
29
+ diff --git a/build/src/start/server/metro/aiErrorCollector.js b/build/src/start/server/metro/aiErrorCollector.js
30
+ new file mode 100644
31
+ index 0000000..febccaf
32
+ --- /dev/null
33
+ +++ b/build/src/start/server/metro/aiErrorCollector.js
34
+ @@ -0,0 +1,222 @@
35
+ +"use strict";
36
+ +Object.defineProperty(exports, "__esModule", {
37
+ + value: true
38
+ +});
39
+ +Object.defineProperty(exports, "createAiErrorCollector", {
40
+ + enumerable: true,
41
+ + get: function() {
42
+ + return createAiErrorCollector;
43
+ + }
44
+ +});
45
+ +const fs = require("fs");
46
+ +const os = require("os");
47
+ +const path = require("path");
48
+ +
49
+ +const MAX_VALUE_DEPTH = 4;
50
+ +const MAX_STRING_LENGTH = 20000;
51
+ +
52
+ +function isEnabled() {
53
+ + return process.env.EXPO_AI_ERROR_COLLECTOR === '1';
54
+ +}
55
+ +
56
+ +function getOutputPath(projectRoot) {
57
+ + const configured = process.env.EXPO_AI_ERROR_FILE;
58
+ + if (!configured) return path.join(projectRoot, 'node_modules', '.miaoda-expo', 'ai-error.json');
59
+ + return path.isAbsolute(configured) ? configured : path.resolve(projectRoot, configured);
60
+ +}
61
+ +
62
+ +function toSerializable(value, depth = 0, seen = new Set()) {
63
+ + if (value == null || typeof value === 'boolean' || typeof value === 'number') return value;
64
+ + if (typeof value === 'string') return value.length > MAX_STRING_LENGTH ? value.slice(0, MAX_STRING_LENGTH) + '...[truncated]' : value;
65
+ + if (depth >= MAX_VALUE_DEPTH) return '[truncated]';
66
+ + if (typeof value === 'function' || typeof value === 'symbol') return String(value);
67
+ + if (seen.has(value)) return '[circular]';
68
+ + seen.add(value);
69
+ + if (value instanceof Error) {
70
+ + const error = {
71
+ + name: value.name,
72
+ + message: value.message,
73
+ + stack: value.stack
74
+ + };
75
+ + for (const key of Object.keys(value)) error[key] = toSerializable(value[key], depth + 1, seen);
76
+ + seen.delete(value);
77
+ + return error;
78
+ + }
79
+ + if (Array.isArray(value)) {
80
+ + const result = value.map((item)=>toSerializable(item, depth + 1, seen));
81
+ + seen.delete(value);
82
+ + return result;
83
+ + }
84
+ + if (typeof value === 'object') {
85
+ + const result = {};
86
+ + for (const key of Object.keys(value)) result[key] = toSerializable(value[key], depth + 1, seen);
87
+ + seen.delete(value);
88
+ + return result;
89
+ + }
90
+ + return String(value);
91
+ +}
92
+ +
93
+ +function normalizeError(error, projectRoot, buildID) {
94
+ + const raw = error && typeof error === 'object' ? error : {
95
+ + message: String(error ?? 'Metro bundling failed')
96
+ + };
97
+ + const message = String(raw.message ?? raw.reason ?? error ?? 'Metro bundling failed');
98
+ + const record = {
99
+ + schemaVersion: 1,
100
+ + id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
101
+ + timestamp: new Date().toISOString(),
102
+ + source: 'metro',
103
+ + eventType: 'bundling_error',
104
+ + buildID: buildID ?? undefined,
105
+ + projectRoot,
106
+ + type: String(raw.type ?? raw.name ?? 'MetroError'),
107
+ + name: raw.name == null ? undefined : String(raw.name),
108
+ + message,
109
+ + filename: raw.filename == null ? undefined : String(raw.filename),
110
+ + lineNumber: typeof raw.lineNumber === 'number' ? raw.lineNumber : undefined,
111
+ + column: typeof raw.column === 'number' ? raw.column : undefined,
112
+ + errors: Array.isArray(raw.errors) ? toSerializable(raw.errors) : [],
113
+ + stack: raw.stack == null ? undefined : String(raw.stack),
114
+ + cause: raw.cause == null ? undefined : toSerializable(raw.cause)
115
+ + };
116
+ + const extra = {};
117
+ + for (const key of Object.keys(raw)) {
118
+ + if (!['type', 'name', 'message', 'filename', 'lineNumber', 'column', 'errors', 'stack', 'cause'].includes(key)) {
119
+ + extra[key] = toSerializable(raw[key]);
120
+ + }
121
+ + }
122
+ + if (Object.keys(extra).length) record.extra = extra;
123
+ + return omitUndefined(record);
124
+ +}
125
+ +
126
+ +function omitUndefined(value) {
127
+ + if (!value || typeof value !== 'object') return value;
128
+ + if (Array.isArray(value)) return value.map(omitUndefined);
129
+ + const result = {};
130
+ + for (const [key, item] of Object.entries(value)) {
131
+ + if (item !== undefined) result[key] = omitUndefined(item);
132
+ + }
133
+ + return result;
134
+ +}
135
+ +
136
+ +function createAiErrorCollector(projectRoot) {
137
+ + if (!isEnabled()) return null;
138
+ + const outputFile = getOutputPath(projectRoot);
139
+ + let activeBuildID = null;
140
+ + let lastFailedBuildID = null;
141
+ + let activeHmrChangeID = null;
142
+ + let state = {
143
+ + schemaVersion: 1,
144
+ + status: 'compiling',
145
+ + buildID: null,
146
+ + updatedAt: new Date().toISOString(),
147
+ + error: null
148
+ + };
149
+ + let warned = false;
150
+ + const publish = (nextState)=>{
151
+ + state = omitUndefined({ ...nextState, updatedAt: new Date().toISOString() });
152
+ + let temporaryFile;
153
+ + try {
154
+ + fs.mkdirSync(path.dirname(outputFile), { recursive: true });
155
+ + temporaryFile = `${outputFile}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
156
+ + fs.writeFileSync(temporaryFile, JSON.stringify(state, null, 2) + os.EOL, 'utf8');
157
+ + fs.renameSync(temporaryFile, outputFile);
158
+ + } catch (error) {
159
+ + try {
160
+ + if (temporaryFile) fs.rmSync(temporaryFile, { force: true });
161
+ + } catch {}
162
+ + if (!warned) {
163
+ + warned = true;
164
+ + console.warn(`[expo-ai-error] Could not write ${outputFile}: ${error.message}`);
165
+ + }
166
+ + }
167
+ + };
168
+ + const isCurrentBuild = (buildID, allowMissing = false)=>buildID == null ? allowMissing || activeBuildID == null : activeBuildID == null || buildID === activeBuildID;
169
+ + return {
170
+ + getState() {
171
+ + return state;
172
+ + },
173
+ + handleMetroEvent(event) {
174
+ + if (!event || typeof event !== 'object') return;
175
+ + const type = event.type;
176
+ + const buildID = event.buildID ?? event.bundleDetails?.buildID ?? null;
177
+ + if (type === 'hmr_update_started') {
178
+ + activeHmrChangeID = typeof event.changeID === 'string' ? event.changeID : null;
179
+ + lastFailedBuildID = null;
180
+ + publish({
181
+ + schemaVersion: 1,
182
+ + status: 'compiling',
183
+ + buildID: activeBuildID,
184
+ + error: null
185
+ + });
186
+ + return;
187
+ + }
188
+ + if (type === 'hmr_update_failed') {
189
+ + if (activeHmrChangeID == null || event.changeID !== activeHmrChangeID) return;
190
+ + publish({
191
+ + schemaVersion: 1,
192
+ + status: 'failed',
193
+ + buildID: activeBuildID,
194
+ + error: normalizeError(event.error, projectRoot, activeBuildID)
195
+ + });
196
+ + return;
197
+ + }
198
+ + if (type === 'hmr_update_done') {
199
+ + if (activeHmrChangeID == null || event.changeID !== activeHmrChangeID) return;
200
+ + activeHmrChangeID = null;
201
+ + publish({
202
+ + schemaVersion: 1,
203
+ + status: 'succeeded',
204
+ + buildID: activeBuildID,
205
+ + error: null
206
+ + });
207
+ + return;
208
+ + }
209
+ + if (type === 'bundle_build_started') {
210
+ + activeBuildID = buildID;
211
+ + activeHmrChangeID = null;
212
+ + lastFailedBuildID = null;
213
+ + publish({ schemaVersion: 1, status: 'compiling', buildID, error: null });
214
+ + return;
215
+ + }
216
+ + if (type === 'bundle_build_failed') {
217
+ + if (!isCurrentBuild(buildID)) return;
218
+ + lastFailedBuildID = buildID ?? activeBuildID;
219
+ + publish({
220
+ + schemaVersion: 1,
221
+ + status: 'failed',
222
+ + buildID: lastFailedBuildID,
223
+ + error: normalizeError(event.error ?? { type: 'MetroBundleError', message: 'Metro bundling failed' }, projectRoot, lastFailedBuildID)
224
+ + });
225
+ + return;
226
+ + }
227
+ + if (type === 'bundling_error') {
228
+ + const isHmrError = event.isHmr === true;
229
+ + if (isHmrError && activeHmrChangeID != null && event.changeID !== activeHmrChangeID) return;
230
+ + const errorBuildID = isHmrError ? activeBuildID : buildID ?? lastFailedBuildID ?? activeBuildID;
231
+ + if (!isCurrentBuild(errorBuildID, true)) return;
232
+ + if (isHmrError) {
233
+ + // HMR 错误不携带 Metro build ID,保留变更标识以匹配后续恢复事件。
234
+ + activeHmrChangeID = event.changeID ?? activeHmrChangeID;
235
+ + }
236
+ + lastFailedBuildID = errorBuildID;
237
+ +
238
+ + publish({
239
+ + schemaVersion: 1,
240
+ + status: 'failed',
241
+ + buildID: errorBuildID,
242
+ + error: normalizeError(event.error, projectRoot, errorBuildID)
243
+ + });
244
+ + return;
245
+ + }
246
+ + if (type === 'bundle_build_done') {
247
+ + if (!isCurrentBuild(buildID)) return;
248
+ + activeHmrChangeID = null;
249
+ + lastFailedBuildID = null;
250
+ + publish({ schemaVersion: 1, status: 'succeeded', buildID: buildID ?? activeBuildID, error: null });
251
+ + }
252
+ + }
253
+ + };
254
+ +}
255
+ +
256
+ +//# sourceMappingURL=aiErrorCollector.js.map
257
+ diff --git a/build/src/start/server/metro/instantiateMetro.js b/build/src/start/server/metro/instantiateMetro.js
258
+ index bd1d604..02db474 100644
259
+ --- a/build/src/start/server/metro/instantiateMetro.js
260
+ +++ b/build/src/start/server/metro/instantiateMetro.js
261
+ @@ -90,6 +90,8 @@ const _MetroTerminalReporter = require("./MetroTerminalReporter");
262
+ const _attachAtlas = require("./debugging/attachAtlas");
263
+ const _createDebugMiddleware = require("./debugging/createDebugMiddleware");
264
+ const _createMetroMiddleware = require("./dev-server/createMetroMiddleware");
265
+ +const _aiErrorCollector = require("./aiErrorCollector");
266
+ +const _miaodaHmrControl = require("./miaodaHmrControl");
267
+ const _runServerfork = require("./runServer-fork");
268
+ const _withMetroMultiPlatform = require("./withMetroMultiPlatform");
269
+ const _events = require("../../../events");
270
+ @@ -273,6 +275,7 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
271
+ const { middleware, messagesSocket, eventsSocket, websocketEndpoints } = (0, _createMetroMiddleware.createMetroMiddleware)(metroConfig, {
272
+ getMetroBundler
273
+ });
274
+ + const aiErrorCollector = !isExporting ? (0, _aiErrorCollector.createAiErrorCollector)(projectRoot) : null;
275
+ // Get local URL to Metro bundler server (typically configured as 127.0.0.1:8081)
276
+ const serverBaseUrl = metroBundler.getUrlCreator().constructUrl({
277
+ scheme: 'http',
278
+ @@ -347,7 +350,10 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
279
+ }
280
+ }), fileBuffer);
281
+ };
282
+ - setEventReporter(eventsSocket.reportMetroEvent);
283
+ + setEventReporter((event)=>{
284
+ + eventsSocket.reportMetroEvent(event);
285
+ + aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent(event);
286
+ + });
287
+ // This function ensures that modules in source maps are sorted in the same
288
+ // order as in a plain JS bundle.
289
+ metro._getSortedModules = function(graph) {
290
+ @@ -381,13 +387,24 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
291
+ // Fork of https://github.com/facebook/metro/blob/3b3e0aaf725cfa6907bf2c8b5fbc0da352d29efe/packages/metro/src/HmrServer.js#L327-L393
292
+ // with patch for `_createModuleId`.
293
+ const logger = !options.isInitialUpdate ? changeEvent == null ? void 0 : changeEvent.logger : null;
294
+ + const hmrChangeID = `${changeEvent == null ? 'initial' : changeEvent.changeId}:${group.revisionId}`;
295
+ + aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
296
+ + type: 'hmr_update_started',
297
+ + changeID: hmrChangeID
298
+ + });
299
+ try {
300
+ var _revision_graph_transformOptions_customTransformOptions;
301
+ const revPromise = this._bundler.getRevision(group.revisionId);
302
+ if (!revPromise) {
303
+ + const revisionError = new (_RevisionNotFoundError()).default(group.revisionId);
304
+ + aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
305
+ + type: 'hmr_update_failed',
306
+ + changeID: hmrChangeID,
307
+ + error: revisionError
308
+ + });
309
+ return {
310
+ type: 'error',
311
+ - body: (0, _formatBundlingError().default)(new (_RevisionNotFoundError()).default(group.revisionId))
312
+ + body: (0, _formatBundlingError().default)(revisionError)
313
+ };
314
+ }
315
+ logger == null ? void 0 : logger.point('updateGraph_start');
316
+ @@ -418,6 +435,10 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
317
+ serverRoot: this._config.server.unstable_serverRoot ?? this._config.projectRoot
318
+ });
319
+ logger == null ? void 0 : logger.point('serialize_end');
320
+ + aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
321
+ + type: 'hmr_update_done',
322
+ + changeID: hmrChangeID
323
+ + });
324
+ return {
325
+ type: 'update',
326
+ body: {
327
+ @@ -430,7 +451,9 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
328
+ const formattedError = (0, _formatBundlingError().default)(error);
329
+ this._config.reporter.update({
330
+ type: 'bundling_error',
331
+ - error
332
+ + error,
333
+ + isHmr: true,
334
+ + changeID: hmrChangeID
335
+ });
336
+ return {
337
+ type: 'error',
338
+ @@ -439,6 +462,16 @@ async function instantiateMetroAsync(metroBundler, options, { isExporting, exp =
339
+ }
340
+ };
341
+ }
342
+ + // Miaoda HMR 批量编辑控制的环境变量开关:只有显式设置
343
+ + // EXPO_MIAODA_HMR_CONTROL=1 才挂载控制端点与消息闸门,普通本地开发
344
+ + // 不受影响。stateFile 指定暂停状态的持久化位置,使编辑批次可以跨越
345
+ + // Metro 重启。注意此处位于 Expo 替换 HMR serializer 之后,attach 包装到
346
+ + // 的才是最终生效的 _prepareMessage。
347
+ + if (hmrServer && process.env.EXPO_MIAODA_HMR_CONTROL === '1') {
348
+ + hmrServer._miaodaAttach({
349
+ + stateFile: process.env.EXPO_MIAODA_HMR_STATE_FILE
350
+ + });
351
+ + }
352
+ return {
353
+ metro,
354
+ hmrServer,
355
+ diff --git a/build/src/start/server/metro/miaodaHmrControl.js b/build/src/start/server/metro/miaodaHmrControl.js
356
+ new file mode 100644
357
+ index 0000000..8d5d129
358
+ --- /dev/null
359
+ +++ b/build/src/start/server/metro/miaodaHmrControl.js
360
+ @@ -0,0 +1,326 @@
361
+ +"use strict";
362
+ +
363
+ +// Miaoda HMR 批量编辑控制,适配 SDK 55 / Metro 0.83.3。
364
+ +//
365
+ +// Agent 批量改写工程文件期间,本模块暂停已连接 Development Build 的 HMR 通道,
366
+ +// 让设备继续运行上一份完好的 JS,而不是执行中间态文件或展示其编译错误。
367
+ +// 批次结束后,一次 resume 只应用一份最终增量。
368
+ +//
369
+ +// 两条传输通道协作:
370
+ +// - 控制消息复用 Metro 已有的 `/hot` WebSocket,以
371
+ +// {type: 'miaoda-hmr-control', version: 1, action} 帧传输(pause / resume /
372
+ +// reload / error)。pause 携带 `id`,客户端在运行时停止应用更新后回复
373
+ +// {type: 'miaoda-hmr-ack', version: 1, id},因此 pause 成功即意味着
374
+ +// 不会再有在途更新落到设备上。
375
+ +// - 客户端每 2 秒轮询 GET __hmr_status,作为 WebSocket 消息丢失时的兜底。
376
+ +// 每次状态变化都会递增 `stateVersion`;轮询方若发现一个从未通过 socket
377
+ +// 收到的版本号,则执行完整 Bundle reload 而不是 resume,因为此时已无法
378
+ +// 证明增量 revision 的连续性。
379
+ +//
380
+ +// 本模块整体遵循 fail-closed:ack 超时(504)、未协商协议的老客户端(409)、
381
+ +// watcher 栅栏缺失或持久化失败(503)、最终编译错误(422)都会让闸门保持
382
+ +// 暂停,任何一次调用失败都不会把写了一半的代码放到设备上。
383
+ +//
384
+ +// 必须在 Expo 替换 HMR serializer 之后再 attach(见 instantiateMetro.js);
385
+ +// 提前包装会包住错误的 _prepareMessage。
386
+ +const fs = require('node:fs');
387
+ +const path = require('node:path');
388
+ +const CONTROL = 'miaoda-hmr-control';
389
+ +const ROUTES = new Map([
390
+ + ['/innerapi/v1/sourcecode/__hmr_off', 'pause'],
391
+ + ['/innerapi/v1/sourcecode/__hmr_on', 'resume'],
392
+ + ['/innerapi/v1/sourcecode/__hmr_reload', 'reload'],
393
+ + ['/innerapi/v1/sourcecode/__hmr_status', 'status'],
394
+ +]);
395
+ +
396
+ +function attachMiaodaHmrControl({hmrServer: hmr, metro, middleware, stateFile, timeout = 5000}) {
397
+ + let paused = false;
398
+ + let reloadPending = false;
399
+ + let stateVersion = 0;
400
+ + let lastMode = null;
401
+ + // 一次编辑批次可能跨越 Metro 重启:状态文件让新进程恢复暂停状态,
402
+ + // 继续闸门重连上来的客户端。
403
+ + if (stateFile && fs.existsSync(stateFile)) {
404
+ + try {
405
+ + const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
406
+ + paused = state.paused === true;
407
+ + reloadPending = state.reloadPending === true;
408
+ + stateVersion = Number.isInteger(state.stateVersion) ? state.stateVersion : 0;
409
+ + lastMode = typeof state.lastMode === 'string' ? state.lastMode : null;
410
+ + } catch {
411
+ + // 损坏的状态文件绝不能放行一个未完成的编辑批次。
412
+ + paused = true;
413
+ + }
414
+ + }
415
+ + // client -> {send, version, registered, buffer, ack}。`send` 是原始 socket
416
+ + // 写入器;HMR 输出走 onClientConnect 里安装的受控包装。`version` 在客户端
417
+ + // 通过 register-entrypoints 协商控制协议后为 1,未协商的老客户端为 null。
418
+ + const clients = new Map();
419
+ + const groupTasks = new WeakMap();
420
+ + const results = new WeakMap();
421
+ + const registrations = new Set();
422
+ + let serial = Promise.resolve();
423
+ + let controlId = 0;
424
+ + let restoring = null;
425
+ +
426
+ + // 崩溃安全的原子写:临时文件 + rename。调用方依赖「持久化状态绝不比
427
+ + // 已告知客户端的内容更新」这一不变量(见 run())。
428
+ + function persist() {
429
+ + if (!stateFile) return;
430
+ + fs.mkdirSync(path.dirname(stateFile), {recursive: true});
431
+ + const temp = `${stateFile}.${process.pid}.tmp`;
432
+ + fs.writeFileSync(temp, JSON.stringify({version: 1, paused, reloadPending, stateVersion, lastMode}));
433
+ + fs.renameSync(temp, stateFile);
434
+ + }
435
+ + // 控制帧只发给已协商的客户端——未协商的客户端不认识这些帧。
436
+ + // stateVersion 随帧捎带,供轮询兜底检测是否有丢失的帧。
437
+ + function control(info, action, extra = {}) {
438
+ + if (info.version === 1) info.send(JSON.stringify({type: CONTROL, version: 1, action, stateVersion, ...extra}));
439
+ + }
440
+ + function transition(mode) {
441
+ + stateVersion += 1;
442
+ + lastMode = mode;
443
+ + }
444
+ + function status() {
445
+ + return {status: 0, paused, reloadPending, stateVersion, lastMode, protocolVersion: 1};
446
+ + }
447
+ + // 单客户端暂停握手:客户端必须在运行时停止应用更新后 ack 这个 id,
448
+ + // 以此证明不会再有缓冲的更新落到设备上。
449
+ + function pauseClient(info) {
450
+ + if (info.version !== 1 || info.ack) return;
451
+ + const id = ++controlId;
452
+ + let resolve;
453
+ + const promise = new Promise(done => { resolve = done; });
454
+ + info.ack = {id, promise, resolve};
455
+ + control(info, 'pause', {id});
456
+ + }
457
+ + function fail(status, message) {
458
+ + const error = new Error(message);
459
+ + error.status = status;
460
+ + throw error;
461
+ + }
462
+ + // 只有当所有已协商客户端都确认后,暂停才算完成。
463
+ + // Agent 必须等它 resolve 之后才能开始写文件。
464
+ + async function awaitPaused() {
465
+ + if ([...clients.values()].some(info => info.registered && info.version !== 1)) {
466
+ + fail(409, 'Connected client lacks Miaoda HMR protocol v1; update its JS dependencies and reload.');
467
+ + }
468
+ + for (const info of clients.values()) pauseClient(info);
469
+ + let timer;
470
+ + try {
471
+ + await Promise.race([
472
+ + Promise.all([...clients.values()].filter(info => info.ack).map(info => info.ack.promise)),
473
+ + new Promise((_, reject) => {
474
+ + timer = setTimeout(() => {
475
+ + const error = new Error('Timed out waiting for HMR pause acknowledgement; HMR remains paused.');
476
+ + error.status = 504;
477
+ + reject(error);
478
+ + }, timeout);
479
+ + }),
480
+ + ]);
481
+ + } finally { clearTimeout(timer); }
482
+ + }
483
+ +
484
+ + // 立即闸门输出,包括 pause 到达时已在编译中的更新:paused 期间 HMR 帧
485
+ + // (update-start/update/update-done/error)按客户端缓冲而不发送。
486
+ + // 控制帧和日志不受闸门限制;缓冲在 resume 时回放,客户端仍能收到基于
487
+ + // 其最后应用 revision 计算的最终增量。
488
+ + const connect = hmr.onClientConnect.bind(hmr);
489
+ + hmr.onClientConnect = async (url, send) => {
490
+ + const info = {send, version: null, registered: false, buffer: [], ack: null};
491
+ + const client = await connect(url, message => {
492
+ + const type = JSON.parse(String(message)).type;
493
+ + if (paused && ['update-start', 'update', 'update-done', 'error'].includes(type)) {
494
+ + info.buffer.push(message);
495
+ + } else send(message);
496
+ + });
497
+ + clients.set(client, info);
498
+ + return client;
499
+ + };
500
+ + const onMessage = hmr.onClientMessage.bind(hmr);
501
+ + hmr.onClientMessage = async (client, message, send) => {
502
+ + let data;
503
+ + try { data = JSON.parse(String(message)); } catch { return onMessage(client, message, send); }
504
+ + const info = clients.get(client);
505
+ + if (data?.type === 'miaoda-hmr-ack' && info) {
506
+ + if (data.version === 1 && data.id === info.ack?.id) info.ack.resolve();
507
+ + return;
508
+ + }
509
+ + if (data?.type === 'register-entrypoints' && info) {
510
+ + // resume 进行中注册的客户端,绝不能先于 resume 提交状态迁移就看到
511
+ + // 已释放的缓冲。
512
+ + if (restoring) await restoring;
513
+ + info.registered = true;
514
+ + info.version = data.miaodaHmr === 1 ? 1 : null;
515
+ + // 在暂停批次中(重)连入的客户端立即被暂停;连入空闲服务端的客户端
516
+ + // 会收到一条 resume,用来清除上一次会话遗留的本地闸门。
517
+ + if (paused) pauseClient(info);
518
+ + else control(info, 'resume');
519
+ + }
520
+ + // 注册错误与图更新一样走受控发送器。
521
+ + // 跟踪注册任务,resume 才能先等新连入客户端的初始图构建完成,再计算最终增量。
522
+ + const task = onMessage(client, message, client.sendFn);
523
+ + if (data?.type !== 'register-entrypoints') return task;
524
+ + registrations.add(task);
525
+ + try { return await task; } finally { registrations.delete(task); }
526
+ + };
527
+ + const disconnect = hmr.onClientDisconnect.bind(hmr);
528
+ + hmr.onClientDisconnect = client => {
529
+ + // 已断开的客户端不可能再应用更新,直接 resolve 它的挂起 ack,
530
+ + // 避免 pause 干等到 504 超时。
531
+ + clients.get(client)?.ack?.resolve();
532
+ + clients.delete(client);
533
+ + return disconnect(client);
534
+ + };
535
+ +
536
+ + // 记录每个分组的构建结果,resume 据此检查最终编译结果,
537
+ + // 在 fast-refresh、reload 与 422 之间做决策。
538
+ + const prepare = hmr._prepareMessage.bind(hmr);
539
+ + hmr._prepareMessage = async (...args) => {
540
+ + const result = await prepare(...args);
541
+ + results.set(args[0], result);
542
+ + return result;
543
+ + };
544
+ + const handle = hmr._handleFileChange.bind(hmr);
545
+ + // 文件变更按分组严格串行处理。paused 时跳过 handler,但这只闸门「投递」:
546
+ + // 各分组的 DeltaCalculator 内部仍在持续累积文件事件,客户端运行时也会合并
547
+ + // disable 期间收到的更新,因此不会丢失任何中间态。
548
+ + function enqueue(group, options, event, force = false) {
549
+ + const previous = groupTasks.get(group) || Promise.resolve();
550
+ + const task = previous.catch(() => {}).then(async () => {
551
+ + // 保持 DeltaCalculator 的累积不变。绝不丢弃已构建的 revision。
552
+ + if (paused && !force) return;
553
+ + await handle(group, options, event);
554
+ + });
555
+ + groupTasks.set(group, task);
556
+ + return task;
557
+ + }
558
+ + hmr._handleFileChange = (group, options, event) => enqueue(group, options, event);
559
+ +
560
+ + async function run(action) {
561
+ + if (action === 'pause') {
562
+ + paused = true;
563
+ + transition('paused');
564
+ + persist();
565
+ + await awaitPaused();
566
+ + return {status: 0, paused, mode: 'paused'};
567
+ + }
568
+ + // 批次中途请求的 reload 会被推迟:暂停的意义就是不让中间态代码执行,
569
+ + // 而 reload 加载的恰恰就是这些中间态。
570
+ + if (action === 'reload' && paused) {
571
+ + reloadPending = true;
572
+ + transition('reload-deferred');
573
+ + persist();
574
+ + return {...status(), mode: 'reload-deferred'};
575
+ + }
576
+ + if (action === 'status') return status();
577
+ + if (action === 'resume' && !paused) return {...status(), mode: 'unchanged'};
578
+ + // resume 与 reload 共用前置的暂停握手:必须先把客户端静默下来再计算
579
+ + // 最终增量,否则仍可能放出一个基于写了一半的目录树编译的更新。
580
+ + paused = true;
581
+ + if (action === 'reload') reloadPending = true;
582
+ + transition(action === 'reload' ? 'reload-requested' : 'paused');
583
+ + persist();
584
+ + await awaitPaused();
585
+ + let release;
586
+ + restoring = new Promise(done => { release = done; });
587
+ + try {
588
+ + await Promise.all([...registrations]);
589
+ + const graph = await metro.getBundler().getBundler().getDependencyGraph();
590
+ + const fileMap = graph.getWatcher();
591
+ + if (typeof fileMap.miaodaFlushChanges !== 'function') {
592
+ + fail(503, 'Metro file-map barrier patch is missing; HMR remains paused.');
593
+ + }
594
+ + // Agent 通过调用 resume 表示「所有写入已完成」。栅栏让这一点对 Metro
595
+ + // 同样成立:watcher cookie -> file-map 处理队列 -> change 事件,
596
+ + // 保证下面的最终构建一定包含最后写入的字节。
597
+ + await fileMap.miaodaFlushChanges(timeout);
598
+ + const groups = [...new Set(hmr._clientGroups.values())];
599
+ + // 每个分组强制构建一次,使最终增量(以及任何编译错误)在放行客户端
600
+ + // 之前就算出来。
601
+ + await Promise.all(groups.map(group => enqueue(group, {isInitialUpdate: false}, undefined, true)));
602
+ + const errors = groups.map(group => results.get(group)).filter(result => result?.type === 'error');
603
+ + // 图/revision 丢失不是代码错误:增量链已断,唯一安全的放行方式是
604
+ + // 完整 Bundle reload。
605
+ + const staleGraph = error => ['GraphNotFoundError', 'RevisionNotFoundError'].includes(error?.type);
606
+ + const compileErrors = errors.filter(result => !staleGraph(result.body));
607
+ + if (compileErrors.length) {
608
+ + // 把最终错误告知客户端(设备端可以据此行动),但保持闸门关闭,
609
+ + // 让 Agent 修好代码后重试。
610
+ + for (const info of clients.values()) control(info, 'error', {error: compileErrors[0].body});
611
+ + fail(422, 'Final code failed to compile; HMR remains paused. Fix the code and retry __hmr_on.');
612
+ + }
613
+ + const reload = reloadPending || errors.some(result => staleGraph(result.body)) || [...clients.values()].some(info => info.buffer.some(message => {
614
+ + const data = JSON.parse(String(message));
615
+ + return data.type === 'error' && staleGraph(data.body);
616
+ + }));
617
+ + // 先持久化再放行客户端。如果进程在放行与持久化之间崩溃,重启后会读到
618
+ + // 陈旧的 paused 状态,把已经 resume 的客户端再次暂停;因此持久化失败
619
+ + // 必须让闸门保持关闭。
620
+ + const previousReloadPending = reloadPending;
621
+ + paused = false;
622
+ + reloadPending = false;
623
+ + transition(reload ? 'reload' : 'fast-refresh');
624
+ + try { persist(); } catch (error) { paused = true; reloadPending = previousReloadPending; throw error; }
625
+ + // 缓冲帧先于 resume 发出:运行时先把它们与最终增量合并,
626
+ + // resume 到达时通道已安静。
627
+ + for (const info of clients.values()) {
628
+ + if (reload) control(info, 'reload');
629
+ + else {
630
+ + for (const message of info.buffer) info.send(message);
631
+ + control(info, 'resume');
632
+ + }
633
+ + info.buffer = [];
634
+ + info.ack = null;
635
+ + }
636
+ + return {...status(), mode: reload ? 'reload' : 'fast-refresh', clients: clients.size};
637
+ + } finally {
638
+ + restoring = null;
639
+ + release();
640
+ + }
641
+ + }
642
+ + const handler = (req, res, next) => {
643
+ + const pathname = new URL(req.url, 'http://localhost').pathname.replace(/\/$/, '');
644
+ + const action = ROUTES.get(pathname);
645
+ + // OPTIONS 直接放行:这些端点服务 native 客户端,native 不强制 CORS,
646
+ + // 而 Web 端按设计不轮询。
647
+ + if (!action || req.method === 'OPTIONS') return next();
648
+ + if (action === 'status') {
649
+ + if (req.method !== 'GET') {
650
+ + res.statusCode = 405;
651
+ + res.setHeader('Allow', 'GET');
652
+ + return res.end();
653
+ + }
654
+ + res.statusCode = 200;
655
+ + res.setHeader('Cache-Control', 'no-store');
656
+ + res.setHeader('Content-Type', 'application/json');
657
+ + return res.end(JSON.stringify(status()));
658
+ + }
659
+ + if (req.method !== 'POST') {
660
+ + res.statusCode = 405;
661
+ + res.setHeader('Allow', 'POST');
662
+ + return res.end();
663
+ + }
664
+ + // 控制操作串行化:并发调用方的 pause 与 resume 一个接一个执行,绝不交错。
665
+ + const task = serial.catch(() => {}).then(() => run(action));
666
+ + serial = task;
667
+ + task.then(body => {
668
+ + res.statusCode = body.mode === 'reload-deferred' ? 202 : 200;
669
+ + res.setHeader('Content-Type', 'application/json');
670
+ + res.end(JSON.stringify(body));
671
+ + }, error => {
672
+ + res.statusCode = error.status || 503;
673
+ + res.setHeader('Content-Type', 'application/json');
674
+ + res.end(JSON.stringify({status: 1, paused, msg: error.message}));
675
+ + });
676
+ + };
677
+ + // Metro 自带兜底 middleware:append 会让这些路由永远不可达。
678
+ + middleware.use(handler);
679
+ + middleware.stack.unshift(middleware.stack.pop());
680
+ + return {run: action => {
681
+ + const task = serial.catch(() => {}).then(() => run(action));
682
+ + serial = task;
683
+ + return task;
684
+ + }};
685
+ +}
686
+ +module.exports = {attachMiaodaHmrControl};
687
+ diff --git a/build/src/start/server/metro/runServer-fork.js b/build/src/start/server/metro/runServer-fork.js
688
+ index 652bee8..a08f26a 100644
689
+ --- a/build/src/start/server/metro/runServer-fork.js
690
+ +++ b/build/src/start/server/metro/runServer-fork.js
691
+ @@ -63,6 +63,7 @@ function _url() {
692
+ return data;
693
+ }
694
+ const _log = require("../../../log");
695
+ +const _miaodaHmrControl = require("./miaodaHmrControl");
696
+ function _interop_require_default(obj) {
697
+ return obj && obj.__esModule ? obj : {
698
+ default: obj
699
+ @@ -150,6 +151,13 @@ const runServer = async (_metroBundler, config, { hasReducedPerformance = false,
700
+ onReady(httpServer);
701
+ }
702
+ const hmrServer = new (_HmrServer()).default(metroServer.getBundler(), metroServer.getCreateModuleId(), config);
703
+ + // 挂载 Miaoda HMR 控制的挂点:此处能同时拿到 hmrServer、metroServer
704
+ + // 与 middleware 三元组(attach 三者都需要),但真正的挂载由
705
+ + // instantiateMetro 在环境变量开启时触发,保证 attach 发生在 Expo
706
+ + // 替换 HMR serializer 之后。
707
+ + hmrServer._miaodaAttach = (options = {}) => (0, _miaodaHmrControl.attachMiaodaHmrControl)({
708
+ + hmrServer, metro: metroServer, middleware: serverApp, ...options
709
+ + });
710
+ Object.assign(websocketEndpoints, {
711
+ '/hot': (0, _createWebsocketServer().default)({
712
+ websocketServer: hmrServer