miaoda-expo-devkit 0.1.1-dev.development-build.1 → 0.1.1-dev.development-build.3
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/README.md +18 -10
- package/dist/babel/plugin-lucide-react-native.js +1 -1
- package/dist/rules/no-bare-web-crypto.js +235 -0
- package/dist/rules/no-expo-file-system-compat.js +2130 -0
- package/dist/rules/no-lucide-star-import.js +78 -0
- package/dist/rules/no-sentry-default-import.js +83 -0
- package/dist/rules/no-typeof-window-check.js +85 -0
- package/oxlint-config.json +11 -1
- package/package.json +6 -1
- package/pnpm-patches/@expo__cli@55.0.16-55.0.22.patch +237 -150
- package/pnpm-patches/expo@55.0.6.patch +81 -31
- package/pnpm-patches/metro-file-map@0.83.3.patch +7 -3
- package/pnpm-patches/metro-runtime@0.83.3.patch +5 -1
- package/pnpm-patches/react-native-worklets@0.7.4.patch +91 -0
- package/pnpmfile.cjs +87 -2
|
@@ -1,118 +1,34 @@
|
|
|
1
|
-
diff --git a/build/src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const _createDebugMiddleware = require("./debugging/createDebugMiddleware");
|
|
7
|
-
const _createMetroMiddleware = require("./dev-server/createMetroMiddleware");
|
|
8
|
-
+const _aiErrorCollector = require("./aiErrorCollector");
|
|
9
|
-
+const _miaodaHmrControl = require("./miaodaHmrControl");
|
|
10
|
-
const _runServerfork = require("./runServer-fork");
|
|
11
|
-
const _withMetroMultiPlatform = require("./withMetroMultiPlatform");
|
|
12
|
-
const _events = require("../../../events");
|
|
13
|
-
@@ -273,6 +275,7 @@
|
|
14
|
-
const { middleware, messagesSocket, eventsSocket, websocketEndpoints } = (0, _createMetroMiddleware.createMetroMiddleware)(metroConfig, {
|
|
15
|
-
getMetroBundler
|
|
16
|
-
});
|
|
17
|
-
+ const aiErrorCollector = !isExporting ? (0, _aiErrorCollector.createAiErrorCollector)(projectRoot) : null;
|
|
18
|
-
// Get local URL to Metro bundler server (typically configured as 127.0.0.1:8081)
|
|
19
|
-
const serverBaseUrl = metroBundler.getUrlCreator().constructUrl({
|
|
20
|
-
scheme: 'http',
|
|
21
|
-
@@ -347,7 +350,10 @@
|
|
22
|
-
}
|
|
23
|
-
}), fileBuffer);
|
|
24
|
-
};
|
|
25
|
-
- setEventReporter(eventsSocket.reportMetroEvent);
|
|
26
|
-
+ setEventReporter((event)=>{
|
|
27
|
-
+ eventsSocket.reportMetroEvent(event);
|
|
28
|
-
+ aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent(event);
|
|
29
|
-
+ });
|
|
30
|
-
// This function ensures that modules in source maps are sorted in the same
|
|
31
|
-
// order as in a plain JS bundle.
|
|
32
|
-
metro._getSortedModules = function(graph) {
|
|
33
|
-
@@ -381,13 +387,24 @@
|
|
34
|
-
// Fork of https://github.com/facebook/metro/blob/3b3e0aaf725cfa6907bf2c8b5fbc0da352d29efe/packages/metro/src/HmrServer.js#L327-L393
|
|
35
|
-
// with patch for `_createModuleId`.
|
|
36
|
-
const logger = !options.isInitialUpdate ? changeEvent == null ? void 0 : changeEvent.logger : null;
|
|
37
|
-
+ const hmrChangeID = `${changeEvent == null ? 'initial' : changeEvent.changeId}:${group.revisionId}`;
|
|
38
|
-
+ aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
|
|
39
|
-
+ type: 'hmr_update_started',
|
|
40
|
-
+ changeID: hmrChangeID
|
|
41
|
-
+ });
|
|
42
|
-
try {
|
|
43
|
-
var _revision_graph_transformOptions_customTransformOptions;
|
|
44
|
-
const revPromise = this._bundler.getRevision(group.revisionId);
|
|
45
|
-
if (!revPromise) {
|
|
46
|
-
+ const revisionError = new (_RevisionNotFoundError()).default(group.revisionId);
|
|
47
|
-
+ aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
|
|
48
|
-
+ type: 'hmr_update_failed',
|
|
49
|
-
+ changeID: hmrChangeID,
|
|
50
|
-
+ error: revisionError
|
|
51
|
-
+ });
|
|
52
|
-
return {
|
|
53
|
-
type: 'error',
|
|
54
|
-
- body: (0, _formatBundlingError().default)(new (_RevisionNotFoundError()).default(group.revisionId))
|
|
55
|
-
+ body: (0, _formatBundlingError().default)(revisionError)
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
logger == null ? void 0 : logger.point('updateGraph_start');
|
|
59
|
-
@@ -418,6 +435,10 @@
|
|
60
|
-
serverRoot: this._config.server.unstable_serverRoot ?? this._config.projectRoot
|
|
61
|
-
});
|
|
62
|
-
logger == null ? void 0 : logger.point('serialize_end');
|
|
63
|
-
+ aiErrorCollector == null ? void 0 : aiErrorCollector.handleMetroEvent({
|
|
64
|
-
+ type: 'hmr_update_done',
|
|
65
|
-
+ changeID: hmrChangeID
|
|
66
|
-
+ });
|
|
67
|
-
return {
|
|
68
|
-
type: 'update',
|
|
69
|
-
body: {
|
|
70
|
-
@@ -430,7 +451,9 @@
|
|
71
|
-
const formattedError = (0, _formatBundlingError().default)(error);
|
|
72
|
-
this._config.reporter.update({
|
|
73
|
-
type: 'bundling_error',
|
|
74
|
-
- error
|
|
75
|
-
+ error,
|
|
76
|
-
+ isHmr: true,
|
|
77
|
-
+ changeID: hmrChangeID
|
|
78
|
-
});
|
|
79
|
-
return {
|
|
80
|
-
type: 'error',
|
|
81
|
-
@@ -438,6 +461,11 @@
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
+ }
|
|
86
|
-
+ if (hmrServer && process.env.EXPO_MIAODA_HMR_CONTROL === '1') {
|
|
87
|
-
+ hmrServer._miaodaAttach({
|
|
88
|
-
+ stateFile: process.env.EXPO_MIAODA_HMR_STATE_FILE
|
|
89
|
-
+ });
|
|
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)=>{
|
|
90
6
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
+
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
+
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
websocketServer: hmrServer
|
|
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
|
+
|
|
114
29
|
diff --git a/build/src/start/server/metro/aiErrorCollector.js b/build/src/start/server/metro/aiErrorCollector.js
|
|
115
30
|
new file mode 100644
|
|
31
|
+
index 0000000..febccaf
|
|
116
32
|
--- /dev/null
|
|
117
33
|
+++ b/build/src/start/server/metro/aiErrorCollector.js
|
|
118
34
|
@@ -0,0 +1,222 @@
|
|
@@ -338,46 +254,145 @@ new file mode 100644
|
|
|
338
254
|
+}
|
|
339
255
|
+
|
|
340
256
|
+//# sourceMappingURL=aiErrorCollector.js.map
|
|
341
|
-
diff --git a/build/src/
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
+
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
+
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
+
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
+
|
|
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);
|
|
363
286
|
+ });
|
|
364
|
-
|
|
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,
|
|
365
355
|
diff --git a/build/src/start/server/metro/miaodaHmrControl.js b/build/src/start/server/metro/miaodaHmrControl.js
|
|
366
356
|
new file mode 100644
|
|
357
|
+
index 0000000..6466921
|
|
367
358
|
--- /dev/null
|
|
368
359
|
+++ b/build/src/start/server/metro/miaodaHmrControl.js
|
|
369
|
-
@@ -0,0 +1,
|
|
360
|
+
@@ -0,0 +1,328 @@
|
|
370
361
|
+"use strict";
|
|
371
362
|
+
|
|
372
|
-
+// SDK 55 / Metro 0.83.3
|
|
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。
|
|
373
386
|
+const fs = require('node:fs');
|
|
374
387
|
+const path = require('node:path');
|
|
375
388
|
+const CONTROL = 'miaoda-hmr-control';
|
|
389
|
+
+// 控制接口直接挂在 Metro 根路径(/_hmr/*),不带内部 API 前缀:端侧裸轮询
|
|
390
|
+
+// 请求不携带平台凭证,带 /innerapi 类前缀的路径可能被路由层权限拦截。
|
|
376
391
|
+const ROUTES = new Map([
|
|
377
|
-
+ ['/
|
|
378
|
-
+ ['/
|
|
379
|
-
+ ['/
|
|
380
|
-
+ ['/
|
|
392
|
+
+ ['/_hmr/off', 'pause'],
|
|
393
|
+
+ ['/_hmr/on', 'resume'],
|
|
394
|
+
+ ['/_hmr/reload', 'reload'],
|
|
395
|
+
+ ['/_hmr/status', 'status'],
|
|
381
396
|
+]);
|
|
382
397
|
+
|
|
383
398
|
+function attachMiaodaHmrControl({hmrServer: hmr, metro, middleware, stateFile, timeout = 5000}) {
|
|
@@ -385,6 +400,8 @@ new file mode 100644
|
|
|
385
400
|
+ let reloadPending = false;
|
|
386
401
|
+ let stateVersion = 0;
|
|
387
402
|
+ let lastMode = null;
|
|
403
|
+
+ // 一次编辑批次可能跨越 Metro 重启:状态文件让新进程恢复暂停状态,
|
|
404
|
+
+ // 继续闸门重连上来的客户端。
|
|
388
405
|
+ if (stateFile && fs.existsSync(stateFile)) {
|
|
389
406
|
+ try {
|
|
390
407
|
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
@@ -393,10 +410,13 @@ new file mode 100644
|
|
|
393
410
|
+ stateVersion = Number.isInteger(state.stateVersion) ? state.stateVersion : 0;
|
|
394
411
|
+ lastMode = typeof state.lastMode === 'string' ? state.lastMode : null;
|
|
395
412
|
+ } catch {
|
|
396
|
-
+ //
|
|
413
|
+
+ // 损坏的状态文件绝不能放行一个未完成的编辑批次。
|
|
397
414
|
+ paused = true;
|
|
398
415
|
+ }
|
|
399
416
|
+ }
|
|
417
|
+
+ // client -> {send, version, registered, buffer, ack}。`send` 是原始 socket
|
|
418
|
+
+ // 写入器;HMR 输出走 onClientConnect 里安装的受控包装。`version` 在客户端
|
|
419
|
+
+ // 通过 register-entrypoints 协商控制协议后为 1,未协商的老客户端为 null。
|
|
400
420
|
+ const clients = new Map();
|
|
401
421
|
+ const groupTasks = new WeakMap();
|
|
402
422
|
+ const results = new WeakMap();
|
|
@@ -405,6 +425,8 @@ new file mode 100644
|
|
|
405
425
|
+ let controlId = 0;
|
|
406
426
|
+ let restoring = null;
|
|
407
427
|
+
|
|
428
|
+
+ // 崩溃安全的原子写:临时文件 + rename。调用方依赖「持久化状态绝不比
|
|
429
|
+
+ // 已告知客户端的内容更新」这一不变量(见 run())。
|
|
408
430
|
+ function persist() {
|
|
409
431
|
+ if (!stateFile) return;
|
|
410
432
|
+ fs.mkdirSync(path.dirname(stateFile), {recursive: true});
|
|
@@ -412,6 +434,8 @@ new file mode 100644
|
|
|
412
434
|
+ fs.writeFileSync(temp, JSON.stringify({version: 1, paused, reloadPending, stateVersion, lastMode}));
|
|
413
435
|
+ fs.renameSync(temp, stateFile);
|
|
414
436
|
+ }
|
|
437
|
+
+ // 控制帧只发给已协商的客户端——未协商的客户端不认识这些帧。
|
|
438
|
+
+ // stateVersion 随帧捎带,供轮询兜底检测是否有丢失的帧。
|
|
415
439
|
+ function control(info, action, extra = {}) {
|
|
416
440
|
+ if (info.version === 1) info.send(JSON.stringify({type: CONTROL, version: 1, action, stateVersion, ...extra}));
|
|
417
441
|
+ }
|
|
@@ -422,6 +446,8 @@ new file mode 100644
|
|
|
422
446
|
+ function status() {
|
|
423
447
|
+ return {status: 0, paused, reloadPending, stateVersion, lastMode, protocolVersion: 1};
|
|
424
448
|
+ }
|
|
449
|
+
+ // 单客户端暂停握手:客户端必须在运行时停止应用更新后 ack 这个 id,
|
|
450
|
+
+ // 以此证明不会再有缓冲的更新落到设备上。
|
|
425
451
|
+ function pauseClient(info) {
|
|
426
452
|
+ if (info.version !== 1 || info.ack) return;
|
|
427
453
|
+ const id = ++controlId;
|
|
@@ -435,6 +461,8 @@ new file mode 100644
|
|
|
435
461
|
+ error.status = status;
|
|
436
462
|
+ throw error;
|
|
437
463
|
+ }
|
|
464
|
+
+ // 只有当所有已协商客户端都确认后,暂停才算完成。
|
|
465
|
+
+ // Agent 必须等它 resolve 之后才能开始写文件。
|
|
438
466
|
+ async function awaitPaused() {
|
|
439
467
|
+ if ([...clients.values()].some(info => info.registered && info.version !== 1)) {
|
|
440
468
|
+ fail(409, 'Connected client lacks Miaoda HMR protocol v1; update its JS dependencies and reload.');
|
|
@@ -455,7 +483,10 @@ new file mode 100644
|
|
|
455
483
|
+ } finally { clearTimeout(timer); }
|
|
456
484
|
+ }
|
|
457
485
|
+
|
|
458
|
-
+ //
|
|
486
|
+
+ // 立即闸门输出,包括 pause 到达时已在编译中的更新:paused 期间 HMR 帧
|
|
487
|
+
+ // (update-start/update/update-done/error)按客户端缓冲而不发送。
|
|
488
|
+
+ // 控制帧和日志不受闸门限制;缓冲在 resume 时回放,客户端仍能收到基于
|
|
489
|
+
+ // 其最后应用 revision 计算的最终增量。
|
|
459
490
|
+ const connect = hmr.onClientConnect.bind(hmr);
|
|
460
491
|
+ hmr.onClientConnect = async (url, send) => {
|
|
461
492
|
+ const info = {send, version: null, registered: false, buffer: [], ack: null};
|
|
@@ -478,14 +509,18 @@ new file mode 100644
|
|
|
478
509
|
+ return;
|
|
479
510
|
+ }
|
|
480
511
|
+ if (data?.type === 'register-entrypoints' && info) {
|
|
481
|
-
+ //
|
|
512
|
+
+ // resume 进行中注册的客户端,绝不能先于 resume 提交状态迁移就看到
|
|
513
|
+
+ // 已释放的缓冲。
|
|
482
514
|
+ if (restoring) await restoring;
|
|
483
515
|
+ info.registered = true;
|
|
484
516
|
+ info.version = data.miaodaHmr === 1 ? 1 : null;
|
|
517
|
+
+ // 在暂停批次中(重)连入的客户端立即被暂停;连入空闲服务端的客户端
|
|
518
|
+
+ // 会收到一条 resume,用来清除上一次会话遗留的本地闸门。
|
|
485
519
|
+ if (paused) pauseClient(info);
|
|
486
520
|
+ else control(info, 'resume');
|
|
487
521
|
+ }
|
|
488
|
-
+ //
|
|
522
|
+
+ // 注册错误与图更新一样走受控发送器。
|
|
523
|
+
+ // 跟踪注册任务,resume 才能先等新连入客户端的初始图构建完成,再计算最终增量。
|
|
489
524
|
+ const task = onMessage(client, message, client.sendFn);
|
|
490
525
|
+ if (data?.type !== 'register-entrypoints') return task;
|
|
491
526
|
+ registrations.add(task);
|
|
@@ -493,11 +528,15 @@ new file mode 100644
|
|
|
493
528
|
+ };
|
|
494
529
|
+ const disconnect = hmr.onClientDisconnect.bind(hmr);
|
|
495
530
|
+ hmr.onClientDisconnect = client => {
|
|
531
|
+
+ // 已断开的客户端不可能再应用更新,直接 resolve 它的挂起 ack,
|
|
532
|
+
+ // 避免 pause 干等到 504 超时。
|
|
496
533
|
+ clients.get(client)?.ack?.resolve();
|
|
497
534
|
+ clients.delete(client);
|
|
498
535
|
+ return disconnect(client);
|
|
499
536
|
+ };
|
|
500
537
|
+
|
|
538
|
+
+ // 记录每个分组的构建结果,resume 据此检查最终编译结果,
|
|
539
|
+
+ // 在 fast-refresh、reload 与 422 之间做决策。
|
|
501
540
|
+ const prepare = hmr._prepareMessage.bind(hmr);
|
|
502
541
|
+ hmr._prepareMessage = async (...args) => {
|
|
503
542
|
+ const result = await prepare(...args);
|
|
@@ -505,10 +544,13 @@ new file mode 100644
|
|
|
505
544
|
+ return result;
|
|
506
545
|
+ };
|
|
507
546
|
+ const handle = hmr._handleFileChange.bind(hmr);
|
|
547
|
+
+ // 文件变更按分组严格串行处理。paused 时跳过 handler,但这只闸门「投递」:
|
|
548
|
+
+ // 各分组的 DeltaCalculator 内部仍在持续累积文件事件,客户端运行时也会合并
|
|
549
|
+
+ // disable 期间收到的更新,因此不会丢失任何中间态。
|
|
508
550
|
+ function enqueue(group, options, event, force = false) {
|
|
509
551
|
+ const previous = groupTasks.get(group) || Promise.resolve();
|
|
510
552
|
+ const task = previous.catch(() => {}).then(async () => {
|
|
511
|
-
+ //
|
|
553
|
+
+ // 保持 DeltaCalculator 的累积不变。绝不丢弃已构建的 revision。
|
|
512
554
|
+ if (paused && !force) return;
|
|
513
555
|
+ await handle(group, options, event);
|
|
514
556
|
+ });
|
|
@@ -525,6 +567,8 @@ new file mode 100644
|
|
|
525
567
|
+ await awaitPaused();
|
|
526
568
|
+ return {status: 0, paused, mode: 'paused'};
|
|
527
569
|
+ }
|
|
570
|
+
+ // 批次中途请求的 reload 会被推迟:暂停的意义就是不让中间态代码执行,
|
|
571
|
+
+ // 而 reload 加载的恰恰就是这些中间态。
|
|
528
572
|
+ if (action === 'reload' && paused) {
|
|
529
573
|
+ reloadPending = true;
|
|
530
574
|
+ transition('reload-deferred');
|
|
@@ -533,6 +577,8 @@ new file mode 100644
|
|
|
533
577
|
+ }
|
|
534
578
|
+ if (action === 'status') return status();
|
|
535
579
|
+ if (action === 'resume' && !paused) return {...status(), mode: 'unchanged'};
|
|
580
|
+
+ // resume 与 reload 共用前置的暂停握手:必须先把客户端静默下来再计算
|
|
581
|
+
+ // 最终增量,否则仍可能放出一个基于写了一半的目录树编译的更新。
|
|
536
582
|
+ paused = true;
|
|
537
583
|
+ if (action === 'reload') reloadPending = true;
|
|
538
584
|
+ transition(action === 'reload' ? 'reload-requested' : 'paused');
|
|
@@ -547,27 +593,39 @@ new file mode 100644
|
|
|
547
593
|
+ if (typeof fileMap.miaodaFlushChanges !== 'function') {
|
|
548
594
|
+ fail(503, 'Metro file-map barrier patch is missing; HMR remains paused.');
|
|
549
595
|
+ }
|
|
550
|
-
+ //
|
|
596
|
+
+ // Agent 通过调用 resume 表示「所有写入已完成」。栅栏让这一点对 Metro
|
|
597
|
+
+ // 同样成立:watcher cookie -> file-map 处理队列 -> change 事件,
|
|
598
|
+
+ // 保证下面的最终构建一定包含最后写入的字节。
|
|
551
599
|
+ await fileMap.miaodaFlushChanges(timeout);
|
|
552
600
|
+ const groups = [...new Set(hmr._clientGroups.values())];
|
|
601
|
+
+ // 每个分组强制构建一次,使最终增量(以及任何编译错误)在放行客户端
|
|
602
|
+
+ // 之前就算出来。
|
|
553
603
|
+ await Promise.all(groups.map(group => enqueue(group, {isInitialUpdate: false}, undefined, true)));
|
|
554
604
|
+ const errors = groups.map(group => results.get(group)).filter(result => result?.type === 'error');
|
|
605
|
+
+ // 图/revision 丢失不是代码错误:增量链已断,唯一安全的放行方式是
|
|
606
|
+
+ // 完整 Bundle reload。
|
|
555
607
|
+ const staleGraph = error => ['GraphNotFoundError', 'RevisionNotFoundError'].includes(error?.type);
|
|
556
608
|
+ const compileErrors = errors.filter(result => !staleGraph(result.body));
|
|
557
609
|
+ if (compileErrors.length) {
|
|
610
|
+
+ // 把最终错误告知客户端(设备端可以据此行动),但保持闸门关闭,
|
|
611
|
+
+ // 让 Agent 修好代码后重试。
|
|
558
612
|
+ for (const info of clients.values()) control(info, 'error', {error: compileErrors[0].body});
|
|
559
|
-
+ fail(422, 'Final code failed to compile; HMR remains paused. Fix the code and retry
|
|
613
|
+
+ fail(422, 'Final code failed to compile; HMR remains paused. Fix the code and retry /_hmr/on.');
|
|
560
614
|
+ }
|
|
561
615
|
+ const reload = reloadPending || errors.some(result => staleGraph(result.body)) || [...clients.values()].some(info => info.buffer.some(message => {
|
|
562
616
|
+ const data = JSON.parse(String(message));
|
|
563
617
|
+ return data.type === 'error' && staleGraph(data.body);
|
|
564
618
|
+ }));
|
|
565
|
-
+ //
|
|
619
|
+
+ // 先持久化再放行客户端。如果进程在放行与持久化之间崩溃,重启后会读到
|
|
620
|
+
+ // 陈旧的 paused 状态,把已经 resume 的客户端再次暂停;因此持久化失败
|
|
621
|
+
+ // 必须让闸门保持关闭。
|
|
566
622
|
+ const previousReloadPending = reloadPending;
|
|
567
623
|
+ paused = false;
|
|
568
624
|
+ reloadPending = false;
|
|
569
625
|
+ transition(reload ? 'reload' : 'fast-refresh');
|
|
570
626
|
+ try { persist(); } catch (error) { paused = true; reloadPending = previousReloadPending; throw error; }
|
|
627
|
+
+ // 缓冲帧先于 resume 发出:运行时先把它们与最终增量合并,
|
|
628
|
+
+ // resume 到达时通道已安静。
|
|
571
629
|
+ for (const info of clients.values()) {
|
|
572
630
|
+ if (reload) control(info, 'reload');
|
|
573
631
|
+ else {
|
|
@@ -586,6 +644,8 @@ new file mode 100644
|
|
|
586
644
|
+ const handler = (req, res, next) => {
|
|
587
645
|
+ const pathname = new URL(req.url, 'http://localhost').pathname.replace(/\/$/, '');
|
|
588
646
|
+ const action = ROUTES.get(pathname);
|
|
647
|
+
+ // OPTIONS 直接放行:这些端点服务 native 客户端,native 不强制 CORS,
|
|
648
|
+
+ // 而 Web 端按设计不轮询。
|
|
589
649
|
+ if (!action || req.method === 'OPTIONS') return next();
|
|
590
650
|
+ if (action === 'status') {
|
|
591
651
|
+ if (req.method !== 'GET') {
|
|
@@ -603,6 +663,7 @@ new file mode 100644
|
|
|
603
663
|
+ res.setHeader('Allow', 'POST');
|
|
604
664
|
+ return res.end();
|
|
605
665
|
+ }
|
|
666
|
+
+ // 控制操作串行化:并发调用方的 pause 与 resume 一个接一个执行,绝不交错。
|
|
606
667
|
+ const task = serial.catch(() => {}).then(() => run(action));
|
|
607
668
|
+ serial = task;
|
|
608
669
|
+ task.then(body => {
|
|
@@ -615,7 +676,7 @@ new file mode 100644
|
|
|
615
676
|
+ res.end(JSON.stringify({status: 1, paused, msg: error.message}));
|
|
616
677
|
+ });
|
|
617
678
|
+ };
|
|
618
|
-
+ // Metro
|
|
679
|
+
+ // Metro 自带兜底 middleware:append 会让这些路由永远不可达。
|
|
619
680
|
+ middleware.use(handler);
|
|
620
681
|
+ middleware.stack.unshift(middleware.stack.pop());
|
|
621
682
|
+ return {run: action => {
|
|
@@ -625,3 +686,29 @@ new file mode 100644
|
|
|
625
686
|
+ }};
|
|
626
687
|
+}
|
|
627
688
|
+module.exports = {attachMiaodaHmrControl};
|
|
689
|
+
diff --git a/build/src/start/server/metro/runServer-fork.js b/build/src/start/server/metro/runServer-fork.js
|
|
690
|
+
index 652bee8..a08f26a 100644
|
|
691
|
+
--- a/build/src/start/server/metro/runServer-fork.js
|
|
692
|
+
+++ b/build/src/start/server/metro/runServer-fork.js
|
|
693
|
+
@@ -63,6 +63,7 @@ function _url() {
|
|
694
|
+
return data;
|
|
695
|
+
}
|
|
696
|
+
const _log = require("../../../log");
|
|
697
|
+
+const _miaodaHmrControl = require("./miaodaHmrControl");
|
|
698
|
+
function _interop_require_default(obj) {
|
|
699
|
+
return obj && obj.__esModule ? obj : {
|
|
700
|
+
default: obj
|
|
701
|
+
@@ -150,6 +151,13 @@ const runServer = async (_metroBundler, config, { hasReducedPerformance = false,
|
|
702
|
+
onReady(httpServer);
|
|
703
|
+
}
|
|
704
|
+
const hmrServer = new (_HmrServer()).default(metroServer.getBundler(), metroServer.getCreateModuleId(), config);
|
|
705
|
+
+ // 挂载 Miaoda HMR 控制的挂点:此处能同时拿到 hmrServer、metroServer
|
|
706
|
+
+ // 与 middleware 三元组(attach 三者都需要),但真正的挂载由
|
|
707
|
+
+ // instantiateMetro 在环境变量开启时触发,保证 attach 发生在 Expo
|
|
708
|
+
+ // 替换 HMR serializer 之后。
|
|
709
|
+
+ hmrServer._miaodaAttach = (options = {}) => (0, _miaodaHmrControl.attachMiaodaHmrControl)({
|
|
710
|
+
+ hmrServer, metro: metroServer, middleware: serverApp, ...options
|
|
711
|
+
+ });
|
|
712
|
+
Object.assign(websocketEndpoints, {
|
|
713
|
+
'/hot': (0, _createWebsocketServer().default)({
|
|
714
|
+
websocketServer: hmrServer
|