@wutiange/log-listener-plugin 1.3.2 → 2.0.1-alpha.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/README.md +130 -24
  2. package/dist/src/HTTPInterceptor.d.ts +1 -0
  3. package/dist/src/HTTPInterceptor.js +12 -9
  4. package/dist/src/HTTPInterceptor.js.map +1 -1
  5. package/dist/src/Server.d.ts +13 -6
  6. package/dist/src/Server.js +119 -41
  7. package/dist/src/Server.js.map +1 -1
  8. package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js +0 -1
  9. package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js.map +1 -1
  10. package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +0 -1
  11. package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js.map +1 -1
  12. package/dist/src/__tests__/Server.test.js +76 -115
  13. package/dist/src/__tests__/Server.test.js.map +1 -1
  14. package/dist/src/common.d.ts +19 -10
  15. package/dist/src/common.js +63 -4
  16. package/dist/src/common.js.map +1 -1
  17. package/dist/src/logPlugin.d.ts +12 -9
  18. package/dist/src/logPlugin.js +87 -82
  19. package/dist/src/logPlugin.js.map +1 -1
  20. package/dist/src/logger.d.ts +6 -0
  21. package/dist/src/logger.js +16 -0
  22. package/dist/src/logger.js.map +1 -0
  23. package/dist/src/utils.js +12 -7
  24. package/dist/src/utils.js.map +1 -1
  25. package/index.ts +3 -0
  26. package/package.json +18 -12
  27. package/src/HTTPInterceptor.ts +339 -0
  28. package/src/Server.ts +164 -0
  29. package/src/__mocks__/react-native/Libraries/Blob/FileReader.js +45 -0
  30. package/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +39 -0
  31. package/src/__tests__/HTTPInterceptor.test.ts +322 -0
  32. package/src/__tests__/Server.test.ts +150 -0
  33. package/src/__tests__/utils.test.ts +113 -0
  34. package/src/common.ts +70 -0
  35. package/src/logPlugin.ts +224 -0
  36. package/src/logger.ts +15 -0
  37. package/src/utils.ts +112 -0
  38. package/tsconfig.json +27 -0
