miaoda-expo-devkit 0.1.1-beta.1

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 ADDED
@@ -0,0 +1,267 @@
1
+ # miaoda-expo-devkit
2
+
3
+ Expo / React Native 开发环境工具集,通过 Metro 构建层注入以下能力:
4
+
5
+ - **Sentry DSN 替换** — 拦截 `@sentry/react-native`,将真实 DSN 替换为无害 stub,防止开发期错误事件发送到 Sentry 服务器
6
+ - **错误 / 网络捕获** — 内置 `SentryCapture`,自动调用 Metro `/symbolicate` 还原源码位置并输出到 `console.warn`
7
+ - **Bundle 首部注入** — 在 expo-router 初始化之前执行自定义脚本
8
+ - **HMR postMessage 控制** — 通过 `window.postMessage` 在运行时启动或停止 Fast Refresh
9
+ - **LogBox 屏蔽** — web 平台禁用 Expo 全屏错误遮罩
10
+
11
+ ## 安装
12
+
13
+ 在 pnpm workspace 中,将包添加到应用的依赖:
14
+
15
+ ```json
16
+ // your-app/package.json
17
+ {
18
+ "dependencies": {
19
+ "miaoda-expo-devkit": "workspace:*"
20
+ }
21
+ }
22
+ ```
23
+
24
+ ```sh
25
+ pnpm install
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ### Metro 配置
31
+
32
+ ```js
33
+ // metro.config.js
34
+ const { getDefaultConfig } = require('expo/metro-config');
35
+ const { withDevStubs, withEntryInjection } = require('miaoda-expo-devkit/metro');
36
+
37
+ const config = getDefaultConfig(__dirname);
38
+ module.exports = withEntryInjection(withDevStubs(config));
39
+ ```
40
+
41
+ 支持与其他 Metro wrapper 链式组合:
42
+
43
+ ```js
44
+ module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });
45
+ ```
46
+
47
+ ### Sentry 初始化
48
+
49
+ 照常调用 `Sentry.init`,stub 自动注入内置捕获器,无需额外配置:
50
+
51
+ ```ts
52
+ // app/_layout.tsx
53
+ import * as Sentry from '@sentry/react-native';
54
+
55
+ Sentry.init({
56
+ dsn: 'https://your-key@sentry.io/your-project',
57
+ // stub 自动拦截 beforeSend / beforeBreadcrumb,错误摘要输出到 console.warn
58
+ });
59
+ ```
60
+
61
+ #### 自定义回调(可选)
62
+
63
+ 传入自己的 `beforeSend` / `beforeBreadcrumb`,会在内置捕获器之后串联执行:
64
+
65
+ ```ts
66
+ import * as Sentry from '@sentry/react-native';
67
+ import { SentryCapture } from 'miaoda-expo-devkit';
68
+ import type { ErrorCaptureInfo, NetworkCaptureInfo } from 'miaoda-expo-devkit';
69
+
70
+ const capture = new SentryCapture({
71
+ onError(info: ErrorCaptureInfo) {
72
+ console.log('Error at:', info.symbolicatedFrames?.[0]);
73
+ },
74
+ onNetwork(info: NetworkCaptureInfo) {
75
+ console.log('Network:', info.method, info.url, info.statusCode);
76
+ },
77
+ });
78
+
79
+ Sentry.init({
80
+ dsn: 'https://your-key@sentry.io/your-project',
81
+ beforeSend: capture.beforeSend,
82
+ beforeBreadcrumb: capture.beforeBreadcrumb,
83
+ });
84
+ ```
85
+
86
+ ### HMR postMessage 控制
87
+
88
+ `withEntryInjection` 会在 bundle 首部注入脚本,该脚本安装一个 `window.postMessage` 监听器,可在运行时控制 Fast Refresh:
89
+
90
+ ```ts
91
+ // 停止 HMR(暂停 Fast Refresh,WebSocket 连接保持)
92
+ window.postMessage({ type: 'devkit:hmr', action: 'disable' }, '*');
93
+
94
+ // 恢复 HMR(flush 期间积压的所有更新)
95
+ window.postMessage({ type: 'devkit:hmr', action: 'enable' }, '*');
96
+ ```
97
+
98
+ 当前 HMR 状态可通过全局标记读取:
99
+
100
+ ```ts
101
+ window.__DEVKIT_HMR_ENABLED__ // true | false
102
+ ```
103
+
104
+ > HMR 客户端调用仅在 `__DEV__` 模式下生效。全局标记在静态导出(生产构建)下同样存在,可用于测试验证。
105
+
106
+ ## API
107
+
108
+ ### `withDevStubs(config)`
109
+
110
+ 从 `miaoda-expo-devkit/metro` 引入。
111
+
112
+ 将 Sentry DSN 替换 stub 和 LogBox 屏蔽注入到 Metro config:
113
+
114
+ - **所有平台**:将 `@sentry/react-native` 重定向到内置 stub
115
+ - **web 平台**:将 `@expo/log-box` 和 `ErrorOverlayWebControls` 重定向到 no-op stub
116
+
117
+ | 参数 | 类型 | 说明 |
118
+ |---|---|---|
119
+ | `config` | `MetroConfig` | 来自 `getDefaultConfig` 或 `getSentryExpoConfig` 的 Metro config |
120
+
121
+ ---
122
+
123
+ ### `withEntryInjection(config, options?)`
124
+
125
+ 从 `miaoda-expo-devkit/metro` 引入。
126
+
127
+ 在 expo-router 启动前注入脚本,注入内容(`dist/stubs/entry-inject.js`):
128
+
129
+ 1. 设置 `globalThis.__DEVKIT_INJECTED__ = true`
130
+ 2. 安装 HMR postMessage 控制器(`window.addEventListener('message', ...)`)
131
+ 3. 打印 `[DevKit] Hello World`
132
+
133
+ | 参数 | 类型 | 说明 |
134
+ |---|---|---|
135
+ | `config` | `MetroConfig` | Metro config 对象 |
136
+ | `options` | `InjectOptions?` | 预留选项(当前无字段) |
137
+
138
+ ---
139
+
140
+ ### `new SentryCapture(options)`
141
+
142
+ 从 `miaoda-expo-devkit` 引入。
143
+
144
+ 用于自定义捕获行为。stub 内置了一个默认实例(输出到 `console.warn`),此类用于叠加自定义处理逻辑。
145
+
146
+ | 选项 | 类型 | 说明 |
147
+ |---|---|---|
148
+ | `onError` | `(info: ErrorCaptureInfo) => void` | 捕获到错误时触发,携带符号化后的调用栈 |
149
+ | `onNetwork` | `(info: NetworkCaptureInfo) => void` | 捕获到 HTTP 请求时触发 |
150
+ | `symbolicator` | `MetroSymbolicator` | 可选,单测时注入 mock 以隔离网络依赖 |
151
+
152
+ 构造完成后,将绑定方法传给 `Sentry.init`:
153
+
154
+ ```ts
155
+ Sentry.init({
156
+ beforeSend: capture.beforeSend,
157
+ beforeBreadcrumb: capture.beforeBreadcrumb,
158
+ });
159
+ ```
160
+
161
+ #### `ErrorCaptureInfo`
162
+
163
+ | 字段 | 类型 | 说明 |
164
+ |---|---|---|
165
+ | `message` | `string` | 错误信息 |
166
+ | `symbolicatedFrames` | `MetroFrame[] \| null` | 经 Metro 符号化后还原到源码位置的调用栈帧 |
167
+ | `componentStack` | `string \| null` | React 组件调用栈(ErrorBoundary 捕获的错误才有值) |
168
+ | `mechanismType` | `string \| null` | Sentry 捕获方式:`'onerror'`、`'generic'`、`'auto.function.react.error_boundary'` 等 |
169
+
170
+ #### `NetworkCaptureInfo`
171
+
172
+ | 字段 | 类型 | 说明 |
173
+ |---|---|---|
174
+ | `method` | `string \| null` | HTTP 方法 |
175
+ | `url` | `string \| null` | 请求 URL |
176
+ | `statusCode` | `number \| null` | HTTP 状态码(连接级失败为 `null`) |
177
+
178
+ ---
179
+
180
+ ### `new MetroSymbolicator(options?)`
181
+
182
+ 从 `miaoda-expo-devkit` 引入。
183
+
184
+ 通过 Metro `/symbolicate` 接口将 bundle 行列号还原为源码位置。`SentryCapture` 内部已默认使用,主要用途是单测时注入 mock。
185
+
186
+ | 选项 | 类型 | 说明 |
187
+ |---|---|---|
188
+ | `baseUrl` | `string` | Metro server 地址。不传则从 `NativeModules.SourceCode.scriptURL` 自动推断,fallback 到 `localhost:8081`。 |
189
+
190
+ **单测示例:**
191
+
192
+ ```ts
193
+ const mockSym = {
194
+ parseErrorStack: jest.fn(() => []),
195
+ symbolicate: jest.fn(async () => null),
196
+ };
197
+ const onError = jest.fn();
198
+ const capture = new SentryCapture({ onError, symbolicator: mockSym });
199
+
200
+ await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
201
+
202
+ expect(onError).toHaveBeenCalledWith(
203
+ expect.objectContaining({ message: 'oops' }),
204
+ );
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 全局标记
210
+
211
+ 注入脚本在 bundle 执行时(早于任何 React 代码)写入以下全局变量,可用于 E2E 测试断言:
212
+
213
+ | 变量 | 类型 | 说明 |
214
+ |---|---|---|
215
+ | `window.__DEVKIT_INJECTED__` | `true` | 注入脚本已执行(由 `withEntryInjection` 保证) |
216
+ | `window.__DEVKIT_HMR_ENABLED__` | `boolean` | 当前 HMR 状态,随 postMessage 消息实时更新 |
217
+
218
+ ## 环境变量
219
+
220
+ | 变量 | 默认值 | 说明 |
221
+ |---|---|---|
222
+ | `SENTRY_OVERRIDE_DSN` | `https://stubPublicKey@o0.ingest.sentry.io/0` | 覆盖 Sentry DSN,可指向本地 relay 等自定义端点 |
223
+
224
+ ## 工作原理
225
+
226
+ ```
227
+ metro.config.js
228
+ └─ withEntryInjection(withDevStubs(config))
229
+
230
+ ├─ withDevStubs → resolver.resolveRequest
231
+ │ ├─ @sentry/react-native → dist/stubs/sentry-react-native-stub.js (全平台)
232
+ │ └─ @expo/log-box → dist/stubs/no-op-logbox.js (仅 web)
233
+
234
+ └─ withEntryInjection → resolver.resolveRequest
235
+ └─ expo-router/entry-classic → dist/stubs/expo-router-entry-stub.js
236
+ ├─ require('./entry-inject') ← 注入脚本(bundle 首部执行)
237
+ │ ├─ globalThis.__DEVKIT_INJECTED__ = true
238
+ │ ├─ setupHMRMessageControl() ← 安装 postMessage 监听器
239
+ │ └─ console.log('[DevKit] Hello World')
240
+ └─ require('expo-router/entry-classic') ← 原 expo-router 入口
241
+
242
+ setupHMRMessageControl()
243
+ └─ window.addEventListener('message', handler)
244
+ ├─ { type: 'devkit:hmr', action: 'disable' }
245
+ │ ├─ globalThis.__DEVKIT_HMR_ENABLED__ = false
246
+ │ └─ hmrClient.disable() (仅 __DEV__)
247
+ └─ { type: 'devkit:hmr', action: 'enable' }
248
+ ├─ globalThis.__DEVKIT_HMR_ENABLED__ = true
249
+ └─ hmrClient.enable() (仅 __DEV__)
250
+
251
+ sentry-react-native-stub.js
252
+ └─ init(options)
253
+ └─ RealSentry.init({ ...options, dsn: OVERRIDE_DSN, beforeSend, beforeBreadcrumb })
254
+ ├─ _capture.beforeSend(event, hint) ← 内置 SentryCapture(自动注入)
255
+ │ ├─ MetroSymbolicator.symbolicate() → POST /symbolicate
256
+ │ └─ onError({ message, frames, ... }) → console.warn(默认)
257
+ └─ options.beforeSend?(event, hint) ← 调用方自定义(可选,串联执行)
258
+ ```
259
+
260
+ ## Package Exports
261
+
262
+ | 子路径 | 文件 | 内容 |
263
+ |---|---|---|
264
+ | `.` | `dist/index.js` | `SentryCapture`、`MetroSymbolicator`、全部类型 |
265
+ | `./metro` | `dist/metro.js` | `withDevStubs`、`withEntryInjection` |
266
+ | `./sentry-react-native-stub` | `dist/stubs/sentry-react-native-stub.js` | `@sentry/react-native` 模块替换 stub |
267
+ | `./no-op-logbox` | `dist/stubs/no-op-logbox.js` | LogBox no-op stub |
@@ -0,0 +1,91 @@
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
+
20
+ // src/babel/plugin-jsx-source.ts
21
+ var plugin_jsx_source_exports = {};
22
+ __export(plugin_jsx_source_exports, {
23
+ default: () => babelPluginJsxSource
24
+ });
25
+ module.exports = __toCommonJS(plugin_jsx_source_exports);
26
+ function createSourceObject(t, fileName, lineNumber, columnNumber) {
27
+ return t.objectExpression([
28
+ t.objectProperty(t.identifier("fileName"), t.stringLiteral(fileName)),
29
+ t.objectProperty(t.identifier("lineNumber"), t.numericLiteral(lineNumber)),
30
+ t.objectProperty(t.identifier("columnNumber"), t.numericLiteral(columnNumber))
31
+ ]);
32
+ }
33
+ function hasSourceAttribute(t, attributes) {
34
+ return attributes.some(
35
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: SOURCE_PROP_NAME })
36
+ );
37
+ }
38
+ var SOURCE_PROP_NAME = "__jsxsource";
39
+ var REACT_SPECIAL_COMPONENTS = /* @__PURE__ */ new Set(["Fragment", "Suspense", "StrictMode", "Profiler"]);
40
+ function shouldSkipElement(t, name) {
41
+ if (t.isJSXIdentifier(name)) {
42
+ return REACT_SPECIAL_COMPONENTS.has(name.name);
43
+ }
44
+ if (t.isJSXMemberExpression(name)) {
45
+ const propertyName = name.property.name;
46
+ if (t.isJSXIdentifier(name.object) && name.object.name === "React") {
47
+ return REACT_SPECIAL_COMPONENTS.has(propertyName);
48
+ }
49
+ if (propertyName === "Provider" || propertyName === "Consumer") {
50
+ return true;
51
+ }
52
+ }
53
+ if (t.isJSXNamespacedName(name)) {
54
+ return true;
55
+ }
56
+ return false;
57
+ }
58
+ function babelPluginJsxSource({
59
+ types: t
60
+ }) {
61
+ return {
62
+ name: "babel-plugin-jsx-source",
63
+ visitor: {
64
+ JSXOpeningElement(path, state) {
65
+ const { opts, filename } = state;
66
+ if (!filename) return;
67
+ if (filename.includes("node_modules")) return;
68
+ if (opts.excludePaths?.some((p) => filename.includes(p))) return;
69
+ if (shouldSkipElement(t, path.node.name)) return;
70
+ if (hasSourceAttribute(t, path.node.attributes)) return;
71
+ const loc = path.node.loc;
72
+ if (!loc) return;
73
+ let filePath = filename;
74
+ if (opts.rootDir && filename.startsWith(opts.rootDir)) {
75
+ filePath = filename.slice(opts.rootDir.length);
76
+ if (filePath.startsWith("/")) {
77
+ filePath = filePath.slice(1);
78
+ }
79
+ }
80
+ const sourceAttribute = t.jsxAttribute(
81
+ t.jsxIdentifier(SOURCE_PROP_NAME),
82
+ t.jsxExpressionContainer(
83
+ createSourceObject(t, filePath, loc.start.line, loc.start.column)
84
+ )
85
+ );
86
+ path.node.attributes.push(sourceAttribute);
87
+ }
88
+ }
89
+ };
90
+ }
91
+ //# sourceMappingURL=plugin-jsx-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/babel/plugin-jsx-source.ts"],"sourcesContent":["/**\n * babel-plugin-jsx-source\n *\n * 为 JSX 元素注入 __jsxsource 属性,包含文件路径和行列号信息,用于开发调试。\n *\n * 转换前:\n * <View style={styles.container} />\n *\n * 转换后:\n * <View style={styles.container} __jsxsource={{ fileName: \"/path/to/file.tsx\", lineNumber: 10, columnNumber: 4 }} />\n *\n * 用法(babel.config.js):\n *\n * module.exports = {\n * plugins: [\n * ['miaoda-expo-devkit/babel-plugin-jsx-source', { rootDir: __dirname }]\n * ]\n * };\n *\n * 选项:\n * - rootDir: 项目根目录,用于计算相对路径(可选,默认使用绝对路径)\n */\n\nimport type { PluginObj, NodePath, types as BabelTypes } from '@babel/core';\n\n/** 插件选项 */\nexport interface JsxSourcePluginOptions {\n /** 项目根目录,用于计算相对路径。若不提供则使用绝对路径 */\n rootDir?: string;\n /** 需要跳过注入的路径模式列表(相对于 rootDir 的路径片段) */\n excludePaths?: string[];\n}\n\n/** 创建 __jsxsource 对象的属性值 */\nfunction createSourceObject(\n t: typeof BabelTypes,\n fileName: string,\n lineNumber: number,\n columnNumber: number\n): BabelTypes.ObjectExpression {\n return t.objectExpression([\n t.objectProperty(t.identifier('fileName'), t.stringLiteral(fileName)),\n t.objectProperty(t.identifier('lineNumber'), t.numericLiteral(lineNumber)),\n t.objectProperty(t.identifier('columnNumber'), t.numericLiteral(columnNumber)),\n ]);\n}\n\n/** 检查 JSX 元素是否已有 __jsxsource 属性 */\nfunction hasSourceAttribute(\n t: typeof BabelTypes,\n attributes: (BabelTypes.JSXAttribute | BabelTypes.JSXSpreadAttribute)[]\n): boolean {\n return attributes.some(\n (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: SOURCE_PROP_NAME })\n );\n}\n\n/**\n * React 内置特殊组件,不接受任意 props\n */\n/** 注入的 prop 名称 */\nconst SOURCE_PROP_NAME = '__jsxsource';\n\nconst REACT_SPECIAL_COMPONENTS = new Set(['Fragment', 'Suspense', 'StrictMode', 'Profiler']);\n\n/**\n * 检查是否为需要跳过的特殊组件\n *\n * 跳过的情况:\n * 1. React 特殊组件: Fragment, Suspense, StrictMode, Profiler\n * 2. React 命名空间组件: React.Fragment, React.Suspense 等\n * 3. Context Provider/Consumer: *.Provider, *.Consumer\n * 4. JSXNamespacedName: <svg:rect> 等\n */\nfunction shouldSkipElement(\n t: typeof BabelTypes,\n name: BabelTypes.JSXOpeningElement['name']\n): boolean {\n // 简单标识符: <Fragment>, <Suspense> 等\n if (t.isJSXIdentifier(name)) {\n return REACT_SPECIAL_COMPONENTS.has(name.name);\n }\n\n // 成员表达式: <React.Fragment>, <Context.Provider> 等\n if (t.isJSXMemberExpression(name)) {\n const propertyName = name.property.name;\n\n // React.* 特殊组件\n if (t.isJSXIdentifier(name.object) && name.object.name === 'React') {\n return REACT_SPECIAL_COMPONENTS.has(propertyName);\n }\n\n // *.Provider 和 *.Consumer (Context 组件)\n if (propertyName === 'Provider' || propertyName === 'Consumer') {\n return true;\n }\n }\n\n // JSXNamespacedName (如 <svg:rect>) - XML 命名空间元素,跳过\n if (t.isJSXNamespacedName(name)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Babel 插件:为 JSX 元素注入 __jsxsource 属性\n */\nexport default function babelPluginJsxSource({\n types: t,\n}: {\n types: typeof BabelTypes;\n}): PluginObj {\n return {\n name: 'babel-plugin-jsx-source',\n visitor: {\n JSXOpeningElement(\n path: NodePath<BabelTypes.JSXOpeningElement>,\n state: { opts: JsxSourcePluginOptions; filename?: string }\n ) {\n const { opts, filename } = state;\n\n // 没有文件名信息则跳过\n if (!filename) return;\n\n // 忽略 node_modules\n if (filename.includes('node_modules')) return;\n\n // 忽略用户配置的排除路径\n if (opts.excludePaths?.some((p) => filename.includes(p))) return;\n\n // 特殊组件不能有自定义属性,跳过\n if (shouldSkipElement(t, path.node.name)) return;\n\n // 已有 __jsxsource 属性则跳过\n if (hasSourceAttribute(t, path.node.attributes)) return;\n\n // 获取位置信息\n const loc = path.node.loc;\n if (!loc) return;\n\n // 计算文件路径(相对路径或绝对路径)\n let filePath = filename;\n if (opts.rootDir && filename.startsWith(opts.rootDir)) {\n filePath = filename.slice(opts.rootDir.length);\n // 移除开头的 /\n if (filePath.startsWith('/')) {\n filePath = filePath.slice(1);\n }\n }\n\n // 创建 __jsxsource 属性\n const sourceAttribute = t.jsxAttribute(\n t.jsxIdentifier(SOURCE_PROP_NAME),\n t.jsxExpressionContainer(\n createSourceObject(t, filePath, loc.start.line, loc.start.column)\n )\n );\n\n // 添加到属性列表末尾\n path.node.attributes.push(sourceAttribute);\n },\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCA,SAAS,mBACP,GACA,UACA,YACA,cAC6B;AAC7B,SAAO,EAAE,iBAAiB;AAAA,IACxB,EAAE,eAAe,EAAE,WAAW,UAAU,GAAG,EAAE,cAAc,QAAQ,CAAC;AAAA,IACpE,EAAE,eAAe,EAAE,WAAW,YAAY,GAAG,EAAE,eAAe,UAAU,CAAC;AAAA,IACzE,EAAE,eAAe,EAAE,WAAW,cAAc,GAAG,EAAE,eAAe,YAAY,CAAC;AAAA,EAC/E,CAAC;AACH;AAGA,SAAS,mBACP,GACA,YACS;AACT,SAAO,WAAW;AAAA,IAChB,CAAC,SAAS,EAAE,eAAe,IAAI,KAAK,EAAE,gBAAgB,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAAA,EAC7F;AACF;AAMA,IAAM,mBAAmB;AAEzB,IAAM,2BAA2B,oBAAI,IAAI,CAAC,YAAY,YAAY,cAAc,UAAU,CAAC;AAW3F,SAAS,kBACP,GACA,MACS;AAET,MAAI,EAAE,gBAAgB,IAAI,GAAG;AAC3B,WAAO,yBAAyB,IAAI,KAAK,IAAI;AAAA,EAC/C;AAGA,MAAI,EAAE,sBAAsB,IAAI,GAAG;AACjC,UAAM,eAAe,KAAK,SAAS;AAGnC,QAAI,EAAE,gBAAgB,KAAK,MAAM,KAAK,KAAK,OAAO,SAAS,SAAS;AAClE,aAAO,yBAAyB,IAAI,YAAY;AAAA,IAClD;AAGA,QAAI,iBAAiB,cAAc,iBAAiB,YAAY;AAC9D,aAAO;AAAA,IACT;AAAA,EACF;AAGA,MAAI,EAAE,oBAAoB,IAAI,GAAG;AAC/B,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKe,SAAR,qBAAsC;AAAA,EAC3C,OAAO;AACT,GAEc;AACZ,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,MACP,kBACE,MACA,OACA;AACA,cAAM,EAAE,MAAM,SAAS,IAAI;AAG3B,YAAI,CAAC,SAAU;AAGf,YAAI,SAAS,SAAS,cAAc,EAAG;AAGvC,YAAI,KAAK,cAAc,KAAK,CAAC,MAAM,SAAS,SAAS,CAAC,CAAC,EAAG;AAG1D,YAAI,kBAAkB,GAAG,KAAK,KAAK,IAAI,EAAG;AAG1C,YAAI,mBAAmB,GAAG,KAAK,KAAK,UAAU,EAAG;AAGjD,cAAM,MAAM,KAAK,KAAK;AACtB,YAAI,CAAC,IAAK;AAGV,YAAI,WAAW;AACf,YAAI,KAAK,WAAW,SAAS,WAAW,KAAK,OAAO,GAAG;AACrD,qBAAW,SAAS,MAAM,KAAK,QAAQ,MAAM;AAE7C,cAAI,SAAS,WAAW,GAAG,GAAG;AAC5B,uBAAW,SAAS,MAAM,CAAC;AAAA,UAC7B;AAAA,QACF;AAGA,cAAM,kBAAkB,EAAE;AAAA,UACxB,EAAE,cAAc,gBAAgB;AAAA,UAChC,EAAE;AAAA,YACA,mBAAmB,GAAG,UAAU,IAAI,MAAM,MAAM,IAAI,MAAM,MAAM;AAAA,UAClE;AAAA,QACF;AAGA,aAAK,KAAK,WAAW,KAAK,eAAe;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,111 @@
1
+ import { ErrorEvent, EventHint, Breadcrumb, BreadcrumbHint } from '@sentry/core';
2
+
3
+ /**
4
+ * Sentry Capture 模块
5
+ *
6
+ * 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。
7
+ */
8
+
9
+ /** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */
10
+ interface MetroFrame {
11
+ file: string;
12
+ lineNumber: number;
13
+ column: number;
14
+ methodName: string;
15
+ }
16
+ /** SentryCapture.onError 回调参数 */
17
+ interface ErrorCaptureInfo {
18
+ message: string;
19
+ symbolicatedFrames: MetroFrame[] | null;
20
+ componentStack: string | null;
21
+ /** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */
22
+ mechanismType: string | null;
23
+ }
24
+ /** SentryCapture.onNetwork 回调参数 */
25
+ interface NetworkCaptureInfo {
26
+ method: string | null;
27
+ url: string | null;
28
+ statusCode: number | null;
29
+ }
30
+ interface SentryCaptureOptions {
31
+ onError?: (info: ErrorCaptureInfo) => void;
32
+ onNetwork?: (info: NetworkCaptureInfo) => void;
33
+ /** 可注入 mock 实例以在单元测试中隔离网络依赖 */
34
+ symbolicator?: MetroSymbolicator;
35
+ }
36
+ /**
37
+ * Metro 符号化器。
38
+ * 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。
39
+ *
40
+ * @example
41
+ * const sym = new MetroSymbolicator();
42
+ *
43
+ * @example
44
+ * // 单测:注入 baseUrl 并 mock fetch
45
+ * const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });
46
+ * global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });
47
+ */
48
+ declare class MetroSymbolicator {
49
+ readonly baseUrl: string;
50
+ constructor({ baseUrl }?: {
51
+ baseUrl?: string;
52
+ });
53
+ /**
54
+ * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
55
+ * web 平台使用相对路径;native 回退到 localhost:8081。
56
+ */
57
+ private static _getDefaultBaseUrl;
58
+ /**
59
+ * 解析 Error.stack 字符串为 Metro 所需的帧数组。
60
+ *
61
+ * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
62
+ * V8/Node.js/Hermes(新):at methodName (url:line:col)
63
+ * Gecko/Firefox/Hermes(旧):methodName@url:line:col
64
+ * JavaScriptCore(iOS):methodName@url:line:col
65
+ *
66
+ * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
67
+ */
68
+ parseErrorStack(stack: string): MetroFrame[];
69
+ /**
70
+ * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
71
+ */
72
+ symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null>;
73
+ }
74
+ /**
75
+ * Sentry 事件捕获器。
76
+ * 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。
77
+ *
78
+ * @example
79
+ * const capture = new SentryCapture({
80
+ * onError(info) { console.log(info.message, info.symbolicatedFrames); },
81
+ * onNetwork(info) { console.log(info.method, info.url, info.statusCode); },
82
+ * });
83
+ * Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });
84
+ *
85
+ * @example
86
+ * // 单测:注入 mock symbolicator
87
+ * const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };
88
+ * const onError = jest.fn();
89
+ * const capture = new SentryCapture({ onError, symbolicator: mockSym });
90
+ * await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
91
+ * expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));
92
+ */
93
+ declare class SentryCapture {
94
+ private readonly onError;
95
+ private readonly onNetwork;
96
+ private readonly symbolicator;
97
+ constructor({ onError, onNetwork, symbolicator }?: SentryCaptureOptions);
98
+ /**
99
+ * Sentry beforeSend hook。
100
+ * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
101
+ * 符号化完成后触发 onError 回调,原样返回 event。
102
+ */
103
+ beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent>;
104
+ /**
105
+ * Sentry beforeBreadcrumb hook。
106
+ * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
107
+ */
108
+ beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb;
109
+ }
110
+
111
+ export { type ErrorCaptureInfo, type MetroFrame, MetroSymbolicator, type NetworkCaptureInfo, SentryCapture, type SentryCaptureOptions };
@@ -0,0 +1,111 @@
1
+ import { ErrorEvent, EventHint, Breadcrumb, BreadcrumbHint } from '@sentry/core';
2
+
3
+ /**
4
+ * Sentry Capture 模块
5
+ *
6
+ * 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。
7
+ */
8
+
9
+ /** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */
10
+ interface MetroFrame {
11
+ file: string;
12
+ lineNumber: number;
13
+ column: number;
14
+ methodName: string;
15
+ }
16
+ /** SentryCapture.onError 回调参数 */
17
+ interface ErrorCaptureInfo {
18
+ message: string;
19
+ symbolicatedFrames: MetroFrame[] | null;
20
+ componentStack: string | null;
21
+ /** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */
22
+ mechanismType: string | null;
23
+ }
24
+ /** SentryCapture.onNetwork 回调参数 */
25
+ interface NetworkCaptureInfo {
26
+ method: string | null;
27
+ url: string | null;
28
+ statusCode: number | null;
29
+ }
30
+ interface SentryCaptureOptions {
31
+ onError?: (info: ErrorCaptureInfo) => void;
32
+ onNetwork?: (info: NetworkCaptureInfo) => void;
33
+ /** 可注入 mock 实例以在单元测试中隔离网络依赖 */
34
+ symbolicator?: MetroSymbolicator;
35
+ }
36
+ /**
37
+ * Metro 符号化器。
38
+ * 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。
39
+ *
40
+ * @example
41
+ * const sym = new MetroSymbolicator();
42
+ *
43
+ * @example
44
+ * // 单测:注入 baseUrl 并 mock fetch
45
+ * const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });
46
+ * global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });
47
+ */
48
+ declare class MetroSymbolicator {
49
+ readonly baseUrl: string;
50
+ constructor({ baseUrl }?: {
51
+ baseUrl?: string;
52
+ });
53
+ /**
54
+ * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
55
+ * web 平台使用相对路径;native 回退到 localhost:8081。
56
+ */
57
+ private static _getDefaultBaseUrl;
58
+ /**
59
+ * 解析 Error.stack 字符串为 Metro 所需的帧数组。
60
+ *
61
+ * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
62
+ * V8/Node.js/Hermes(新):at methodName (url:line:col)
63
+ * Gecko/Firefox/Hermes(旧):methodName@url:line:col
64
+ * JavaScriptCore(iOS):methodName@url:line:col
65
+ *
66
+ * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
67
+ */
68
+ parseErrorStack(stack: string): MetroFrame[];
69
+ /**
70
+ * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
71
+ */
72
+ symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null>;
73
+ }
74
+ /**
75
+ * Sentry 事件捕获器。
76
+ * 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。
77
+ *
78
+ * @example
79
+ * const capture = new SentryCapture({
80
+ * onError(info) { console.log(info.message, info.symbolicatedFrames); },
81
+ * onNetwork(info) { console.log(info.method, info.url, info.statusCode); },
82
+ * });
83
+ * Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });
84
+ *
85
+ * @example
86
+ * // 单测:注入 mock symbolicator
87
+ * const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };
88
+ * const onError = jest.fn();
89
+ * const capture = new SentryCapture({ onError, symbolicator: mockSym });
90
+ * await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });
91
+ * expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));
92
+ */
93
+ declare class SentryCapture {
94
+ private readonly onError;
95
+ private readonly onNetwork;
96
+ private readonly symbolicator;
97
+ constructor({ onError, onNetwork, symbolicator }?: SentryCaptureOptions);
98
+ /**
99
+ * Sentry beforeSend hook。
100
+ * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
101
+ * 符号化完成后触发 onError 回调,原样返回 event。
102
+ */
103
+ beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent>;
104
+ /**
105
+ * Sentry beforeBreadcrumb hook。
106
+ * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
107
+ */
108
+ beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb;
109
+ }
110
+
111
+ export { type ErrorCaptureInfo, type MetroFrame, MetroSymbolicator, type NetworkCaptureInfo, SentryCapture, type SentryCaptureOptions };