@rn-dev-inspector/rn-compat 0.0.0-stage → 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Vahe Saroyan
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,40 @@
1
- # Temporary Holding Version
1
+ # @rn-dev-inspector/rn-compat
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 version compatibility layer for [React Native Dev Inspector](https://github.com/VaheSaroyan/react-native-dev-inspector).
4
+
5
+ A Metro resolver shim that keeps the inspector hierarchy working across React Native versions — the internal `getInspectorDataForViewAtPoint` module moved to a new private path on RN 0.82+, and its fiber contract differs between Paper (≤ 0.72) and Fabric (0.73+).
6
+
7
+ ## Supported RN versions
8
+
9
+ | RN version | Architecture | Status | Notes |
10
+ | --- | --- | --- | --- |
11
+ | ≤ 0.72 | Paper | ✅ Supported | Rebuilds the hierarchy by looking the tapped host fiber up via `touchedViewTag`; wrapper components that don't forward props (e.g. `Button`) still resolve to the business JSX line |
12
+ | 0.73 – 0.81 | Fabric | ✅ Supported | Restores hierarchy fibers and finds source in the subtree |
13
+ | 0.82+ | Fabric | ✅ Supported | Intercepts the new private devsupport path; shim rebuilds the hierarchy (verified on 0.86) |
14
+
15
+ > **Compatibility caveat:** the layer intercepts react-native's *internal* module paths (`react-native/src/private/...`, `react-native/Libraries/Inspector/...`). These are not public API and can move in any release. When upgrading RN past the versions listed above, verify the inspector still works; if a path moved, the compat layer will log a warning and degrade to the stock inspector instead of breaking the bundler.
16
+
17
+ ## Usage
18
+
19
+ ```js
20
+ // metro.config.js
21
+ const { getDefaultConfig } = require('@react-native/metro-config');
22
+ const { withInspector } = require('react-native-dev-inspector/metro');
23
+ const { withInspectorCompat } = require('@rn-dev-inspector/rn-compat');
24
+
25
+ module.exports = withInspector(withInspectorCompat(getDefaultConfig(__dirname)));
26
+ ```
27
+
28
+ `withInspectorCompat` never mutates the config you pass in (it returns a new object) and never throws at config load time: if the react-native implementation cannot be located it logs a warning and returns the config without the compat resolver, so the bundler keeps running.
29
+
30
+ Options:
31
+
32
+ - `root` — app root directory used to locate react-native; defaults to `config.projectRoot` or `process.cwd()`. In a monorepo the lookup walks up parent directories, so a hoisted react-native in the workspace root's `node_modules` is found automatically.
33
+
34
+ ## How it works
35
+
36
+ 1. **Interception** — the resolver redirects react-native's inspector module (both the legacy `Libraries/Inspector` path and the 0.82+ `src/private` path) to a bundled shim.
37
+ 2. **Shim** — the shim requires the *real* implementation through the marker name `__rn_dev_inspector_original_impl__`, which the resolver maps back to the actual file in the project's react-native install (resolved to an absolute path at config time, since the shim is not on the project's node_modules chain).
38
+ 3. **Polyfills** — `@rn-dev-inspector/rn-compat/runtime` (re-exported as `react-native-dev-inspector/rn-compat`) contains runtime fixes for Paper/Fabric differences.
39
+ 4. **Watch folders** — the shim package (and the react-native package, when it lives outside the project, e.g. in a monorepo workspace root) is appended to `config.watchFolders` so it participates in bundling.
40
+ 5. **Optional peers** — `expo-dev-menu` / `expo-dev-client` are resolved to an empty module when not installed, so Metro doesn't generate a stub that throws "Requiring unknown module" at runtime.
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @rn-dev-inspector/rn-compat
3
+ *
4
+ * React Native version compatibility layer. One-line integration in
5
+ * metro.config.js:
6
+ *
7
+ * const { withInspector } = require('react-native-dev-inspector/metro');
8
+ * const { withInspectorCompat } = require('@rn-dev-inspector/rn-compat');
9
+ * module.exports = withInspector(withInspectorCompat(getDefaultConfig(__dirname)));
10
+ *
11
+ * Adaptation matrix:
12
+ * RN <= 0.72 (Paper) : fix Metro optional-dependency mis-resolution +
13
+ * rebuild hierarchy by looking the host fiber up
14
+ * via touchedViewTag (wrapper components that do
15
+ * not forward props, like Button, also resolve to
16
+ * the business JSX line)
17
+ * RN 0.73-0.81 (Fabric) : fix optional-dependency mis-resolution + restore
18
+ * hierarchy fibers + find source in subtree
19
+ * RN 0.82+ : intercept the new private path -> shim rebuilds
20
+ * the hierarchy (verified on 0.86)
21
+ *
22
+ * The shim refers back to the current RN's real getInspectorDataForViewAtPoint
23
+ * implementation through the marker module name
24
+ * `__rn_dev_inspector_original_impl__`; the marker name is resolved to an
25
+ * absolute path at config time, so the shim is decoupled from the project's
26
+ * location (the shim file is not on the project's node_modules chain — a bare
27
+ * module name would fail to resolve).
28
+ */
29
+ interface WithInspectorCompatOptions {
30
+ /** App root directory; defaults to config.projectRoot or process.cwd() */
31
+ root?: string;
32
+ }
33
+ type MetroResolver = (context: any, moduleName: string, platform: string | null) => {
34
+ type: string;
35
+ filePath: string;
36
+ } | any;
37
+ interface MetroConfigLike {
38
+ projectRoot?: string;
39
+ watchFolders?: string[];
40
+ resolver?: {
41
+ resolveRequest?: MetroResolver;
42
+ [key: string]: any;
43
+ };
44
+ [key: string]: any;
45
+ }
46
+ /**
47
+ * Wrap the metro config:
48
+ * - intercept the 0.82 private path -> bundled shim (the shim requires the
49
+ * real implementation internally)
50
+ * - intercept the marker module name -> real implementation file
51
+ * - all other requests continue with the original resolveRequest (if any)
52
+ *
53
+ * Never throws and never mutates the input config: the returned object is a
54
+ * shallow copy, so it composes safely
55
+ * (`withInspector(withInspectorCompat(config))`). When the react-native
56
+ * implementation cannot be located it warns and returns the config with the
57
+ * compat resolver omitted — a broken integration must not take down the
58
+ * entire bundler.
59
+ */
60
+ export declare function withInspectorCompat<T extends MetroConfigLike = MetroConfigLike>(config: T, options?: WithInspectorCompatOptions): T;
61
+ export default withInspectorCompat;
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAuBH,UAAU,0BAA0B;IAClC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,KAAK,aAAa,GAAG,CACnB,OAAO,EAAE,GAAG,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,KACpB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAAC;AAE9C,UAAU,eAAe;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE;QACT,cAAc,CAAC,EAAE,aAAa,CAAC;QAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;IACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AA+BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,GAAG,eAAe,EAC7E,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,0BAA+B,GACvC,CAAC,CAkFH;AAED,eAAe,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /**
3
+ * @rn-dev-inspector/rn-compat
4
+ *
5
+ * React Native version compatibility layer. One-line integration in
6
+ * metro.config.js:
7
+ *
8
+ * const { withInspector } = require('react-native-dev-inspector/metro');
9
+ * const { withInspectorCompat } = require('@rn-dev-inspector/rn-compat');
10
+ * module.exports = withInspector(withInspectorCompat(getDefaultConfig(__dirname)));
11
+ *
12
+ * Adaptation matrix:
13
+ * RN <= 0.72 (Paper) : fix Metro optional-dependency mis-resolution +
14
+ * rebuild hierarchy by looking the host fiber up
15
+ * via touchedViewTag (wrapper components that do
16
+ * not forward props, like Button, also resolve to
17
+ * the business JSX line)
18
+ * RN 0.73-0.81 (Fabric) : fix optional-dependency mis-resolution + restore
19
+ * hierarchy fibers + find source in subtree
20
+ * RN 0.82+ : intercept the new private path -> shim rebuilds
21
+ * the hierarchy (verified on 0.86)
22
+ *
23
+ * The shim refers back to the current RN's real getInspectorDataForViewAtPoint
24
+ * implementation through the marker module name
25
+ * `__rn_dev_inspector_original_impl__`; the marker name is resolved to an
26
+ * absolute path at config time, so the shim is decoupled from the project's
27
+ * location (the shim file is not on the project's node_modules chain — a bare
28
+ * module name would fail to resolve).
29
+ */
30
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
31
+ if (k2 === undefined) k2 = k;
32
+ var desc = Object.getOwnPropertyDescriptor(m, k);
33
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
34
+ desc = { enumerable: true, get: function() { return m[k]; } };
35
+ }
36
+ Object.defineProperty(o, k2, desc);
37
+ }) : (function(o, m, k, k2) {
38
+ if (k2 === undefined) k2 = k;
39
+ o[k2] = m[k];
40
+ }));
41
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
42
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
43
+ }) : function(o, v) {
44
+ o["default"] = v;
45
+ });
46
+ var __importStar = (this && this.__importStar) || (function () {
47
+ var ownKeys = function(o) {
48
+ ownKeys = Object.getOwnPropertyNames || function (o) {
49
+ var ar = [];
50
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
51
+ return ar;
52
+ };
53
+ return ownKeys(o);
54
+ };
55
+ return function (mod) {
56
+ if (mod && mod.__esModule) return mod;
57
+ var result = {};
58
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
59
+ __setModuleDefault(result, mod);
60
+ return result;
61
+ };
62
+ })();
63
+ Object.defineProperty(exports, "__esModule", { value: true });
64
+ exports.withInspectorCompat = withInspectorCompat;
65
+ const fs = __importStar(require("fs"));
66
+ const path = __importStar(require("path"));
67
+ // New private path on RN 0.82+, which Inspector.js requires first
68
+ const PRIVATE_082 = 'react-native/src/private/devsupport/devmenu/elementinspector/getInspectorDataForViewAtPoint';
69
+ // Public path on RN <= 0.80, required by core on Paper projects (e.g. 0.72).
70
+ // Intercepted to the shim so Paper can rebuild the hierarchy too (the shim
71
+ // comes back to this file via MARKER).
72
+ const PUBLIC_LE080 = 'react-native/Libraries/Inspector/getInspectorDataForViewAtPoint';
73
+ // The shim requires the real implementation through this marker name; the
74
+ // adapter resolves it to an absolute path at config time
75
+ const MARKER = '__rn_dev_inspector_original_impl__';
76
+ // Optional peers: resolve to an empty module when not installed so core's
77
+ // try/catch degrades gracefully (otherwise Metro generates a stub that throws
78
+ // at runtime and dev reports "Requiring unknown module")
79
+ const OPTIONAL_PEERS = ['expo-dev-menu', 'expo-dev-client'];
80
+ /**
81
+ * Probe the project's react-native for the real getInspectorDataForViewAtPoint
82
+ * implementation, by priority:
83
+ * 0.82+ : src/private/devsupport/devmenu/elementinspector/...
84
+ * <=0.80: Libraries/Inspector/...
85
+ *
86
+ * Walks up from `root` (node resolution style) so a monorepo app whose
87
+ * react-native is hoisted into a workspace root's node_modules is found too;
88
+ * in that case the impl package lives outside the project root and must be
89
+ * added to watchFolders separately (see withInspectorCompat).
90
+ */
91
+ function findRealImpl(root) {
92
+ // Derived from the intercepted module specifiers above so the two stay in sync
93
+ const candidates = [PRIVATE_082, PUBLIC_LE080].map(moduleName => `node_modules/${moduleName}.js`);
94
+ let dir = path.resolve(root);
95
+ for (;;) {
96
+ for (const rel of candidates) {
97
+ const p = path.join(dir, rel);
98
+ if (fs.existsSync(p))
99
+ return p;
100
+ }
101
+ const parent = path.dirname(dir);
102
+ // Reached the filesystem root without a hit
103
+ if (parent === dir)
104
+ return null;
105
+ dir = parent;
106
+ }
107
+ }
108
+ /**
109
+ * Wrap the metro config:
110
+ * - intercept the 0.82 private path -> bundled shim (the shim requires the
111
+ * real implementation internally)
112
+ * - intercept the marker module name -> real implementation file
113
+ * - all other requests continue with the original resolveRequest (if any)
114
+ *
115
+ * Never throws and never mutates the input config: the returned object is a
116
+ * shallow copy, so it composes safely
117
+ * (`withInspector(withInspectorCompat(config))`). When the react-native
118
+ * implementation cannot be located it warns and returns the config with the
119
+ * compat resolver omitted — a broken integration must not take down the
120
+ * entire bundler.
121
+ */
122
+ function withInspectorCompat(config, options = {}) {
123
+ // Prefer Metro's projectRoot (in a monorepo the repo root is not the app
124
+ // root; the real implementation must be found in the app's own node_modules)
125
+ const root = options.root || config.projectRoot || process.cwd();
126
+ const realImpl = findRealImpl(root);
127
+ if (!realImpl) {
128
+ // Degrade instead of throwing: without the real implementation the shim
129
+ // cannot resolve back to RN's own code, so compat interception is skipped
130
+ // entirely and the bundler keeps working with the stock inspector.
131
+ console.warn('[rn-dev-inspector] @rn-dev-inspector/rn-compat: could not find the ' +
132
+ 'react-native getInspectorDataForViewAtPoint implementation under ' +
133
+ root +
134
+ ' — skipping the compat resolver. Pass { root } if your project ' +
135
+ 'root is not where react-native is installed.');
136
+ return { ...config };
137
+ }
138
+ // After compilation this file lives in dist/; the shim is in the package
139
+ // root's shims/ directory
140
+ const shimPath = path.resolve(__dirname, '..', 'shims', 'inspectorDataShim.js');
141
+ const emptyModulePath = path.resolve(__dirname, '..', 'shims', 'emptyModule.js');
142
+ // Approximate Metro's resolution with Node's rules (config time, node side):
143
+ // an expo package counts as "installed" only if resolvable from the project
144
+ // (or the monorepo root)
145
+ const isInstalled = (name) => {
146
+ try {
147
+ require.resolve(`${name}/package.json`, { paths: [root] });
148
+ return true;
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ };
154
+ const prev = (config.resolver && config.resolver.resolveRequest) || null;
155
+ // Both the shim and the real implementation file must be within Metro's
156
+ // watch scope to participate in bundling (in a monorepo the real
157
+ // implementation may sit in the workspace root's node_modules).
158
+ // Not needed when the react-native package is inside the project (Metro
159
+ // 0.76's TreeFS throws "Could not add directory" because watchFolders
160
+ // duplicates a directory already under the project root), and adding it is
161
+ // actively harmful; only add it when the package lives outside the project
162
+ // (monorepo).
163
+ const pkgRoot = path.resolve(__dirname, '..');
164
+ const implPkgRoot = realImpl.match(/^.*node_modules[\\/]react-native/);
165
+ const implOutsideProject = implPkgRoot &&
166
+ (() => {
167
+ const rel = path.relative(root, implPkgRoot[0]);
168
+ return !!rel && (rel.startsWith('..') || path.isAbsolute(rel));
169
+ })();
170
+ const extraWatch = [
171
+ pkgRoot,
172
+ ...(implOutsideProject ? [implPkgRoot[0]] : []),
173
+ ];
174
+ // Build a new config object instead of mutating the input, so wrapping is
175
+ // idempotent-safe and composes with other with* helpers
176
+ const nextConfig = {
177
+ ...config,
178
+ watchFolders: [
179
+ ...new Set([...(config.watchFolders || []), ...extraWatch]),
180
+ ],
181
+ resolver: {
182
+ ...(config.resolver || {}),
183
+ resolveRequest(context, moduleName, platform) {
184
+ if (moduleName === PRIVATE_082 || moduleName === PUBLIC_LE080) {
185
+ return { type: 'sourceFile', filePath: shimPath };
186
+ }
187
+ if (moduleName === MARKER) {
188
+ return { type: 'sourceFile', filePath: realImpl };
189
+ }
190
+ if (OPTIONAL_PEERS.includes(moduleName) && !isInstalled(moduleName)) {
191
+ return { type: 'sourceFile', filePath: emptyModulePath };
192
+ }
193
+ if (prev)
194
+ return prev(context, moduleName, platform);
195
+ return context.resolveRequest(context, moduleName, platform);
196
+ },
197
+ },
198
+ };
199
+ return nextConfig;
200
+ }
201
+ exports.default = withInspectorCompat;
202
+ //# 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,32 @@
1
+ /**
2
+ * @rn-dev-inspector/rn-compat — runtime patch entry
3
+ *
4
+ * Decoupled from withInspectorCompat (the build-time resolver adaptation in
5
+ * metro.config.js). This file collects side-effect polyfills for "runtime API
6
+ * gaps on older RN".
7
+ * Usage (already imported by default by the umbrella package
8
+ * react-native-dev-inspector, no manual import needed):
9
+ *
10
+ * import '@rn-dev-inspector/rn-compat/dist/runtime';
11
+ *
12
+ * Constraint: the build output of this file gets bundled into any RN project,
13
+ * so it must stay ES5-compatible (plain for loops, no destructuring/spread/
14
+ * async) to avoid tsc downleveling to @babel/runtime helpers — rn-compat is a
15
+ * zero-runtime-dependency config package that may not resolve in every
16
+ * project. Each patch must feature-detect and only fill in what is missing;
17
+ * it is a no-op on Node/newer RN.
18
+ */
19
+ /**
20
+ * React Native defines this in every bundle; it is absent under plain Node
21
+ * (tests), where we treat the environment as development.
22
+ */
23
+ declare const __DEV__: boolean | undefined;
24
+ /**
25
+ * URLSearchParams polyfill for RN <= 0.72 (react-native/Libraries/Blob/URL.js):
26
+ * only get/getAll/has/append exist; set() throws outright with
27
+ * "URLSearchParams.set is not implemented". The inspector's Open-in-editor
28
+ * needs set() to build the query string, so the missing write methods are
29
+ * filled in here with WHATWG semantics.
30
+ */
31
+ declare function patchURLSearchParams(): void;
32
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;GAGG;AACH,OAAO,CAAC,MAAM,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;AAE3C;;;;;;GAMG;AACH,iBAAS,oBAAoB,IAAI,IAAI,CAkEpC"}
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ /**
3
+ * @rn-dev-inspector/rn-compat — runtime patch entry
4
+ *
5
+ * Decoupled from withInspectorCompat (the build-time resolver adaptation in
6
+ * metro.config.js). This file collects side-effect polyfills for "runtime API
7
+ * gaps on older RN".
8
+ * Usage (already imported by default by the umbrella package
9
+ * react-native-dev-inspector, no manual import needed):
10
+ *
11
+ * import '@rn-dev-inspector/rn-compat/dist/runtime';
12
+ *
13
+ * Constraint: the build output of this file gets bundled into any RN project,
14
+ * so it must stay ES5-compatible (plain for loops, no destructuring/spread/
15
+ * async) to avoid tsc downleveling to @babel/runtime helpers — rn-compat is a
16
+ * zero-runtime-dependency config package that may not resolve in every
17
+ * project. Each patch must feature-detect and only fill in what is missing;
18
+ * it is a no-op on Node/newer RN.
19
+ */
20
+ /**
21
+ * URLSearchParams polyfill for RN <= 0.72 (react-native/Libraries/Blob/URL.js):
22
+ * only get/getAll/has/append exist; set() throws outright with
23
+ * "URLSearchParams.set is not implemented". The inspector's Open-in-editor
24
+ * needs set() to build the query string, so the missing write methods are
25
+ * filled in here with WHATWG semantics.
26
+ */
27
+ function patchURLSearchParams() {
28
+ const ParamsAny = globalThis.URLSearchParams;
29
+ if (typeof ParamsAny !== 'function')
30
+ return;
31
+ const proto = ParamsAny.prototype;
32
+ // Behavioral probe: RN 0.72's polyfill "has" set() on its prototype, but
33
+ // calling it throws "URLSearchParams.set is not implemented" — a typeof
34
+ // check alone would be a false positive.
35
+ let setWorks = false;
36
+ try {
37
+ const probe = new ParamsAny('a=1');
38
+ probe.set('a', '2');
39
+ setWorks = probe.toString() === 'a=2';
40
+ }
41
+ catch {
42
+ setWorks = false;
43
+ }
44
+ // set: replace the first same-name entry (keeping order), or append at the end
45
+ if (!setWorks) {
46
+ proto.set = function set(name, value) {
47
+ name = String(name);
48
+ value = String(value);
49
+ const hasGet = typeof this.get === 'function';
50
+ if (hasGet && this.get(name) === null) {
51
+ this.append(name, value);
52
+ return;
53
+ }
54
+ const internal = this._searchParams;
55
+ if (Array.isArray(internal)) {
56
+ let replaced = false;
57
+ const kept = [];
58
+ for (let i = 0; i < internal.length; i++) {
59
+ const pair = internal[i];
60
+ if (pair && pair.name === name) {
61
+ if (!replaced) {
62
+ pair.value = value;
63
+ replaced = true;
64
+ kept.push(pair);
65
+ }
66
+ }
67
+ else {
68
+ kept.push(pair);
69
+ }
70
+ }
71
+ this._searchParams = kept;
72
+ }
73
+ else {
74
+ // Unknown internal structure: cannot replace precisely when get()
75
+ // returns a value, degrade to append (get() returns the first
76
+ // same-name entry, so the result is still correct)
77
+ this.append(name, value);
78
+ }
79
+ };
80
+ }
81
+ if (typeof proto.delete !== 'function') {
82
+ proto.delete = function deleteParam(name) {
83
+ const internal = this._searchParams;
84
+ if (Array.isArray(internal)) {
85
+ const kept = [];
86
+ for (let i = 0; i < internal.length; i++) {
87
+ const pair = internal[i];
88
+ if (!(pair && pair.name === String(name)))
89
+ kept.push(pair);
90
+ }
91
+ this._searchParams = kept;
92
+ }
93
+ };
94
+ }
95
+ }
96
+ // Dev-only. This mutates a global prototype, so it must never run in a
97
+ // release build: a developer tool has no business changing the runtime
98
+ // semantics of the shipped app. The inspector's own use of URLSearchParams
99
+ // is already safe without this patch - buildEditorQueryString (see
100
+ // packages/core/src/editorQuery.ts) probes behaviourally and falls back to
101
+ // manual percent-encoding - so the patch is a convenience for app code on
102
+ // old RN, not something the inspector depends on.
103
+ if (typeof __DEV__ === 'undefined' || __DEV__) {
104
+ patchURLSearchParams();
105
+ }
106
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@rn-dev-inspector/rn-compat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.3.0",
4
+ "description": "React Native version compatibility layer for React Native Dev Inspector - Metro resolver shim that rebuilds the inspector hierarchy on Fabric (RN 0.73+/0.86+)",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "shims"
10
+ ],
11
+ "keywords": [
12
+ "react-native",
13
+ "inspector",
14
+ "devtools",
15
+ "metro",
16
+ "fabric",
17
+ "compat"
18
+ ],
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/VaheSaroyan/react-native-dev-inspector.git",
23
+ "directory": "packages/rn-compat"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/VaheSaroyan/react-native-dev-inspector/issues"
27
+ },
28
+ "homepage": "https://github.com/VaheSaroyan/react-native-dev-inspector#readme",
29
+ "devDependencies": {
30
+ "@types/node": "^20.10.0"
31
+ },
32
+ "exports": {
33
+ ".": "./dist/index.js",
34
+ "./runtime": "./dist/runtime.js",
35
+ "./runtime.js": "./dist/runtime.js",
36
+ "./dist/runtime": "./dist/runtime.js",
37
+ "./dist/runtime.js": "./dist/runtime.js",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "clean": "rm -rf dist",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "pnpm build && node --test \"test/*.test.js\""
45
+ }
6
46
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Empty module placeholder. The rn-compat resolver maps optional peer
3
+ * dependencies (expo-dev-menu / expo-dev-client) here when they are not
4
+ * installed, so core's `try { require('expo-dev-menu') } catch {}`
5
+ * degrades to a no-op instead of Metro's runtime-throwing stub.
6
+ */
7
+ module.exports = {};
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Adaptation shim for getInspectorDataForViewAtPoint (works on both Fabric and Paper).
3
+ *
4
+ * Adaptation point 2: the inspector hierarchy on RN 0.8x/0.86 Fabric has two problems:
5
+ * 1. Hierarchy items carry only {props, measure} and no fiber, and _debugSource was
6
+ * removed in React 19, so the library has to rely on the babel-injected
7
+ * __callerSource — without the fiber there is no source info at all;
8
+ * 2. The owner chain RN provides may skip levels (intermediate composite components
9
+ * are omitted by RN's implementation), so the panel breadcrumb jumps from Text
10
+ * straight to an ancestor, missing intermediate levels.
11
+ *
12
+ * Adaptation point 3 (Paper, RN <= 0.72): RN Paper's createHierarchy only provides
13
+ * the props of the "nearest host descendant" (getHostProps) for each owner-chain
14
+ * fiber, never the fiber itself. Wrapper components such as RN's Button/Touchable
15
+ * that do not forward props have the babel-injected __callerSource on their
16
+ * composite fiber, but their inner host elements (rendered from node_modules) do
17
+ * not — so the nearest item with codeInfo on the tapped chain jumps straight to the
18
+ * business screen component. Fix: use the container view's
19
+ * _internalFiberInstanceHandleDEV (fiber) plus viewData.touchedViewTag (native tag)
20
+ * to find the tapped host fiber, then rebuild the hierarchy along the owner chain
21
+ * (composite components included), with each item reading fiber.memoizedProps
22
+ * directly, so __callerSource on <Button> becomes visible.
23
+ *
24
+ * Implementation (both architectures share one rebuild path):
25
+ * - Fabric: start from viewData.closestInstance (the real fiber); pickSourceFiber
26
+ * finds the target fiber carrying __callerSource;
27
+ * - Paper: walk up from inspectedView._internalFiberInstanceHandleDEV to the root,
28
+ * find the host fiber whose stateNode._nativeTag === touchedViewTag, then walk
29
+ * up .return for the nearest fiber carrying __callerSource (subtree DFS fallback);
30
+ * - then collect the full composite chain via _debugOwner and rebuild the
31
+ * hierarchy (host components included);
32
+ * - each item's getInspectorData returns {props: fiber.memoizedProps, measure,
33
+ * fiber}, with measure taken from the first host node (stateNode) in that
34
+ * fiber's subtree.
35
+ */
36
+ const mod = require('__rn_dev_inspector_original_impl__');
37
+ const original = mod.default || mod;
38
+
39
+ function nameOfFiber(fiber) {
40
+ try {
41
+ const t = fiber.type;
42
+ if (typeof t === 'string') return t;
43
+ if (typeof t === 'function') return t.displayName || t.name || null;
44
+ if (t && typeof t === 'object') {
45
+ return t.displayName || (t.render && (t.render.displayName || t.render.name)) || null;
46
+ }
47
+ } catch (e) {}
48
+ return null;
49
+ }
50
+
51
+ // Shared bounded DFS over a fiber subtree. visit(node) returns a truthy value
52
+ // to stop the walk with that result, or a falsy value to keep going. maxDepth
53
+ // (levels below root, undefined = unlimited) and maxVisited mirror the caps
54
+ // the individual walkers used before they were deduplicated.
55
+ function walkFiberTree(root, maxVisited, visit, maxDepth) {
56
+ try {
57
+ let found = null;
58
+ let visited = 0;
59
+ (function dfs(node, depth) {
60
+ if (found || !node || visited > maxVisited) return;
61
+ if (maxDepth !== undefined && depth > maxDepth) return;
62
+ visited++;
63
+ const result = visit(node);
64
+ if (result) {
65
+ found = result;
66
+ return;
67
+ }
68
+ dfs(node.child, depth + 1);
69
+ if (!found) dfs(node.sibling, depth + 1);
70
+ })(root, 0);
71
+ return found;
72
+ } catch (e) {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function pickSourceFiber(fiber) {
78
+ try {
79
+ if (fiber && fiber.memoizedProps && fiber.memoizedProps.__callerSource) {
80
+ return fiber;
81
+ }
82
+ // DFS from the child (the root fiber itself was already checked above);
83
+ // depth cap 11 = 12 levels below the child, same as before deduplication
84
+ const found = walkFiberTree(
85
+ fiber && fiber.child,
86
+ 300,
87
+ node => {
88
+ const p = node.memoizedProps;
89
+ return p && p.__callerSource ? node : null;
90
+ },
91
+ 11
92
+ );
93
+ return found || fiber;
94
+ } catch (e) {
95
+ return fiber;
96
+ }
97
+ }
98
+
99
+ // Paper: on the parent chain (.return) of the tapped host fiber, find the
100
+ // nearest fiber carrying __callerSource. Wrapper component elements written in
101
+ // business JSX, like <Button>, get __callerSource injected by babel, which
102
+ // lands on the wrapper's composite fiber memoizedProps, while its inner host
103
+ // elements (rendered from node_modules) have none — so the first hop up from
104
+ // the host hits it.
105
+ function pickSourceUpChain(fiber) {
106
+ try {
107
+ let f = fiber;
108
+ let n = 0;
109
+ while (f && typeof f.tag === 'number' && n < 40) {
110
+ if (parseCallerSourceRaw(f.memoizedProps)) return f;
111
+ f = f.return;
112
+ n++;
113
+ }
114
+ } catch (e) {}
115
+ return null;
116
+ }
117
+
118
+ // Paper: find the tapped host fiber in the subtree of the container fiber
119
+ // (walked up to the root). On Paper a host fiber's stateNode is a
120
+ // ReactNativeFiberHostComponent whose _nativeTag is the native view tag,
121
+ // matching viewData.touchedViewTag.
122
+ function findTouchedHostFiber(containerFiber, nativeTag) {
123
+ try {
124
+ // Walk up to the root, covering nodes rendered outside the container's
125
+ // subtree (Modal/Portal and friends)
126
+ let root = containerFiber;
127
+ let up = 0;
128
+ while (root && root.return && up < 200) {
129
+ root = root.return;
130
+ up++;
131
+ }
132
+ return walkFiberTree(root, 5000, node => {
133
+ if (node.tag === 5 && node.stateNode) {
134
+ const sn = node.stateNode;
135
+ const tag =
136
+ sn._nativeTag != null ? sn._nativeTag : typeof sn.getTag === 'function' ? sn.getTag() : null;
137
+ return tag === nativeTag ? node : null;
138
+ }
139
+ return null;
140
+ });
141
+ } catch (e) {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ // First host node (tag 5 = HostComponent) within this fiber's subtree.
147
+ // A host fiber's stateNode carries measure(cb) itself (Fabric wraps it on the
148
+ // ShadowNode side, Paper uses ReactNativeFiberHostComponent — both go through
149
+ // UIManager.measure), so require('react-native') is not needed here (the shim
150
+ // lives outside the project directory and Metro would fail to resolve it).
151
+ function findHostStateNode(fiber) {
152
+ return walkFiberTree(fiber, 300, node => {
153
+ return node.tag === 5 && node.stateNode ? node.stateNode : null;
154
+ });
155
+ }
156
+
157
+ function measureFor(stateNode) {
158
+ return function measure(callback) {
159
+ if (!stateNode || typeof stateNode.measure !== 'function') return;
160
+ try {
161
+ stateNode.measure((x, y, width, height, pageX, pageY) => {
162
+ callback(x, y, width, height, pageX, pageY);
163
+ });
164
+ } catch (e) {
165
+ // Node may already be unmounted; ignore
166
+ }
167
+ };
168
+ }
169
+
170
+ // Composite components named like RN native components (e.g. a business
171
+ // ui/Text wrapper) must be renamed, otherwise core's isUserComponentName
172
+ // filters them out as native and the hierarchy loses a level
173
+ const NATIVE_NAMES = new Set([
174
+ 'View',
175
+ 'Text',
176
+ 'TouchableOpacity',
177
+ 'ScrollView',
178
+ 'Image',
179
+ 'TextInput',
180
+ 'Animated',
181
+ ]);
182
+
183
+ // __callerSource may be an object {fileName,lineNumber,columnNumber} or a JSON string
184
+ function parseCallerSourceRaw(props) {
185
+ try {
186
+ const cs = props && props.__callerSource;
187
+ if (!cs) return null;
188
+ return typeof cs === 'string' ? JSON.parse(cs) : cs;
189
+ } catch (e) {
190
+ return null;
191
+ }
192
+ }
193
+
194
+ function baseNameToComponentName(fileName) {
195
+ if (!fileName) return null;
196
+ const base = String(fileName).replace(/\\/g, '/').split('/').pop() || '';
197
+ let stem = base.replace(/\.(tsx?|jsx?)$/, '');
198
+ if (stem === 'index') {
199
+ // index.tsx: use the parent directory name
200
+ const dir = String(fileName).replace(/\\/g, '/').split('/');
201
+ stem = dir[dir.length - 2] || stem;
202
+ }
203
+ if (!stem) return null;
204
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
205
+ }
206
+
207
+ // Anonymous composite components (typically React.forwardRef((props)=>...)
208
+ // with no function name, common in business code): derive the component name
209
+ // from the file name of the first child carrying __callerSource inside its
210
+ // render, e.g. <AnimatedBaseButton> at button.tsx:161 -> "Button"
211
+ function deriveAnonymousName(fiber) {
212
+ try {
213
+ const fileName = walkFiberTree(fiber && fiber.child, 60, node => {
214
+ const parsed = parseCallerSourceRaw(node.memoizedProps);
215
+ return parsed && parsed.fileName ? parsed.fileName : null;
216
+ });
217
+ return fileName ? baseNameToComponentName(fileName) : null;
218
+ } catch (e) {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ function displayNameForFiber(fiber, name) {
224
+ const t = fiber.type;
225
+ const isComposite = typeof t === 'function' || (t && typeof t === 'object');
226
+ if (isComposite && NATIVE_NAMES.has(name)) {
227
+ return `${name} (custom)`;
228
+ }
229
+ return name;
230
+ }
231
+
232
+ // ---- Source filtering (breadcrumb shows only business-source levels) ----
233
+ // The babel plugin does not inject __callerSource into node_modules, so "the
234
+ // fiber's own props carry __callerSource" implies the element was written in
235
+ // business-source JSX. Wrapper components instantiated inside third-party
236
+ // libraries (e.g. ViewHolderCollection / ViewHolderInternal) lack the marker
237
+ // and are hidden from the breadcrumb; the deepest tapped element is always
238
+ // kept, so picking itself is unaffected.
239
+ const PROJECT_SOURCE_ONLY = true;
240
+
241
+ function isProjectSourceFiber(fiber) {
242
+ const parsed = parseCallerSourceRaw(fiber.memoizedProps);
243
+ if (!parsed || !parsed.fileName) return false;
244
+ return !String(parsed.fileName).includes('node_modules');
245
+ }
246
+
247
+ // Some library wrappers still get instantiated in business
248
+ // JSX (e.g. nativewind's styled(Pressable), written on a button.tsx render
249
+ // line, so the fiber carries __callerSource) and a path check would mistake
250
+ // them for business source — filter them out by name as a fallback;
251
+ // 'Xxx (custom)' is a business marker and is never filtered.
252
+ // Note the prefix is "AnimatedComponent", not "Animated" — business
253
+ // components may legitimately be named AnimatedXxx.
254
+ const LIB_WRAPPER_NAMES = new Set([
255
+ 'Pressable', 'TouchableHighlight', 'TouchableNativeFeedback',
256
+ 'TouchableWithoutFeedback', 'FlatList', 'SectionList',
257
+ 'KeyboardAvoidingView', 'Switch', 'StatusBar', 'MaskedView',
258
+ 'RefreshControl', 'ActivityIndicator', 'ProgressBar',
259
+ 'GestureHandlerButton',
260
+ ]);
261
+
262
+ function isLibraryWrapperName(name) {
263
+ if (!name) return false;
264
+ if (name.endsWith(' (custom)')) return false;
265
+ if (name.includes('(')) return true; // wrapper levels like AnimatedComponent(Pressable)
266
+ if (name.startsWith('AnimatedComponent')) return true;
267
+ return LIB_WRAPPER_NAMES.has(name);
268
+ }
269
+
270
+ // Collect the full owner chain up from the source fiber via _debugOwner and
271
+ // rebuild the hierarchy (shared by Fabric and Paper; on Paper the composite
272
+ // fibers of wrappers like Button join the chain so core can read the
273
+ // __callerSource on their memoizedProps)
274
+ function rebuildHierarchyFromSource(sourceFiber) {
275
+ const chain = [];
276
+ let f = sourceFiber;
277
+ while (f && typeof f.tag === 'number' && chain.length < 40) {
278
+ chain.push(f);
279
+ f = f._debugOwner;
280
+ }
281
+ chain.reverse(); // [0] = closest to root, last = deepest (the tapped element)
282
+ return chain
283
+ .map((fiber, idx) => {
284
+ let raw = nameOfFiber(fiber);
285
+ const t = fiber.type;
286
+ const isComposite = typeof t === 'function' || (t && typeof t === 'object');
287
+ if (!raw && isComposite) {
288
+ raw = deriveAnonymousName(fiber);
289
+ }
290
+ if (!raw) return null;
291
+ // Hide intermediate levels that are not business source or are library
292
+ // wrappers (last item = tapped element, always kept)
293
+ if (
294
+ PROJECT_SOURCE_ONLY &&
295
+ idx < chain.length - 1 &&
296
+ (!isProjectSourceFiber(fiber) || isLibraryWrapperName(raw))
297
+ ) {
298
+ return null;
299
+ }
300
+ // nativewind's CssInterop wrapper is pure forwarding, and its
301
+ // __callerSource points inside the wrapper implementation (e.g. a
302
+ // ui/Text/index.tsx render line) — meaningless to users, skip it
303
+ if (raw.startsWith('CssInterop')) return null;
304
+ return {
305
+ name: displayNameForFiber(fiber, raw),
306
+ getInspectorData: () => ({
307
+ props: fiber.memoizedProps,
308
+ measure: measureFor(findHostStateNode(fiber)),
309
+ fiber,
310
+ }),
311
+ };
312
+ })
313
+ .filter(Boolean);
314
+ }
315
+
316
+ function getInspectorDataForViewAtPoint(inspectedView, locationX, locationY, callback) {
317
+ original(inspectedView, locationX, locationY, viewData => {
318
+ try {
319
+ if (viewData && viewData.closestInstance && typeof viewData.closestInstance.tag === 'number') {
320
+ // Fabric (RN 0.73+): closestInstance is the real fiber
321
+ const sourceFiber = pickSourceFiber(viewData.closestInstance);
322
+ viewData.hierarchy = rebuildHierarchyFromSource(sourceFiber);
323
+ } else if (
324
+ viewData &&
325
+ typeof viewData.touchedViewTag === 'number' &&
326
+ inspectedView &&
327
+ inspectedView._internalFiberInstanceHandleDEV &&
328
+ typeof inspectedView._internalFiberInstanceHandleDEV.tag === 'number'
329
+ ) {
330
+ // Paper (RN <= 0.72): RN provides only host-descendant props, not the
331
+ // fiber. Look the host fiber up via touchedViewTag, then walk the
332
+ // parent chain for the nearest business element fiber
333
+ const touched = findTouchedHostFiber(
334
+ inspectedView._internalFiberInstanceHandleDEV,
335
+ viewData.touchedViewTag
336
+ );
337
+ if (touched) {
338
+ const sourceFiber = pickSourceUpChain(touched) || pickSourceFiber(touched);
339
+ viewData.hierarchy = rebuildHierarchyFromSource(sourceFiber);
340
+ }
341
+ }
342
+ if (__DEV__) {
343
+ try {
344
+ console.log(
345
+ '[rn-inspector-adapter] chain:',
346
+ (viewData && viewData.hierarchy ? viewData.hierarchy : []).map(h => h.name).join(' > ')
347
+ );
348
+ } catch (e) {}
349
+ }
350
+ } catch (e) {
351
+ if (__DEV__) console.log('[rn-inspector-adapter] augment error', e && e.message);
352
+ }
353
+ callback(viewData);
354
+ });
355
+ }
356
+
357
+ module.exports = getInspectorDataForViewAtPoint;
358
+ module.exports.default = getInspectorDataForViewAtPoint;