react-native-vconsole 0.0.1 → 0.3.0

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.
Files changed (49) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +39 -9
  3. package/Vconsole.podspec +25 -0
  4. package/android/build.gradle +61 -129
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +1 -2
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/java/com/vconsole/VconsoleModule.kt +80 -0
  9. package/android/src/main/java/com/vconsole/VconsolePackage.kt +17 -0
  10. package/ios/Vconsole.h +6 -0
  11. package/ios/Vconsole.mm +64 -0
  12. package/lib/module/VConsole.js +780 -0
  13. package/lib/module/VConsole.js.map +1 -0
  14. package/lib/module/core/consoleProxy.js +86 -0
  15. package/lib/module/core/consoleProxy.js.map +1 -0
  16. package/lib/module/core/xhrProxy.js +247 -0
  17. package/lib/module/core/xhrProxy.js.map +1 -0
  18. package/lib/module/index.js +24 -0
  19. package/lib/module/index.js.map +1 -0
  20. package/lib/module/package.json +1 -0
  21. package/lib/module/types.js +2 -0
  22. package/lib/module/types.js.map +1 -0
  23. package/lib/typescript/package.json +1 -0
  24. package/lib/typescript/src/VConsole.d.ts +6 -0
  25. package/lib/typescript/src/VConsole.d.ts.map +1 -0
  26. package/lib/typescript/src/core/consoleProxy.d.ts +9 -0
  27. package/lib/typescript/src/core/consoleProxy.d.ts.map +1 -0
  28. package/lib/typescript/src/core/xhrProxy.d.ts +12 -0
  29. package/lib/typescript/src/core/xhrProxy.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +9 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -0
  32. package/lib/typescript/src/types.d.ts +37 -0
  33. package/lib/typescript/src/types.d.ts.map +1 -0
  34. package/package.json +138 -25
  35. package/src/VConsole.tsx +887 -0
  36. package/src/core/consoleProxy.ts +108 -0
  37. package/src/core/xhrProxy.ts +319 -0
  38. package/src/index.tsx +36 -0
  39. package/src/types.ts +42 -0
  40. package/android/README.md +0 -14
  41. package/android/src/main/java/wiki/qdc/rn/vconsole/ReactNativeVconsoleModule.java +0 -27
  42. package/android/src/main/java/wiki/qdc/rn/vconsole/ReactNativeVconsolePackage.java +0 -23
  43. package/index.js +0 -5
  44. package/ios/.DS_Store +0 -0
  45. package/ios/ReactNativeVconsole.h +0 -5
  46. package/ios/ReactNativeVconsole.m +0 -13
  47. package/ios/ReactNativeVconsole.xcodeproj/project.pbxproj +0 -281
  48. package/ios/ReactNativeVconsole.xcworkspace/contents.xcworkspacedata +0 -7
  49. package/react-native-vconsole.podspec +0 -28
