|
@@ -0,0 +1,414 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * React Native cross-language bridge resolver.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Closes the JS ↔ native flow gap in React Native projects. Covers:
|
|
|
|
|
+ *
|
|
|
|
|
+ * **Legacy bridge** (older / still-prevalent in mid-tier RN libs):
|
|
|
|
|
+ * - ObjC: `RCT_EXPORT_MODULE([opt_name])` declares a module; the module
|
|
|
|
|
+ * name defaults to the class name minus an `RCT` prefix when no
|
|
|
|
|
+ * argument is given. `RCT_EXPORT_METHOD(selector:(args))` declares a
|
|
|
|
|
+ * JS-callable method whose JS name is the selector's first keyword.
|
|
|
|
|
+ * `RCT_REMAP_METHOD(jsName, nativeSelector:(args))` overrides the JS
|
|
|
|
|
+ * name explicitly.
|
|
|
|
|
+ * - Java/Kotlin: `@ReactMethod` annotated methods on a
|
|
|
|
|
+ * `ReactContextBaseJavaModule` subclass; the module name comes from
|
|
|
|
|
+ * `getName()` returning a literal string.
|
|
|
|
|
+ *
|
|
|
|
|
+ * **TurboModules** (modern, used by react-native-svg, screens, FBSDK
|
|
|
|
|
+ * Next-gen libraries):
|
|
|
|
|
+ * - TS spec interface declared in a `Native<X>.ts` file exporting
|
|
|
|
|
+ * `TurboModuleRegistry.getEnforcing<Spec>('<ModuleName>')` (or
|
|
|
|
|
+ * `.get<Spec>('<ModuleName>')`). The Spec interface methods are the
|
|
|
|
|
+ * JS-callable surface; the matching native implementation is a class
|
|
|
|
|
+ * whose method names match (selector first-keyword on ObjC,
|
|
|
|
|
+ * identifier on Kotlin/Java).
|
|
|
|
|
+ *
|
|
|
|
|
+ * The two mechanisms share an end shape: a map from `(moduleName,
|
|
|
|
|
+ * jsMethodName)` to a native method node, plus a smaller map from
|
|
|
|
|
+ * `jsMethodName` alone for cases where the JS callsite doesn't carry
|
|
|
|
|
+ * the module qualifier (the most common JS pattern is
|
|
|
|
|
+ * `import Geo from './NativeGeolocation'; Geo.getPosition()` — the
|
|
|
|
|
+ * receiver is the default export, not literally `NativeModules.<Mod>`,
|
|
|
|
|
+ * so name-by-method-only is what actually resolves in practice).
|
|
|
|
|
+ *
|
|
|
|
|
+ * **Not covered** (deferred to a follow-up phase, per design doc §6):
|
|
|
|
|
+ * - Fabric view components (`RCT_EXPORT_VIEW_PROPERTY` / Codegen view
|
|
|
|
|
+ * specs) — these connect JSX props to native renderers, a different
|
|
|
|
|
+ * flow shape that composes with the existing JSX synthesizer.
|
|
|
|
|
+ * - Native → JS events (`RCTEventEmitter` / `NativeEventEmitter`) —
|
|
|
|
|
+ * belongs in the callback synthesizer's cross-language channel.
|
|
|
|
|
+ */
|
|
|
|
|
+import type { Node } from '../../types';
|
|
|
|
|
+import {
|
|
|
|
|
+ FrameworkResolver,
|
|
|
|
|
+ ResolutionContext,
|
|
|
|
|
+} from '../types';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * One native RN method known to the resolver. Indexed by JS-visible name.
|
|
|
|
|
+ */
|
|
|
|
|
+interface NativeMethod {
|
|
|
|
|
+ /** Module name as seen from JS (`Geolocation`, `RNSVGRenderableModule`, …). */
|
|
|
|
|
+ moduleName: string;
|
|
|
|
|
+ /** JS-visible method name. */
|
|
|
|
|
+ jsName: string;
|
|
|
|
|
+ /** Native implementation node (ObjC method / Java method / Kotlin function). */
|
|
|
|
|
+ node: Node;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** Per-context lazy map cache. */
|
|
|
|
|
+const nativeMethodMaps: WeakMap<
|
|
|
|
|
+ ResolutionContext,
|
|
|
|
|
+ { byJsName: Map<string, NativeMethod[]> }
|
|
|
|
|
+> = new WeakMap();
|
|
|
|
|
+
|
|
|
|
|
+// ─── Native-side extraction ─────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Default ObjC module name when `RCT_EXPORT_MODULE()` has no argument:
|
|
|
|
|
+ * strip a leading `RCT` prefix from the class name (Apple's convention)
|
|
|
|
|
+ * and treat the rest as the JS-visible module name. `RCTGeolocation` →
|
|
|
|
|
+ * `Geolocation`. Class names without an `RCT` prefix are returned
|
|
|
|
|
+ * unchanged.
|
|
|
|
|
+ */
|
|
|
|
|
+function defaultObjcModuleName(className: string): string {
|
|
|
|
|
+ return className.startsWith('RCT') && className.length > 3
|
|
|
|
|
+ ? className.slice(3)
|
|
|
|
|
+ : className;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Parse an ObjC `.m`/`.mm` file's source for `RCT_EXPORT_MODULE` and
|
|
|
|
|
+ * `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` declarations, returning the
|
|
|
|
|
+ * inferred (moduleName, jsMethodName) pairs.
|
|
|
|
|
+ *
|
|
|
|
|
+ * The macro forms (a single `RCT_EXPORT_MODULE` per file conventionally
|
|
|
|
|
+ * matched to a single `@implementation`):
|
|
|
|
|
+ * - `RCT_EXPORT_MODULE()` — module name = class name with `RCT` prefix
|
|
|
|
|
+ * stripped
|
|
|
|
|
+ * - `RCT_EXPORT_MODULE(jsName)` — explicit name
|
|
|
|
|
+ * - `RCT_EXPORT_METHOD(selector:(arg1)label1:(arg2)label2)` — JS name =
|
|
|
|
|
+ * `selector` (the first keyword)
|
|
|
|
|
+ * - `RCT_REMAP_METHOD(jsName, selector:(arg1)label1:(arg2)label2)` —
|
|
|
|
|
+ * JS name = literal `jsName`
|
|
|
|
|
+ *
|
|
|
|
|
+ * Regex-based scan is sufficient — these macros are highly stylized and
|
|
|
|
|
+ * appear at top level. Pulling them out of the full AST would require a
|
|
|
|
|
+ * macro-aware ObjC parse the tree-sitter grammar doesn't provide.
|
|
|
|
|
+ */
|
|
|
|
|
+function parseObjcRNExports(
|
|
|
|
|
+ source: string,
|
|
|
|
|
+ className: string | null
|
|
|
|
|
+): Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> {
|
|
|
|
|
+ const results: Array<{ moduleName: string; jsName: string; nativeSelectorFirstKw: string }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ // RCT_EXPORT_MODULE — one per file by convention. Capture the optional arg.
|
|
|
|
|
+ const moduleMatch = source.match(/RCT_EXPORT_MODULE\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)?\s*\)/);
|
|
|
|
|
+ // Need a module name to attribute methods. Prefer the explicit macro arg,
|
|
|
|
|
+ // then the class name, then bail (no module = nothing useful to register).
|
|
|
|
|
+ const moduleName =
|
|
|
|
|
+ moduleMatch?.[1] ??
|
|
|
|
|
+ (className ? defaultObjcModuleName(className) : null);
|
|
|
|
|
+ if (!moduleName) return results;
|
|
|
|
|
+
|
|
|
|
|
+ // RCT_EXPORT_METHOD(selectorFirstKw:(args)…)
|
|
|
|
|
+ // The first keyword (everything up to the first `:` or open paren) is the
|
|
|
|
|
+ // JS-visible name. We don't try to parse full multi-keyword selectors —
|
|
|
|
|
+ // RN's JS view of the method uses only the first keyword.
|
|
|
|
|
+ const exportRegex = /RCT_EXPORT_METHOD\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
|
|
+ while ((m = exportRegex.exec(source)) !== null) {
|
|
|
|
|
+ const kw = m[1];
|
|
|
|
|
+ if (kw) results.push({ moduleName, jsName: kw, nativeSelectorFirstKw: kw });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // RCT_REMAP_METHOD(jsName, nativeSelectorFirstKw:(args)…)
|
|
|
|
|
+ const remapRegex =
|
|
|
|
|
+ /RCT_REMAP_METHOD\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
|
|
|
+ while ((m = remapRegex.exec(source)) !== null) {
|
|
|
|
|
+ const jsName = m[1];
|
|
|
|
|
+ const nativeKw = m[2];
|
|
|
|
|
+ if (jsName && nativeKw) {
|
|
|
|
|
+ results.push({ moduleName, jsName, nativeSelectorFirstKw: nativeKw });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return results;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Find the `@implementation` class name in an ObjC file — used as the
|
|
|
|
|
+ * fallback module name when `RCT_EXPORT_MODULE()` has no argument.
|
|
|
|
|
+ * (Categories of the form `@implementation Foo (Bar)` are correctly
|
|
|
|
|
+ * captured here as `Foo`, but a category file probably isn't where a
|
|
|
|
|
+ * fresh `RCT_EXPORT_MODULE` lives anyway.)
|
|
|
|
|
+ */
|
|
|
|
|
+function findObjcClassName(source: string): string | null {
|
|
|
|
|
+ const m = source.match(/@implementation\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
|
|
|
+ return m?.[1] ?? null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Parse a Java/Kotlin source file for `@ReactMethod` annotated methods
|
|
|
|
|
+ * and the surrounding class's `getName()` return value (the JS-visible
|
|
|
|
|
+ * module name).
|
|
|
|
|
+ *
|
|
|
|
|
+ * Java: `@ReactMethod public void getCurrentPosition(Callback cb) { … }`
|
|
|
|
|
+ * Kotlin: `@ReactMethod fun getCurrentPosition(cb: Callback) { … }`
|
|
|
|
|
+ *
|
|
|
|
|
+ * Class name comes from `class XxxModule extends ReactContextBaseJavaModule`
|
|
|
|
|
+ * (Java) or `class XxxModule : ReactContextBaseJavaModule(...)` (Kotlin).
|
|
|
|
|
+ * The JS-visible module name comes from `getName()` returning a literal
|
|
|
|
|
+ * string — fall back to the class name with a `Module` suffix stripped
|
|
|
|
|
+ * when the literal isn't present.
|
|
|
|
|
+ */
|
|
|
|
|
+function parseJvmRNExports(
|
|
|
|
|
+ source: string
|
|
|
|
|
+): Array<{ moduleName: string; jsName: string }> {
|
|
|
|
|
+ const results: Array<{ moduleName: string; jsName: string }> = [];
|
|
|
|
|
+
|
|
|
|
|
+ // getName() literal — Java + Kotlin both look something like:
|
|
|
|
|
+ // public String getName() { return "Geolocation"; }
|
|
|
|
|
+ // fun getName(): String = "Geolocation"
|
|
|
|
|
+ // fun getName() = "Geolocation"
|
|
|
|
|
+ const getName = source.match(
|
|
|
|
|
+ /\bgetName\s*\([^)]*\)\s*(?::\s*String)?\s*(?:=\s*|\{[^}]*return\s*)"([^"]+)"/
|
|
|
|
|
+ );
|
|
|
|
|
+ // Class name fallback.
|
|
|
|
|
+ const classMatch =
|
|
|
|
|
+ source.match(/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b[^{]*ReactContextBaseJavaModule/) ??
|
|
|
|
|
+ source.match(/\bclass\s+([A-Za-z_][A-Za-z0-9_]*)\b[^{]*ReactPackage/);
|
|
|
|
|
+ const moduleName =
|
|
|
|
|
+ getName?.[1] ?? (classMatch?.[1] ? classMatch[1].replace(/Module$/, '') : null);
|
|
|
|
|
+ if (!moduleName) return results;
|
|
|
|
|
+
|
|
|
|
|
+ // @ReactMethod annotations — followed (after optional modifiers / args /
|
|
|
|
|
+ // newlines) by either `void <name>(` (Java) or `fun <name>(` (Kotlin).
|
|
|
|
|
+ const methodRegex =
|
|
|
|
|
+ /@ReactMethod\b[^{]*?(?:\bfun\s+|\bvoid\s+|\bpublic\s+\w[\w<>\[\]]*\s+)([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
|
|
+ while ((m = methodRegex.exec(source)) !== null) {
|
|
|
|
|
+ const jsName = m[1];
|
|
|
|
|
+ if (jsName) results.push({ moduleName, jsName });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return results;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Parse a TS file for a TurboModule spec declaration. The spec file is
|
|
|
|
|
+ * the JS↔native source-of-truth in the new architecture — its interface
|
|
|
|
|
+ * lists every JS-visible method, and a `TurboModuleRegistry.get*<Spec>(...)`
|
|
|
|
|
+ * default export pins the module name.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Returns `null` when the file isn't a TurboModule spec.
|
|
|
|
|
+ */
|
|
|
|
|
+function parseTurboModuleSpec(
|
|
|
|
|
+ source: string
|
|
|
|
|
+): { moduleName: string; methods: string[] } | null {
|
|
|
|
|
+ // `TurboModuleRegistry.getEnforcing<Spec>('ModuleName')` or
|
|
|
|
|
+ // `TurboModuleRegistry.get<Spec>('ModuleName')`. The literal must be a
|
|
|
|
|
+ // single-or-double-quoted string.
|
|
|
|
|
+ const regMatch = source.match(
|
|
|
|
|
+ /TurboModuleRegistry\.(?:getEnforcing|get)\s*<[^>]*>\s*\(\s*['"]([^'"]+)['"]\s*\)/
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!regMatch || !regMatch[1]) return null;
|
|
|
|
|
+ const moduleName = regMatch[1];
|
|
|
|
|
+
|
|
|
|
|
+ // Find `export interface Spec extends TurboModule { … }` and pull each
|
|
|
|
|
+ // method declaration's name. We don't need types — just names.
|
|
|
|
|
+ const ifaceMatch = source.match(
|
|
|
|
|
+ /export\s+interface\s+Spec\b[^{]*\{([\s\S]*?)\n\}/
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!ifaceMatch || !ifaceMatch[1]) return null;
|
|
|
|
|
+ const body = ifaceMatch[1];
|
|
|
|
|
+
|
|
|
|
|
+ const methods: string[] = [];
|
|
|
|
|
+ // Method shape: `name(args): ReturnType;` or `name(): void;`. Skip
|
|
|
|
|
+ // properties (no parens before colon).
|
|
|
|
|
+ const methodRegex = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(/gm;
|
|
|
|
|
+ let m: RegExpExecArray | null;
|
|
|
|
|
+ while ((m = methodRegex.exec(body)) !== null) {
|
|
|
|
|
+ const name = m[1];
|
|
|
|
|
+ if (name) methods.push(name);
|
|
|
|
|
+ }
|
|
|
|
|
+ return { moduleName, methods };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ─── Map building ───────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+function buildRNMaps(context: ResolutionContext): { byJsName: Map<string, NativeMethod[]> } {
|
|
|
|
|
+ const cached = nativeMethodMaps.get(context);
|
|
|
|
|
+ if (cached) return cached;
|
|
|
|
|
+
|
|
|
|
|
+ const byJsName = new Map<string, NativeMethod[]>();
|
|
|
|
|
+ const allFiles = context.getAllFiles();
|
|
|
|
|
+ // Pre-index native methods by name for fast lookup when matching to
|
|
|
|
|
+ // their bridge exports.
|
|
|
|
|
+ const objcMethodsByFirstKw = new Map<string, Node[]>();
|
|
|
|
|
+ const jvmMethodsByName = new Map<string, Node[]>();
|
|
|
|
|
+ for (const node of context.getNodesByKind('method')) {
|
|
|
|
|
+ if (node.language === 'objc') {
|
|
|
|
|
+ const firstKw = node.name.includes(':') ? node.name.split(':')[0] : node.name;
|
|
|
|
|
+ if (firstKw) {
|
|
|
|
|
+ const arr = objcMethodsByFirstKw.get(firstKw);
|
|
|
|
|
+ if (arr) arr.push(node);
|
|
|
|
|
+ else objcMethodsByFirstKw.set(firstKw, [node]);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (node.language === 'java' || node.language === 'kotlin') {
|
|
|
|
|
+ const arr = jvmMethodsByName.get(node.name);
|
|
|
|
|
+ if (arr) arr.push(node);
|
|
|
|
|
+ else jvmMethodsByName.set(node.name, [node]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const file of allFiles) {
|
|
|
|
|
+ // Legacy bridge — ObjC side.
|
|
|
|
|
+ if (file.endsWith('.m') || file.endsWith('.mm')) {
|
|
|
|
|
+ const source = context.readFile(file);
|
|
|
|
|
+ if (!source) continue;
|
|
|
|
|
+ const className = findObjcClassName(source);
|
|
|
|
|
+ const exports = parseObjcRNExports(source, className);
|
|
|
|
|
+ for (const exp of exports) {
|
|
|
|
|
+ // Resolve to the native node by selector first-keyword. Multiple
|
|
|
|
|
+ // ObjC methods may share a first keyword across modules; filter by
|
|
|
|
|
+ // file path to attribute the export to this module's
|
|
|
|
|
+ // implementation file.
|
|
|
|
|
+ const candidates = objcMethodsByFirstKw.get(exp.nativeSelectorFirstKw) ?? [];
|
|
|
|
|
+ const node = candidates.find((c) => c.filePath === file) ?? candidates[0];
|
|
|
|
|
+ if (!node) continue;
|
|
|
|
|
+ const entry: NativeMethod = { moduleName: exp.moduleName, jsName: exp.jsName, node };
|
|
|
|
|
+ const arr = byJsName.get(exp.jsName);
|
|
|
|
|
+ if (arr) arr.push(entry);
|
|
|
|
|
+ else byJsName.set(exp.jsName, [entry]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Legacy bridge — Java/Kotlin side.
|
|
|
|
|
+ if (file.endsWith('.java') || file.endsWith('.kt')) {
|
|
|
|
|
+ const source = context.readFile(file);
|
|
|
|
|
+ if (!source) continue;
|
|
|
|
|
+ const exports = parseJvmRNExports(source);
|
|
|
|
|
+ for (const exp of exports) {
|
|
|
|
|
+ const candidates = jvmMethodsByName.get(exp.jsName) ?? [];
|
|
|
|
|
+ const node = candidates.find((c) => c.filePath === file) ?? candidates[0];
|
|
|
|
|
+ if (!node) continue;
|
|
|
|
|
+ const entry: NativeMethod = { moduleName: exp.moduleName, jsName: exp.jsName, node };
|
|
|
|
|
+ const arr = byJsName.get(exp.jsName);
|
|
|
|
|
+ if (arr) arr.push(entry);
|
|
|
|
|
+ else byJsName.set(exp.jsName, [entry]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // TurboModule spec — TS side.
|
|
|
|
|
+ if (file.endsWith('.ts') || file.endsWith('.tsx')) {
|
|
|
|
|
+ const source = context.readFile(file);
|
|
|
|
|
+ if (!source) continue;
|
|
|
|
|
+ const spec = parseTurboModuleSpec(source);
|
|
|
|
|
+ if (!spec) continue;
|
|
|
|
|
+ // For each spec method, find a matching native implementation by
|
|
|
|
|
+ // name. The spec's module name doesn't determine the native file
|
|
|
|
|
+ // path (Codegen wires it via name convention), so we match across
|
|
|
|
|
+ // all native methods of the right name.
|
|
|
|
|
+ for (const methodName of spec.methods) {
|
|
|
|
|
+ // ObjC first-keyword match, then JVM bare-name match. Don't
|
|
|
|
|
+ // require module-name match for ObjC because the native side may
|
|
|
|
|
+ // have stripped a prefix.
|
|
|
|
|
+ const objcCands = objcMethodsByFirstKw.get(methodName) ?? [];
|
|
|
|
|
+ const jvmCands = jvmMethodsByName.get(methodName) ?? [];
|
|
|
|
|
+ for (const node of [...objcCands, ...jvmCands]) {
|
|
|
|
|
+ const entry: NativeMethod = { moduleName: spec.moduleName, jsName: methodName, node };
|
|
|
|
|
+ const arr = byJsName.get(methodName);
|
|
|
|
|
+ if (arr) arr.push(entry);
|
|
|
|
|
+ else byJsName.set(methodName, [entry]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const result = { byJsName };
|
|
|
|
|
+ nativeMethodMaps.set(context, result);
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// ─── Resolver ───────────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+export const reactNativeBridgeResolver: FrameworkResolver = {
|
|
|
|
|
+ name: 'react-native-bridge',
|
|
|
|
|
+ languages: ['javascript', 'typescript', 'tsx', 'jsx'],
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Detect: package.json depends on `react-native`, OR any source file
|
|
|
|
|
+ * uses the `RCT_EXPORT_MODULE` / `RCT_EXPORT_METHOD` /
|
|
|
|
|
+ * `TurboModuleRegistry` markers. Either signal is enough — different
|
|
|
|
|
+ * libraries split the JS package from the native code (`react-native-svg`'s
|
|
|
|
|
+ * apple/ + android/ directories vs its src/), so we don't require both.
|
|
|
|
|
+ */
|
|
|
|
|
+ detect(context) {
|
|
|
|
|
+ const pkg = context.readFile('package.json');
|
|
|
|
|
+ if (pkg && /["']react-native["']\s*:/.test(pkg)) return true;
|
|
|
|
|
+ // Fallback: scan a small number of files for the macro markers — only
|
|
|
|
|
+ // looking at the first ones returned by getAllFiles to keep detect()
|
|
|
|
|
+ // fast on huge repos.
|
|
|
|
|
+ const files = context.getAllFiles();
|
|
|
|
|
+ for (let i = 0; i < Math.min(files.length, 200); i++) {
|
|
|
|
|
+ const f = files[i];
|
|
|
|
|
+ if (!f) continue;
|
|
|
|
|
+ if (f.endsWith('.mm') || f.endsWith('.m')) {
|
|
|
|
|
+ const src = context.readFile(f);
|
|
|
|
|
+ if (src && /RCT_EXPORT_MODULE\b/.test(src)) return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (f.endsWith('.ts') || f.endsWith('.tsx')) {
|
|
|
|
|
+ const src = context.readFile(f);
|
|
|
|
|
+ if (src && /TurboModuleRegistry\.(?:get|getEnforcing)\s*</.test(src)) return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ claimsReference(_name) {
|
|
|
|
|
+ // JS-visible method names are ordinary identifiers and are typically
|
|
|
|
|
+ // already in `knownNames` (every TurboModule spec method, every
|
|
|
|
|
+ // RCT_EXPORT_METHOD, has a node somewhere). So we don't need to
|
|
|
|
|
+ // claim through the pre-filter — the ref reaches us via the normal
|
|
|
|
|
+ // hasAnyPossibleMatch path.
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ resolve(ref, context) {
|
|
|
|
|
+ // We only redirect JS callers — native callers don't need this resolver.
|
|
|
|
|
+ if (
|
|
|
|
|
+ ref.language !== 'javascript' &&
|
|
|
|
|
+ ref.language !== 'typescript' &&
|
|
|
|
|
+ ref.language !== 'tsx' &&
|
|
|
|
|
+ ref.language !== 'jsx'
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // JS callsites of `obj.method()` reach the resolver as either
|
|
|
|
|
+ // `obj.method` (qualified) or `method` (bare). Strip a single dot
|
|
|
|
|
+ // prefix to get the JS-visible method name.
|
|
|
|
|
+ const name = ref.referenceName.includes('.')
|
|
|
|
|
+ ? ref.referenceName.slice(ref.referenceName.lastIndexOf('.') + 1)
|
|
|
|
|
+ : ref.referenceName;
|
|
|
|
|
+
|
|
|
|
|
+ const maps = buildRNMaps(context);
|
|
|
|
|
+ const entries = maps.byJsName.get(name);
|
|
|
|
|
+ if (!entries || entries.length === 0) return null;
|
|
|
|
|
+
|
|
|
|
|
+ // Prefer the iOS (ObjC) target over Android when both exist — iOS is
|
|
|
|
|
+ // the conventional first-class platform for RN library docs and most
|
|
|
|
|
+ // graph queries. We still record only one edge; a JVM-only resolution
|
|
|
|
|
+ // is fine when no ObjC target exists.
|
|
|
|
|
+ const objc = entries.find((e) => e.node.language === 'objc');
|
|
|
|
|
+ const target = objc ?? entries[0];
|
|
|
|
|
+ if (!target) return null;
|
|
|
|
|
+ return {
|
|
|
|
|
+ original: ref,
|
|
|
|
|
+ targetNodeId: target.node.id,
|
|
|
|
|
+ confidence: 0.6,
|
|
|
|
|
+ resolvedBy: 'framework',
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+};
|