fabric.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. /**
  2. * React Native Fabric / Codegen view components — Phase 6 of the
  3. * mixed-iOS/RN bridging effort.
  4. *
  5. * In the new RN architecture, JS-visible view components are declared via
  6. * Codegen TS spec files of the shape:
  7. *
  8. * // src/fabric/MyComponentNativeComponent.ts
  9. * import { codegenNativeComponent } from 'react-native';
  10. * import type { ViewProps, CodegenTypes as CT } from 'react-native';
  11. *
  12. * export interface NativeProps extends ViewProps {
  13. * color?: ColorValue;
  14. * onTap?: CT.DirectEventHandler<TapEvent>;
  15. * }
  16. *
  17. * export default codegenNativeComponent<NativeProps>('MyComponent');
  18. *
  19. * Codegen then generates a native ComponentDescriptor that wires the JS
  20. * component name to a native implementation class — by RN convention,
  21. * one of `MyComponent`, `MyComponentView`, `MyComponentComponentView`,
  22. * `MyComponentManager`, `MyComponentViewManager`. The actual implementation
  23. * lives in ObjC++ (.mm) on iOS or Kotlin/Java on Android.
  24. *
  25. * Without bridging, JSX `<MyComponent color="red"/>` in a consumer app has
  26. * nothing in the graph to land on — the JS-visible name `MyComponent` isn't
  27. * a node anywhere (only `MyComponentView` is, in the .mm), and the JSX
  28. * synthesizer matches strictly by name.
  29. *
  30. * What this extractor does:
  31. * 1. Parse the spec file's `codegenNativeComponent<Props>('Name', ...)`
  32. * literal — emit a `component` node named `Name`, attributed to the
  33. * spec file.
  34. * 2. Parse the `NativeProps` interface and emit one `property` node per
  35. * prop, attributed to the spec file. Props like `onTap` /
  36. * `onFinishTransitioning` are JS-callable event-handler bindings;
  37. * surfacing them as nodes lets the agent discover the JS surface of
  38. * the component.
  39. *
  40. * A companion synthesizer (`fabricNativeImplEdges` in
  41. * callback-synthesizer.ts) links the emitted component node to its
  42. * native implementation class via the convention-based name+suffix
  43. * lookup — that produces the cross-language hop the JSX synthesizer's
  44. * `<MyComponent>` edges naturally chain through.
  45. */
  46. import type { Node } from '../../types';
  47. import {
  48. FrameworkExtractionResult,
  49. FrameworkResolver,
  50. } from '../types';
  51. const CODEGEN_DECL_RE =
  52. /codegenNativeComponent\s*(?:<[^>]+>)?\s*\(\s*['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g;
  53. /**
  54. * Cheap source-level detector — must contain `codegenNativeComponent` to
  55. * be worth parsing. The presence of that import is the canonical Fabric
  56. * spec signal.
  57. */
  58. function isFabricSpec(source: string): boolean {
  59. return source.includes('codegenNativeComponent');
  60. }
  61. /**
  62. * Pull the `NativeProps` interface body out of a Fabric spec source.
  63. * Returns `null` when the interface isn't declared in the expected shape.
  64. */
  65. function findNativePropsBody(source: string): string | null {
  66. // Permissive: `export interface NativeProps [extends X, Y] { … }`.
  67. const m = source.match(/export\s+interface\s+NativeProps\b[^{]*\{([\s\S]*?)\n\}/);
  68. return m?.[1] ?? null;
  69. }
  70. /**
  71. * Parse the NativeProps interface body and return prop names.
  72. * Each prop is `name?: Type;` or `name: Type;` on its own line.
  73. * We don't care about types — just the JS-visible name.
  74. */
  75. function extractPropNames(body: string): string[] {
  76. const props: string[] = [];
  77. // Anchor to start-of-line (after optional whitespace), then capture an
  78. // identifier, then optional `?`, then `:`. Skip lines that look like
  79. // method declarations (`name(`) — those are TurboModule spec methods,
  80. // not view props.
  81. const regex = /^\s*([A-Za-z_][A-Za-z0-9_]*)\??\s*:/gm;
  82. let m: RegExpExecArray | null;
  83. while ((m = regex.exec(body)) !== null) {
  84. const name = m[1]!;
  85. // Exclude any line that immediately turns into a function-shape (e.g.
  86. // `onTap?: () => void` is fine — it's a prop, not a method body —
  87. // but a literal `name(arg: T): R` is a method declaration).
  88. const after = body.slice(m.index + m[0].length, m.index + m[0].length + 80);
  89. if (/^\s*\(/.test(after)) continue; // method-shape, skip
  90. props.push(name);
  91. }
  92. return props;
  93. }
  94. function extractFabricNodes(filePath: string, source: string): Node[] {
  95. if (!isFabricSpec(source)) return [];
  96. const now = Date.now();
  97. const nodes: Node[] = [];
  98. CODEGEN_DECL_RE.lastIndex = 0;
  99. let m: RegExpExecArray | null;
  100. while ((m = CODEGEN_DECL_RE.exec(source)) !== null) {
  101. const componentName = m[1]!;
  102. const before = source.slice(0, m.index);
  103. const startLine = before.split('\n').length;
  104. const startColumn = before.length - before.lastIndexOf('\n') - 1;
  105. // The component itself — kind: 'component' so the existing
  106. // reactJsxChildEdges synthesizer matches `<MyComponent>` JSX tags to
  107. // it (its name+kind filter is the gate).
  108. const componentId = `fabric-component:${filePath}:${componentName}:${startLine}`;
  109. nodes.push({
  110. id: componentId,
  111. kind: 'component',
  112. name: componentName,
  113. qualifiedName: `${filePath}::${componentName}`,
  114. filePath,
  115. // The spec file is .ts or .tsx; use the file's apparent language
  116. // by extension. Trim to a known Language value.
  117. language: filePath.endsWith('.tsx') ? 'tsx' : 'typescript',
  118. startLine,
  119. endLine: startLine,
  120. startColumn,
  121. endColumn: startColumn + 'codegenNativeComponent'.length,
  122. docstring: `Fabric/Codegen native component '${componentName}'`,
  123. signature: `codegenNativeComponent<NativeProps>('${componentName}')`,
  124. isExported: true,
  125. updatedAt: now,
  126. });
  127. }
  128. // Props from the NativeProps interface. These are not "method" semantic
  129. // — they're JS-visible bindings the consumer sets via JSX attributes —
  130. // so use `property` kind. (The JSX synthesizer doesn't currently
  131. // produce per-attribute edges, but surfacing the prop names as nodes
  132. // lets `codegraph_search('onFinishTransitioning')` discover them.)
  133. const body = findNativePropsBody(source);
  134. if (body) {
  135. const props = extractPropNames(body);
  136. for (const propName of props) {
  137. const propBefore = source.indexOf(propName, source.indexOf(body));
  138. const propLine =
  139. propBefore >= 0 ? source.slice(0, propBefore).split('\n').length : 1;
  140. nodes.push({
  141. id: `fabric-prop:${filePath}:${propName}:${propLine}`,
  142. kind: 'property',
  143. name: propName,
  144. qualifiedName: `${filePath}::NativeProps.${propName}`,
  145. filePath,
  146. language: filePath.endsWith('.tsx') ? 'tsx' : 'typescript',
  147. startLine: propLine,
  148. endLine: propLine,
  149. startColumn: 0,
  150. endColumn: propName.length,
  151. docstring: `Fabric NativeProps prop '${propName}'`,
  152. isExported: true,
  153. updatedAt: now,
  154. });
  155. }
  156. }
  157. return nodes;
  158. }
  159. export const fabricViewResolver: FrameworkResolver = {
  160. name: 'fabric-view',
  161. languages: ['typescript', 'tsx'],
  162. detect(context) {
  163. // Detect on package.json alone: an RN project has the dep. We
  164. // initially scanned for a `codegenNativeComponent` marker file too,
  165. // but on big repos (RNScreens has ~1500 source files; fabric specs
  166. // come alphabetically after FabricExample/ etc., past any reasonable
  167. // scan budget) the marker check times out and produces false-
  168. // negatives. Detect lightly, and let the per-file `extract()` decide
  169. // which files actually have Fabric specs — extract() is essentially
  170. // free on non-spec files (a short `includes('codegenNativeComponent')`).
  171. const pkg = context.readFile('package.json');
  172. return pkg ? /["']react-native["']\s*:/.test(pkg) : false;
  173. },
  174. extract(filePath, source): FrameworkExtractionResult {
  175. return {
  176. nodes: extractFabricNodes(filePath, source),
  177. references: [],
  178. };
  179. },
  180. resolve() {
  181. // The companion synthesizer (`fabricNativeImplEdges`) handles
  182. // cross-language edges; standard name resolution handles
  183. // <MyComponent> → component-node via the JSX synthesizer.
  184. return null;
  185. },
  186. };