@@ -0,0 +1,108 @@
1
+ import type { LogEntry, LogLevel } from '../types';
2
+
3
+ type LogListener = (entries: LogEntry[]) => void;
4
+ type ConsoleMethod = (...args: unknown[]) => void;
5
+
6
+ const MAX_LOG_COUNT = 1000;
7
+
8
+ const listeners = new Set<LogListener>();
9
+ const entries: LogEntry[] = [];
10
+ const originalConsole: Partial<Record<LogLevel, ConsoleMethod>> = {};
11
+
12
+ let isInstalled = false;
13
+ let logId = 1;
14
+
15
+ function safeSerialize(value: unknown): string {
16
+ if (typeof value === 'string') {
17
+ return value;
18
+ }
19
+
20
+ if (value instanceof Error) {
21
+ return `${value.name}: ${value.message}`;
22
+ }
23
+
24
+ try {
25
+ return JSON.stringify(value);
26
+ } catch {
27
+ return String(value);
28
+ }
29
+ }
30
+
31
+ function buildText(args: unknown[]): string {
32
+ return args.map((item) => safeSerialize(item)).join(' ');
33
+ }
34
+
35
+ function notify() {
36
+ const next = [...entries];
37
+ listeners.forEach((listener) => {
38
+ listener(next);
39
+ });
40
+ }
41
+
42
+ function pushEntry(level: LogLevel, args: unknown[]) {
43
+ entries.push({
44
+ id: logId++,
45
+ level,
46
+ args,
47
+ text: buildText(args),
48
+ timestamp: Date.now(),
49
+ });
50
+
51
+ if (entries.length > MAX_LOG_COUNT) {
52
+ entries.splice(0, entries.length - MAX_LOG_COUNT);
53
+ }
54
+ }
55
+
56
+ export function installConsoleProxy() {
57
+ if (isInstalled) {
58
+ return;
59
+ }
60
+
61
+ const levels: LogLevel[] = ['log', 'info', 'warn', 'error'];
62
+ levels.forEach((level) => {
63
+ const original = console[level] as ConsoleMethod;
64
+ originalConsole[level] = original;
65
+
66
+ console[level] = (...args: unknown[]) => {
67
+ pushEntry(level, args);
68
+ notify();
69
+ original(...args);
70
+ };
71
+ });
72
+
73
+ isInstalled = true;
74
+ }
75
+
76
+ export function uninstallConsoleProxy() {
77
+ if (!isInstalled) {
78
+ return;
79
+ }
80
+
81
+ const levels: LogLevel[] = ['log', 'info', 'warn', 'error'];
82
+ levels.forEach((level) => {
83
+ const original = originalConsole[level];
84
+ if (original) {
85
+ console[level] = original as ConsoleMethod;
86
+ }
87
+ });
88
+
89
+ isInstalled = false;
90
+ }
91
+
92
+ export function getLogEntries(): LogEntry[] {
93
+ return [...entries];
94
+ }
95
+
96
+ export function clearLogEntries() {
97
+ entries.length = 0;
98
+ notify();
99
+ }
100
+
101
+ export function subscribeLogEntries(listener: LogListener) {
102
+ listeners.add(listener);
103
+ listener(getLogEntries());
104
+
105
+ return () => {
106
+ listeners.delete(listener);
107
+ };
108
+ }
@@ -0,0 +1,319 @@
1
+ import type { NetworkEntry } from '../types';
2
+
3
+ type NetworkListener = (entries: NetworkEntry[]) => void;
4
+ type InstallXhrProxyOptions = {
5
+ filterHosts?: string[];
6
+ };
7
+
8
+ const MAX_NETWORK_COUNT = 500;
9
+
10
+ const listeners = new Set<NetworkListener>();
11
+ const entries: NetworkEntry[] = [];
12
+
13
+ let isInstalled = false;
14
+ let networkId = 1;
15
+ let OriginalXHR: typeof XMLHttpRequest | undefined;
16
+ let ignoredHosts = new Set<string>();
17
+
18
+ function normalizeHosts(hosts?: string[]): Set<string> {
19
+ return new Set(
20
+ (hosts ?? []).map((item) => item.trim().toLowerCase()).filter(Boolean)
21
+ );
22
+ }
23
+
24
+ function getHostFromUrl(rawUrl: string): string | undefined {
25
+ if (!rawUrl) {
26
+ return undefined;
27
+ }
28
+
29
+ const isAbsoluteHttp = /^https?:\/\//i.test(rawUrl);
30
+ const isProtocolRelative = /^\/\//.test(rawUrl);
31
+ if (!isAbsoluteHttp && !isProtocolRelative) {
32
+ return undefined;
33
+ }
34
+
35
+ try {
36
+ const normalizedUrl = isProtocolRelative ? `https:${rawUrl}` : rawUrl;
37
+ return new URL(normalizedUrl).host.toLowerCase();
38
+ } catch {
39
+ const normalizedUrl = isProtocolRelative ? `https:${rawUrl}` : rawUrl;
40
+ const matched = normalizedUrl.match(/^https?:\/\/([^/]+)/i);
41
+ return matched?.[1]?.toLowerCase();
42
+ }
43
+ }
44
+
45
+ function shouldSkipNetworkCapture(rawUrl: string): boolean {
46
+ const host = getHostFromUrl(rawUrl);
47
+ if (!host) {
48
+ return false;
49
+ }
50
+ return ignoredHosts.has(host);
51
+ }
52
+
53
+ function notify() {
54
+ const next = [...entries];
55
+ listeners.forEach((listener) => {
56
+ listener(next);
57
+ });
58
+ }
59
+
60
+ function trimEntries() {
61
+ if (entries.length > MAX_NETWORK_COUNT) {
62
+ entries.splice(0, entries.length - MAX_NETWORK_COUNT);
63
+ }
64
+ }
65
+
66
+ function isPromiseLike(value: unknown): value is Promise<unknown> {
67
+ return (
68
+ typeof value === 'object' &&
69
+ value !== null &&
70
+ 'then' in value &&
71
+ typeof (value as { then?: unknown }).then === 'function'
72
+ );
73
+ }
74
+
75
+ function getHeaderCaseInsensitive(
76
+ headers: Record<string, string>,
77
+ key: string
78
+ ): string | undefined {
79
+ const target = key.toLowerCase();
80
+ const foundKey = Object.keys(headers).find(
81
+ (item) => item.toLowerCase() === target
82
+ );
83
+ return foundKey ? headers[foundKey] : undefined;
84
+ }
85
+
86
+ function parseBlobLikeJson(blobLike: unknown): Promise<unknown> | undefined {
87
+ if (
88
+ typeof blobLike === 'object' &&
89
+ blobLike !== null &&
90
+ 'text' in blobLike &&
91
+ typeof (blobLike as { text?: unknown }).text === 'function'
92
+ ) {
93
+ const textFn = (blobLike as { text: () => Promise<string> }).text;
94
+ return textFn()
95
+ .then((text) => {
96
+ try {
97
+ return JSON.parse(text);
98
+ } catch {
99
+ return text;
100
+ }
101
+ })
102
+ .catch(() => '[Blob response]');
103
+ }
104
+
105
+ if (typeof Response !== 'undefined') {
106
+ try {
107
+ const response = new Response(blobLike as never);
108
+ return response
109
+ .text()
110
+ .then((text) => {
111
+ try {
112
+ return JSON.parse(text);
113
+ } catch {
114
+ return text;
115
+ }
116
+ })
117
+ .catch(() => '[Blob response]');
118
+ } catch {
119
+ return undefined;
120
+ }
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ function parseResponseData(
127
+ xhr: XMLHttpRequest,
128
+ responseHeaders: Record<string, string>
129
+ ): unknown | Promise<unknown> {
130
+ const responseType = xhr.responseType ?? '';
131
+ const canReadResponseText = responseType === '' || responseType === 'text';
132
+
133
+ if (canReadResponseText) {
134
+ try {
135
+ const responseText = xhr.responseText;
136
+ if (!responseText) {
137
+ return '';
138
+ }
139
+ try {
140
+ return JSON.parse(responseText);
141
+ } catch {
142
+ return responseText;
143
+ }
144
+ } catch {
145
+ // Some RN runtimes still throw for responseText access.
146
+ return xhr.response ?? '';
147
+ }
148
+ }
149
+
150
+ if (responseType === 'json') {
151
+ return xhr.response ?? null;
152
+ }
153
+
154
+ if (responseType === 'blob') {
155
+ const contentType =
156
+ getHeaderCaseInsensitive(responseHeaders, 'content-type') ?? '';
157
+ if (contentType.toLowerCase().includes('application/json')) {
158
+ const parsed = parseBlobLikeJson(xhr.response);
159
+ if (parsed) {
160
+ return parsed;
161
+ }
162
+ }
163
+ return '[Blob response]';
164
+ }
165
+
166
+ if (responseType === 'arraybuffer') {
167
+ return '[ArrayBuffer response]';
168
+ }
169
+
170
+ return xhr.response ?? '';
171
+ }
172
+
173
+ function parseHeaders(rawHeaders: string): Record<string, string> {
174
+ const result: Record<string, string> = {};
175
+ rawHeaders
176
+ .split('\r\n')
177
+ .filter(Boolean)
178
+ .forEach((line) => {
179
+ const splitIndex = line.indexOf(':');
180
+ if (splitIndex > 0) {
181
+ const key = line.slice(0, splitIndex).trim();
182
+ const value = line.slice(splitIndex + 1).trim();
183
+ result[key] = value;
184
+ }
185
+ });
186
+ return result;
187
+ }
188
+
189
+ export function installXhrProxy(options?: InstallXhrProxyOptions) {
190
+ ignoredHosts = normalizeHosts(options?.filterHosts);
191
+
192
+ if (isInstalled || typeof XMLHttpRequest === 'undefined') {
193
+ return;
194
+ }
195
+
196
+ OriginalXHR = global.XMLHttpRequest;
197
+ if (!OriginalXHR) {
198
+ return;
199
+ }
200
+
201
+ class PatchedXMLHttpRequest extends OriginalXHR {
202
+ private _entryId = 0;
203
+ private _requestHeaders: Record<string, string> = {};
204
+ private _method = 'GET';
205
+ private _url = '';
206
+
207
+ open(method: string, url: string, ...rest: unknown[]) {
208
+ this._method = (method || 'GET').toUpperCase();
209
+ this._url = url;
210
+ return super.open(method, url, ...(rest as []));
211
+ }
212
+
213
+ setRequestHeader(header: string, value: string) {
214
+ this._requestHeaders[header] = value;
215
+ return super.setRequestHeader(header, value);
216
+ }
217
+
218
+ send(body?: unknown) {
219
+ if (shouldSkipNetworkCapture(this._url)) {
220
+ return super.send(body as never);
221
+ }
222
+
223
+ const entry: NetworkEntry = {
224
+ id: networkId++,
225
+ method: this._method,
226
+ url: this._url,
227
+ startedAt: Date.now(),
228
+ requestHeaders: { ...this._requestHeaders },
229
+ requestBody: body ?? undefined,
230
+ responseHeaders: {},
231
+ };
232
+
233
+ this._entryId = entry.id;
234
+ entries.push(entry);
235
+ trimEntries();
236
+ notify();
237
+
238
+ this.addEventListener('readystatechange', () => {
239
+ if (this.readyState !== 4) {
240
+ return;
241
+ }
242
+
243
+ const current = entries.find((item) => item.id === this._entryId);
244
+ if (!current) {
245
+ return;
246
+ }
247
+
248
+ // Some internal RN/Metro requests start as relative paths (e.g. /symbolicate).
249
+ // Re-check with responseURL at completion to apply host filters correctly.
250
+ const finalUrl = this.responseURL || this._url;
251
+ if (shouldSkipNetworkCapture(finalUrl)) {
252
+ const index = entries.findIndex((item) => item.id === this._entryId);
253
+ if (index >= 0) {
254
+ entries.splice(index, 1);
255
+ notify();
256
+ }
257
+ return;
258
+ }
259
+
260
+ const finishedAt = Date.now();
261
+ current.status = this.status;
262
+ current.finishedAt = finishedAt;
263
+ current.durationMs = finishedAt - current.startedAt;
264
+ current.responseHeaders = parseHeaders(this.getAllResponseHeaders());
265
+ try {
266
+ const parsedData = parseResponseData(this, current.responseHeaders);
267
+ if (isPromiseLike(parsedData)) {
268
+ current.responseData = '[Parsing blob response...]';
269
+ notify();
270
+ parsedData
271
+ .then((resolved) => {
272
+ current.responseData = resolved;
273
+ notify();
274
+ })
275
+ .catch(() => {
276
+ current.responseData = '[Blob response]';
277
+ notify();
278
+ });
279
+ return;
280
+ }
281
+ current.responseData = parsedData;
282
+ } catch {
283
+ current.responseData = '[Unreadable response]';
284
+ }
285
+ notify();
286
+ });
287
+
288
+ return super.send(body as never);
289
+ }
290
+ }
291
+
292
+ global.XMLHttpRequest = PatchedXMLHttpRequest as typeof XMLHttpRequest;
293
+ isInstalled = true;
294
+ }
295
+
296
+ export function uninstallXhrProxy() {
297
+ if (!isInstalled || !OriginalXHR) {
298
+ return;
299
+ }
300
+ global.XMLHttpRequest = OriginalXHR;
301
+ isInstalled = false;
302
+ }
303
+
304
+ export function getNetworkEntries(): NetworkEntry[] {
305
+ return [...entries];
306
+ }
307
+
308
+ export function clearNetworkEntries() {
309
+ entries.length = 0;
310
+ notify();
311
+ }
312
+
313
+ export function subscribeNetworkEntries(listener: NetworkListener) {
314
+ listeners.add(listener);
315
+ listener(getNetworkEntries());
316
+ return () => {
317
+ listeners.delete(listener);
318
+ };
319
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,36 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+ import { VConsole } from './VConsole';
3
+ import type { AppInfo, SystemInfo } from './types';
4
+ import type { VConsoleProps } from './VConsole';
5
+
6
+ const LINKING_ERROR =
7
+ `The package 'react-native-vconsole' doesn't seem to be linked. Make sure: \n\n` +
8
+ Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) +
9
+ '- You rebuilt the app after installing the package\n' +
10
+ '- You are not using Expo Go\n';
11
+
12
+ const Vconsole = NativeModules.Vconsole
13
+ ? NativeModules.Vconsole
14
+ : new Proxy(
15
+ {},
16
+ {
17
+ get() {
18
+ throw new Error(LINKING_ERROR);
19
+ },
20
+ }
21
+ );
22
+
23
+ export function multiply(a: number, b: number): Promise<number> {
24
+ return Vconsole.multiply(a, b);
25
+ }
26
+
27
+ export function getSystemInfo(): Promise<SystemInfo> {
28
+ return Vconsole.getSystemInfo();
29
+ }
30
+
31
+ export function getAppInfo(): Promise<AppInfo> {
32
+ return Vconsole.getAppInfo();
33
+ }
34
+
35
+ export { VConsole };
36
+ export type { AppInfo, SystemInfo, VConsoleProps };
package/src/types.ts ADDED
@@ -0,0 +1,42 @@
1
+ export type VConsoleTab = 'Log' | 'Network' | 'System' | 'App';
2
+
3
+ export type LogLevel = 'log' | 'info' | 'warn' | 'error';
4
+
5
+ export type LogFilterTab = 'All' | LogLevel;
6
+
7
+ export interface LogEntry {
8
+ id: number;
9
+ level: LogLevel;
10
+ args: unknown[];
11
+ text: string;
12
+ timestamp: number;
13
+ }
14
+
15
+ export interface NetworkEntry {
16
+ id: number;
17
+ method: string;
18
+ url: string;
19
+ status?: number;
20
+ startedAt: number;
21
+ finishedAt?: number;
22
+ durationMs?: number;
23
+ requestHeaders: Record<string, string>;
24
+ requestBody?: unknown;
25
+ responseHeaders: Record<string, string>;
26
+ responseData?: unknown;
27
+ }
28
+
29
+ export interface SystemInfo {
30
+ manufacturer: string;
31
+ model: string;
32
+ osVersion: string;
33
+ networkType: string;
34
+ isNetworkReachable: boolean;
35
+ totalMemory: number;
36
+ availableMemory: number;
37
+ }
38
+
39
+ export interface AppInfo {
40
+ appVersion: string;
41
+ buildNumber: string;
42
+ }
package/android/README.md DELETED
@@ -1,14 +0,0 @@
1
- README
2
- ======
3
-
4
- If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm:
5
-
6
- 1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed
7
- 2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK
8
- ```
9
- ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle
10
- sdk.dir=/Users/{username}/Library/Android/sdk
11
- ```
12
- 3. Delete the `maven` folder
13
- 4. Run `./gradlew installArchives`
14
- 5. Verify that latest set of generated files is in the maven folder with the correct version number
@@ -1,27 +0,0 @@
1
- package wiki.qdc.rn.vconsole;
2
-
3
- import com.facebook.react.bridge.ReactApplicationContext;
4
- import com.facebook.react.bridge.ReactContextBaseJavaModule;
5
- import com.facebook.react.bridge.ReactMethod;
6
- import com.facebook.react.bridge.Callback;
7
-
8
- public class ReactNativeVconsoleModule extends ReactContextBaseJavaModule {
9
-
10
- private final ReactApplicationContext reactContext;
11
-
12
- public ReactNativeVconsoleModule(ReactApplicationContext reactContext) {
13
- super(reactContext);
14
- this.reactContext = reactContext;
15
- }
16
-
17
- @Override
18
- public String getName() {
19
- return "ReactNativeVconsole";
20
- }
21
-
22
- @ReactMethod
23
- public void sampleMethod(String stringArgument, int numberArgument, Callback callback) {
24
- // TODO: Implement some actually useful functionality
25
- callback.invoke("Received numberArgument: " + numberArgument + " stringArgument: " + stringArgument);
26
- }
27
- }
@@ -1,23 +0,0 @@
1
- package wiki.qdc.rn.vconsole;
2
-
3
- import java.util.Arrays;
4
- import java.util.Collections;
5
- import java.util.List;
6
-
7
- import com.facebook.react.ReactPackage;
8
- import com.facebook.react.bridge.NativeModule;
9
- import com.facebook.react.bridge.ReactApplicationContext;
10
- import com.facebook.react.uimanager.ViewManager;
11
- import com.facebook.react.bridge.JavaScriptModule;
12
-
13
- public class ReactNativeVconsolePackage implements ReactPackage {
14
- @Override
15
- public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
16
- return Arrays.<NativeModule>asList(new ReactNativeVconsoleModule(reactContext));
17
- }
18
-
19
- @Override
20
- public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
21
- return Collections.emptyList();
22
- }
23
- }
package/index.js DELETED
@@ -1,5 +0,0 @@
1
- import { NativeModules } from 'react-native';
2
-
3
- const { ReactNativeVconsole } = NativeModules;
4
-
5
- export default ReactNativeVconsole;
package/ios/.DS_Store DELETED
Binary file
@@ -1,5 +0,0 @@
1
- #import <React/RCTBridgeModule.h>
2
-
3
- @interface ReactNativeVconsole : NSObject <RCTBridgeModule>
4
-
5
- @end
@@ -1,13 +0,0 @@
1
- #import "ReactNativeVconsole.h"
2
-
3
- @implementation ReactNativeVconsole
4
-
5
- RCT_EXPORT_MODULE()
6
-
7
- RCT_EXPORT_METHOD(sampleMethod:(NSString *)stringArgument numberParameter:(nonnull NSNumber *)numberArgument callback:(RCTResponseSenderBlock)callback)
8
- {
9
- // TODO: Implement some actually useful functionality
10
- callback(@[[NSString stringWithFormat: @"numberArgument: %@ stringArgument: %@", numberArgument, stringArgument]]);
11
- }
12
-
13
- @end