miaoda-expo-devkit 0.1.1-beta.69 → 0.1.1-beta.70

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 CHANGED
@@ -7,9 +7,18 @@ Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下
7
7
  - **Bundle 首部注入** — 在 expo-router 初始化之前执行自定义脚本
8
8
  - **HMR postMessage 控制** — 通过 `window.postMessage` 在运行时启动或停止 Fast Refresh
9
9
  - **LogBox 屏蔽** — web 平台禁用 Expo 全屏错误遮罩
10
+ - **Metro transform 缓存持久化** — 将缓存目录固定到项目根目录(可通过 `METRO_CACHE_DIR` 指定),容器/CI 重启后不丢失
11
+ - **构建耗时日志** — 将 bundle 总耗时和每个 cache miss 文件的 transform 耗时写入 JSONL 文件(通过 `METRO_TRANSFORM_LOG` 启用)
12
+ - **esbuild minifier** — 生产构建时将 Metro minifier 切换为 esbuild(比 terser 快数十倍)
13
+ - **WASM 支持** — 将 `.wasm` 加入 assetExts,修复 expo-sqlite web worker 打包失败
14
+ - **lucide-react-native 路径解析** — 配合 babel 插件,消除图标子路径 exports 未声明的 warning
15
+ - **workspace node_modules 修复** — 修复沙箱环境中 node_modules 位于祖先目录时 Metro 模块解析和 bundle 请求失败的问题
10
16
  - **expo-notifications stub** — Expo Go(Android)中提供 no-op 实现,核心 API 调用时弹出带参数校验的调试 Alert,Dev Build 透传真实模块
11
17
  - **expo-media-library stub** — Expo Go / Web 中提供 no-op 实现,`saveToLibraryAsync`、`createAssetAsync`、权限请求等 API 调用时弹出 Alert 提示,Dev Build 原生环境透传真实模块
12
18
  - **expo-calendar stub** — Expo Go / Web 中提供 no-op 实现,`getEventsAsync`、`createEventAsync`、`getCalendarsAsync`、权限请求等核心 API 调用时弹出 Alert 提示并校验参数,Dev Build 原生环境透传真实模块
19
+ - **expo-file-system stub** — Web 中将 `expo-file-system` 和 `expo-file-system/legacy` 替换为 no-op stub,核心 API 弹 Dialog 提示,Expo Go / Dev Build 透传真实模块
20
+ - **expo-haptics stub** — Web 中提供 no-op 实现,触觉 API 调用时弹 Dialog 提示参数信息,native 不受影响
21
+ - **expo-contacts stub** — Web / Expo Go 中提供 no-op 实现,联系人 API 调用时弹 Dialog 提示,Dev Build 透传真实模块
13
22
  - **expo-image-picker stub** — 桌面 Web 中 `launchCameraAsync` 通过 `getUserMedia` 打开摄像头预览弹窗(浏览器原生 `capture` 属性在 PC 端被忽略),移动端浏览器透传 expo 原实现,Native 不受影响
14
23
 
15
24
  ## 安装
