/** * React Native Fabric / Codegen view components — Phase 6 of the * mixed-iOS/RN bridging effort. * * In the new RN architecture, JS-visible view components are declared via * Codegen TS spec files of the shape: * * // src/fabric/MyComponentNativeComponent.ts * import { codegenNativeComponent } from 'react-native'; * import type { ViewProps, CodegenTypes as CT } from 'react-native'; * * export interface NativeProps extends ViewProps { * color?: ColorValue; * onTap?: CT.DirectEventHandler; * } * * export default codegenNativeComponent('MyComponent'); * * Codegen then generates a native ComponentDescriptor that wires the JS * component name to a native implementation class — by RN convention, * one of `MyComponent`, `MyComponentView`, `MyComponentComponentView`, * `MyComponentManager`, `MyComponentViewManager`. The actual implementation * lives in ObjC++ (.mm) on iOS or Kotlin/Java on Android. * * Without bridging, JSX `` in a consumer app has * nothing in the graph to land on — the JS-visible name `MyComponent` isn't * a node anywhere (only `MyComponentView` is, in the .mm), and the JSX * synthesizer matches strictly by name. * * What this extractor does: * 1. Parse the spec file's `codegenNativeComponent('Name', ...)` * literal — emit a `component` node named `Name`, attributed to the * spec file. * 2. Parse the `NativeProps` interface and emit one `property` node per * prop, attributed to the spec file. Props like `onTap` / * `onFinishTransitioning` are JS-callable event-handler bindings; * surfacing them as nodes lets the agent discover the JS surface of * the component. * * A companion synthesizer (`fabricNativeImplEdges` in * callback-synthesizer.ts) links the emitted component node to its * native implementation class via the convention-based name+suffix * lookup — that produces the cross-language hop the JSX synthesizer's * `` edges naturally chain through. */ import type { Node } from '../../types'; import { FrameworkExtractionResult, FrameworkResolver, } from '../types'; const CODEGEN_DECL_RE = /codegenNativeComponent\s*(?:<[^>]+>)?\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g; /** * Cheap source-level detector — must contain `codegenNativeComponent` to * be worth parsing. The presence of that import is the canonical Fabric * spec signal. */ function isFabricSpec(source: string): boolean { return source.includes('codegenNativeComponent'); } /** * Pull the `NativeProps` interface body out of a Fabric spec source. * Returns `null` when the interface isn't declared in the expected shape. */ function findNativePropsBody(source: string): string | null { // Permissive: `export interface NativeProps [extends X, Y] { … }`. const m = source.match(/export\s+interface\s+NativeProps\b[^{]*\{([\s\S]*?)\n\}/); return m?.[1] ?? null; } /** * Parse the NativeProps interface body and return prop names. * Each prop is `name?: Type;` or `name: Type;` on its own line. * We don't care about types — just the JS-visible name. */ function extractPropNames(body: string): string[] { const props: string[] = []; // Anchor to start-of-line (after optional whitespace), then capture an // identifier, then optional `?`, then `:`. Skip lines that look like // method declarations (`name(`) — those are TurboModule spec methods, // not view props. const regex = /^\s*([A-Za-z_][A-Za-z0-9_]*)\??\s*:/gm; let m: RegExpExecArray | null; while ((m = regex.exec(body)) !== null) { const name = m[1]!; // Exclude any line that immediately turns into a function-shape (e.g. // `onTap?: () => void` is fine — it's a prop, not a method body — // but a literal `name(arg: T): R` is a method declaration). const after = body.slice(m.index + m[0].length, m.index + m[0].length + 80); if (/^\s*\(/.test(after)) continue; // method-shape, skip props.push(name); } return props; } function extractFabricNodes(filePath: string, source: string): Node[] { if (!isFabricSpec(source)) return []; const now = Date.now(); const nodes: Node[] = []; CODEGEN_DECL_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = CODEGEN_DECL_RE.exec(source)) !== null) { const componentName = m[1]!; const before = source.slice(0, m.index); const startLine = before.split('\n').length; const startColumn = before.length - before.lastIndexOf('\n') - 1; // The component itself — kind: 'component' so the existing // reactJsxChildEdges synthesizer matches `` JSX tags to // it (its name+kind filter is the gate). const componentId = `fabric-component:${filePath}:${componentName}:${startLine}`; nodes.push({ id: componentId, kind: 'component', name: componentName, qualifiedName: `${filePath}::${componentName}`, filePath, // The spec file is .ts or .tsx; use the file's apparent language // by extension. Trim to a known Language value. language: filePath.endsWith('.tsx') ? 'tsx' : 'typescript', startLine, endLine: startLine, startColumn, endColumn: startColumn + 'codegenNativeComponent'.length, docstring: `Fabric/Codegen native component '${componentName}'`, signature: `codegenNativeComponent('${componentName}')`, isExported: true, updatedAt: now, }); } // Props from the NativeProps interface. These are not "method" semantic // — they're JS-visible bindings the consumer sets via JSX attributes — // so use `property` kind. (The JSX synthesizer doesn't currently // produce per-attribute edges, but surfacing the prop names as nodes // lets `codegraph_search('onFinishTransitioning')` discover them.) const body = findNativePropsBody(source); if (body) { const props = extractPropNames(body); for (const propName of props) { const propBefore = source.indexOf(propName, source.indexOf(body)); const propLine = propBefore >= 0 ? source.slice(0, propBefore).split('\n').length : 1; nodes.push({ id: `fabric-prop:${filePath}:${propName}:${propLine}`, kind: 'property', name: propName, qualifiedName: `${filePath}::NativeProps.${propName}`, filePath, language: filePath.endsWith('.tsx') ? 'tsx' : 'typescript', startLine: propLine, endLine: propLine, startColumn: 0, endColumn: propName.length, docstring: `Fabric NativeProps prop '${propName}'`, isExported: true, updatedAt: now, }); } } return nodes; } export const fabricViewResolver: FrameworkResolver = { name: 'fabric-view', languages: ['typescript', 'tsx'], detect(context) { // Detect on package.json alone: an RN project has the dep. We // initially scanned for a `codegenNativeComponent` marker file too, // but on big repos (RNScreens has ~1500 source files; fabric specs // come alphabetically after FabricExample/ etc., past any reasonable // scan budget) the marker check times out and produces false- // negatives. Detect lightly, and let the per-file `extract()` decide // which files actually have Fabric specs — extract() is essentially // free on non-spec files (a short `includes('codegenNativeComponent')`). const pkg = context.readFile('package.json'); return pkg ? /["']react-native["']\s*:/.test(pkg) : false; }, extract(filePath, source): FrameworkExtractionResult { return { nodes: extractFabricNodes(filePath, source), references: [], }; }, resolve() { // The companion synthesizer (`fabricNativeImplEdges`) handles // cross-language edges; standard name resolution handles // → component-node via the JSX synthesizer. return null; }, };