@capybara-devtools/react-native 0.0.0-stage → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lhphuc99
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @capybara-devtools/react-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React Native SDK for [Capybara](https://github.com/lhphuc99/capybara#readme): sends your app's logs, network requests (including WebSockets) and storage to the Capybara UI on your computer, and lets you edit AsyncStorage and MMKV values from it.
4
+
5
+ On React Native 0.85 or later, Capybara reads logs, network and storage through Metro without this package. Add it to edit storage, to see storage changes as they happen, or on older React Native versions.
6
+
7
+ ```bash
8
+ npm install @capybara-devtools/react-native
9
+ ```
10
+
11
+ Import it before the rest of your app:
12
+
13
+ ```ts
14
+ import AsyncStorage from '@react-native-async-storage/async-storage';
15
+ import { asyncStorage, init, mmkvStorage } from '@capybara-devtools/react-native';
16
+ import { storage } from './storage'; // your react-native-mmkv instance
17
+
18
+ if (__DEV__) init({ appName: 'My app', storage: [asyncStorage(AsyncStorage), mmkvStorage(storage)] });
19
+ ```
20
+
21
+ Then run `npx capybara-devtools` (or open the desktop app) on the computer running Metro. The SDK finds it from the bundle URL, so simulators and phones on the same Wi-Fi connect without configuration.
22
+
23
+ Documentation: [github.com/lhphuc99/capybara](https://github.com/lhphuc99/capybara#optional-react-native-sdk-storage-editing)
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@capybara-devtools/core';
2
+ /** Records uncaught JS exceptions, then hands them to the previous handler (LogBox, crash reporters). */
3
+ export declare function errorUtilsPlugin(): Plugin;
4
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAStD,yGAAyG;AACzG,wBAAgB,gBAAgB,IAAI,MAAM,CAuBzC"}
package/dist/errors.js ADDED
@@ -0,0 +1,30 @@
1
+ /** Records uncaught JS exceptions, then hands them to the previous handler (LogBox, crash reporters). */
2
+ export function errorUtilsPlugin() {
3
+ return {
4
+ name: 'errors',
5
+ setup(ctx) {
6
+ const errorUtils = globalThis.ErrorUtils;
7
+ if (!errorUtils?.getGlobalHandler || !errorUtils.setGlobalHandler)
8
+ return;
9
+ const previous = errorUtils.getGlobalHandler();
10
+ const handler = (error, isFatal) => {
11
+ try {
12
+ const args = [ctx.serialize(error)];
13
+ if (isFatal)
14
+ args.push(ctx.serialize('(fatal)'));
15
+ ctx.emit('log', { level: 'error', source: 'uncaught', args });
16
+ }
17
+ catch {
18
+ // the original handler must still run
19
+ }
20
+ previous?.(error, isFatal);
21
+ };
22
+ errorUtils.setGlobalHandler(handler);
23
+ return () => {
24
+ if (errorUtils.getGlobalHandler() === handler && previous)
25
+ errorUtils.setGlobalHandler(previous);
26
+ };
27
+ },
28
+ };
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AASA,yGAAyG;AACzG,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,KAAK,CAAC,GAAG;YACP,MAAM,UAAU,GAAI,UAA8C,CAAC,UAAU,CAAC;YAC9E,IAAI,CAAC,UAAU,EAAE,gBAAgB,IAAI,CAAC,UAAU,CAAC,gBAAgB;gBAAE,OAAO;YAC1E,MAAM,QAAQ,GAAG,UAAU,CAAC,gBAAgB,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;gBAChD,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;oBACpC,IAAI,OAAO;wBAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC;oBACjD,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChE,CAAC;gBAAC,MAAM,CAAC;oBACP,sCAAsC;gBACxC,CAAC;gBACD,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;YAC7B,CAAC,CAAC;YACF,UAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC;YACrC,OAAO,GAAG,EAAE;gBACV,IAAI,UAAU,CAAC,gBAAgB,EAAE,KAAK,OAAO,IAAI,QAAQ;oBAAE,UAAU,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YACnG,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
package/dist/host.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Host of a URL (IPv6 keeps its brackets), or undefined for host-less URLs such as file:// bundles. */
2
+ export declare function hostFromUrl(url: string): string | undefined;
3
+ /** Explicit port of a URL, or undefined when it has none. */
4
+ export declare function portFromUrl(url: string): string | undefined;
5
+ /**
6
+ * Sockets of dev tools: Metro's own (HMR, inspector) on the bundle's port, React DevTools (8097) and
7
+ * Reactotron (9090). React Native opens some of them with an http:// URL, so both schemes match.
8
+ */
9
+ export declare function devToolSockets(metroPort: string | undefined): RegExp[];
10
+ export declare function formatHost(host: string): string;
11
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE3D;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE3D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAMtE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C"}
package/dist/host.js ADDED
@@ -0,0 +1,23 @@
1
+ /** Host of a URL (IPv6 keeps its brackets), or undefined for host-less URLs such as file:// bundles. */
2
+ export function hostFromUrl(url) {
3
+ return /^[a-z][a-z\d+.-]*:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url)?.[1];
4
+ }
5
+ /** Explicit port of a URL, or undefined when it has none. */
6
+ export function portFromUrl(url) {
7
+ return /^[a-z][a-z\d+.-]*:\/\/(?:[^@/?#]*@)?(?:\[[^\]]+\]|[^:/?#]+):(\d+)/i.exec(url)?.[1];
8
+ }
9
+ /**
10
+ * Sockets of dev tools: Metro's own (HMR, inspector) on the bundle's port, React DevTools (8097) and
11
+ * Reactotron (9090). React Native opens some of them with an http:// URL, so both schemes match.
12
+ */
13
+ export function devToolSockets(metroPort) {
14
+ const patterns = [/^(?:wss?|https?):\/\/[^/]+:(?:8097|9090)(?:\/|$)/];
15
+ if (metroPort) {
16
+ patterns.push(new RegExp(`^(?:wss?|https?)://[^/]+:${metroPort}/(?:hot|message|events|inspector|debugger-proxy)(?:[/?]|$)`));
17
+ }
18
+ return patterns;
19
+ }
20
+ export function formatHost(host) {
21
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
22
+ }
23
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.js","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"AAAA,wGAAwG;AACxG,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,4DAA4D,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,oEAAoE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,SAA6B;IAC1D,MAAM,QAAQ,GAAG,CAAC,kDAAkD,CAAC,CAAC;IACtE,IAAI,SAAS,EAAE,CAAC;QACd,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,4BAA4B,SAAS,4DAA4D,CAAC,CAAC,CAAC;IAC/H,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { type CapybaraClient, type NetworkPluginOptions, type Plugin, type StorageAdapter } from '@capybara-devtools/core';
2
+ export declare const SDK_VERSION = "0.2.0";
3
+ export interface InitOptions {
4
+ /** Shown in the session picker. */
5
+ appName?: string;
6
+ /** Machine running `capybara`. Defaults to the host serving the JS bundle (Metro). */
7
+ host?: string;
8
+ port?: number;
9
+ /** Pass false to skip setup, e.g. `enabled: __DEV__`. */
10
+ enabled?: boolean;
11
+ /** Capture console output and uncaught errors. Default true. */
12
+ logs?: boolean;
13
+ network?: boolean | NetworkPluginOptions;
14
+ /** e.g. `[asyncStorage(AsyncStorage), mmkvStorage(storage)]` */
15
+ storage?: StorageAdapter[];
16
+ plugins?: Plugin[];
17
+ }
18
+ export declare function init(options?: InitOptions): CapybaraClient | undefined;
19
+ export { getActiveClient } from '@capybara-devtools/core';
20
+ export type { CapybaraClient, NetworkPluginOptions, Plugin, StorageAdapter } from '@capybara-devtools/core';
21
+ export { asyncStorage, mmkvStorage } from './storage.js';
22
+ export type { AdapterOptions, AsyncStorageLike, MMKVLike } from './storage.js';
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,MAAM,EACX,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAMjC,eAAO,MAAM,WAAW,UAAU,CAAC;AAKnC,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;IACzC,gEAAgE;IAChE,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,cAAc,GAAG,SAAS,CAkC1E;AAgED,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC5G,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,100 @@
1
+ import { consolePlugin, createClient, getActiveClient, networkPlugin, storagePlugin, } from '@capybara-devtools/core';
2
+ import { CLIENT_PATH, DEFAULT_PORT } from '@capybara-devtools/protocol';
3
+ import { NativeModules, Platform } from 'react-native';
4
+ import { errorUtilsPlugin } from './errors.js';
5
+ import { devToolSockets, formatHost, hostFromUrl, portFromUrl } from './host.js';
6
+ export const SDK_VERSION = '0.2.0';
7
+ /** Requests React Native sends to Metro on its own (LogBox symbolication, "open in editor"). */
8
+ const DEV_SERVER_NOISE = [/\/symbolicate(\?|$)/, /\/open-stack-frame(\?|$)/];
9
+ export function init(options = {}) {
10
+ if (options.enabled === false)
11
+ return undefined;
12
+ // Fast Refresh can re-run the entry file; patching again would record everything twice.
13
+ const existing = getActiveClient();
14
+ if (existing)
15
+ return existing;
16
+ const plugins = [];
17
+ if (options.logs !== false)
18
+ plugins.push(consolePlugin(), errorUtilsPlugin());
19
+ if (options.network !== false) {
20
+ const network = typeof options.network === 'object' ? options.network : {};
21
+ plugins.push(networkPlugin({
22
+ fetch: !fetchRunsOnXhr(),
23
+ xhr: true,
24
+ ...network,
25
+ ignoreUrls: [...DEV_SERVER_NOISE, ...devToolSockets(portFromUrl(bundleUrl() ?? '')), ...(network.ignoreUrls ?? [])],
26
+ }));
27
+ }
28
+ // Registered even when empty so the UI can explain how to add adapters.
29
+ plugins.push(storagePlugin(options.storage ?? []));
30
+ plugins.push(...(options.plugins ?? []));
31
+ return createClient({
32
+ url: `ws://${formatHost(options.host ?? defaultHost())}:${options.port ?? DEFAULT_PORT}${CLIENT_PATH}`,
33
+ session: {
34
+ appName: options.appName ?? `React Native (${Platform.OS})`,
35
+ platform: platformName(),
36
+ sdk: { name: '@capybara-devtools/react-native', version: SDK_VERSION },
37
+ device: deviceInfo(),
38
+ runtime: runtimeName(),
39
+ },
40
+ plugins,
41
+ });
42
+ }
43
+ /**
44
+ * Bare React Native ships whatwg-fetch, which runs on XHR, so patching fetch as well would record
45
+ * every request twice. Expo (SDK 52+) swaps in a native fetch that never touches XHR.
46
+ */
47
+ function fetchRunsOnXhr() {
48
+ return globalThis.fetch?.polyfill === true;
49
+ }
50
+ function bundleUrl() {
51
+ try {
52
+ const sourceCode = NativeModules.SourceCode;
53
+ const scriptURL = sourceCode?.getConstants?.().scriptURL ?? sourceCode?.scriptURL;
54
+ return typeof scriptURL === 'string' ? scriptURL : undefined;
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function defaultHost() {
61
+ const url = bundleUrl();
62
+ const host = url ? hostFromUrl(url) : undefined;
63
+ if (host)
64
+ return host;
65
+ // Bundles loaded from disk have no host; these addresses reach the dev machine from simulators and emulators.
66
+ return Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
67
+ }
68
+ function platformName() {
69
+ const os = Platform.OS;
70
+ return os === 'ios' || os === 'android' || os === 'windows' || os === 'macos' || os === 'web' ? os : 'unknown';
71
+ }
72
+ function platformConstants() {
73
+ return (Platform.constants ?? {});
74
+ }
75
+ function deviceInfo() {
76
+ const c = platformConstants();
77
+ if (Platform.OS === 'android') {
78
+ const model = [c.Manufacturer, c.Model].filter(Boolean).join(' ');
79
+ return {
80
+ model: model || undefined,
81
+ osVersion: `Android ${String(c.Release ?? Platform.Version)}`,
82
+ isEmulator: /generic|emulator|sdk_gphone/i.test(String(c.Fingerprint ?? '')),
83
+ };
84
+ }
85
+ if (Platform.OS === 'ios') {
86
+ return {
87
+ model: c.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone',
88
+ osVersion: `${String(c.systemName ?? 'iOS')} ${String(c.osVersion ?? Platform.Version)}`,
89
+ };
90
+ }
91
+ return { osVersion: `${Platform.OS} ${String(Platform.Version)}` };
92
+ }
93
+ function runtimeName() {
94
+ const engine = globalThis.HermesInternal ? 'Hermes' : 'JSC';
95
+ const version = platformConstants().reactNativeVersion;
96
+ return version ? `${engine} · RN ${version.major}.${version.minor}.${version.patch}` : engine;
97
+ }
98
+ export { getActiveClient } from '@capybara-devtools/core';
99
+ export { asyncStorage, mmkvStorage } from './storage.js';
100
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { StorageAdapter } from '@capybara-devtools/core';
2
+ /** Matches the default export of @react-native-async-storage/async-storage. */
3
+ export interface AsyncStorageLike {
4
+ getAllKeys(): Promise<readonly string[]>;
5
+ getItem(key: string): Promise<string | null>;
6
+ setItem(key: string, value: string): Promise<void>;
7
+ removeItem(key: string): Promise<void>;
8
+ multiGet?(keys: readonly string[]): Promise<ReadonlyArray<readonly [string, string | null]>>;
9
+ clear?(): Promise<void>;
10
+ }
11
+ /** Matches an instance from react-native-mmkv (v2 to v4). */
12
+ export interface MMKVLike {
13
+ getAllKeys(): string[];
14
+ getString(key: string): string | undefined;
15
+ getNumber?(key: string): number | undefined;
16
+ getBoolean?(key: string): boolean | undefined;
17
+ set(key: string, value: string | number | boolean): void;
18
+ /** v2/v3 */
19
+ delete?(key: string): void;
20
+ /** v4 */
21
+ remove?(key: string): unknown;
22
+ clearAll?(): void;
23
+ addOnValueChangedListener?(listener: (key: string) => void): {
24
+ remove(): void;
25
+ };
26
+ }
27
+ export interface AdapterOptions {
28
+ id?: string;
29
+ label?: string;
30
+ }
31
+ export declare function asyncStorage(storage: AsyncStorageLike, options?: AdapterOptions): StorageAdapter;
32
+ export declare function mmkvStorage(mmkv: MMKVLike, options?: AdapterOptions): StorageAdapter;
33
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,UAAU,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACzC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,QAAQ,CAAC,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7F,KAAK,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACzB;AAED,6DAA6D;AAC7D,MAAM,WAAW,QAAQ;IACvB,UAAU,IAAI,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC3C,SAAS,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5C,UAAU,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;IAC9C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACzD,YAAY;IACZ,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS;IACT,MAAM,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,QAAQ,CAAC,IAAI,IAAI,CAAC;IAClB,yBAAyB,CAAC,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,GAAG;QAAE,MAAM,IAAI,IAAI,CAAA;KAAE,CAAC;CACjF;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,GAAE,cAAmB,GAAG,cAAc,CAwBpG;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,GAAG,cAAc,CAoBxF"}
@@ -0,0 +1,116 @@
1
+ export function asyncStorage(storage, options = {}) {
2
+ const multiGet = storage.multiGet?.bind(storage);
3
+ const clear = storage.clear?.bind(storage);
4
+ return {
5
+ id: options.id ?? 'async-storage',
6
+ label: options.label ?? 'AsyncStorage',
7
+ keys: () => storage.getAllKeys(),
8
+ get: (key) => storage.getItem(key),
9
+ getMany: multiGet ? (keys) => multiGet(keys) : undefined,
10
+ set: (key, value) => storage.setItem(key, value),
11
+ remove: (key) => storage.removeItem(key),
12
+ clear: clear ? () => clear() : undefined,
13
+ // AsyncStorage has no change events, so wrap the module's write methods.
14
+ watch: (onChange) => wrapMethods(storage, {
15
+ setItem: (key) => onChange('set', key),
16
+ removeItem: (key) => onChange('remove', key),
17
+ mergeItem: (key) => onChange('merge', key),
18
+ clear: () => onChange('clear'),
19
+ multiSet: () => onChange('set'),
20
+ multiRemove: () => onChange('remove'),
21
+ multiMerge: () => onChange('merge'),
22
+ }),
23
+ };
24
+ }
25
+ export function mmkvStorage(mmkv, options = {}) {
26
+ return {
27
+ id: options.id ?? 'mmkv',
28
+ label: options.label ?? 'MMKV',
29
+ note: 'MMKV keeps no value types, so they are inferred: false and an empty string are stored alike and show as false, and binary values may not display. Edits are saved as strings.',
30
+ keys: () => mmkv.getAllKeys(),
31
+ get: (key) => readMmkv(mmkv, key),
32
+ set: (key, value) => mmkv.set(key, value),
33
+ remove: (key) => {
34
+ if (mmkv.remove)
35
+ mmkv.remove(key);
36
+ else
37
+ mmkv.delete?.(key);
38
+ },
39
+ clear: mmkv.clearAll ? () => mmkv.clearAll() : undefined,
40
+ watch: mmkv.addOnValueChangedListener
41
+ ? (onChange) => {
42
+ const listener = mmkv.addOnValueChangedListener((key) => onChange('unknown', key));
43
+ return () => listener.remove();
44
+ }
45
+ : undefined,
46
+ };
47
+ }
48
+ function readMmkv(mmkv, key) {
49
+ const text = mmkv.getString(key);
50
+ // On the web, MMKV keeps the text it was given; trying getNumber would turn '007' into 7.
51
+ if (isWeb())
52
+ return text ?? null;
53
+ // Native MMKV keeps no types and every getter decodes the same bytes, so 42 reads as '' through
54
+ // getString. A string takes a length byte plus its UTF-8 bytes, while getNumber needs at least 8
55
+ // bytes, so a decoded string shorter than 7 bytes is real only if getNumber fails.
56
+ if (text !== undefined && utf8Size(text) >= 7)
57
+ return text;
58
+ const number = mmkv.getNumber?.(key);
59
+ if (number !== undefined && !Number.isNaN(number))
60
+ return String(number);
61
+ if (text)
62
+ return text;
63
+ // `false` and '' are the same single zero byte.
64
+ const bool = mmkv.getBoolean?.(key);
65
+ return bool === undefined ? (text ?? null) : String(bool);
66
+ }
67
+ /** Some React Native libraries polyfill `document`, but React Native always reports its navigator.product. */
68
+ function isWeb() {
69
+ const scope = globalThis;
70
+ return scope.document !== undefined && scope.navigator?.product !== 'ReactNative';
71
+ }
72
+ /** TextEncoder may be missing or lazily installed in React Native runtimes. */
73
+ function utf8Size(value) {
74
+ let size = 0;
75
+ for (let i = 0; i < value.length; i++) {
76
+ const code = value.charCodeAt(i);
77
+ if (code < 0x80)
78
+ size++;
79
+ else if (code < 0x800)
80
+ size += 2;
81
+ else if (code >= 0xd800 && code <= 0xdbff && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
82
+ size += 4;
83
+ i++;
84
+ }
85
+ else
86
+ size += 3;
87
+ }
88
+ return size;
89
+ }
90
+ /** Runs `hooks[name](firstArg)` after the wrapped method's promise resolves. Returns an undo function. */
91
+ function wrapMethods(target, hooks) {
92
+ const originals = [];
93
+ for (const [name, hook] of Object.entries(hooks)) {
94
+ const original = target[name];
95
+ if (typeof original !== 'function')
96
+ continue;
97
+ const wrapped = function (...args) {
98
+ const result = original.apply(this, args);
99
+ const key = typeof args[0] === 'string' ? args[0] : undefined;
100
+ Promise.resolve(result).then(() => hook(key), () => { });
101
+ return result;
102
+ };
103
+ try {
104
+ target[name] = wrapped;
105
+ originals.push([name, original]);
106
+ }
107
+ catch {
108
+ // frozen module object: change events are unavailable, everything else still works
109
+ }
110
+ }
111
+ return () => {
112
+ for (const [name, original] of originals)
113
+ target[name] = original;
114
+ };
115
+ }
116
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@capybara-devtools/react-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "React Native SDK for Capybara: logs, network and storage for React Native and Expo apps",
5
+ "keywords": [
6
+ "capybara",
7
+ "react-native",
8
+ "expo",
9
+ "devtools",
10
+ "debugger",
11
+ "logs",
12
+ "network",
13
+ "asyncstorage",
14
+ "mmkv"
15
+ ],
16
+ "homepage": "https://github.com/lhphuc99/capybara#readme",
17
+ "bugs": "https://github.com/lhphuc99/capybara/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/lhphuc99/capybara.git",
21
+ "directory": "packages/react-native"
22
+ },
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src"
37
+ ],
38
+ "sideEffects": false,
39
+ "scripts": {
40
+ "build": "tsc -p tsconfig.json",
41
+ "dev": "tsc -p tsconfig.json --watch",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit"
44
+ },
45
+ "dependencies": {
46
+ "@capybara-devtools/core": "0.2.0",
47
+ "@capybara-devtools/protocol": "0.2.0"
48
+ },
49
+ "peerDependencies": {
50
+ "react-native": ">=0.76"
51
+ },
52
+ "devDependencies": {
53
+ "react-native": "0.86.3"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,34 @@
1
+ import type { Plugin } from '@capybara-devtools/core';
2
+
3
+ type GlobalHandler = (error: unknown, isFatal?: boolean) => void;
4
+
5
+ interface ErrorUtilsLike {
6
+ getGlobalHandler(): GlobalHandler | undefined;
7
+ setGlobalHandler(handler: GlobalHandler): void;
8
+ }
9
+
10
+ /** Records uncaught JS exceptions, then hands them to the previous handler (LogBox, crash reporters). */
11
+ export function errorUtilsPlugin(): Plugin {
12
+ return {
13
+ name: 'errors',
14
+ setup(ctx) {
15
+ const errorUtils = (globalThis as { ErrorUtils?: ErrorUtilsLike }).ErrorUtils;
16
+ if (!errorUtils?.getGlobalHandler || !errorUtils.setGlobalHandler) return;
17
+ const previous = errorUtils.getGlobalHandler();
18
+ const handler: GlobalHandler = (error, isFatal) => {
19
+ try {
20
+ const args = [ctx.serialize(error)];
21
+ if (isFatal) args.push(ctx.serialize('(fatal)'));
22
+ ctx.emit('log', { level: 'error', source: 'uncaught', args });
23
+ } catch {
24
+ // the original handler must still run
25
+ }
26
+ previous?.(error, isFatal);
27
+ };
28
+ errorUtils.setGlobalHandler(handler);
29
+ return () => {
30
+ if (errorUtils.getGlobalHandler() === handler && previous) errorUtils.setGlobalHandler(previous);
31
+ };
32
+ },
33
+ };
34
+ }
package/src/host.ts ADDED
@@ -0,0 +1,25 @@
1
+ /** Host of a URL (IPv6 keeps its brackets), or undefined for host-less URLs such as file:// bundles. */
2
+ export function hostFromUrl(url: string): string | undefined {
3
+ return /^[a-z][a-z\d+.-]*:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url)?.[1];
4
+ }
5
+
6
+ /** Explicit port of a URL, or undefined when it has none. */
7
+ export function portFromUrl(url: string): string | undefined {
8
+ return /^[a-z][a-z\d+.-]*:\/\/(?:[^@/?#]*@)?(?:\[[^\]]+\]|[^:/?#]+):(\d+)/i.exec(url)?.[1];
9
+ }
10
+
11
+ /**
12
+ * Sockets of dev tools: Metro's own (HMR, inspector) on the bundle's port, React DevTools (8097) and
13
+ * Reactotron (9090). React Native opens some of them with an http:// URL, so both schemes match.
14
+ */
15
+ export function devToolSockets(metroPort: string | undefined): RegExp[] {
16
+ const patterns = [/^(?:wss?|https?):\/\/[^/]+:(?:8097|9090)(?:\/|$)/];
17
+ if (metroPort) {
18
+ patterns.push(new RegExp(`^(?:wss?|https?)://[^/]+:${metroPort}/(?:hot|message|events|inspector|debugger-proxy)(?:[/?]|$)`));
19
+ }
20
+ return patterns;
21
+ }
22
+
23
+ export function formatHost(host: string): string {
24
+ return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,139 @@
1
+ import {
2
+ consolePlugin,
3
+ createClient,
4
+ getActiveClient,
5
+ networkPlugin,
6
+ storagePlugin,
7
+ type CapybaraClient,
8
+ type NetworkPluginOptions,
9
+ type Plugin,
10
+ type StorageAdapter,
11
+ } from '@capybara-devtools/core';
12
+ import { CLIENT_PATH, DEFAULT_PORT, type DeviceInfo, type Platform as CapybaraPlatform } from '@capybara-devtools/protocol';
13
+ import { NativeModules, Platform } from 'react-native';
14
+ import { errorUtilsPlugin } from './errors.js';
15
+ import { devToolSockets, formatHost, hostFromUrl, portFromUrl } from './host.js';
16
+
17
+ export const SDK_VERSION = '0.2.0';
18
+
19
+ /** Requests React Native sends to Metro on its own (LogBox symbolication, "open in editor"). */
20
+ const DEV_SERVER_NOISE = [/\/symbolicate(\?|$)/, /\/open-stack-frame(\?|$)/];
21
+
22
+ export interface InitOptions {
23
+ /** Shown in the session picker. */
24
+ appName?: string;
25
+ /** Machine running `capybara`. Defaults to the host serving the JS bundle (Metro). */
26
+ host?: string;
27
+ port?: number;
28
+ /** Pass false to skip setup, e.g. `enabled: __DEV__`. */
29
+ enabled?: boolean;
30
+ /** Capture console output and uncaught errors. Default true. */
31
+ logs?: boolean;
32
+ network?: boolean | NetworkPluginOptions;
33
+ /** e.g. `[asyncStorage(AsyncStorage), mmkvStorage(storage)]` */
34
+ storage?: StorageAdapter[];
35
+ plugins?: Plugin[];
36
+ }
37
+
38
+ export function init(options: InitOptions = {}): CapybaraClient | undefined {
39
+ if (options.enabled === false) return undefined;
40
+ // Fast Refresh can re-run the entry file; patching again would record everything twice.
41
+ const existing = getActiveClient();
42
+ if (existing) return existing;
43
+
44
+ const plugins: Plugin[] = [];
45
+ if (options.logs !== false) plugins.push(consolePlugin(), errorUtilsPlugin());
46
+ if (options.network !== false) {
47
+ const network = typeof options.network === 'object' ? options.network : {};
48
+ plugins.push(
49
+ networkPlugin({
50
+ fetch: !fetchRunsOnXhr(),
51
+ xhr: true,
52
+ ...network,
53
+ ignoreUrls: [...DEV_SERVER_NOISE, ...devToolSockets(portFromUrl(bundleUrl() ?? '')), ...(network.ignoreUrls ?? [])],
54
+ }),
55
+ );
56
+ }
57
+ // Registered even when empty so the UI can explain how to add adapters.
58
+ plugins.push(storagePlugin(options.storage ?? []));
59
+ plugins.push(...(options.plugins ?? []));
60
+
61
+ return createClient({
62
+ url: `ws://${formatHost(options.host ?? defaultHost())}:${options.port ?? DEFAULT_PORT}${CLIENT_PATH}`,
63
+ session: {
64
+ appName: options.appName ?? `React Native (${Platform.OS})`,
65
+ platform: platformName(),
66
+ sdk: { name: '@capybara-devtools/react-native', version: SDK_VERSION },
67
+ device: deviceInfo(),
68
+ runtime: runtimeName(),
69
+ },
70
+ plugins,
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Bare React Native ships whatwg-fetch, which runs on XHR, so patching fetch as well would record
76
+ * every request twice. Expo (SDK 52+) swaps in a native fetch that never touches XHR.
77
+ */
78
+ function fetchRunsOnXhr(): boolean {
79
+ return (globalThis as { fetch?: { polyfill?: boolean } }).fetch?.polyfill === true;
80
+ }
81
+
82
+ function bundleUrl(): string | undefined {
83
+ try {
84
+ const sourceCode = NativeModules.SourceCode as
85
+ | { scriptURL?: string; getConstants?: () => { scriptURL?: string } }
86
+ | undefined;
87
+ const scriptURL = sourceCode?.getConstants?.().scriptURL ?? sourceCode?.scriptURL;
88
+ return typeof scriptURL === 'string' ? scriptURL : undefined;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
94
+ function defaultHost(): string {
95
+ const url = bundleUrl();
96
+ const host = url ? hostFromUrl(url) : undefined;
97
+ if (host) return host;
98
+ // Bundles loaded from disk have no host; these addresses reach the dev machine from simulators and emulators.
99
+ return Platform.OS === 'android' ? '10.0.2.2' : 'localhost';
100
+ }
101
+
102
+ function platformName(): CapybaraPlatform {
103
+ const os: string = Platform.OS;
104
+ return os === 'ios' || os === 'android' || os === 'windows' || os === 'macos' || os === 'web' ? os : 'unknown';
105
+ }
106
+
107
+ function platformConstants(): Record<string, unknown> {
108
+ return ((Platform as unknown as { constants?: Record<string, unknown> }).constants ?? {}) as Record<string, unknown>;
109
+ }
110
+
111
+ function deviceInfo(): DeviceInfo {
112
+ const c = platformConstants();
113
+ if (Platform.OS === 'android') {
114
+ const model = [c.Manufacturer, c.Model].filter(Boolean).join(' ');
115
+ return {
116
+ model: model || undefined,
117
+ osVersion: `Android ${String(c.Release ?? Platform.Version)}`,
118
+ isEmulator: /generic|emulator|sdk_gphone/i.test(String(c.Fingerprint ?? '')),
119
+ };
120
+ }
121
+ if (Platform.OS === 'ios') {
122
+ return {
123
+ model: c.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone',
124
+ osVersion: `${String(c.systemName ?? 'iOS')} ${String(c.osVersion ?? Platform.Version)}`,
125
+ };
126
+ }
127
+ return { osVersion: `${Platform.OS} ${String(Platform.Version)}` };
128
+ }
129
+
130
+ function runtimeName(): string {
131
+ const engine = (globalThis as { HermesInternal?: unknown }).HermesInternal ? 'Hermes' : 'JSC';
132
+ const version = platformConstants().reactNativeVersion as { major: number; minor: number; patch: number } | undefined;
133
+ return version ? `${engine} · RN ${version.major}.${version.minor}.${version.patch}` : engine;
134
+ }
135
+
136
+ export { getActiveClient } from '@capybara-devtools/core';
137
+ export type { CapybaraClient, NetworkPluginOptions, Plugin, StorageAdapter } from '@capybara-devtools/core';
138
+ export { asyncStorage, mmkvStorage } from './storage.js';
139
+ export type { AdapterOptions, AsyncStorageLike, MMKVLike } from './storage.js';
package/src/storage.ts ADDED
@@ -0,0 +1,143 @@
1
+ import type { StorageAdapter } from '@capybara-devtools/core';
2
+
3
+ /** Matches the default export of @react-native-async-storage/async-storage. */
4
+ export interface AsyncStorageLike {
5
+ getAllKeys(): Promise<readonly string[]>;
6
+ getItem(key: string): Promise<string | null>;
7
+ setItem(key: string, value: string): Promise<void>;
8
+ removeItem(key: string): Promise<void>;
9
+ multiGet?(keys: readonly string[]): Promise<ReadonlyArray<readonly [string, string | null]>>;
10
+ clear?(): Promise<void>;
11
+ }
12
+
13
+ /** Matches an instance from react-native-mmkv (v2 to v4). */
14
+ export interface MMKVLike {
15
+ getAllKeys(): string[];
16
+ getString(key: string): string | undefined;
17
+ getNumber?(key: string): number | undefined;
18
+ getBoolean?(key: string): boolean | undefined;
19
+ set(key: string, value: string | number | boolean): void;
20
+ /** v2/v3 */
21
+ delete?(key: string): void;
22
+ /** v4 */
23
+ remove?(key: string): unknown;
24
+ clearAll?(): void;
25
+ addOnValueChangedListener?(listener: (key: string) => void): { remove(): void };
26
+ }
27
+
28
+ export interface AdapterOptions {
29
+ id?: string;
30
+ label?: string;
31
+ }
32
+
33
+ export function asyncStorage(storage: AsyncStorageLike, options: AdapterOptions = {}): StorageAdapter {
34
+ const multiGet = storage.multiGet?.bind(storage);
35
+ const clear = storage.clear?.bind(storage);
36
+ return {
37
+ id: options.id ?? 'async-storage',
38
+ label: options.label ?? 'AsyncStorage',
39
+ keys: () => storage.getAllKeys(),
40
+ get: (key) => storage.getItem(key),
41
+ getMany: multiGet ? (keys) => multiGet(keys) : undefined,
42
+ set: (key, value) => storage.setItem(key, value),
43
+ remove: (key) => storage.removeItem(key),
44
+ clear: clear ? () => clear() : undefined,
45
+ // AsyncStorage has no change events, so wrap the module's write methods.
46
+ watch: (onChange) =>
47
+ wrapMethods(storage as unknown as Record<string, unknown>, {
48
+ setItem: (key) => onChange('set', key),
49
+ removeItem: (key) => onChange('remove', key),
50
+ mergeItem: (key) => onChange('merge', key),
51
+ clear: () => onChange('clear'),
52
+ multiSet: () => onChange('set'),
53
+ multiRemove: () => onChange('remove'),
54
+ multiMerge: () => onChange('merge'),
55
+ }),
56
+ };
57
+ }
58
+
59
+ export function mmkvStorage(mmkv: MMKVLike, options: AdapterOptions = {}): StorageAdapter {
60
+ return {
61
+ id: options.id ?? 'mmkv',
62
+ label: options.label ?? 'MMKV',
63
+ note: 'MMKV keeps no value types, so they are inferred: false and an empty string are stored alike and show as false, and binary values may not display. Edits are saved as strings.',
64
+ keys: () => mmkv.getAllKeys(),
65
+ get: (key) => readMmkv(mmkv, key),
66
+ set: (key, value) => mmkv.set(key, value),
67
+ remove: (key) => {
68
+ if (mmkv.remove) mmkv.remove(key);
69
+ else mmkv.delete?.(key);
70
+ },
71
+ clear: mmkv.clearAll ? () => mmkv.clearAll!() : undefined,
72
+ watch: mmkv.addOnValueChangedListener
73
+ ? (onChange) => {
74
+ const listener = mmkv.addOnValueChangedListener!((key) => onChange('unknown', key));
75
+ return () => listener.remove();
76
+ }
77
+ : undefined,
78
+ };
79
+ }
80
+
81
+ function readMmkv(mmkv: MMKVLike, key: string): string | null {
82
+ const text = mmkv.getString(key);
83
+ // On the web, MMKV keeps the text it was given; trying getNumber would turn '007' into 7.
84
+ if (isWeb()) return text ?? null;
85
+ // Native MMKV keeps no types and every getter decodes the same bytes, so 42 reads as '' through
86
+ // getString. A string takes a length byte plus its UTF-8 bytes, while getNumber needs at least 8
87
+ // bytes, so a decoded string shorter than 7 bytes is real only if getNumber fails.
88
+ if (text !== undefined && utf8Size(text) >= 7) return text;
89
+ const number = mmkv.getNumber?.(key);
90
+ if (number !== undefined && !Number.isNaN(number)) return String(number);
91
+ if (text) return text;
92
+ // `false` and '' are the same single zero byte.
93
+ const bool = mmkv.getBoolean?.(key);
94
+ return bool === undefined ? (text ?? null) : String(bool);
95
+ }
96
+
97
+ /** Some React Native libraries polyfill `document`, but React Native always reports its navigator.product. */
98
+ function isWeb(): boolean {
99
+ const scope = globalThis as { document?: unknown; navigator?: { product?: string } };
100
+ return scope.document !== undefined && scope.navigator?.product !== 'ReactNative';
101
+ }
102
+
103
+ /** TextEncoder may be missing or lazily installed in React Native runtimes. */
104
+ function utf8Size(value: string): number {
105
+ let size = 0;
106
+ for (let i = 0; i < value.length; i++) {
107
+ const code = value.charCodeAt(i);
108
+ if (code < 0x80) size++;
109
+ else if (code < 0x800) size += 2;
110
+ else if (code >= 0xd800 && code <= 0xdbff && value.charCodeAt(i + 1) >= 0xdc00 && value.charCodeAt(i + 1) <= 0xdfff) {
111
+ size += 4;
112
+ i++;
113
+ } else size += 3;
114
+ }
115
+ return size;
116
+ }
117
+
118
+ /** Runs `hooks[name](firstArg)` after the wrapped method's promise resolves. Returns an undo function. */
119
+ function wrapMethods(target: Record<string, unknown>, hooks: Record<string, (key?: string) => void>): () => void {
120
+ const originals: Array<[string, unknown]> = [];
121
+ for (const [name, hook] of Object.entries(hooks)) {
122
+ const original = target[name];
123
+ if (typeof original !== 'function') continue;
124
+ const wrapped = function (this: unknown, ...args: unknown[]) {
125
+ const result = (original as (...a: unknown[]) => unknown).apply(this, args);
126
+ const key = typeof args[0] === 'string' ? args[0] : undefined;
127
+ Promise.resolve(result).then(
128
+ () => hook(key),
129
+ () => {},
130
+ );
131
+ return result;
132
+ };
133
+ try {
134
+ target[name] = wrapped;
135
+ originals.push([name, original]);
136
+ } catch {
137
+ // frozen module object: change events are unavailable, everything else still works
138
+ }
139
+ }
140
+ return () => {
141
+ for (const [name, original] of originals) target[name] = original;
142
+ };
143
+ }