@@ -224,12 +233,43 @@ expect(onError).toHaveBeenCalledWith(
224
233
  | 变量 | 默认值 | 说明 |
225
234
  |---|---|---|
226
235
  | `SENTRY_OVERRIDE_DSN` | `https://stubPublicKey@o0.ingest.sentry.io/0` | 覆盖 Sentry DSN,可指向本地 relay 等自定义端点 |
236
+ | `METRO_CACHE_DIR` | `projectRoot/.metro-cache` | Metro transform 缓存目录绝对路径,优先级最高,适合容器/CI 挂载外部持久目录 |
237
+ | `METRO_TRANSFORM_LOG` | _(未设置时不记录)_ | 构建日志输出文件的绝对路径(JSONL 格式),仅开发模式(`__DEV__`)下生效 |
227
238
 
228
239
  ## 工作原理
229
240
 
230
241
  ```
231
242
  metro.config.js
232
243
  └─ withDevkit(config)
244
+
245
+ ├─ withTransformLogger → unstable_perfLoggerFactory + metro-core Logger
246
+ │ ├─ CACHE_CONFIG 条目(启动时由 withPersistentCache 写入,含 cache_root / store_class / source)
247
+ │ ├─ BUNDLING_REQUEST 条目(每次 bundle 请求写一条)
248
+ │ │ ├─ duration_ms、status、initial_build、graph_node_count
249
+ │ │ └─ transform_miss_count(cache miss 文件数;0 = 完全命中)
250
+ │ └─ TRANSFORM_FILE 条目(每个 cache miss 文件写一条,命中则不写)
251
+ │ └─ file、duration_ms
252
+ │ (需设置 METRO_TRANSFORM_LOG 且 __DEV__ 才生效)
253
+
254
+ ├─ withPersistentCache → config.cacheStores
255
+ │ ├─ 缓存路径优先级:METRO_CACHE_DIR > options.cacheDir > projectRoot/.metro-cache
256
+ │ └─ 保留 @expo/metro-config FileStore 子类行为(NativeWind skipCache 标志)
257
+
258
+ ├─ withWorkspaceNodeModules → watchFolders + resolver.nodeModulesPaths
259
+ │ ├─ 向上查找祖先目录的 node_modules,加入 watchFolders
260
+ │ └─ 若 .pnpm 是指向外部路径的 symlink,也将外部真实路径加入 watchFolders
261
+
262
+ ├─ withWasmSupport → resolver.assetExts
263
+ │ └─ 将 .wasm 加入 assetExts,修复 expo-sqlite web worker 打包
264
+
265
+ ├─ withCssInterop → 为 expo-image / expo-camera 等注入 NativeWind cssInterop
266
+
267
+ ├─ withEsbuildMinify → transformer.minifierPath(仅生产构建)
268
+ │ └─ 切换为 metro-minify-esbuild,清空 terser 专属 minifierConfig
269
+
270
+ ├─ withLucideResolver → resolver.resolveRequest
271
+ │ └─ lucide-react-native/dist/** 子路径 → 绝对文件路径(绕过 exports 检查)
272
+ │ └─ 自动检测 .mjs(>= 1.9)vs .js(1.8.x)扩展名
233
273
 
234
274
  ├─ withDevStubs → resolver.resolveRequest
235
275
  │ ├─ @sentry/react-native → dist/stubs/sentry-react-native-stub.js (全平台)
@@ -246,19 +286,35 @@ metro.config.js
246
286
  │ └─ Dev Build(原生):透传真实 expo-media-library
247
287
 
248
288
  ├─ withExpoCalendarStub → resolver.resolveRequest
249
- │ └─ expo-calendar → dist/stubs/expo-calendar-stub.js (全平台)
289
+ │ └─ expo-calendar → dist/stubs/expo-calendar-stub.js (全平台)
250
290
  │ ├─ Expo Go / Web:no-op + Alert 提示 + 参数校验(不崩溃)
251
291
  │ └─ Dev Build(原生):透传真实 expo-calendar
252
292
 
293
+ ├─ withExpoFileSystemStub → resolver.resolveRequest
294
+ │ ├─ expo-file-system/legacy → dist/stubs/expo-file-system-stub.js (全平台)
295
+ │ └─ expo-file-system → dist/stubs/expo-file-system-next-stub.js
296
+ │ ├─ Web:no-op + Dialog 提示(不崩溃)
297
+ │ └─ Expo Go / Dev Build(原生):透传真实模块
298
+
299
+ ├─ withExpoHapticsStub → resolver.resolveRequest
300
+ │ └─ expo-haptics → dist/stubs/expo-haptics-stub.js (仅 web)
301
+ │ ├─ Web:no-op + Dialog 提示(不崩溃)
302
+ │ └─ native:透传真实 expo-haptics
303
+
304
+ ├─ withExpoContactsStub → resolver.resolveRequest
305
+ │ └─ expo-contacts → dist/stubs/expo-contacts-stub.js (仅 web)
306
+ │ ├─ Web / Expo Go:no-op + Dialog 提示(不崩溃)
307
+ │ └─ Dev Build(原生):透传真实 expo-contacts
308
+
253
309
  ├─ withExpoImagePickerStub → resolver.resolveRequest
254
- │ └─ expo-image-picker → dist/stubs/expo-image-picker-stub.js (仅 web)
310
+ │ └─ expo-image-picker → dist/stubs/expo-image-picker-stub.js (仅 web)
255
311
  │ ├─ 桌面 Web:launchCameraAsync → getUserMedia + #__devkit_camera_overlay__ 弹窗
256
312
  │ │ ├─ 点"拍照":canvas.toDataURL → ImagePickerResult { canceled:false, assets }
257
313
  │ │ └─ 点"取消":返回 { canceled:true, assets:null }
258
314
  │ ├─ 移动端浏览器:透传 expo 原实现(capture 属性正常工作)
259
315
  │ └─ getUserMedia 不可用:降级透传 expo 原实现(不崩溃)
260
316
 
261
- └─ withEntryInjection → resolver.resolveRequest
317
+ └─ withEntryInjection → resolver.resolveRequest(仅 __DEV__)
262
318
  └─ expo-router/entry-classic → dist/stubs/expo-router-entry-stub.js
263
319
  ├─ require('./entry-inject') ← 注入脚本(bundle 首部执行)
264
320
  │ ├─ globalThis.__DEVKIT_INJECTED__ = true
@@ -289,7 +345,7 @@ sentry-react-native-stub.js
289
345
  | 子路径 | 文件 | 内容 |
290
346
  |---|---|---|
291
347
  | `.` | `dist/index.js` | `SentryCapture`、`MetroSymbolicator`、全部类型 |
292
- | `./metro` | `dist/metro.js` | `withDevkit`、`withDevStubs`、`withEntryInjection`、`withExpoNotificationsStub`、`withExpoMediaLibraryStub`、`withExpoCalendarStub` |
348
+ | `./metro` | `dist/metro.js` | `withDevkit`、`withDevStubs`、`withEntryInjection`、`withPersistentCache`、`withTransformLogger`、`withExpoNotificationsStub`、`withExpoMediaLibraryStub`、`withExpoCalendarStub` 等全部 Metro wrapper |
293
349
  | `./babel-plugin-jsx-source` | `dist/babel/plugin-jsx-source.js` | Babel 插件:为 JSX 注入 source 信息 |
294
350
  | `./babel-preset` | `dist/babel/preset.js` | Babel Preset:集成 jsx-source 和 lucide 插件 |
295
351
  | `./sentry-react-native-stub` | `dist/stubs/sentry-react-native-stub.js` | `@sentry/react-native` 模块替换 stub |
@@ -297,6 +353,10 @@ sentry-react-native-stub.js
297
353
  | `./expo-notifications-stub` | `dist/stubs/expo-notifications-stub.js` | `expo-notifications` Expo Go Android stub |
298
354
  | `./expo-media-library-stub` | `dist/stubs/expo-media-library-stub.js` | `expo-media-library` Expo Go / Web stub |
299
355
  | `./expo-calendar-stub` | `dist/stubs/expo-calendar-stub.js` | `expo-calendar` Expo Go / Web stub |
356
+ | `./expo-file-system-stub` | `dist/stubs/expo-file-system-stub.js` | `expo-file-system/legacy` Web stub |
357
+ | `./expo-file-system-next-stub` | `dist/stubs/expo-file-system-next-stub.js` | `expo-file-system` 新版 API Web stub |
358
+ | `./expo-haptics-stub` | `dist/stubs/expo-haptics-stub.js` | `expo-haptics` Web stub |
359
+ | `./expo-contacts-stub` | `dist/stubs/expo-contacts-stub.js` | `expo-contacts` Web / Expo Go stub |
300
360
  | `./expo-image-picker-stub` | `dist/stubs/expo-image-picker-stub.js` | `expo-image-picker` 桌面 Web stub |
301
361
 
302
362
  ---
@@ -505,6 +565,44 @@ Expo Go 扫码预览不支持访问手机相册
505
565
 
506
566
  ---
507
567
 
568
+ ### expo-file-system-stub.js / expo-file-system-next-stub.js
569
+
570
+ `expo-file-system` 模块替换 stub,由 `withExpoFileSystemStub()` 在 Metro 层注入(**全平台**)。
571
+
572
+ **背景:** `expo-file-system` 的文件 API 在 Web 端不可用:legacy API 底层方法缺失会抛 `UnavailabilityError`;新版 API(`File` / `Directory`)基类缺少 `validatePath()`,`new File(...)` 会抛 `TypeError: this.validatePath is not a function`。
573
+
574
+ **拦截路径:**
575
+ - `expo-file-system/legacy` → `expo-file-system-stub.js`(legacy API)
576
+ - `expo-file-system` → `expo-file-system-next-stub.js`(新版 File/Directory/Paths API)
577
+
578
+ **运行时行为:**
579
+ - **Web**:提供 no-op 实现,核心 API 调用时弹出 Dialog 提示(不崩溃)
580
+ - **Expo Go / Development Build(原生)**:透传真实 `expo-file-system`,功能完全正常
581
+
582
+ ---
583
+
584
+ ### expo-haptics-stub.js
585
+
586
+ `expo-haptics` 模块替换 stub,由 `withExpoHapticsStub()` 在 Metro 层注入(**仅 web 平台**)。
587
+
588
+ **背景:** Web 没有振动/触觉硬件 API,`expo-haptics` 在 web 会运行时崩溃。
589
+
590
+ **运行时行为:**
591
+ - **Web**:提供 no-op 实现,`impactAsync`、`notificationAsync`、`selectionAsync` 调用时弹出 Dialog 提示参数信息(不崩溃);枚举常量(`ImpactFeedbackStyle`、`NotificationFeedbackType` 等)正常可用
592
+ - **Expo Go / Development Build(native)**:透传真实 `expo-haptics`,功能完全正常
593
+
594
+ ---
595
+
596
+ ### expo-contacts-stub.js
597
+
598
+ `expo-contacts` 模块替换 stub,由 `withExpoContactsStub()` 在 Metro 层注入(**仅 web 平台**)。
599
+
600
+ **背景:** `expo-contacts` 依赖 native module,在 Web 和 Expo Go 中不可用。
601
+
602
+ **运行时行为:**
603
+ - **Web / Expo Go**:提供 no-op 实现,联系人 API 调用时弹出 Dialog 提示(不崩溃)
604
+ - **Development Build(原生)**:透传真实 `expo-contacts`,功能完全正常
605
+
508
606
  ---
509
607
 
510
608
  ### expo-image-picker-stub.js
package/dist/metro.d.mts CHANGED
@@ -415,6 +415,7 @@ declare function withWasmSupport(config: MetroConfig): MetroConfig;
415
415
  * "bundle_url": "index.bundle",
416
416
  * "initial_build": true, // true=首次构建 false=增量更新
417
417
  * "graph_node_count": 220,
418
+ * "transform_miss_count": 5, // cache miss 的文件数;0 = 完全命中缓存
418
419
  * "bundle_length": 512000,
419
420
  * "points": {
420
421
  * "resolvingAndTransformingDependencies_start": 5,
@@ -460,6 +461,15 @@ declare function withTransformLogger(config: MetroConfig): MetroConfig;
460
461
  *
461
462
  * cache key 由文件内容 sha1 + 相对路径 + Babel/Metro 版本共同决定,
462
463
  * 与项目绝对路径无关,换目录 clone 后缓存同样有效。
464
+ *
465
+ * 若 METRO_TRANSFORM_LOG 已设置,启动时向日志追加一条 CACHE_CONFIG 条目:
466
+ * {
467
+ * "type": "CACHE_CONFIG",
468
+ * "cache_root": "/workspace/.metro-cache",
469
+ * "store_class": "ExpoFileStore", // 生效的 store 类名
470
+ * "source": "env" | "option" | "default",
471
+ * "t": 1234567890
472
+ * }
463
473
  */
464
474
 
465
475
  interface PersistentCacheOptions {
package/dist/metro.d.ts CHANGED
@@ -415,6 +415,7 @@ declare function withWasmSupport(config: MetroConfig): MetroConfig;
415
415
  * "bundle_url": "index.bundle",
416
416
  * "initial_build": true, // true=首次构建 false=增量更新
417
417
  * "graph_node_count": 220,
418
+ * "transform_miss_count": 5, // cache miss 的文件数;0 = 完全命中缓存
418
419
  * "bundle_length": 512000,
419
420
  * "points": {
420
421
  * "resolvingAndTransformingDependencies_start": 5,
@@ -460,6 +461,15 @@ declare function withTransformLogger(config: MetroConfig): MetroConfig;
460
461
  *
461
462
  * cache key 由文件内容 sha1 + 相对路径 + Babel/Metro 版本共同决定,
462
463
  * 与项目绝对路径无关,换目录 clone 后缓存同样有效。
464
+ *
465
+ * 若 METRO_TRANSFORM_LOG 已设置,启动时向日志追加一条 CACHE_CONFIG 条目:
466
+ * {
467
+ * "type": "CACHE_CONFIG",
468
+ * "cache_root": "/workspace/.metro-cache",
469
+ * "store_class": "ExpoFileStore", // 生效的 store 类名
470
+ * "source": "env" | "option" | "default",
471
+ * "t": 1234567890
472
+ * }
463
473
  */
464
474
 
465
475
  interface PersistentCacheOptions {
package/dist/metro.js CHANGED
@@ -504,6 +504,7 @@ function withWasmSupport(config) {
504
504
  var import_node_fs = __toESM(require("fs"));
505
505
  var import_node_path = __toESM(require("path"));
506
506
  var import_metro_core = require("metro-core");
507
+ var currentMissCount = 0;
507
508
  function installPerfLogger(logFile, existingFactory) {
508
509
  const dir = import_node_path.default.dirname(logFile);
509
510
  if (!import_node_fs.default.existsSync(dir)) {
@@ -515,6 +516,7 @@ function installPerfLogger(logFile, existingFactory) {
515
516
  const startTime = process.hrtime.bigint();
516
517
  const points = {};
517
518
  const annotations = {};
519
+ const missCountAtStart = currentMissCount;
518
520
  const logger = {
519
521
  start() {
520
522
  upstream?.start?.();
@@ -526,6 +528,7 @@ function installPerfLogger(logFile, existingFactory) {
526
528
  status,
527
529
  duration_ms: Math.round(duration_ms),
528
530
  ...annotations,
531
+ transform_miss_count: currentMissCount - missCountAtStart,
529
532
  points,
530
533
  t: Date.now()
531
534
  });
@@ -557,6 +560,7 @@ function installFileLogger(logFile) {
557
560
  fileLoggerInstalled = true;
558
561
  import_metro_core.Logger.on("log", (entry) => {
559
562
  if (entry.action_name === "Transforming file" && entry.action_phase === "end") {
563
+ currentMissCount++;
560
564
  const line = JSON.stringify({
561
565
  type: "TRANSFORM_FILE",
562
566
  file: entry.file_name,
@@ -580,6 +584,7 @@ function withTransformLogger(config) {
580
584
  }
581
585
 
582
586
  // src/metro/withPersistentCache.ts
587
+ var import_node_fs2 = __toESM(require("fs"));
583
588
  var import_path14 = __toESM(require("path"));
584
589
  var import_metro_cache = require("metro-cache");
585
590
  function withPersistentCache(config, options = {}) {
@@ -597,6 +602,17 @@ function withPersistentCache(config, options = {}) {
597
602
  const existingStores = config.cacheStores;
598
603
  const firstStore = Array.isArray(existingStores) ? existingStores[0] : void 0;
599
604
  const StoreClass = firstStore ? Object.getPrototypeOf(firstStore).constructor : import_metro_cache.FileStore;
605
+ const logFile = process.env["METRO_TRANSFORM_LOG"];
606
+ if (logFile) {
607
+ const line = JSON.stringify({
608
+ type: "CACHE_CONFIG",
609
+ cache_root: cacheRoot,
610
+ store_class: StoreClass.name || "unknown",
611
+ source: envCacheDir ? "env" : cacheDir ? "option" : "default",
612
+ t: Date.now()
613
+ });
614
+ import_node_fs2.default.appendFileSync(logFile, line + "\n");
615
+ }
600
616
  return {
601
617
  ...config,
602
618
  cacheStores: [new StoreClass({ root: cacheRoot })]
package/dist/metro.mjs CHANGED
@@ -455,6 +455,7 @@ function withWasmSupport(config) {
455
455
  import fs4 from "fs";
456
456
  import path14 from "path";
457
457
  import { Logger } from "metro-core";
458
+ var currentMissCount = 0;
458
459
  function installPerfLogger(logFile, existingFactory) {
459
460
  const dir = path14.dirname(logFile);
460
461
  if (!fs4.existsSync(dir)) {
@@ -466,6 +467,7 @@ function installPerfLogger(logFile, existingFactory) {
466
467
  const startTime = process.hrtime.bigint();
467
468
  const points = {};
468
469
  const annotations = {};
470
+ const missCountAtStart = currentMissCount;
469
471
  const logger = {
470
472
  start() {
471
473
  upstream?.start?.();
@@ -477,6 +479,7 @@ function installPerfLogger(logFile, existingFactory) {
477
479
  status,
478
480
  duration_ms: Math.round(duration_ms),
479
481
  ...annotations,
482
+ transform_miss_count: currentMissCount - missCountAtStart,
480
483
  points,
481
484
  t: Date.now()
482
485
  });
@@ -508,6 +511,7 @@ function installFileLogger(logFile) {
508
511
  fileLoggerInstalled = true;
509
512
  Logger.on("log", (entry) => {
510
513
  if (entry.action_name === "Transforming file" && entry.action_phase === "end") {
514
+ currentMissCount++;
511
515
  const line = JSON.stringify({
512
516
  type: "TRANSFORM_FILE",
513
517
  file: entry.file_name,
@@ -531,6 +535,7 @@ function withTransformLogger(config) {
531
535
  }
532
536
 
533
537
  // src/metro/withPersistentCache.ts
538
+ import fs5 from "fs";
534
539
  import path15 from "path";
535
540
  import { FileStore } from "metro-cache";
536
541
  function withPersistentCache(config, options = {}) {
@@ -548,6 +553,17 @@ function withPersistentCache(config, options = {}) {
548
553
  const existingStores = config.cacheStores;
549
554
  const firstStore = Array.isArray(existingStores) ? existingStores[0] : void 0;
550
555
  const StoreClass = firstStore ? Object.getPrototypeOf(firstStore).constructor : FileStore;
556
+ const logFile = process.env["METRO_TRANSFORM_LOG"];
557
+ if (logFile) {
558
+ const line = JSON.stringify({
559
+ type: "CACHE_CONFIG",
560
+ cache_root: cacheRoot,
561
+ store_class: StoreClass.name || "unknown",
562
+ source: envCacheDir ? "env" : cacheDir ? "option" : "default",
563
+ t: Date.now()
564
+ });
565
+ fs5.appendFileSync(logFile, line + "\n");
566
+ }
551
567
  return {
552
568
  ...config,
553
569
  cacheStores: [new StoreClass({ root: cacheRoot })]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-expo-devkit",
3
- "version": "0.1.1-beta.69",
3
+ "version": "0.1.1-beta.70",
4
4
  "description": "Expo 应用开发工具集:Sentry DSN 替换 stub、错误/网络捕获、Metro 符号化",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",