miaoda-expo-devkit 0.1.1-beta.30 → 0.1.1-beta.32

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,6 +7,8 @@ Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下
7
7
  - **Bundle 首部注入** — 在 expo-router 初始化之前执行自定义脚本
8
8
  - **HMR postMessage 控制** — 通过 `window.postMessage` 在运行时启动或停止 Fast Refresh
9
9
  - **LogBox 屏蔽** — web 平台禁用 Expo 全屏错误遮罩
10
+ - **expo-notifications stub** — Expo Go(Android)中提供 no-op 实现,核心 API 调用时弹出带参数校验的调试 Alert,Dev Build 透传真实模块
11
+ - **expo-media-library stub** — Expo Go / Web 中提供 no-op 实现,`saveToLibraryAsync`、`createAssetAsync`、权限请求等 API 调用时弹出 Alert 提示,Dev Build 原生环境透传真实模块
10
12
 
11
13
  ## 安装
12
14
 
@@ -32,16 +34,16 @@ pnpm install
32
34
  ```js
33
35
  // metro.config.js
34
36
  const { getDefaultConfig } = require('expo/metro-config');
35
- const { withDevStubs, withEntryInjection } = require('miaoda-expo-devkit/metro');
37
+ const { withDevkit } = require('miaoda-expo-devkit/metro');
36
38
 
37
- const config = getDefaultConfig(__dirname);
38
- module.exports = withEntryInjection(withDevStubs(config));
39
+ module.exports = withDevkit(getDefaultConfig(__dirname));
39
40
  ```
40
41
 
41
- 支持与其他 Metro wrapper 链式组合:
42
+ `withDevkit` 已内置所有 wrapper(含 expo-notifications、expo-media-library stub),无需手动叠加。也可单独使用各 wrapper:
42
43
 
43
44
  ```js
44
- module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });
45
+ const { withDevStubs, withEntryInjection, withExpoMediaLibraryStub } = require('miaoda-expo-devkit/metro');
46
+ module.exports = withExpoMediaLibraryStub(withEntryInjection(withDevStubs(config)));
45
47
  ```
46
48
 
47
49
  ### Sentry 初始化
