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/dist/index.js ADDED
@@ -0,0 +1,161 @@
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/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ MetroSymbolicator: () => MetroSymbolicator,
24
+ SentryCapture: () => SentryCapture
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+
28
+ // src/capture.ts
29
+ var import_stacktrace_parser = require("stacktrace-parser");
30
+ var import_react_native = require("react-native");
31
+ var MetroSymbolicator = class _MetroSymbolicator {
32
+ constructor({ baseUrl } = {}) {
33
+ this.baseUrl = baseUrl ?? _MetroSymbolicator._getDefaultBaseUrl();
34
+ }
35
+ /**
36
+ * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
37
+ * web 平台使用相对路径;native 回退到 localhost:8081。
38
+ */
39
+ static _getDefaultBaseUrl() {
40
+ try {
41
+ const scriptURL = import_react_native.NativeModules?.SourceCode?.scriptURL;
42
+ if (scriptURL) {
43
+ const url = new URL(scriptURL);
44
+ return `${url.protocol}//${url.host}`;
45
+ }
46
+ } catch (_) {
47
+ }
48
+ return import_react_native.Platform.OS === "web" ? "" : "http://localhost:8081";
49
+ }
50
+ /**
51
+ * 解析 Error.stack 字符串为 Metro 所需的帧数组。
52
+ *
53
+ * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
54
+ * V8/Node.js/Hermes(新):at methodName (url:line:col)
55
+ * Gecko/Firefox/Hermes(旧):methodName@url:line:col
56
+ * JavaScriptCore(iOS):methodName@url:line:col
57
+ *
58
+ * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
59
+ */
60
+ parseErrorStack(stack) {
61
+ return (0, import_stacktrace_parser.parse)(stack).filter(
62
+ (f) => f.file != null && f.lineNumber != null
63
+ ).map((f) => ({
64
+ file: f.file,
65
+ lineNumber: f.lineNumber,
66
+ column: f.column != null ? f.column - 1 : 0,
67
+ methodName: f.methodName === "<unknown>" ? "(anonymous)" : f.methodName
68
+ }));
69
+ }
70
+ /**
71
+ * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
72
+ */
73
+ async symbolicate(stack) {
74
+ if (stack.length === 0) return null;
75
+ const endpoint = `${this.baseUrl}/symbolicate`;
76
+ try {
77
+ const res = await fetch(endpoint, {
78
+ method: "POST",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({ stack })
81
+ });
82
+ if (!res.ok) {
83
+ console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);
84
+ return null;
85
+ }
86
+ const data = await res.json();
87
+ return data.stack ?? null;
88
+ } catch (e) {
89
+ console.warn("[SentryCapture] symbolication failed:", e?.message ?? e);
90
+ return null;
91
+ }
92
+ }
93
+ };
94
+ var SentryCapture = class {
95
+ constructor({ onError, onNetwork, symbolicator } = {}) {
96
+ this.onError = onError;
97
+ this.onNetwork = onNetwork;
98
+ this.symbolicator = symbolicator ?? new MetroSymbolicator();
99
+ this.beforeSend = this.beforeSend.bind(this);
100
+ this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);
101
+ }
102
+ /**
103
+ * Sentry beforeSend hook。
104
+ * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
105
+ * 符号化完成后触发 onError 回调,原样返回 event。
106
+ */
107
+ async beforeSend(event, hint) {
108
+ globalThis.__SENTRY_LAST_RAW__ = { event, hint };
109
+ const message = event.exception?.values?.[0]?.value ?? event.message ?? "(unknown)";
110
+ let symbolicatedFrames = null;
111
+ const originalException = hint?.originalException;
112
+ const rawStack = originalException instanceof Error && typeof originalException.stack === "string" ? originalException.stack : null;
113
+ if (rawStack !== null) {
114
+ const frames = this.symbolicator.parseErrorStack(rawStack);
115
+ if (frames.length > 0) {
116
+ symbolicatedFrames = await this.symbolicator.symbolicate(frames);
117
+ }
118
+ }
119
+ if (symbolicatedFrames === null) {
120
+ const sentryFrames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
121
+ const mapped = sentryFrames.filter(
122
+ (f) => f.filename != null && f.lineno != null
123
+ ).map(
124
+ (f) => ({
125
+ file: f.filename,
126
+ lineNumber: f.lineno,
127
+ column: f.colno ?? 0,
128
+ methodName: f.function ?? "(anonymous)"
129
+ })
130
+ );
131
+ if (mapped.length > 0) {
132
+ symbolicatedFrames = await this.symbolicator.symbolicate(mapped);
133
+ }
134
+ }
135
+ const componentStack = event.contexts?.react?.componentStack ?? null;
136
+ const mechanismType = event.exception?.values?.[0]?.mechanism?.type ?? null;
137
+ this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });
138
+ return event;
139
+ }
140
+ /**
141
+ * Sentry beforeBreadcrumb hook。
142
+ * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
143
+ */
144
+ beforeBreadcrumb(breadcrumb, _hint) {
145
+ if (breadcrumb.type === "http") {
146
+ const data = breadcrumb.data;
147
+ this.onNetwork?.({
148
+ method: data?.method ?? null,
149
+ url: data?.url ?? null,
150
+ statusCode: data?.status_code ?? null
151
+ });
152
+ }
153
+ return breadcrumb;
154
+ }
155
+ };
156
+ // Annotate the CommonJS export names for ESM import in node:
157
+ 0 && (module.exports = {
158
+ MetroSymbolicator,
159
+ SentryCapture
160
+ });
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/capture.ts"],"sourcesContent":["/**\n * miaoda-expo-devkit\n *\n * 主入口。统一导出所有公开 API。\n *\n * 使用示例:\n * import { SentryCapture } from 'miaoda-expo-devkit';\n * import type { ErrorCaptureInfo, MetroFrame } from 'miaoda-expo-devkit';\n */\n\nexport { MetroSymbolicator, SentryCapture } from './capture';\n\nexport type {\n ErrorCaptureInfo,\n MetroFrame,\n NetworkCaptureInfo,\n SentryCaptureOptions,\n} from './capture';\n","/**\n * Sentry Capture 模块\n *\n * 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。\n */\n\nimport type { Breadcrumb, BreadcrumbHint, ErrorEvent, EventHint, StackFrame } from '@sentry/core';\nimport { parse as parseStackTrace } from 'stacktrace-parser';\nimport { NativeModules, Platform } from 'react-native';\n\n// Breadcrumb.data 为 Record<string, unknown>,此处仅声明 HTTP 场景实际用到的字段\ninterface FetchBreadcrumbData {\n method?: string;\n url?: string;\n status_code?: number;\n}\n\n// ── 类型定义 ───────────────────────────────────────────────────────────────────\n\n/** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */\nexport interface MetroFrame {\n file: string;\n lineNumber: number;\n column: number;\n methodName: string;\n}\n\n/** SentryCapture.onError 回调参数 */\nexport interface ErrorCaptureInfo {\n message: string;\n symbolicatedFrames: MetroFrame[] | null;\n componentStack: string | null;\n /** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */\n mechanismType: string | null;\n}\n\n/** SentryCapture.onNetwork 回调参数 */\nexport interface NetworkCaptureInfo {\n method: string | null;\n url: string | null;\n statusCode: number | null;\n}\n\nexport interface SentryCaptureOptions {\n onError?: (info: ErrorCaptureInfo) => void;\n onNetwork?: (info: NetworkCaptureInfo) => void;\n /** 可注入 mock 实例以在单元测试中隔离网络依赖 */\n symbolicator?: MetroSymbolicator;\n}\n\n// ── MetroSymbolicator ──────────────────────────────────────────────────────────\n\n/**\n * Metro 符号化器。\n * 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。\n *\n * @example\n * const sym = new MetroSymbolicator();\n *\n * @example\n * // 单测:注入 baseUrl 并 mock fetch\n * const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });\n * global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });\n */\nexport class MetroSymbolicator {\n readonly baseUrl: string;\n\n constructor({ baseUrl }: { baseUrl?: string } = {}) {\n this.baseUrl = baseUrl ?? MetroSymbolicator._getDefaultBaseUrl();\n }\n\n /**\n * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。\n * web 平台使用相对路径;native 回退到 localhost:8081。\n */\n private static _getDefaultBaseUrl(): string {\n try {\n const scriptURL = NativeModules?.SourceCode?.scriptURL as string | undefined;\n if (scriptURL) {\n const url = new URL(scriptURL);\n return `${url.protocol}//${url.host}`;\n }\n } catch (_) {}\n return Platform.OS === 'web' ? '' : 'http://localhost:8081';\n }\n\n /**\n * 解析 Error.stack 字符串为 Metro 所需的帧数组。\n *\n * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:\n * V8/Node.js/Hermes(新):at methodName (url:line:col)\n * Gecko/Firefox/Hermes(旧):methodName@url:line:col\n * JavaScriptCore(iOS):methodName@url:line:col\n *\n * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。\n */\n parseErrorStack(stack: string): MetroFrame[] {\n return parseStackTrace(stack)\n .filter(\n (f): f is typeof f & { file: string; lineNumber: number } =>\n f.file != null && f.lineNumber != null,\n )\n .map(f => ({\n file: f.file,\n lineNumber: f.lineNumber,\n column: f.column != null ? f.column - 1 : 0,\n methodName: f.methodName === '<unknown>' ? '(anonymous)' : f.methodName,\n }));\n }\n\n /**\n * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。\n */\n async symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null> {\n if (stack.length === 0) return null;\n const endpoint = `${this.baseUrl}/symbolicate`;\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ stack }),\n });\n if (!res.ok) {\n console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);\n return null;\n }\n const data = (await res.json()) as { stack?: MetroFrame[] };\n return data.stack ?? null;\n } catch (e) {\n console.warn('[SentryCapture] symbolication failed:', (e as Error)?.message ?? e);\n return null;\n }\n }\n}\n\n// ── SentryCapture ─────────────────────────────────────────────────────────────\n\n/**\n * Sentry 事件捕获器。\n * 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。\n *\n * @example\n * const capture = new SentryCapture({\n * onError(info) { console.log(info.message, info.symbolicatedFrames); },\n * onNetwork(info) { console.log(info.method, info.url, info.statusCode); },\n * });\n * Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });\n *\n * @example\n * // 单测:注入 mock symbolicator\n * const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };\n * const onError = jest.fn();\n * const capture = new SentryCapture({ onError, symbolicator: mockSym });\n * await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });\n * expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));\n */\nexport class SentryCapture {\n private readonly onError: SentryCaptureOptions['onError'];\n private readonly onNetwork: SentryCaptureOptions['onNetwork'];\n private readonly symbolicator: MetroSymbolicator;\n\n constructor({ onError, onNetwork, symbolicator }: SentryCaptureOptions = {}) {\n this.onError = onError;\n this.onNetwork = onNetwork;\n this.symbolicator = symbolicator ?? new MetroSymbolicator();\n\n // bind 保证方法作为回调传入 Sentry.init() 时 this 不丢失\n this.beforeSend = this.beforeSend.bind(this);\n this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);\n }\n\n /**\n * Sentry beforeSend hook。\n * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。\n * 符号化完成后触发 onError 回调,原样返回 event。\n */\n async beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent> {\n globalThis.__SENTRY_LAST_RAW__ = { event, hint };\n\n const message: string =\n event.exception?.values?.[0]?.value ?? event.message ?? '(unknown)';\n\n let symbolicatedFrames: MetroFrame[] | null = null;\n\n const originalException: unknown = hint?.originalException;\n const rawStack: string | null =\n originalException instanceof Error && typeof originalException.stack === 'string'\n ? originalException.stack\n : null;\n\n if (rawStack !== null) {\n const frames: MetroFrame[] = this.symbolicator.parseErrorStack(rawStack);\n if (frames.length > 0) {\n symbolicatedFrames = await this.symbolicator.symbolicate(frames);\n }\n }\n\n // 回退:使用 Sentry 事件帧\n if (symbolicatedFrames === null) {\n const sentryFrames: StackFrame[] =\n event.exception?.values?.[0]?.stacktrace?.frames ?? [];\n const mapped: MetroFrame[] = sentryFrames\n .filter(\n (f): f is StackFrame & Required<Pick<StackFrame, 'filename' | 'lineno'>> =>\n f.filename != null && f.lineno != null,\n )\n .map(\n (f): MetroFrame => ({\n file: f.filename,\n lineNumber: f.lineno,\n column: f.colno ?? 0,\n methodName: f.function ?? '(anonymous)',\n }),\n );\n if (mapped.length > 0) {\n symbolicatedFrames = await this.symbolicator.symbolicate(mapped);\n }\n }\n\n const componentStack: string | null =\n (event.contexts?.react?.componentStack as string | undefined) ?? null;\n\n const mechanismType: string | null =\n event.exception?.values?.[0]?.mechanism?.type ?? null;\n\n this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });\n\n return event;\n }\n\n /**\n * Sentry beforeBreadcrumb hook。\n * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。\n */\n beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb {\n if (breadcrumb.type === 'http') {\n const data = breadcrumb.data as FetchBreadcrumbData | undefined;\n this.onNetwork?.({\n method: data?.method ?? null,\n url: data?.url ?? null,\n statusCode: data?.status_code ?? null,\n });\n }\n return breadcrumb;\n }\n}\n"],"mappings":"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","names":["parseStackTrace"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,133 @@
1
+ // src/capture.ts
2
+ import { parse as parseStackTrace } from "stacktrace-parser";
3
+ import { NativeModules, Platform } from "react-native";
4
+ var MetroSymbolicator = class _MetroSymbolicator {
5
+ constructor({ baseUrl } = {}) {
6
+ this.baseUrl = baseUrl ?? _MetroSymbolicator._getDefaultBaseUrl();
7
+ }
8
+ /**
9
+ * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。
10
+ * web 平台使用相对路径;native 回退到 localhost:8081。
11
+ */
12
+ static _getDefaultBaseUrl() {
13
+ try {
14
+ const scriptURL = NativeModules?.SourceCode?.scriptURL;
15
+ if (scriptURL) {
16
+ const url = new URL(scriptURL);
17
+ return `${url.protocol}//${url.host}`;
18
+ }
19
+ } catch (_) {
20
+ }
21
+ return Platform.OS === "web" ? "" : "http://localhost:8081";
22
+ }
23
+ /**
24
+ * 解析 Error.stack 字符串为 Metro 所需的帧数组。
25
+ *
26
+ * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:
27
+ * V8/Node.js/Hermes(新):at methodName (url:line:col)
28
+ * Gecko/Firefox/Hermes(旧):methodName@url:line:col
29
+ * JavaScriptCore(iOS):methodName@url:line:col
30
+ *
31
+ * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。
32
+ */
33
+ parseErrorStack(stack) {
34
+ return parseStackTrace(stack).filter(
35
+ (f) => f.file != null && f.lineNumber != null
36
+ ).map((f) => ({
37
+ file: f.file,
38
+ lineNumber: f.lineNumber,
39
+ column: f.column != null ? f.column - 1 : 0,
40
+ methodName: f.methodName === "<unknown>" ? "(anonymous)" : f.methodName
41
+ }));
42
+ }
43
+ /**
44
+ * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。
45
+ */
46
+ async symbolicate(stack) {
47
+ if (stack.length === 0) return null;
48
+ const endpoint = `${this.baseUrl}/symbolicate`;
49
+ try {
50
+ const res = await fetch(endpoint, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ stack })
54
+ });
55
+ if (!res.ok) {
56
+ console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);
57
+ return null;
58
+ }
59
+ const data = await res.json();
60
+ return data.stack ?? null;
61
+ } catch (e) {
62
+ console.warn("[SentryCapture] symbolication failed:", e?.message ?? e);
63
+ return null;
64
+ }
65
+ }
66
+ };
67
+ var SentryCapture = class {
68
+ constructor({ onError, onNetwork, symbolicator } = {}) {
69
+ this.onError = onError;
70
+ this.onNetwork = onNetwork;
71
+ this.symbolicator = symbolicator ?? new MetroSymbolicator();
72
+ this.beforeSend = this.beforeSend.bind(this);
73
+ this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);
74
+ }
75
+ /**
76
+ * Sentry beforeSend hook。
77
+ * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。
78
+ * 符号化完成后触发 onError 回调,原样返回 event。
79
+ */
80
+ async beforeSend(event, hint) {
81
+ globalThis.__SENTRY_LAST_RAW__ = { event, hint };
82
+ const message = event.exception?.values?.[0]?.value ?? event.message ?? "(unknown)";
83
+ let symbolicatedFrames = null;
84
+ const originalException = hint?.originalException;
85
+ const rawStack = originalException instanceof Error && typeof originalException.stack === "string" ? originalException.stack : null;
86
+ if (rawStack !== null) {
87
+ const frames = this.symbolicator.parseErrorStack(rawStack);
88
+ if (frames.length > 0) {
89
+ symbolicatedFrames = await this.symbolicator.symbolicate(frames);
90
+ }
91
+ }
92
+ if (symbolicatedFrames === null) {
93
+ const sentryFrames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
94
+ const mapped = sentryFrames.filter(
95
+ (f) => f.filename != null && f.lineno != null
96
+ ).map(
97
+ (f) => ({
98
+ file: f.filename,
99
+ lineNumber: f.lineno,
100
+ column: f.colno ?? 0,
101
+ methodName: f.function ?? "(anonymous)"
102
+ })
103
+ );
104
+ if (mapped.length > 0) {
105
+ symbolicatedFrames = await this.symbolicator.symbolicate(mapped);
106
+ }
107
+ }
108
+ const componentStack = event.contexts?.react?.componentStack ?? null;
109
+ const mechanismType = event.exception?.values?.[0]?.mechanism?.type ?? null;
110
+ this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });
111
+ return event;
112
+ }
113
+ /**
114
+ * Sentry beforeBreadcrumb hook。
115
+ * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。
116
+ */
117
+ beforeBreadcrumb(breadcrumb, _hint) {
118
+ if (breadcrumb.type === "http") {
119
+ const data = breadcrumb.data;
120
+ this.onNetwork?.({
121
+ method: data?.method ?? null,
122
+ url: data?.url ?? null,
123
+ statusCode: data?.status_code ?? null
124
+ });
125
+ }
126
+ return breadcrumb;
127
+ }
128
+ };
129
+ export {
130
+ MetroSymbolicator,
131
+ SentryCapture
132
+ };
133
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/capture.ts"],"sourcesContent":["/**\n * Sentry Capture 模块\n *\n * 提供 Metro 符号化(MetroSymbolicator)和 Sentry 事件捕获(SentryCapture)。\n */\n\nimport type { Breadcrumb, BreadcrumbHint, ErrorEvent, EventHint, StackFrame } from '@sentry/core';\nimport { parse as parseStackTrace } from 'stacktrace-parser';\nimport { NativeModules, Platform } from 'react-native';\n\n// Breadcrumb.data 为 Record<string, unknown>,此处仅声明 HTTP 场景实际用到的字段\ninterface FetchBreadcrumbData {\n method?: string;\n url?: string;\n status_code?: number;\n}\n\n// ── 类型定义 ───────────────────────────────────────────────────────────────────\n\n/** Metro /symbolicate 接口的帧格式(请求与响应共用)。 */\nexport interface MetroFrame {\n file: string;\n lineNumber: number;\n column: number;\n methodName: string;\n}\n\n/** SentryCapture.onError 回调参数 */\nexport interface ErrorCaptureInfo {\n message: string;\n symbolicatedFrames: MetroFrame[] | null;\n componentStack: string | null;\n /** Sentry Mechanism.type,如 'onerror' / 'generic' / 'auto.function.react.error_boundary' */\n mechanismType: string | null;\n}\n\n/** SentryCapture.onNetwork 回调参数 */\nexport interface NetworkCaptureInfo {\n method: string | null;\n url: string | null;\n statusCode: number | null;\n}\n\nexport interface SentryCaptureOptions {\n onError?: (info: ErrorCaptureInfo) => void;\n onNetwork?: (info: NetworkCaptureInfo) => void;\n /** 可注入 mock 实例以在单元测试中隔离网络依赖 */\n symbolicator?: MetroSymbolicator;\n}\n\n// ── MetroSymbolicator ──────────────────────────────────────────────────────────\n\n/**\n * Metro 符号化器。\n * 将 bundle 行列号还原为源码位置(通过 Metro /symbolicate 接口)。\n *\n * @example\n * const sym = new MetroSymbolicator();\n *\n * @example\n * // 单测:注入 baseUrl 并 mock fetch\n * const sym = new MetroSymbolicator({ baseUrl: 'http://localhost:8081' });\n * global.fetch = jest.fn().mockResolvedValue({ ok: true, json: async () => ({ stack: [...] }) });\n */\nexport class MetroSymbolicator {\n readonly baseUrl: string;\n\n constructor({ baseUrl }: { baseUrl?: string } = {}) {\n this.baseUrl = baseUrl ?? MetroSymbolicator._getDefaultBaseUrl();\n }\n\n /**\n * 从 NativeModules.SourceCode.scriptURL 推断 Metro server base URL。\n * web 平台使用相对路径;native 回退到 localhost:8081。\n */\n private static _getDefaultBaseUrl(): string {\n try {\n const scriptURL = NativeModules?.SourceCode?.scriptURL as string | undefined;\n if (scriptURL) {\n const url = new URL(scriptURL);\n return `${url.protocol}//${url.host}`;\n }\n } catch (_) {}\n return Platform.OS === 'web' ? '' : 'http://localhost:8081';\n }\n\n /**\n * 解析 Error.stack 字符串为 Metro 所需的帧数组。\n *\n * 使用 stacktrace-parser 覆盖各 JS 引擎的格式:\n * V8/Node.js/Hermes(新):at methodName (url:line:col)\n * Gecko/Firefox/Hermes(旧):methodName@url:line:col\n * JavaScriptCore(iOS):methodName@url:line:col\n *\n * 无法符号化的帧(file 或 lineNumber 为 null)会被过滤掉。\n */\n parseErrorStack(stack: string): MetroFrame[] {\n return parseStackTrace(stack)\n .filter(\n (f): f is typeof f & { file: string; lineNumber: number } =>\n f.file != null && f.lineNumber != null,\n )\n .map(f => ({\n file: f.file,\n lineNumber: f.lineNumber,\n column: f.column != null ? f.column - 1 : 0,\n methodName: f.methodName === '<unknown>' ? '(anonymous)' : f.methodName,\n }));\n }\n\n /**\n * 将帧列表发送到 Metro /symbolicate,返回符号化后的帧数组。\n */\n async symbolicate(stack: MetroFrame[]): Promise<MetroFrame[] | null> {\n if (stack.length === 0) return null;\n const endpoint = `${this.baseUrl}/symbolicate`;\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ stack }),\n });\n if (!res.ok) {\n console.warn(`[SentryCapture] /symbolicate returned ${res.status}`);\n return null;\n }\n const data = (await res.json()) as { stack?: MetroFrame[] };\n return data.stack ?? null;\n } catch (e) {\n console.warn('[SentryCapture] symbolication failed:', (e as Error)?.message ?? e);\n return null;\n }\n }\n}\n\n// ── SentryCapture ─────────────────────────────────────────────────────────────\n\n/**\n * Sentry 事件捕获器。\n * 封装 beforeSend / beforeBreadcrumb 逻辑,通过注入的回调将事件数据交给调用方处理。\n *\n * @example\n * const capture = new SentryCapture({\n * onError(info) { console.log(info.message, info.symbolicatedFrames); },\n * onNetwork(info) { console.log(info.method, info.url, info.statusCode); },\n * });\n * Sentry.init({ dsn: '...', beforeSend: capture.beforeSend, beforeBreadcrumb: capture.beforeBreadcrumb });\n *\n * @example\n * // 单测:注入 mock symbolicator\n * const mockSym = { parseErrorStack: jest.fn(() => []), symbolicate: jest.fn(async () => null) };\n * const onError = jest.fn();\n * const capture = new SentryCapture({ onError, symbolicator: mockSym });\n * await capture.beforeSend(fakeEvent, { originalException: new Error('oops') });\n * expect(onError).toHaveBeenCalledWith(expect.objectContaining({ message: 'oops' }));\n */\nexport class SentryCapture {\n private readonly onError: SentryCaptureOptions['onError'];\n private readonly onNetwork: SentryCaptureOptions['onNetwork'];\n private readonly symbolicator: MetroSymbolicator;\n\n constructor({ onError, onNetwork, symbolicator }: SentryCaptureOptions = {}) {\n this.onError = onError;\n this.onNetwork = onNetwork;\n this.symbolicator = symbolicator ?? new MetroSymbolicator();\n\n // bind 保证方法作为回调传入 Sentry.init() 时 this 不丢失\n this.beforeSend = this.beforeSend.bind(this);\n this.beforeBreadcrumb = this.beforeBreadcrumb.bind(this);\n }\n\n /**\n * Sentry beforeSend hook。\n * 优先使用原始 Error.stack 进行符号化;若无法获取则回退到 Sentry 事件帧。\n * 符号化完成后触发 onError 回调,原样返回 event。\n */\n async beforeSend(event: ErrorEvent, hint: EventHint): Promise<ErrorEvent> {\n globalThis.__SENTRY_LAST_RAW__ = { event, hint };\n\n const message: string =\n event.exception?.values?.[0]?.value ?? event.message ?? '(unknown)';\n\n let symbolicatedFrames: MetroFrame[] | null = null;\n\n const originalException: unknown = hint?.originalException;\n const rawStack: string | null =\n originalException instanceof Error && typeof originalException.stack === 'string'\n ? originalException.stack\n : null;\n\n if (rawStack !== null) {\n const frames: MetroFrame[] = this.symbolicator.parseErrorStack(rawStack);\n if (frames.length > 0) {\n symbolicatedFrames = await this.symbolicator.symbolicate(frames);\n }\n }\n\n // 回退:使用 Sentry 事件帧\n if (symbolicatedFrames === null) {\n const sentryFrames: StackFrame[] =\n event.exception?.values?.[0]?.stacktrace?.frames ?? [];\n const mapped: MetroFrame[] = sentryFrames\n .filter(\n (f): f is StackFrame & Required<Pick<StackFrame, 'filename' | 'lineno'>> =>\n f.filename != null && f.lineno != null,\n )\n .map(\n (f): MetroFrame => ({\n file: f.filename,\n lineNumber: f.lineno,\n column: f.colno ?? 0,\n methodName: f.function ?? '(anonymous)',\n }),\n );\n if (mapped.length > 0) {\n symbolicatedFrames = await this.symbolicator.symbolicate(mapped);\n }\n }\n\n const componentStack: string | null =\n (event.contexts?.react?.componentStack as string | undefined) ?? null;\n\n const mechanismType: string | null =\n event.exception?.values?.[0]?.mechanism?.type ?? null;\n\n this.onError?.({ message, symbolicatedFrames, componentStack, mechanismType });\n\n return event;\n }\n\n /**\n * Sentry beforeBreadcrumb hook。\n * 识别 HTTP 类型的 breadcrumb,触发 onNetwork 回调,原样返回 breadcrumb。\n */\n beforeBreadcrumb(breadcrumb: Breadcrumb, _hint?: BreadcrumbHint): Breadcrumb {\n if (breadcrumb.type === 'http') {\n const data = breadcrumb.data as FetchBreadcrumbData | undefined;\n this.onNetwork?.({\n method: data?.method ?? null,\n url: data?.url ?? null,\n statusCode: data?.status_code ?? null,\n });\n }\n return breadcrumb;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,118 @@
1
+ import { MetroConfig } from 'metro-config';
2
+
3
+ /**
4
+ * miaoda-expo-devkit/metro
5
+ *
6
+ * 提供 Metro 配置增强函数:
7
+ * - withDevStubs(config) 模块替换:Sentry DSN 覆盖、LogBox 屏蔽
8
+ * - withEntryInjection(config) bundle 首部脚本注入
9
+ * - withRouteEndpoint(config) 添加 /__routes 端点,返回路由树 JSON
10
+ * - withWorkspaceNodeModules(config) 修复 node_modules 位于项目父级目录时的资源加载 500 问题
11
+ *
12
+ * 用法(metro.config.js):
13
+ *
14
+ * const { getDefaultConfig } = require('expo/metro-config');
15
+ * const { withDevStubs, withEntryInjection, withRouteEndpoint } = require('miaoda-expo-devkit/metro');
16
+ *
17
+ * const config = getDefaultConfig(__dirname);
18
+ * module.exports = withRouteEndpoint(
19
+ * withEntryInjection(withDevStubs(config)),
20
+ * { appDir: path.join(__dirname, 'src', 'app') }
21
+ * );
22
+ *
23
+ * 链式用法(与其他 Metro wrapper 组合):
24
+ *
25
+ * module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });
26
+ */
27
+
28
+ /**
29
+ * 将开发阶段 stub 注入到 Metro config 中。
30
+ *
31
+ * 注入行为:
32
+ * 1. web 平台:@expo/log-box 及 ErrorOverlayWebControls → no-op-logbox stub(屏蔽全屏错误遮罩)
33
+ * 2. 所有平台:@sentry/react-native → sentry-react-native-stub(替换 DSN,注入内置捕获器)
34
+ *
35
+ * 自动保留并调用已有的 resolveRequest,支持与其他 Metro 配置链式组合。
36
+ *
37
+ * @param config Metro config 对象(来自 getDefaultConfig 或 getSentryExpoConfig)
38
+ * @returns 注入 resolveRequest 后的新 Metro config
39
+ */
40
+ declare function withDevStubs(config: MetroConfig): MetroConfig;
41
+ /** withEntryInjection 的配置选项(预留,供未来支持自定义注入脚本路径扩展) */
42
+ interface InjectOptions {
43
+ }
44
+ /**
45
+ * 在 expo-router 启动前注入一段脚本。
46
+ *
47
+ * 注入原理:
48
+ * 拦截 Metro 对 expo-router/entry-classic 的解析,将其重定向到
49
+ * dist/stubs/expo-router-entry-stub.js。expo-router-entry-stub.js 先 require entry-inject.js
50
+ * (注入脚本),再 require expo-router/entry-classic(原入口),
51
+ * 从而在 expo-router 启动前执行注入逻辑。
52
+ *
53
+ * 拦截 entry-classic 而非 entry 本身,原因:
54
+ * - expo-router/entry 以 <script src="...entry.bundle..."> 方式直接加载,
55
+ * 不经过 resolveRequest。
56
+ * - expo-router/entry 内部第一行是 import 'expo-router/entry-classic',
57
+ * 这条 import 语句经过 resolveRequest,可以被拦截。
58
+ *
59
+ * 执行顺序(在 bundle 中):
60
+ * entry-inject.js(注入脚本:设置 __DEVKIT_INJECTED__ + 安装 HMR 控制器)
61
+ * → expo-router/entry-classic(原 expo-router 入口)
62
+ * → renderRootComponent(App)
63
+ *
64
+ * 注入的脚本经过完整 Babel 编译,支持现代语法,web + native 双平台均有效。
65
+ *
66
+ * 支持与 withDevStubs 及其他 Metro wrapper 链式组合:
67
+ * module.exports = withEntryInjection(withDevStubs(config));
68
+ *
69
+ * @param config Metro config 对象
70
+ * @param options 注入选项(当前预留,未来支持自定义注入脚本路径)
71
+ * @returns 注入 resolveRequest 后的新 Metro config
72
+ */
73
+ declare function withEntryInjection(config: MetroConfig, options?: InjectOptions): MetroConfig;
74
+ /** withRouteEndpoint 的配置选项 */
75
+ interface RouteEndpointOptions {
76
+ /** app 目录的绝对路径(包含路由文件的目录) */
77
+ appDir: string;
78
+ /** 自定义端点路径,默认 /__routes */
79
+ endpoint?: string;
80
+ }
81
+ /**
82
+ * 为 Metro server 添加 /__routes 端点,返回路由树 JSON。
83
+ *
84
+ * 端点返回格式:
85
+ * {
86
+ * "routes": [
87
+ * { "title": "首页", "pageId": "/", "pageName": "index", "visible": true, "path": "index.tsx" },
88
+ * { "title": "id", "pageId": "/user/[id]", "pageName": "user-[id]", "visible": false, "path": "user/[id].tsx" }
89
+ * ]
90
+ * }
91
+ *
92
+ * @param config Metro config 对象
93
+ * @param options 配置选项,必须提供 appDir
94
+ * @returns 增强 server.enhanceMiddleware 后的新 Metro config
95
+ */
96
+ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOptions): MetroConfig;
97
+ /**
98
+ * 修复沙箱环境中 node_modules 位于项目父级目录时,资源文件(字体、图片等)加载返回 500 的问题。
99
+ *
100
+ * 问题根因:
101
+ * pnpm 在沙箱中将 node_modules 安装到 projectRoot 的上层目录(如 /workspace/node_modules),
102
+ * 而非 projectRoot 内(如 /workspace/app/node_modules)。
103
+ * Metro 处理 /assets/?unstable_path=./node_modules/.pnpm/... 请求时,会将路径解析为
104
+ * path.resolve(projectRoot, './node_modules/.pnpm/...'),得到一个不存在的路径,返回 500。
105
+ * 实际文件在父级的 node_modules/.pnpm/ 下。
106
+ *
107
+ * 修复方式:
108
+ * 向上查找实际的 node_modules 目录,将其加入 watchFolders(让 Metro 索引其中的文件)
109
+ * 和 resolver.nodeModulesPaths(让模块解析也能找到包)。
110
+ *
111
+ * 如果 node_modules 就在 projectRoot 下,则不做任何修改,直接返回原始 config。
112
+ *
113
+ * @param config Metro config 对象(来自 getDefaultConfig)
114
+ * @returns 修正 watchFolders 和 nodeModulesPaths 后的新 Metro config
115
+ */
116
+ declare function withWorkspaceNodeModules(config: MetroConfig): MetroConfig;
117
+
118
+ export { type InjectOptions, type RouteEndpointOptions, withDevStubs, withEntryInjection, withRouteEndpoint, withWorkspaceNodeModules };
@@ -0,0 +1,118 @@
1
+ import { MetroConfig } from 'metro-config';
2
+
3
+ /**
4
+ * miaoda-expo-devkit/metro
5
+ *
6
+ * 提供 Metro 配置增强函数:
7
+ * - withDevStubs(config) 模块替换:Sentry DSN 覆盖、LogBox 屏蔽
8
+ * - withEntryInjection(config) bundle 首部脚本注入
9
+ * - withRouteEndpoint(config) 添加 /__routes 端点,返回路由树 JSON
10
+ * - withWorkspaceNodeModules(config) 修复 node_modules 位于项目父级目录时的资源加载 500 问题
11
+ *
12
+ * 用法(metro.config.js):
13
+ *
14
+ * const { getDefaultConfig } = require('expo/metro-config');
15
+ * const { withDevStubs, withEntryInjection, withRouteEndpoint } = require('miaoda-expo-devkit/metro');
16
+ *
17
+ * const config = getDefaultConfig(__dirname);
18
+ * module.exports = withRouteEndpoint(
19
+ * withEntryInjection(withDevStubs(config)),
20
+ * { appDir: path.join(__dirname, 'src', 'app') }
21
+ * );
22
+ *
23
+ * 链式用法(与其他 Metro wrapper 组合):
24
+ *
25
+ * module.exports = withNativeWind(withEntryInjection(withDevStubs(config)), { input: './global.css' });
26
+ */
27
+
28
+ /**
29
+ * 将开发阶段 stub 注入到 Metro config 中。
30
+ *
31
+ * 注入行为:
32
+ * 1. web 平台:@expo/log-box 及 ErrorOverlayWebControls → no-op-logbox stub(屏蔽全屏错误遮罩)
33
+ * 2. 所有平台:@sentry/react-native → sentry-react-native-stub(替换 DSN,注入内置捕获器)
34
+ *
35
+ * 自动保留并调用已有的 resolveRequest,支持与其他 Metro 配置链式组合。
36
+ *
37
+ * @param config Metro config 对象(来自 getDefaultConfig 或 getSentryExpoConfig)
38
+ * @returns 注入 resolveRequest 后的新 Metro config
39
+ */
40
+ declare function withDevStubs(config: MetroConfig): MetroConfig;
41
+ /** withEntryInjection 的配置选项(预留,供未来支持自定义注入脚本路径扩展) */
42
+ interface InjectOptions {
43
+ }
44
+ /**
45
+ * 在 expo-router 启动前注入一段脚本。
46
+ *
47
+ * 注入原理:
48
+ * 拦截 Metro 对 expo-router/entry-classic 的解析,将其重定向到
49
+ * dist/stubs/expo-router-entry-stub.js。expo-router-entry-stub.js 先 require entry-inject.js
50
+ * (注入脚本),再 require expo-router/entry-classic(原入口),
51
+ * 从而在 expo-router 启动前执行注入逻辑。
52
+ *
53
+ * 拦截 entry-classic 而非 entry 本身,原因:
54
+ * - expo-router/entry 以 <script src="...entry.bundle..."> 方式直接加载,
55
+ * 不经过 resolveRequest。
56
+ * - expo-router/entry 内部第一行是 import 'expo-router/entry-classic',
57
+ * 这条 import 语句经过 resolveRequest,可以被拦截。
58
+ *
59
+ * 执行顺序(在 bundle 中):
60
+ * entry-inject.js(注入脚本:设置 __DEVKIT_INJECTED__ + 安装 HMR 控制器)
61
+ * → expo-router/entry-classic(原 expo-router 入口)
62
+ * → renderRootComponent(App)
63
+ *
64
+ * 注入的脚本经过完整 Babel 编译,支持现代语法,web + native 双平台均有效。
65
+ *
66
+ * 支持与 withDevStubs 及其他 Metro wrapper 链式组合:
67
+ * module.exports = withEntryInjection(withDevStubs(config));
68
+ *
69
+ * @param config Metro config 对象
70
+ * @param options 注入选项(当前预留,未来支持自定义注入脚本路径)
71
+ * @returns 注入 resolveRequest 后的新 Metro config
72
+ */
73
+ declare function withEntryInjection(config: MetroConfig, options?: InjectOptions): MetroConfig;
74
+ /** withRouteEndpoint 的配置选项 */
75
+ interface RouteEndpointOptions {
76
+ /** app 目录的绝对路径(包含路由文件的目录) */
77
+ appDir: string;
78
+ /** 自定义端点路径,默认 /__routes */
79
+ endpoint?: string;
80
+ }
81
+ /**
82
+ * 为 Metro server 添加 /__routes 端点,返回路由树 JSON。
83
+ *
84
+ * 端点返回格式:
85
+ * {
86
+ * "routes": [
87
+ * { "title": "首页", "pageId": "/", "pageName": "index", "visible": true, "path": "index.tsx" },
88
+ * { "title": "id", "pageId": "/user/[id]", "pageName": "user-[id]", "visible": false, "path": "user/[id].tsx" }
89
+ * ]
90
+ * }
91
+ *
92
+ * @param config Metro config 对象
93
+ * @param options 配置选项,必须提供 appDir
94
+ * @returns 增强 server.enhanceMiddleware 后的新 Metro config
95
+ */
96
+ declare function withRouteEndpoint(config: MetroConfig, options: RouteEndpointOptions): MetroConfig;
97
+ /**
98
+ * 修复沙箱环境中 node_modules 位于项目父级目录时,资源文件(字体、图片等)加载返回 500 的问题。
99
+ *
100
+ * 问题根因:
101
+ * pnpm 在沙箱中将 node_modules 安装到 projectRoot 的上层目录(如 /workspace/node_modules),
102
+ * 而非 projectRoot 内(如 /workspace/app/node_modules)。
103
+ * Metro 处理 /assets/?unstable_path=./node_modules/.pnpm/... 请求时,会将路径解析为
104
+ * path.resolve(projectRoot, './node_modules/.pnpm/...'),得到一个不存在的路径,返回 500。
105
+ * 实际文件在父级的 node_modules/.pnpm/ 下。
106
+ *
107
+ * 修复方式:
108
+ * 向上查找实际的 node_modules 目录,将其加入 watchFolders(让 Metro 索引其中的文件)
109
+ * 和 resolver.nodeModulesPaths(让模块解析也能找到包)。
110
+ *
111
+ * 如果 node_modules 就在 projectRoot 下,则不做任何修改,直接返回原始 config。
112
+ *
113
+ * @param config Metro config 对象(来自 getDefaultConfig)
114
+ * @returns 修正 watchFolders 和 nodeModulesPaths 后的新 Metro config
115
+ */
116
+ declare function withWorkspaceNodeModules(config: MetroConfig): MetroConfig;
117
+
118
+ export { type InjectOptions, type RouteEndpointOptions, withDevStubs, withEntryInjection, withRouteEndpoint, withWorkspaceNodeModules };