@@ -0,0 +1,150 @@
1
+ import Server from '../Server';
2
+ import { sleep } from '../utils';
3
+
4
+ // Mock dependencies
5
+ jest.mock('../utils', () => {
6
+ const actual = jest.requireActual('../utils');
7
+ return {
8
+ ...actual, // 保留所有真实实现
9
+ sleep: jest.fn() // 只模拟 sleep 函数
10
+ };
11
+ });
12
+
13
+
14
+ // Mock require for react-native-zeroconf
15
+ jest.mock('react-native-zeroconf', () => undefined, { virtual: true });
16
+
17
+ // Mock fetch
18
+ global.fetch = jest.fn();
19
+
20
+ describe('Server', () => {
21
+ let server: Server;
22
+
23
+ beforeEach(() => {
24
+ // Clear all mocks before each test
25
+ jest.clearAllMocks();
26
+ (global.fetch as jest.Mock).mockReset();
27
+ });
28
+
29
+ describe('Constructor and URL Management', () => {
30
+ it('should initialize with default values', () => {
31
+ server = new Server();
32
+ expect(server.getUrls()).toEqual([]);
33
+ });
34
+
35
+ it('should initialize with custom URL', () => {
36
+ server = new Server('localhost:8080');
37
+ expect(server.getUrls()).toEqual(['http://localhost:8080']);
38
+ });
39
+
40
+ it('should handle URLs with and without http prefix', () => {
41
+ server = new Server();
42
+ server.updateUrl('localhost:8080');
43
+ expect(server.getUrls()).toEqual(['http://localhost:8080']);
44
+
45
+ server.updateUrl('http://localhost:8080');
46
+ expect(server.getUrls()).toEqual(['http://localhost:8080']);
47
+ });
48
+ });
49
+
50
+ describe('ZeroConf Handling', () => {
51
+ it('should handle case when zeroconf is not available', () => {
52
+ server = new Server();
53
+ const mockListener = jest.fn();
54
+ server.addUrlsListener(mockListener);
55
+
56
+ // Since Zeroconf is not available, the listener should not be called
57
+ expect(mockListener).not.toHaveBeenCalled();
58
+ });
59
+
60
+ // Test with mock Zeroconf implementation
61
+ it('should handle case when zeroconf is available', () => {
62
+ // Temporarily mock require to return a mock Zeroconf implementation
63
+ const mockZeroconfInstance = {
64
+ on: jest.fn(),
65
+ scan: jest.fn()
66
+ };
67
+
68
+ jest.doMock('react-native-zeroconf', () => ({
69
+ __esModule: true,
70
+ default: jest.fn(() => mockZeroconfInstance)
71
+ }), { virtual: true });
72
+
73
+ server = new Server();
74
+ const mockListener = jest.fn();
75
+ server.addUrlsListener(mockListener);
76
+
77
+ // Verify that Zeroconf methods were not called since module is mocked as undefined
78
+ expect(mockListener).not.toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ describe('Data Sending', () => {
83
+ beforeEach(() => {
84
+ server = new Server('localhost:8080');
85
+ (global.fetch as jest.Mock).mockImplementation(() =>
86
+ Promise.resolve({ ok: true })
87
+ );
88
+ });
89
+
90
+ it('should send log data', async () => {
91
+ const testData = { message: 'test log' };
92
+ await server.log(testData);
93
+
94
+ expect(global.fetch).toHaveBeenCalledWith(
95
+ 'http://localhost:8080/log',
96
+ expect.objectContaining({
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json;charset=utf-8'
100
+ },
101
+ body: expect.any(String)
102
+ })
103
+ );
104
+ });
105
+
106
+ it('should send network data', async () => {
107
+ const testData = { url: 'test.com' };
108
+ await server.network(testData);
109
+
110
+ expect(global.fetch).toHaveBeenCalledWith(
111
+ 'http://localhost:8080/network',
112
+ expect.objectContaining({
113
+ method: 'POST',
114
+ headers: {
115
+ 'Content-Type': 'application/json;charset=utf-8'
116
+ },
117
+ body: expect.any(String)
118
+ })
119
+ );
120
+ });
121
+
122
+ it('should handle timeout', async () => {
123
+ server.updateTimeout(100);
124
+ (sleep as jest.Mock).mockImplementation(() => Promise.reject(new Error('Timeout')));
125
+
126
+ const testData = { message: 'test' };
127
+ await server.log(testData);
128
+
129
+ expect(global.fetch).toHaveBeenCalled();
130
+ expect(sleep).toHaveBeenCalledWith(100, true);
131
+ });
132
+ });
133
+
134
+ describe('Base Data Management', () => {
135
+ it('should update base data', async () => {
136
+ server = new Server('localhost:8080');
137
+ const baseData = { userId: '123' };
138
+ server.updateBaseData(baseData);
139
+
140
+ await server.log({ message: 'test' });
141
+
142
+ expect(global.fetch).toHaveBeenCalledWith(
143
+ expect.any(String),
144
+ expect.objectContaining({
145
+ body: expect.stringContaining('"userId":"123"')
146
+ })
147
+ );
148
+ });
149
+ });
150
+ });
@@ -0,0 +1,113 @@
1
+ import { createClassWithErrorHandling, hasPort } from '../utils';
2
+
3
+ describe('createClassWithErrorHandling', () => {
4
+ class TestClass {
5
+ normalMethod(): string {
6
+ return 'normal';
7
+ }
8
+
9
+ errorMethod(): void {
10
+ throw new Error('Test error');
11
+ }
12
+
13
+ async asyncMethod(): Promise<string> {
14
+ return 'async';
15
+ }
16
+
17
+ async asyncErrorMethod(): Promise<void> {
18
+ throw new Error('Async test error');
19
+ }
20
+ }
21
+
22
+ let consoleErrorSpy: jest.SpyInstance;
23
+
24
+ beforeEach(() => {
25
+ consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ });
27
+
28
+ afterEach(() => {
29
+ consoleErrorSpy.mockRestore();
30
+ });
31
+
32
+ test('should not interfere with normal methods', () => {
33
+ const EnhancedClass = createClassWithErrorHandling(TestClass);
34
+ const instance = new EnhancedClass();
35
+ expect(instance.normalMethod()).toBe('normal');
36
+ });
37
+
38
+ test('should catch and log errors from methods', () => {
39
+ const EnhancedClass = createClassWithErrorHandling(TestClass);
40
+ const instance = new EnhancedClass();
41
+ expect(() => instance.errorMethod()).toThrow('Test error');
42
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error in errorMethod:', expect.any(Error));
43
+ });
44
+
45
+ test('should not interfere with async methods that resolve', async () => {
46
+ const EnhancedClass = createClassWithErrorHandling(TestClass);
47
+ const instance = new EnhancedClass();
48
+ await expect(instance.asyncMethod()).resolves.toBe('async');
49
+ });
50
+
51
+ test('should catch and log errors from async methods that reject', async () => {
52
+ const EnhancedClass = createClassWithErrorHandling(TestClass);
53
+ const instance = new EnhancedClass();
54
+ await expect(instance.asyncErrorMethod()).rejects.toThrow('Async test error');
55
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error in asyncErrorMethod:', expect.any(Error));
56
+ });
57
+
58
+ test('should handle methods added after instantiation', () => {
59
+ const EnhancedClass = createClassWithErrorHandling(TestClass);
60
+ const instance = new EnhancedClass();
61
+ (instance as any).dynamicMethod = function(): void {
62
+ throw new Error('Dynamic method error');
63
+ };
64
+ expect(() => (instance as any).dynamicMethod()).toThrow('Dynamic method error');
65
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error in dynamicMethod:', expect.any(Error));
66
+ });
67
+ });
68
+
69
+
70
+ describe('hasPort function', () => {
71
+ test('should return true for URLs with explicit ports', () => {
72
+ expect(hasPort('http://example.com:8080')).toBe(true);
73
+ expect(hasPort('ftp://example.com:210')).toBe(true);
74
+ });
75
+
76
+ test('should return false for URLs without explicit ports', () => {
77
+ expect(hasPort('http://example.com')).toBe(false);
78
+ expect(hasPort('https://example.com')).toBe(false);
79
+ expect(hasPort('ftp://example.com')).toBe(false);
80
+ });
81
+
82
+ test('should return false for invalid URLs', () => {
83
+ expect(hasPort('not a url')).toBe(false);
84
+ expect(hasPort('http:/example.com')).toBe(false);
85
+ expect(hasPort('example.com:8080')).toBe(false);
86
+ });
87
+
88
+ test('should return false for empty input', () => {
89
+ expect(hasPort('')).toBe(false);
90
+ });
91
+
92
+ test('should return false for non-string input', () => {
93
+ expect(hasPort(null as any)).toBe(false);
94
+ expect(hasPort(undefined as any)).toBe(false);
95
+ expect(hasPort(123 as any)).toBe(false);
96
+ expect(hasPort({} as any)).toBe(false);
97
+ });
98
+
99
+ test('should handle URLs with default ports correctly', () => {
100
+ expect(hasPort('http://example.com:80')).toBe(false);
101
+ expect(hasPort('https://example.com:443')).toBe(false);
102
+ });
103
+
104
+ test('should handle URLs with IPv6 addresses', () => {
105
+ expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
106
+ expect(hasPort('https://[2001:db8::1]')).toBe(false);
107
+ });
108
+
109
+ test('should handle URLs with userinfo', () => {
110
+ expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
111
+ expect(hasPort('http://user:pass@example.com')).toBe(false);
112
+ });
113
+ });
package/src/common.ts ADDED
@@ -0,0 +1,70 @@
1
+ import logger from "./logger";
2
+
3
+ export const URLS_KEY = 'log-listener-plugin-urls$$key'
4
+ export const DEFAULT_TIMEOUT = 3000
5
+ export const LOG_KEY = '[@wutiange/log-listener-plugin 日志]'
6
+ export enum Level {
7
+ LOG = 'log',
8
+ WARN = 'warn',
9
+ ERROR = 'error',
10
+ }
11
+
12
+ export enum Tag {
13
+ LOG_PLUGIN_INTERNAL_ERROR = 'log-plugin-internal-error',
14
+ DEFAULT = 'default',
15
+ }
16
+
17
+ const getDefaultDeviceINfo = () => {
18
+ try {
19
+ const {Platform} = require('react-native')
20
+ return {
21
+ SystemName: Platform.OS,
22
+ Version: Platform.Version,
23
+ ...Platform.constants
24
+ }
25
+ } catch (error) {
26
+ logger.warn(LOG_KEY, '这个插件只能在 react-native 中使用')
27
+ return {}
28
+ }
29
+ }
30
+
31
+ export const getBaseData = (): Record<string, string> => {
32
+
33
+ try {
34
+ const DeviceInfo = require("react-native-device-info")?.default;
35
+ return {
36
+ Brand: DeviceInfo.getBrand(),
37
+ Model: DeviceInfo.getModel(),
38
+ AppVersion: DeviceInfo.getVersion(),
39
+ Carrier: DeviceInfo.getCarrierSync(),
40
+ Manufacturer: DeviceInfo.getManufacturerSync(),
41
+ SystemName: DeviceInfo.getSystemName(),
42
+ ...getDefaultDeviceINfo()
43
+ };
44
+ } catch (error) {
45
+ return getDefaultDeviceINfo()
46
+ }
47
+ }
48
+
49
+ export const getDefaultStorage = (): Storage => {
50
+ try {
51
+ const AsyncStorage = require("@react-native-async-storage/async-storage")?.default;
52
+ return AsyncStorage;
53
+ } catch (error) {
54
+ return null;
55
+ }
56
+ }
57
+
58
+
59
+ export const getErrMsg = (error: any) => {
60
+ return {
61
+ message: [
62
+ `${
63
+ error?.message ?? error
64
+ }--->这是@wutiange/log-listener-plugin内部错误,请提issue反馈,issue地址:https://github.com/wutiange/log-listener-plugin/issues`,
65
+ ],
66
+ tag: Tag.LOG_PLUGIN_INTERNAL_ERROR,
67
+ level: Level.ERROR,
68
+ createTime: Date.now(),
69
+ }
70
+ }
@@ -0,0 +1,224 @@
1
+ import Server from './Server';
2
+ import { createClassWithErrorHandling } from './utils';
3
+ import { httpInterceptor } from './HTTPInterceptor';
4
+ import { DEFAULT_TIMEOUT, getDefaultStorage, Level, LOG_KEY, Tag, URLS_KEY } from './common';
5
+ import logger from './logger';
6
+
7
+ type Options = {
8
+ /**
9
+ * storage 用于存储已设置的日志系统的 url
10
+ * @default @react-native-async-storage/async-storage
11
+ */
12
+ storage?: Storage
13
+ /**
14
+ * 设置上传日志的超时时间,单位为毫秒
15
+ * @default 3000
16
+ */
17
+ timeout?: number
18
+ /**
19
+ * 日志系统的url
20
+ */
21
+ testUrl?: string
22
+ /**
23
+ * 是否自动开启日志记录
24
+ * @default false
25
+ */
26
+ isAuto?: boolean
27
+ /**
28
+ * 设置日志系统的基础数据,这些数据会自动添加到每条日志中
29
+ */
30
+ baseData?: Record<string, any>
31
+ }
32
+
33
+ class LogPlugin {
34
+ private server: Server | null = null;
35
+ private timeout: number | null = null;
36
+ private isAuto = false
37
+ private storage: Storage | null = getDefaultStorage();
38
+
39
+ constructor() {
40
+ this.init()
41
+ }
42
+
43
+ private init = async () => {
44
+ this.server = new Server();
45
+ if (!this.storage) {
46
+ logger.warn(LOG_KEY, '你并没有设置 storage ,这会导致 App 杀死后可能需要重新加入日志系统才能收集日志数据,建议你设置 storage 。')
47
+ } else {
48
+ const urlsStr = await this.storage.getItem(URLS_KEY)
49
+ if (urlsStr) {
50
+ const urls = JSON.parse(urlsStr)
51
+ this.server.setBaseUrlObj(urls)
52
+ }
53
+ }
54
+
55
+
56
+ this.server.addUrlsListener((_, urlsObj) => {
57
+ if (this.storage) {
58
+ this.storage.setItem(URLS_KEY, JSON.stringify(urlsObj))
59
+ }
60
+ httpInterceptor.setIgnoredUrls(this.handleIgnoredUrls())
61
+ })
62
+ }
63
+
64
+ config = ({ storage, timeout, testUrl, isAuto, baseData = {} }: Options) => {
65
+ if (isAuto) {
66
+ this.auto()
67
+ } else {
68
+ this.unAuto()
69
+ }
70
+ this.storage = storage ?? getDefaultStorage();
71
+ this.setTimeout(timeout ?? DEFAULT_TIMEOUT)
72
+ this.setBaseUrl(testUrl)
73
+ this.setBaseData(baseData)
74
+ };
75
+
76
+ /**
77
+ * @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({isAuto: true}) 替代。
78
+ */
79
+ auto = () => {
80
+ this.startRecordNetwork();
81
+ this.startRecordLog();
82
+ this.isAuto = true
83
+ }
84
+
85
+ unAuto = () => {
86
+ this.stopRecordLog()
87
+ httpInterceptor.disable()
88
+ httpInterceptor.removeAllListener()
89
+ this.isAuto = false
90
+ }
91
+
92
+ startRecordLog = () => {
93
+ console.log = (...data: any[]) => {
94
+ logger.log(...data)
95
+ this.log(...data);
96
+ };
97
+
98
+ console.warn = (...data: any[]) => {
99
+ logger.warn(...data)
100
+ this.warn(...data);
101
+ };
102
+
103
+ console.error = (...data: any[]) => {
104
+ logger.error(...data)
105
+ this.error(...data);
106
+ };
107
+ }
108
+
109
+ stopRecordLog = () => {
110
+ console.log = logger.log
111
+ console.warn = logger.warn
112
+ console.error = logger.error
113
+ }
114
+
115
+ private handleIgnoredUrls = () => {
116
+ const urls = this.server?.getUrls?.()
117
+ let ignoredUrls: string[] = []
118
+ if (urls?.length) {
119
+ ignoredUrls = urls.reduce((acc, url) => {
120
+ acc.push(`${url}/log`, `${url}/network`, `${url}/join`)
121
+ return acc
122
+ }, [] as string[])
123
+ }
124
+ return ignoredUrls
125
+ }
126
+
127
+ startRecordNetwork = () => {
128
+ httpInterceptor.addListener("send", (data) => {
129
+ this.server?.network({
130
+ url: data.url,
131
+ id: data.id,
132
+ method: data.method,
133
+ headers: data.requestHeaders,
134
+ body: data.requestData,
135
+ createTime: data.startTime
136
+ })
137
+ })
138
+ httpInterceptor.addListener("response", (data) => {
139
+ this.server?.network({
140
+ headers: data.responseHeaders,
141
+ body: data.responseData,
142
+ requestId: data.id,
143
+ statusCode: data.status,
144
+ endTime: data.endTime
145
+ })
146
+ })
147
+ httpInterceptor.enable({ignoredUrls: this.handleIgnoredUrls()})
148
+ }
149
+
150
+ /**
151
+ * @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({testUrl: ''}) 替代。
152
+ */
153
+ setBaseUrl = (url: string) => {
154
+ const tempUrl = url?.trim()
155
+ if (this.server) {
156
+ this.server.updateUrl(tempUrl);
157
+ } else {
158
+ this.server = new Server(tempUrl);
159
+ }
160
+ httpInterceptor.setIgnoredUrls(this.handleIgnoredUrls())
161
+ if (this.isAuto) {
162
+ this.startRecordNetwork();
163
+ this.startRecordLog()
164
+ }
165
+ }
166
+
167
+ /**
168
+ * @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({timeout: 3000}) 替代。
169
+ */
170
+ setTimeout = (timeout: number) => {
171
+ if (typeof timeout === 'number') {
172
+ this.timeout = timeout;
173
+ this.server?.updateTimeout(this.timeout);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * @deprecated 这个方法将在下一个主要版本中被移除。移除后将不再支持获取超时时间。
179
+ */
180
+ getTimeout = () => {
181
+ if (typeof this.timeout === 'number') {
182
+ return this.timeout;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * @deprecated 这个方法将在下一个主要版本中被移除。请使用 config({baseData: {}}) 替代。
189
+ */
190
+ setBaseData = (data: Record<string, any> = {}) => {
191
+ this.server.updateBaseData(data)
192
+ }
193
+
194
+ private _log = (level: string, tag: string, ...data: any[]) => {
195
+ const sendData = {
196
+ message: data,
197
+ tag,
198
+ level: level ?? 'log',
199
+ createTime: Date.now(),
200
+ };
201
+ this.server?.log(sendData);
202
+ }
203
+
204
+ tag = (tag: string, ...data: any[]) => {
205
+ this._log(Level.LOG, tag, ...data);
206
+ }
207
+
208
+ log = (...data: any[]) => {
209
+ this._log(Level.LOG, Tag.DEFAULT, ...data);
210
+ }
211
+
212
+ warn = (...data: any[]) => {
213
+ this._log(Level.WARN, Tag.DEFAULT, ...data);
214
+ }
215
+
216
+ error = (...data: any[]) => {
217
+ this._log(Level.ERROR, Tag.DEFAULT, ...data);
218
+ }
219
+
220
+ }
221
+ const SafeLogPlugin = createClassWithErrorHandling(LogPlugin)
222
+ const logPlugin = new SafeLogPlugin();
223
+ export { SafeLogPlugin };
224
+ export default logPlugin;
package/src/logger.ts ADDED
@@ -0,0 +1,15 @@
1
+ const [log, warn, error] = [console.log, console.warn, console.error];
2
+
3
+ const logger = {
4
+ log: (...data: any[]) => {
5
+ log(...data)
6
+ },
7
+ warn: (...data: any[]) => {
8
+ warn(...data)
9
+ },
10
+ error: (...data: any[]) => {
11
+ error(...data)
12
+ },
13
+ }
14
+
15
+ export default logger
package/src/utils.ts ADDED
@@ -0,0 +1,112 @@
1
+ import URL from "url-parse";
2
+ import logger from "./logger";
3
+
4
+ export function sleep(ms: number, isReject: boolean = false) {
5
+ return new Promise((resolve, reject) => {
6
+ setTimeout(isReject ? () => reject({
7
+ code: 11001,
8
+ key: '@wutiange/log-listener-plugin%%timeout',
9
+ message: 'Timeout'
10
+ }) : resolve, ms)
11
+ })
12
+ }
13
+
14
+ // 检查 url 是否有端口号,不包含内置的端口号,比如 80 ,443 等
15
+ export function hasPort(url: string) {
16
+ // 如果 url 是空的或不是字符串,返回 false
17
+ if (!url || typeof url !== 'string') {
18
+ return false;
19
+ }
20
+
21
+ try {
22
+ // 使用 URL 构造函数解析 URL
23
+ const parsedUrl = new URL(url);
24
+
25
+ // 检查 port 属性是否为空
26
+ // 注意:如果使用默认端口(如 HTTP 的 80 或 HTTPS 的 443),port 会是空字符串
27
+ return parsedUrl.port !== '';
28
+ } catch (error) {
29
+ logger.error(error)
30
+ // 如果 URL 无效,捕获错误并返回 false
31
+ return false;
32
+ }
33
+ }
34
+
35
+
36
+ type Constructor<T = {}> = new (...args: any[]) => T;
37
+
38
+ export function createClassWithErrorHandling<T extends Constructor>(BaseClass: T): T {
39
+ return new Proxy(BaseClass, {
40
+ construct(target: T, args: any[]): object {
41
+ const instance = new target(...args);
42
+ return new Proxy(instance, {
43
+ get(target: any, prop: string | symbol): any {
44
+ const value = target[prop];
45
+ if (typeof value === 'function') {
46
+ return function(this: any, ...args: any[]): any {
47
+ try {
48
+ const result = value.apply(this, args);
49
+ if (result instanceof Promise) {
50
+ return result.catch((error: Error) => {
51
+ console.error(`Error in ${String(prop)}:`, error);
52
+ throw error; // 重新抛出错误,以便调用者可以捕获它
53
+ });
54
+ }
55
+ return result;
56
+ } catch (error) {
57
+ console.error(`Error in ${String(prop)}:`, error);
58
+ throw error; // 重新抛出错误,以便调用者可以捕获它
59
+ }
60
+ };
61
+ }
62
+ return value;
63
+ },
64
+ set(target: any, prop: string | symbol, value: any): boolean {
65
+ if (typeof value === 'function') {
66
+ target[prop] = function(this: any, ...args: any[]): any {
67
+ try {
68
+ const result = value.apply(this, args);
69
+ if (result instanceof Promise) {
70
+ return result.catch((error: Error) => {
71
+ console.error(`Error in ${String(prop)}:`, error);
72
+ throw error;
73
+ });
74
+ }
75
+ return result;
76
+ } catch (error) {
77
+ console.error(`Error in ${String(prop)}:`, error);
78
+ throw error;
79
+ }
80
+ };
81
+ } else {
82
+ target[prop] = value;
83
+ }
84
+ return true;
85
+ }
86
+ });
87
+ }
88
+ });
89
+ }
90
+
91
+
92
+ export function formDataToString(formData: FormData): string {
93
+ const boundary =
94
+ '----WebKitFormBoundary' + Math.random().toString(36).substr(2);
95
+ let result = '';
96
+ // 这是 react-native 中的实现,这里面是存在这个方法的
97
+ const parts = (formData as any).getParts();
98
+ for (const part of parts) {
99
+ result += `--${boundary}\r\n`;
100
+ result += `Content-Disposition: ${part.headers['content-disposition']}\r\n`;
101
+ if (part.headers['content-type']) {
102
+ result += `Content-Type: ${part.headers['content-type']}\r\n`;
103
+ }
104
+ const value = 'string' in part ? part.string : part.uri;
105
+ result += `Content-Length: ${value.length}\r\n\r\n`;
106
+ result += `${value}\r\n`;
107
+ }
108
+ result += `--${boundary}--\r\n`;
109
+ return result;
110
+ }
111
+
112
+