@@ -225,12 +227,22 @@ expect(onError).toHaveBeenCalledWith(
225
227
 
226
228
  ```
227
229
  metro.config.js
228
- └─ withEntryInjection(withDevStubs(config))
230
+ └─ withDevkit(config)
229
231
 
230
232
  ├─ withDevStubs → resolver.resolveRequest
231
233
  │ ├─ @sentry/react-native → dist/stubs/sentry-react-native-stub.js (全平台)
232
234
  │ └─ @expo/log-box → dist/stubs/no-op-logbox.js (仅 web)
233
235
 
236
+ ├─ withExpoNotificationsStub → resolver.resolveRequest
237
+ │ └─ expo-notifications → dist/stubs/expo-notifications-stub.js (仅 Android)
238
+ │ ├─ Expo Go:no-op + 调试 Alert(含参数校验)
239
+ │ └─ Dev Build:透传真实 expo-notifications
240
+
241
+ ├─ withExpoMediaLibraryStub → resolver.resolveRequest
242
+ │ └─ expo-media-library → dist/stubs/expo-media-library-stub.js (全平台)
243
+ │ ├─ Expo Go / Web:no-op + Alert 提示(不崩溃)
244
+ │ └─ Dev Build(原生):透传真实 expo-media-library
245
+
234
246
  └─ withEntryInjection → resolver.resolveRequest
235
247
  └─ expo-router/entry-classic → dist/stubs/expo-router-entry-stub.js
236
248
  ├─ require('./entry-inject') ← 注入脚本(bundle 首部执行)
@@ -262,11 +274,13 @@ sentry-react-native-stub.js
262
274
  | 子路径 | 文件 | 内容 |
263
275
  |---|---|---|
264
276
  | `.` | `dist/index.js` | `SentryCapture`、`MetroSymbolicator`、全部类型 |
265
- | `./metro` | `dist/metro.js` | `withDevStubs`、`withEntryInjection` |
277
+ | `./metro` | `dist/metro.js` | `withDevkit`、`withDevStubs`、`withEntryInjection`、`withExpoNotificationsStub`、`withExpoMediaLibraryStub` |
266
278
  | `./babel-plugin-jsx-source` | `dist/babel/plugin-jsx-source.js` | Babel 插件:为 JSX 注入 source 信息 |
267
279
  | `./babel-preset` | `dist/babel/preset.js` | Babel Preset:集成 jsx-source 和 lucide 插件 |
268
280
  | `./sentry-react-native-stub` | `dist/stubs/sentry-react-native-stub.js` | `@sentry/react-native` 模块替换 stub |
269
281
  | `./no-op-logbox` | `dist/stubs/no-op-logbox.js` | LogBox no-op stub |
282
+ | `./expo-notifications-stub` | `dist/stubs/expo-notifications-stub.js` | `expo-notifications` Expo Go Android stub |
283
+ | `./expo-media-library-stub` | `dist/stubs/expo-media-library-stub.js` | `expo-media-library` Expo Go / Web stub |
270
284
 
271
285
  ---
272
286
 
@@ -389,6 +403,48 @@ LogBox no-op stub,用于 web 平台禁用 Expo 全屏错误遮罩。
389
403
 
390
404
  ---
391
405
 
406
+ ### expo-notifications-stub.js
407
+
408
+ `expo-notifications` 模块替换 stub,由 `withExpoNotificationsStub()` 在 Metro 层注入(**仅 Android 平台**)。
409
+
410
+ **背景:** Expo SDK 53 起,`expo-notifications` 的 Android native module 已从 Expo Go 中移除,直接 import 会在 Expo Go 启动时崩溃。
411
+
412
+ **运行时行为:**
413
+ - **Expo Go(Android)**:提供 no-op 实现,`requestPermissionsAsync`、`setNotificationChannelAsync`、`scheduleNotificationAsync` 等核心 API 调用时弹出带参数校验的调试 Alert
414
+ - **Development Build(Android)**:透传真实 `expo-notifications`,功能完全正常
415
+ - **iOS(任意)**:不经过此 stub,直接使用真实 `expo-notifications`
416
+
417
+ **手动验证:** 在 `devkit-e2e` App 中扫码进入「Notification Stub 验证」页面,逐按钮触发并对照期望结果。
418
+
419
+ ---
420
+
421
+ ### expo-media-library-stub.js
422
+
423
+ `expo-media-library` 模块替换 stub,由 `withExpoMediaLibraryStub()` 在 Metro 层注入(**全平台**)。
424
+
425
+ **背景:** `expo-media-library` 依赖原生相册 API,在 Expo Go 和 Web 环境中不可用,调用 `saveToLibraryAsync` 等 API 会直接崩溃。
426
+
427
+ **运行时行为:**
428
+ - **Expo Go / Web**:提供 no-op 实现,以下 API 调用时弹出 Alert 提示(不崩溃):
429
+ - `usePermissions()` — 返回 `{ status: 'undetermined', granted: false }`,`requestPermission()` 弹 Alert
430
+ - `requestPermissionsAsync()` / `getPermissionsAsync()` — 前者弹 Alert,后者静默返回 denied
431
+ - `saveToLibraryAsync(uri)` — 弹 Alert 显示操作和 URI(超 60 字符自动截断)
432
+ - `createAssetAsync(uri)` — 弹 Alert 并返回合法的伪资产对象
433
+ - 其他未知 API — Proxy 兜底,静默返回 `undefined`;以 `PermissionsAsync` 结尾的 API 返回 denied 结构
434
+ - **Development Build(原生)**:透传真实 `expo-media-library`,功能完全正常
435
+
436
+ **Alert 消息格式:**
437
+ ```
438
+ 保存到相册
439
+ Expo Go 扫码预览不支持访问手机相册
440
+ 操作: 图片: file:///tmp/test.jpg
441
+ 发布为正式 App 后可正常使用
442
+ ```
443
+
444
+ **手动验证:** 在 `devkit-e2e` App 中扫码进入「Media Library Stub 验证」页面,逐按钮触发并对照期望结果。
445
+
446
+ ---
447
+
392
448
  ## Babel 插件:jsx-source
393
449
 
394
450
  `babel-plugin-jsx-source` 为 JSX 元素注入 source 属性(文件路径、行列号),用于开发调试。通过 `dataSet` 对象注入,这是 React Web 可识别的数据通道。
package/dist/metro.d.mts CHANGED
@@ -116,6 +116,7 @@ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOp
116
116
  * withEsbuildMinify — 将 Metro minifier 切换为 esbuild(仅生产构建生效)
117
117
  * withLucideResolver — 消除 lucide 子路径未在 exports 声明时的 warning
118
118
  * withExpoNotificationsStub — Android:expo-notifications → stub(Expo Go no-op,Dev Build 透传)
119
+ * withExpoMediaLibraryStub — Web / Expo Go:expo-media-library → stub(弹 Alert 提示,Dev Build 透传)
119
120
  * withEntryInjection — 在 expo-router 启动前注入脚本(仅 __DEV__)
120
121
  * withDevStubs — 替换 Sentry DSN / 屏蔽 LogBox(仅 __DEV__)
121
122
  * withRouteEndpoint — 添加 /__routes 端点(仅 __DEV__)
@@ -258,6 +259,23 @@ declare function withLucideResolver(config: MetroConfig, options?: WithLucideRes
258
259
  */
259
260
  declare function withExpoNotificationsStub(config: MetroConfig): MetroConfig;
260
261
 
262
+ /**
263
+ * withExpoMediaLibraryStub — Web / Expo Go 将 expo-media-library 替换为 stub
264
+ *
265
+ * expo-media-library 依赖 native module,在 Web 和 Expo Go 中不可用。
266
+ * stub 在运行时通过 Platform.OS 和 isRunningInExpoGo() 判断:
267
+ * - Web / Expo Go:no-op(核心 API 弹 Alert 提示,不崩溃)
268
+ * - Development Build(原生):透传真实 expo-media-library,功能完全正常
269
+ *
270
+ * 此 wrapper 需要在开发和生产构建中均生效,因为 native module 缺失是运行时问题。
271
+ */
272
+
273
+ /**
274
+ * @param config Metro config 对象
275
+ * @returns 注入 resolveRequest 后的新 Metro config
276
+ */
277
+ declare function withExpoMediaLibraryStub(config: MetroConfig): MetroConfig;
278
+
261
279
  /**
262
280
  * 将 Metro transformer 的 minifier 切换为 esbuild。
263
281
  *
@@ -270,4 +288,4 @@ declare function withExpoNotificationsStub(config: MetroConfig): MetroConfig;
270
288
  */
271
289
  declare function withEsbuildMinify(config: MetroConfig): MetroConfig;
272
290
 
273
- export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withWorkspaceNodeModules };
291
+ export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withWorkspaceNodeModules };
package/dist/metro.d.ts CHANGED
@@ -116,6 +116,7 @@ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOp
116
116
  * withEsbuildMinify — 将 Metro minifier 切换为 esbuild(仅生产构建生效)
117
117
  * withLucideResolver — 消除 lucide 子路径未在 exports 声明时的 warning
118
118
  * withExpoNotificationsStub — Android:expo-notifications → stub(Expo Go no-op,Dev Build 透传)
119
+ * withExpoMediaLibraryStub — Web / Expo Go:expo-media-library → stub(弹 Alert 提示,Dev Build 透传)
119
120
  * withEntryInjection — 在 expo-router 启动前注入脚本(仅 __DEV__)
120
121
  * withDevStubs — 替换 Sentry DSN / 屏蔽 LogBox(仅 __DEV__)
121
122
  * withRouteEndpoint — 添加 /__routes 端点(仅 __DEV__)
@@ -258,6 +259,23 @@ declare function withLucideResolver(config: MetroConfig, options?: WithLucideRes
258
259
  */
259
260
  declare function withExpoNotificationsStub(config: MetroConfig): MetroConfig;
260
261
 
262
+ /**
263
+ * withExpoMediaLibraryStub — Web / Expo Go 将 expo-media-library 替换为 stub
264
+ *
265
+ * expo-media-library 依赖 native module,在 Web 和 Expo Go 中不可用。
266
+ * stub 在运行时通过 Platform.OS 和 isRunningInExpoGo() 判断:
267
+ * - Web / Expo Go:no-op(核心 API 弹 Alert 提示,不崩溃)
268
+ * - Development Build(原生):透传真实 expo-media-library,功能完全正常
269
+ *
270
+ * 此 wrapper 需要在开发和生产构建中均生效,因为 native module 缺失是运行时问题。
271
+ */
272
+
273
+ /**
274
+ * @param config Metro config 对象
275
+ * @returns 注入 resolveRequest 后的新 Metro config
276
+ */
277
+ declare function withExpoMediaLibraryStub(config: MetroConfig): MetroConfig;
278
+
261
279
  /**
262
280
  * 将 Metro transformer 的 minifier 切换为 esbuild。
263
281
  *
@@ -270,4 +288,4 @@ declare function withExpoNotificationsStub(config: MetroConfig): MetroConfig;
270
288
  */
271
289
  declare function withEsbuildMinify(config: MetroConfig): MetroConfig;
272
290
 
273
- export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withWorkspaceNodeModules };
291
+ export { type DevkitOptions, type InjectOptions, type PatchNativeWindCacheOptions, type RouteEndpointOptions, patchNativeWindCachePath, withCssInterop, withDevStubs, withDevkit, withEntryInjection, withEsbuildMinify, withExpoMediaLibraryStub, withExpoNotificationsStub, withLucideResolver, withNativeWind, withRouteEndpoint, withWorkspaceNodeModules };
package/dist/metro.js CHANGED
@@ -36,6 +36,7 @@ __export(metro_exports, {
36
36
  withDevkit: () => withDevkit,
37
37
  withEntryInjection: () => withEntryInjection,
38
38
  withEsbuildMinify: () => withEsbuildMinify,
39
+ withExpoMediaLibraryStub: () => withExpoMediaLibraryStub,
39
40
  withExpoNotificationsStub: () => withExpoNotificationsStub,
40
41
  withLucideResolver: () => withLucideResolver,
41
42
  withNativeWind: () => withNativeWind,
@@ -283,7 +284,7 @@ function withCssInterop(config) {
283
284
  }
284
285
 
285
286
  // src/metro/withDevkit.ts
286
- var import_path9 = __toESM(require("path"));
287
+ var import_path10 = __toESM(require("path"));
287
288
 
288
289
  // src/metro/withEsbuildMinify.ts
289
290
  function withEsbuildMinify(config) {
@@ -353,7 +354,7 @@ var EXPO_NOTIFICATIONS_STUB_PATH = import_path8.default.resolve(__dirname, "stub
353
354
  function withExpoNotificationsStub(config) {
354
355
  const upstream = config.resolver?.resolveRequest ?? null;
355
356
  const resolveRequest = (context, moduleName, platform) => {
356
- if (platform === "android" && moduleName === "expo-notifications" && !context.originModulePath.includes(EXPO_NOTIFICATIONS_STUB_FILENAME)) {
357
+ if ((platform === "android" || platform === "web") && moduleName === "expo-notifications" && !context.originModulePath.includes(EXPO_NOTIFICATIONS_STUB_FILENAME)) {
357
358
  return { filePath: EXPO_NOTIFICATIONS_STUB_PATH, type: "sourceFile" };
358
359
  }
359
360
  if (upstream) return upstream(context, moduleName, platform);
@@ -362,6 +363,26 @@ function withExpoNotificationsStub(config) {
362
363
  return { ...config, resolver: { ...config.resolver, resolveRequest } };
363
364
  }
364
365
 
366
+ // src/metro/withExpoMediaLibraryStub.ts
367
+ var import_path9 = __toESM(require("path"));
368
+ var EXPO_MEDIA_LIBRARY_STUB_FILENAME = "expo-media-library-stub.js";
369
+ var EXPO_MEDIA_LIBRARY_STUB_PATH = import_path9.default.resolve(
370
+ __dirname,
371
+ "stubs",
372
+ EXPO_MEDIA_LIBRARY_STUB_FILENAME
373
+ );
374
+ function withExpoMediaLibraryStub(config) {
375
+ const upstream = config.resolver?.resolveRequest ?? null;
376
+ const resolveRequest = (context, moduleName, platform) => {
377
+ if (moduleName === "expo-media-library" && !context.originModulePath.includes(EXPO_MEDIA_LIBRARY_STUB_FILENAME)) {
378
+ return { filePath: EXPO_MEDIA_LIBRARY_STUB_PATH, type: "sourceFile" };
379
+ }
380
+ if (upstream) return upstream(context, moduleName, platform);
381
+ return context.resolveRequest(context, moduleName, platform);
382
+ };
383
+ return { ...config, resolver: { ...config.resolver, resolveRequest } };
384
+ }
385
+
365
386
  // src/metro/withDevkit.ts
366
387
  function withDevkit(config, options = {}) {
367
388
  const projectRoot = config.projectRoot ?? process.cwd();
@@ -371,10 +392,11 @@ function withDevkit(config, options = {}) {
371
392
  config = withEsbuildMinify(config);
372
393
  config = withLucideResolver(config);
373
394
  config = withExpoNotificationsStub(config);
395
+ config = withExpoMediaLibraryStub(config);
374
396
  if (typeof __DEV__ !== "undefined" && __DEV__) {
375
397
  config = withEntryInjection(config);
376
398
  config = withDevStubs(config);
377
- config = withRouteEndpoint(config, { appDir: import_path9.default.join(projectRoot, "src", "app") });
399
+ config = withRouteEndpoint(config, { appDir: import_path10.default.join(projectRoot, "src", "app") });
378
400
  }
379
401
  return withNativeWind(config, { input, inlineRem: 16 });
380
402
  }
@@ -394,6 +416,7 @@ try {
394
416
  withDevkit,
395
417
  withEntryInjection,
396
418
  withEsbuildMinify,
419
+ withExpoMediaLibraryStub,
397
420
  withExpoNotificationsStub,
398
421
  withLucideResolver,
399
422
  withNativeWind,
package/dist/metro.mjs CHANGED
@@ -244,7 +244,7 @@ function withCssInterop(config) {
244
244
  }
245
245
 
246
246
  // src/metro/withDevkit.ts
247
- import path9 from "path";
247
+ import path10 from "path";
248
248
 
249
249
  // src/metro/withEsbuildMinify.ts
250
250
  function withEsbuildMinify(config) {
@@ -314,7 +314,7 @@ var EXPO_NOTIFICATIONS_STUB_PATH = path8.resolve(__dirname, "stubs", EXPO_NOTIFI
314
314
  function withExpoNotificationsStub(config) {
315
315
  const upstream = config.resolver?.resolveRequest ?? null;
316
316
  const resolveRequest = (context, moduleName, platform) => {
317
- if (platform === "android" && moduleName === "expo-notifications" && !context.originModulePath.includes(EXPO_NOTIFICATIONS_STUB_FILENAME)) {
317
+ if ((platform === "android" || platform === "web") && moduleName === "expo-notifications" && !context.originModulePath.includes(EXPO_NOTIFICATIONS_STUB_FILENAME)) {
318
318
  return { filePath: EXPO_NOTIFICATIONS_STUB_PATH, type: "sourceFile" };
319
319
  }
320
320
  if (upstream) return upstream(context, moduleName, platform);
@@ -323,6 +323,26 @@ function withExpoNotificationsStub(config) {
323
323
  return { ...config, resolver: { ...config.resolver, resolveRequest } };
324
324
  }
325
325
 
326
+ // src/metro/withExpoMediaLibraryStub.ts
327
+ import path9 from "path";
328
+ var EXPO_MEDIA_LIBRARY_STUB_FILENAME = "expo-media-library-stub.js";
329
+ var EXPO_MEDIA_LIBRARY_STUB_PATH = path9.resolve(
330
+ __dirname,
331
+ "stubs",
332
+ EXPO_MEDIA_LIBRARY_STUB_FILENAME
333
+ );
334
+ function withExpoMediaLibraryStub(config) {
335
+ const upstream = config.resolver?.resolveRequest ?? null;
336
+ const resolveRequest = (context, moduleName, platform) => {
337
+ if (moduleName === "expo-media-library" && !context.originModulePath.includes(EXPO_MEDIA_LIBRARY_STUB_FILENAME)) {
338
+ return { filePath: EXPO_MEDIA_LIBRARY_STUB_PATH, type: "sourceFile" };
339
+ }
340
+ if (upstream) return upstream(context, moduleName, platform);
341
+ return context.resolveRequest(context, moduleName, platform);
342
+ };
343
+ return { ...config, resolver: { ...config.resolver, resolveRequest } };
344
+ }
345
+
326
346
  // src/metro/withDevkit.ts
327
347
  function withDevkit(config, options = {}) {
328
348
  const projectRoot = config.projectRoot ?? process.cwd();
@@ -332,10 +352,11 @@ function withDevkit(config, options = {}) {
332
352
  config = withEsbuildMinify(config);
333
353
  config = withLucideResolver(config);
334
354
  config = withExpoNotificationsStub(config);
355
+ config = withExpoMediaLibraryStub(config);
335
356
  if (typeof __DEV__ !== "undefined" && __DEV__) {
336
357
  config = withEntryInjection(config);
337
358
  config = withDevStubs(config);
338
- config = withRouteEndpoint(config, { appDir: path9.join(projectRoot, "src", "app") });
359
+ config = withRouteEndpoint(config, { appDir: path10.join(projectRoot, "src", "app") });
339
360
  }
340
361
  return withNativeWind(config, { input, inlineRem: 16 });
341
362
  }
@@ -354,6 +375,7 @@ export {
354
375
  withDevkit,
355
376
  withEntryInjection,
356
377
  withEsbuildMinify,
378
+ withExpoMediaLibraryStub,
357
379
  withExpoNotificationsStub,
358
380
  withLucideResolver,
359
381
  withNativeWind,
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var import_react_native = require("react-native");
3
+ var import_expo = require("expo");
4
+ var import_web_stub_dialog = require("./web-stub-dialog");
5
+ if (import_react_native.Platform.OS !== "web" && !(0, import_expo.isRunningInExpoGo)()) {
6
+ module.exports = require("expo-media-library");
7
+ } else {
8
+ let showStubAlert = function(title, detail) {
9
+ if (import_react_native.Platform.OS === "web") {
10
+ (0, import_web_stub_dialog.showWebStubDialog)({ title, details: [detail] });
11
+ } else {
12
+ import_react_native.Alert.alert(
13
+ title,
14
+ ["\u79D2\u54D2\u626B\u7801\u9884\u89C8\u4E0D\u652F\u6301\u8BBF\u95EE\u624B\u673A\u76F8\u518C", `\u64CD\u4F5C: ${detail}`, "", "\u53D1\u5E03\u4E3A\u6B63\u5F0F App \u540E\u53EF\u6B63\u5E38\u4F7F\u7528"].join("\n"),
15
+ [{ text: "\u77E5\u9053\u4E86" }]
16
+ );
17
+ }
18
+ }, usePermissions = function() {
19
+ const [permission, setPermission] = useState(UNDETERMINED_PERMISSION);
20
+ const requestPermission = useCallback(async () => {
21
+ showStubAlert("\u8BF7\u6C42\u76F8\u518C\u6743\u9650", "\u8BFB\u5199\u624B\u673A\u76F8\u518C");
22
+ setPermission(DENIED_PERMISSION);
23
+ return DENIED_PERMISSION;
24
+ }, []);
25
+ const getPermission = useCallback(async () => permission, [permission]);
26
+ return [permission, requestPermission, getPermission];
27
+ };
28
+ var showStubAlert2 = showStubAlert, usePermissions2 = usePermissions;
29
+ const UNDETERMINED_PERMISSION = {
30
+ status: "undetermined",
31
+ granted: false,
32
+ canAskAgain: true,
33
+ expires: "never"
34
+ };
35
+ const DENIED_PERMISSION = {
36
+ status: "denied",
37
+ granted: false,
38
+ canAskAgain: false,
39
+ expires: "never"
40
+ };
41
+ const { useState, useCallback } = require("react");
42
+ const saveToLibraryAsync = async (uri) => {
43
+ const uriStr = typeof uri === "string" ? uri.length > 60 ? uri.slice(0, 60) + "\u2026" : uri : String(uri);
44
+ showStubAlert("\u4FDD\u5B58\u5230\u76F8\u518C", `\u56FE\u7247: ${uriStr}`);
45
+ };
46
+ const createAssetAsync = async (uri) => {
47
+ const uriStr = typeof uri === "string" ? uri.slice(0, 60) : String(uri);
48
+ showStubAlert("\u521B\u5EFA\u5A92\u4F53\u8D44\u4EA7", `\u6587\u4EF6: ${uriStr}`);
49
+ return {
50
+ id: "stub",
51
+ filename: "stub.jpg",
52
+ uri: String(uri),
53
+ mediaType: "photo",
54
+ mediaSubtypes: [],
55
+ width: 0,
56
+ height: 0,
57
+ creationTime: 0,
58
+ modificationTime: 0,
59
+ duration: 0
60
+ };
61
+ };
62
+ const requestPermissionsAsync = async () => {
63
+ showStubAlert("\u8BF7\u6C42\u76F8\u518C\u6743\u9650", "\u8BFB\u5199\u624B\u673A\u76F8\u518C");
64
+ return DENIED_PERMISSION;
65
+ };
66
+ const getPermissionsAsync = async () => DENIED_PERMISSION;
67
+ const enums = {
68
+ PermissionStatus: {
69
+ GRANTED: "granted",
70
+ DENIED: "denied",
71
+ UNDETERMINED: "undetermined",
72
+ LIMITED: "limited"
73
+ },
74
+ MediaType: {
75
+ photo: "photo",
76
+ video: "video",
77
+ audio: "audio",
78
+ unknown: "unknown"
79
+ },
80
+ SortBy: {
81
+ default: "default",
82
+ creationTime: "creationTime",
83
+ modificationTime: "modificationTime",
84
+ mediaType: "mediaType",
85
+ width: "width",
86
+ height: "height",
87
+ duration: "duration"
88
+ }
89
+ };
90
+ const coreHandlers = {
91
+ usePermissions,
92
+ saveToLibraryAsync,
93
+ createAssetAsync,
94
+ requestPermissionsAsync,
95
+ getPermissionsAsync
96
+ };
97
+ const noopPermission = async () => DENIED_PERMISSION;
98
+ const noop = async () => void 0;
99
+ module.exports = new Proxy(enums, {
100
+ get(target, key) {
101
+ if (key in target) return target[key];
102
+ if (key in coreHandlers) return coreHandlers[key];
103
+ if (key.endsWith("PermissionsAsync")) return noopPermission;
104
+ return noop;
105
+ }
106
+ });
107
+ }
108
+ //# sourceMappingURL=expo-media-library-stub.js.map
@@ -1,10 +1,19 @@
1
1
  "use strict";
2
2
  var import_expo = require("expo");
3
3
  var import_react_native = require("react-native");
4
- if (!(0, import_expo.isRunningInExpoGo)()) {
4
+ var import_web_stub_dialog = require("./web-stub-dialog");
5
+ if (import_react_native.Platform.OS !== "web" && !(0, import_expo.isRunningInExpoGo)()) {
5
6
  module.exports = require("expo-notifications");
6
7
  } else {
7
8
  let showDetailedAlert = function(apiName, lines, isValid, errors) {
9
+ if (import_react_native.Platform.OS === "web") {
10
+ (0, import_web_stub_dialog.showWebStubDialog)({
11
+ title: apiName,
12
+ details: lines,
13
+ errors: !isValid && errors && errors.length > 0 ? errors : void 0
14
+ });
15
+ return;
16
+ }
8
17
  const statusLine = isValid ? "\u79D2\u54D2\u901A\u77E5\u60A8 \u6D88\u606F\u63A8\u9001\u9A8C\u8BC1\u901A\u8FC7" : "\u9A8C\u8BC1\u672A\u901A\u8FC7";
9
18
  const parts = [statusLine, "", ...lines];
10
19
  if (!isValid && errors && errors.length > 0) {
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var web_stub_dialog_exports = {};
20
+ __export(web_stub_dialog_exports, {
21
+ showWebStubDialog: () => showWebStubDialog
22
+ });
23
+ module.exports = __toCommonJS(web_stub_dialog_exports);
24
+ const DIALOG_ID = "__devkit_stub_dialog__";
25
+ const STYLES = {
26
+ backdrop: `
27
+ position: fixed;
28
+ inset: 0;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ background: rgba(0,0,0,0.55);
33
+ z-index: 99999;
34
+ padding: 24px;
35
+ box-sizing: border-box;
36
+ `,
37
+ card: `
38
+ background: #1e1e1e;
39
+ border-radius: 14px;
40
+ padding: 20px 24px 16px;
41
+ max-width: 380px;
42
+ width: 100%;
43
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
44
+ display: flex;
45
+ flex-direction: column;
46
+ gap: 0;
47
+ `,
48
+ badge: `
49
+ display: inline-block;
50
+ font-size: 11px;
51
+ font-weight: 600;
52
+ letter-spacing: 0.5px;
53
+ color: #00E676;
54
+ background: rgba(0,230,118,0.12);
55
+ border-radius: 6px;
56
+ padding: 2px 8px;
57
+ margin-bottom: 10px;
58
+ font-family: system-ui, sans-serif;
59
+ `,
60
+ title: `
61
+ font-size: 16px;
62
+ font-weight: 700;
63
+ color: #ffffff;
64
+ margin: 0 0 12px;
65
+ font-family: system-ui, sans-serif;
66
+ line-height: 1.3;
67
+ `,
68
+ body: `
69
+ font-size: 13px;
70
+ color: #aaaaaa;
71
+ font-family: system-ui, sans-serif;
72
+ line-height: 1.6;
73
+ margin: 0 0 16px;
74
+ white-space: pre-wrap;
75
+ `,
76
+ highlight: `
77
+ color: #e0e0e0;
78
+ font-family: ui-monospace, monospace;
79
+ font-size: 12px;
80
+ background: rgba(255,255,255,0.06);
81
+ border-radius: 6px;
82
+ padding: 8px 10px;
83
+ margin: 0 0 16px;
84
+ white-space: pre-wrap;
85
+ word-break: break-all;
86
+ `,
87
+ footer: `
88
+ font-size: 12px;
89
+ color: #666;
90
+ font-family: system-ui, sans-serif;
91
+ margin: 0 0 16px;
92
+ `,
93
+ button: `
94
+ align-self: flex-end;
95
+ background: #00E676;
96
+ color: #121212;
97
+ border: none;
98
+ border-radius: 8px;
99
+ padding: 8px 20px;
100
+ font-size: 14px;
101
+ font-weight: 700;
102
+ font-family: system-ui, sans-serif;
103
+ cursor: pointer;
104
+ transition: opacity 0.15s;
105
+ `
106
+ };
107
+ function showWebStubDialog(opts) {
108
+ if (typeof document === "undefined") return;
109
+ document.getElementById(DIALOG_ID)?.remove();
110
+ const backdrop = document.createElement("div");
111
+ backdrop.id = DIALOG_ID;
112
+ backdrop.setAttribute("style", STYLES.backdrop);
113
+ const card = document.createElement("div");
114
+ card.setAttribute("style", STYLES.card);
115
+ const badge = document.createElement("span");
116
+ badge.setAttribute("style", STYLES.badge);
117
+ badge.textContent = "\u79D2\u54D2\u9884\u89C8\u6A21\u5F0F";
118
+ const titleEl = document.createElement("p");
119
+ titleEl.setAttribute("style", STYLES.title);
120
+ titleEl.textContent = opts.title;
121
+ const bodyEl = document.createElement("p");
122
+ bodyEl.setAttribute("style", STYLES.body);
123
+ bodyEl.textContent = "\u6B64\u529F\u80FD\u5728\u79D2\u54D2\u9884\u89C8\u4E2D\u4E0D\u53EF\u7528\uFF0C\u53D1\u5E03\u4E3A\u6B63\u5F0F App \u540E\u53EF\u6B63\u5E38\u4F7F\u7528\u3002";
124
+ const detailEl = document.createElement("div");
125
+ detailEl.setAttribute("style", STYLES.highlight);
126
+ detailEl.textContent = opts.details.join("\n");
127
+ card.appendChild(badge);
128
+ card.appendChild(titleEl);
129
+ card.appendChild(bodyEl);
130
+ card.appendChild(detailEl);
131
+ if (opts.errors && opts.errors.length > 0) {
132
+ const errEl = document.createElement("p");
133
+ errEl.setAttribute(
134
+ "style",
135
+ STYLES.footer + "color: #ff5252;"
136
+ );
137
+ errEl.textContent = "\u53C2\u6570\u95EE\u9898: " + opts.errors.join(" / ");
138
+ card.appendChild(errEl);
139
+ }
140
+ const btn = document.createElement("button");
141
+ btn.setAttribute("style", STYLES.button);
142
+ btn.textContent = "\u77E5\u9053\u4E86";
143
+ const close = () => backdrop.remove();
144
+ btn.addEventListener("click", close);
145
+ backdrop.addEventListener("click", (e) => {
146
+ if (e.target === backdrop) close();
147
+ });
148
+ card.appendChild(btn);
149
+ backdrop.appendChild(card);
150
+ document.body.appendChild(backdrop);
151
+ }
152
+ // Annotate the CommonJS export names for ESM import in node:
153
+ 0 && (module.exports = {
154
+ showWebStubDialog
155
+ });
156
+ //# sourceMappingURL=web-stub-dialog.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-expo-devkit",
3
- "version": "0.1.1-beta.30",
3
+ "version": "0.1.1-beta.32",
4
4
  "description": "Expo 应用开发工具集:Sentry DSN 替换 stub、错误/网络捕获、Metro 符号化",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -45,6 +45,7 @@
45
45
  "./sentry-react-native-stub": "./dist/stubs/sentry-react-native-stub.js",
46
46
  "./no-op-logbox": "./dist/stubs/no-op-logbox.js",
47
47
  "./expo-notifications-stub": "./dist/stubs/expo-notifications-stub.js",
48
+ "./expo-media-library-stub": "./dist/stubs/expo-media-library-stub.js",
48
49
  "./rules/no-undeclared-expo-plugin": "./dist/rules/no-undeclared-expo-plugin.js",
49
50
  "./rules/no-unused-expo-plugin": "./dist/rules/no-unused-expo-plugin.js",
50
51
  "./rules/no-unstable-expo-router": "./dist/rules/no-unstable-expo-router.js",
package/pnpm-config.json CHANGED
@@ -1,8 +1,3 @@
1
1
  {
2
- "blockedPackages": [
3
- {
4
- "name": "expo-media-library",
5
- "reason": "Use \"expo-image-picker\" instead — it covers most image/video picking needs with simpler permissions. Only use expo-media-library if you need to save files to the album or browse the full library."
6
- }
7
- ]
2
+ "blockedPackages": []
8
3
  }