main
md 704 lines 28.5 KB
Rendered Raw
1 # Rust Port Step 5: Babel Plugin (`babel-plugin-react-compiler-rust`)
2
3 ## Goal
4
5 Create a new, minimal Babel plugin package (`babel-plugin-react-compiler-rust`) that serves as a thin JavaScript shim over the Rust compiler. The JS side does only three things:
6
7 1. **Pre-filter**: Quick name-based scan for potential React functions (capitalized or hook-like names)
8 2. **Invoke Rust**: Serialize the Babel AST, scope info, and resolved options to JSON; call the Rust compiler via NAPI
9 3. **Apply result**: Replace the program AST with the Rust-returned AST and forward logger events
10
11 All complex logic — function detection, compilation mode decisions, directives, suppressions, gating rewrites, import insertion, outlined functions — lives in Rust. This ensures the logic is implemented once and reused across future OXC and SWC integrations.
12
13 **Current status**: Implementation complete. All entrypoint logic ported to Rust: compile_program orchestration, shouldSkipCompilation, findFunctionsToCompile, getReactFunctionType/getComponentOrHookLike (with all name heuristics, callsHooksOrCreatesJsx, returnsNonNode, isValidComponentParams), directive parsing, suppression detection/filtering, ProgramContext (uid generation, import tracking), gating rewrites, import insertion. The actual per-function compilation (compileFn) returns a skip event pending full pipeline implementation.
14
15 **Prerequisites**: [rust-port-0001-babel-ast.md](rust-port-0001-babel-ast.md) (complete), [rust-port-0002-scope-types.md](rust-port-0002-scope-types.md) (complete), core compilation pipeline in Rust (in progress).
16
17 ---
18
19 ## Architecture Overview
20
21 ```
22 ┌─────────────────────────────────────────────────────────┐
23 │ Babel │
24 │ │
25 │ 1. Parse source → Babel AST │
26 │ 2. babel-plugin-react-compiler-rust │
27 │ ┌─────────────────────────────────────────────┐ │
28 │ │ JS Shim (~50 lines) │ │
29 │ │ │ │
30 │ │ a) Pre-filter: any capitalized/hook fns? │ │
31 │ │ b) Pre-resolve: sources filter, reanimated,│ │
32 │ │ isDev → serializable options │ │
33 │ │ c) Extract scope tree (rust-port-0002) │ │
34 │ │ d) JSON.stringify(ast, scope, options) │ │
35 │ │ e) Call Rust via NAPI │ │
36 │ │ f) Parse result, forward logger events │ │
37 │ │ g) Replace program AST if changed │ │
38 │ └──────────────┬──────────────────────────────┘ │
39 │ │ JSON │
40 │ ┌──────────────▼──────────────────────────────┐ │
41 │ │ Rust Compiler (via napi-rs) │ │
42 │ │ │ │
43 │ │ - shouldSkipCompilation │ │
44 │ │ - findFunctionsToCompile │ │
45 │ │ (all compilation modes, directives, │ │
46 │ │ forwardRef/memo, suppressions, etc.) │ │
47 │ │ - compileFn (full pipeline) │ │
48 │ │ - gating rewrites │ │
49 │ │ - import insertion │ │
50 │ │ - outlined function insertion │ │
51 │ │ - panicThreshold handling │ │
52 │ │ │ │
53 │ │ Returns: modified AST | null + events │ │
54 │ └─────────────────────────────────────────────┘ │
55 │ │
56 │ 3. Babel continues with modified (or original) AST │
57 └─────────────────────────────────────────────────────────┘
58 ```
59
60 ### Why This Split
61
62 The guiding principle is **implement once in Rust, integrate thinly per tool**. The current TS plugin has ~1300 lines of complex entrypoint logic (`Program.ts`, `Imports.ts`, `Gating.ts`, `Suppression.ts`, `Reanimated.ts`, `Options.ts`). If this logic stayed in JS, it would need to be reimplemented for OXC and SWC integrations. By moving it all to Rust:
63
64 - **Babel shim**: ~50 lines of JS
65 - **Future OXC integration**: ~50 lines of Rust (native `Traverse` trait, serialize to same JSON format)
66 - **Future SWC integration**: ~50 lines of Rust (native `VisitMut` trait, serialize to same JSON format)
67
68 Each integration only needs to: (1) do a cheap pre-filter, (2) serialize AST + scope to the Babel JSON format, (3) call `compile()`, (4) apply the result.
69
70 ---
71
72 ## Rust Public API
73
74 The Rust compiler exposes a single entry point. This extends the existing planned API from `rust-port-notes.md` with structured results:
75
76 ```rust
77 /// Main entry point for the React Compiler.
78 ///
79 /// Receives a full program AST, scope information, and resolved options.
80 /// Returns a CompileResult containing either a modified AST or null,
81 /// along with structured logger events.
82 #[napi]
83 pub fn compile(
84 ast_json: String,
85 scope_json: String,
86 options_json: String,
87 ) -> napi::Result<String> {
88 let ast: babel_ast::File = serde_json::from_str(&ast_json)?;
89 let scope: ScopeInfo = serde_json::from_str(&scope_json)?;
90 let opts: PluginOptions = serde_json::from_str(&options_json)?;
91
92 let result = react_compiler::compile_program(ast, scope, opts);
93
94 Ok(serde_json::to_string(&result)?)
95 }
96 ```
97
98 ### Result Type
99
100 ```rust
101 #[derive(Serialize)]
102 #[serde(tag = "kind")]
103 pub enum CompileResult {
104 /// Compilation succeeded (or no functions needed compilation).
105 /// `ast` is None if no changes were made to the program.
106 Success {
107 ast: Option<babel_ast::File>,
108 events: Vec<LoggerEvent>,
109 },
110 /// A fatal error occurred and panicThreshold dictates it should throw.
111 /// The JS shim re-throws this as a CompilerError.
112 Error {
113 error: CompilerErrorInfo,
114 events: Vec<LoggerEvent>,
115 },
116 }
117
118 #[derive(Serialize)]
119 pub struct CompilerErrorInfo {
120 pub reason: String,
121 pub description: Option<String>,
122 pub details: Vec<CompilerErrorDetail>,
123 }
124 ```
125
126 ### Logger Events
127
128 Rust returns the same structured events as the current TS compiler. The JS shim forwards them to the user-provided logger:
129
130 ```rust
131 #[derive(Serialize)]
132 #[serde(tag = "kind")]
133 pub enum LoggerEvent {
134 CompileSuccess {
135 fn_loc: Option<SourceLocation>,
136 fn_name: Option<String>,
137 memo_slots: u32,
138 memo_blocks: u32,
139 memo_values: u32,
140 pruned_memo_blocks: u32,
141 pruned_memo_values: u32,
142 },
143 CompileError {
144 fn_loc: Option<SourceLocation>,
145 detail: CompilerErrorDetail,
146 },
147 CompileSkip {
148 fn_loc: Option<SourceLocation>,
149 reason: String,
150 loc: Option<SourceLocation>,
151 },
152 CompileUnexpectedThrow {
153 fn_loc: Option<SourceLocation>,
154 data: String,
155 },
156 PipelineError {
157 fn_loc: Option<SourceLocation>,
158 data: String,
159 },
160 // Note: Timing events are handled on the JS side (performance.mark/measure)
161 }
162 ```
163
164 ---
165
166 ## Resolved Options
167
168 Options that involve JS functions or runtime checks (like `sources` filter, Reanimated detection) cannot cross the NAPI boundary. The JS shim pre-resolves these before calling Rust:
169
170 ### JS-Side Resolution
171
172 | Option | JS Resolves | Rust Receives |
173 |--------|------------|---------------|
174 | `sources` | Calls `sources(filename)` or checks string array | `should_compile: bool` |
175 | `enableReanimatedCheck` | Calls `pipelineUsesReanimatedPlugin()` | `enable_reanimated: bool` |
176 | `isDev` (for `enableResetCacheOnSourceFileChanges`) | Checks `__DEV__` / `NODE_ENV` | `is_dev: bool` |
177 | `logger` | Kept on JS side | Not sent (events returned instead) |
178
179 ### Serializable Options (Passed Directly to Rust)
180
181 ```typescript
182 // Options that serialize directly to Rust
183 interface RustPluginOptions {
184 // Pre-resolved by JS
185 shouldCompile: boolean;
186 enableReanimated: boolean;
187 isDev: boolean;
188 filename: string | null;
189
190 // Passed through as-is
191 compilationMode: 'infer' | 'syntax' | 'annotation' | 'all';
192 panicThreshold: 'all_errors' | 'critical_errors' | 'none';
193 target: '17' | '18' | '19' | { kind: 'donotuse_meta_internal'; runtimeModule: string };
194 gating: { source: string; importSpecifierName: string } | null;
195 dynamicGating: { source: string } | null;
196 noEmit: boolean;
197 outputMode: 'ssr' | 'client' | 'lint' | null;
198 eslintSuppressionRules: string[] | null;
199 flowSuppressions: boolean;
200 ignoreUseNoForget: boolean;
201 customOptOutDirectives: string[] | null;
202 environment: EnvironmentConfig;
203 }
204 ```
205
206 ---
207
208 ## JS Shim: `babel-plugin-react-compiler-rust`
209
210 ### Package Structure
211
212 ```
213 compiler/packages/babel-plugin-react-compiler-rust/
214 package.json
215 tsconfig.json
216 src/
217 index.ts # Babel plugin entry point (main export)
218 BabelPlugin.ts # Program visitor, pre-filter, bridge call
219 prefilter.ts # Name-based React function detection
220 bridge.ts # NAPI invocation, JSON serialization
221 scope.ts # Babel scope → ScopeInfo extraction (from rust-port-0002)
222 options.ts # Option resolution (pre-resolve JS-only options)
223 ```
224
225 ### `BabelPlugin.ts` — Babel Plugin Entry Point
226
227 ```typescript
228 import type * as BabelCore from '@babel/core';
229 import {hasReactLikeFunctions} from './prefilter';
230 import {compileWithRust} from './bridge';
231 import {extractScopeInfo} from './scope';
232 import {resolveOptions, type PluginOptions} from './options';
233
234 export default function BabelPluginReactCompilerRust(
235 _babel: typeof BabelCore,
236 ): BabelCore.PluginObj {
237 return {
238 name: 'react-compiler-rust',
239 visitor: {
240 Program: {
241 enter(prog, pass): void {
242 const filename = pass.filename ?? null;
243
244 // Step 1: Resolve options (pre-resolve JS-only values)
245 const opts = resolveOptions(pass.opts, pass.file, filename);
246
247 // Step 2: Quick bail — should we compile this file at all?
248 if (!opts.shouldCompile) {
249 return;
250 }
251
252 // Step 3: Pre-filter — any potential React functions?
253 if (!hasReactLikeFunctions(prog)) {
254 return;
255 }
256
257 // Step 4: Extract scope info
258 const scopeInfo = extractScopeInfo(prog);
259
260 // Step 5: Call Rust compiler
261 const result = compileWithRust(
262 prog.node,
263 scopeInfo,
264 opts,
265 pass.file.ast.comments ?? [],
266 );
267
268 // Step 6: Forward logger events
269 if (pass.opts.logger && result.events) {
270 for (const event of result.events) {
271 pass.opts.logger.logEvent(filename, event);
272 }
273 }
274
275 // Step 7: Handle result
276 if (result.kind === 'error') {
277 // panicThreshold triggered — throw
278 const err = new Error(result.error.reason);
279 // Attach details for CompilerError compatibility
280 (err as any).details = result.error.details;
281 throw err;
282 }
283
284 if (result.ast != null) {
285 // Replace the entire program body with Rust's output
286 prog.replaceWith(result.ast);
287 prog.skip(); // Don't re-traverse
288 }
289 },
290 },
291 },
292 };
293 }
294 ```
295
296 ### `prefilter.ts` — Name-Based Pre-Filter
297
298 The pre-filter is intentionally loose. It checks only whether any function in the program has a name that *could* be a React component or hook. False positives (like `ParseURL` or `FormatDate`) are acceptable — Rust will quickly determine these aren't React functions and return `null`.
299
300 ```typescript
301 import {NodePath} from '@babel/core';
302 import * as t from '@babel/types';
303
304 /**
305 * Quick check: does this program contain any functions with names that
306 * could be React components (capitalized) or hooks (useXxx)?
307 *
308 * This is intentionally loose — Rust handles the precise detection.
309 * We just want to avoid serializing files that definitely have no
310 * React functions (e.g., pure utility modules, CSS-in-JS, configs).
311 */
312 export function hasReactLikeFunctions(
313 program: NodePath<t.Program>,
314 ): boolean {
315 let found = false;
316 program.traverse({
317 // Skip classes — their methods are not compiled
318 ClassDeclaration(path) { path.skip(); },
319 ClassExpression(path) { path.skip(); },
320
321 FunctionDeclaration(path) {
322 if (found) return;
323 const name = path.node.id?.name;
324 if (name && isReactLikeName(name)) {
325 found = true;
326 path.stop();
327 }
328 },
329 FunctionExpression(path) {
330 if (found) return;
331 const name = inferFunctionName(path);
332 if (name && isReactLikeName(name)) {
333 found = true;
334 path.stop();
335 }
336 },
337 ArrowFunctionExpression(path) {
338 if (found) return;
339 const name = inferFunctionName(path);
340 if (name && isReactLikeName(name)) {
341 found = true;
342 path.stop();
343 }
344 },
345 });
346 return found;
347 }
348
349 function isReactLikeName(name: string): boolean {
350 return /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name);
351 }
352
353 /**
354 * Infer the name of an anonymous function expression from its parent
355 * (e.g., `const Foo = () => {}` → 'Foo').
356 */
357 function inferFunctionName(
358 path: NodePath<t.FunctionExpression | t.ArrowFunctionExpression>,
359 ): string | null {
360 const parent = path.parentPath;
361 if (
362 parent.isVariableDeclarator() &&
363 parent.get('init').node === path.node &&
364 parent.get('id').isIdentifier()
365 ) {
366 return (parent.get('id').node as t.Identifier).name;
367 }
368 if (
369 parent.isAssignmentExpression() &&
370 parent.get('right').node === path.node &&
371 parent.get('left').isIdentifier()
372 ) {
373 return (parent.get('left').node as t.Identifier).name;
374 }
375 return null;
376 }
377 ```
378
379 ### `bridge.ts` — NAPI Bridge
380
381 ```typescript
382 // The napi-rs generated binding
383 import {compile as rustCompile} from '../native';
384
385 import type {ResolvedOptions} from './options';
386 import type {ScopeInfo} from './scope';
387 import type * as t from '@babel/types';
388
389 export interface CompileSuccess {
390 kind: 'success';
391 ast: t.Program | null;
392 events: Array<LoggerEvent>;
393 }
394
395 export interface CompileError {
396 kind: 'error';
397 error: {
398 reason: string;
399 description?: string;
400 details: Array<unknown>;
401 };
402 events: Array<LoggerEvent>;
403 }
404
405 export type CompileResult = CompileSuccess | CompileError;
406
407 export type LoggerEvent = {
408 kind: string;
409 [key: string]: unknown;
410 };
411
412 export function compileWithRust(
413 ast: t.Program,
414 scopeInfo: ScopeInfo,
415 options: ResolvedOptions,
416 comments: Array<t.Comment>,
417 ): CompileResult {
418 // Attach comments to the AST for Rust (Babel stores them separately)
419 const astWithComments = {...ast, comments};
420
421 const resultJson = rustCompile(
422 JSON.stringify(astWithComments),
423 JSON.stringify(scopeInfo),
424 JSON.stringify(options),
425 );
426
427 return JSON.parse(resultJson) as CompileResult;
428 }
429 ```
430
431 ### `options.ts` — Option Resolution
432
433 ```typescript
434 import type * as BabelCore from '@babel/core';
435 import {
436 pipelineUsesReanimatedPlugin,
437 injectReanimatedFlag,
438 } from './reanimated'; // Thin copy or import from existing
439
440 export interface ResolvedOptions {
441 // Pre-resolved by JS
442 shouldCompile: boolean;
443 enableReanimated: boolean;
444 isDev: boolean;
445 filename: string | null;
446
447 // Pass-through
448 compilationMode: string;
449 panicThreshold: string;
450 target: unknown;
451 gating: unknown;
452 dynamicGating: unknown;
453 noEmit: boolean;
454 outputMode: string | null;
455 eslintSuppressionRules: string[] | null;
456 flowSuppressions: boolean;
457 ignoreUseNoForget: boolean;
458 customOptOutDirectives: string[] | null;
459 environment: Record<string, unknown>;
460 }
461
462 export type PluginOptions = Partial<ResolvedOptions> & Record<string, unknown>;
463
464 export function resolveOptions(
465 rawOpts: PluginOptions,
466 file: BabelCore.BabelFile,
467 filename: string | null,
468 ): ResolvedOptions {
469 // Resolve sources filter (may be a function)
470 let shouldCompile = true;
471 if (rawOpts.sources != null && filename != null) {
472 if (typeof rawOpts.sources === 'function') {
473 shouldCompile = rawOpts.sources(filename);
474 } else if (Array.isArray(rawOpts.sources)) {
475 shouldCompile = rawOpts.sources.some(
476 (prefix: string) => filename.indexOf(prefix) !== -1,
477 );
478 }
479 } else if (rawOpts.sources != null && filename == null) {
480 shouldCompile = false; // sources specified but no filename
481 }
482
483 // Resolve reanimated check
484 const enableReanimated =
485 (rawOpts.enableReanimatedCheck !== false) &&
486 pipelineUsesReanimatedPlugin(file.opts.plugins);
487
488 // Resolve isDev
489 const isDev =
490 (typeof __DEV__ !== 'undefined' && __DEV__ === true) ||
491 process.env['NODE_ENV'] === 'development';
492
493 return {
494 shouldCompile,
495 enableReanimated,
496 isDev,
497 filename,
498 compilationMode: rawOpts.compilationMode ?? 'infer',
499 panicThreshold: rawOpts.panicThreshold ?? 'none',
500 target: rawOpts.target ?? '19',
501 gating: rawOpts.gating ?? null,
502 dynamicGating: rawOpts.dynamicGating ?? null,
503 noEmit: rawOpts.noEmit ?? false,
504 outputMode: rawOpts.outputMode ?? null,
505 eslintSuppressionRules: rawOpts.eslintSuppressionRules ?? null,
506 flowSuppressions: rawOpts.flowSuppressions ?? true,
507 ignoreUseNoForget: rawOpts.ignoreUseNoForget ?? false,
508 customOptOutDirectives: rawOpts.customOptOutDirectives ?? null,
509 environment: rawOpts.environment ?? {},
510 };
511 }
512 ```
513
514 ---
515
516 ## What Rust Implements (from `Program.ts` and friends)
517
518 The following logic moves entirely from the TS entrypoint into Rust. Rust operates on the deserialized Babel AST and scope info, and returns a modified AST.
519
520 ### From `Program.ts`
521
522 | Function | What It Does | Rust Module |
523 |----------|-------------|-------------|
524 | `shouldSkipCompilation` | Check sources filter (pre-resolved), check for existing `c` import from runtime module | `entrypoint/program.rs` |
525 | `findFunctionsToCompile` | Traverse program, skip classes, apply compilation mode, call `getReactFunctionType` | `entrypoint/program.rs` |
526 | `getReactFunctionType` | Determine if a function is Component/Hook/Other based on compilation mode, names, directives | `entrypoint/program.rs` |
527 | `getComponentOrHookLike` | Name-based heuristics + `callsHooksOrCreatesJsx` + `isValidComponentParams` + `returnsNonNode` + `isForwardRefCallback` + `isMemoCallback` | `entrypoint/program.rs` |
528 | `processFn` | Per-function: check directives (opt-in/opt-out), compile, check output mode | `entrypoint/program.rs` |
529 | `tryCompileFunction` | Check suppressions, call `compileFn`, handle errors | `entrypoint/program.rs` |
530 | `applyCompiledFunctions` | Replace original functions with compiled versions, handle gating, insert outlined functions | `entrypoint/program.rs` |
531 | `createNewFunctionNode` | Build replacement AST node matching original function type | `entrypoint/program.rs` |
532 | `handleError` / `logError` | Apply panicThreshold, log to events | `entrypoint/program.rs` |
533
534 ### From `Imports.ts`
535
536 | Function | What It Does | Rust Module |
537 |----------|-------------|-------------|
538 | `ProgramContext` | Track compiled functions, generate unique names, manage imports | `entrypoint/imports.rs` |
539 | `addImportsToProgram` | Insert import declarations (or require calls) into program body | `entrypoint/imports.rs` |
540 | `validateRestrictedImports` | Check for blocklisted import modules | `entrypoint/imports.rs` |
541
542 ### From `Gating.ts`
543
544 | Function | What It Does | Rust Module |
545 |----------|-------------|-------------|
546 | `insertGatedFunctionDeclaration` | Rewrite function with gating conditional (optimized vs unoptimized) | `entrypoint/gating.rs` |
547 | `insertAdditionalFunctionDeclaration` | Handle hoisted function declarations referenced before declaration | `entrypoint/gating.rs` |
548
549 ### From `Suppression.ts`
550
551 | Function | What It Does | Rust Module |
552 |----------|-------------|-------------|
553 | `findProgramSuppressions` | Parse eslint-disable/enable and Flow suppression comments | `entrypoint/suppression.rs` |
554 | `filterSuppressionsThatAffectFunction` | Check if suppression ranges overlap a function | `entrypoint/suppression.rs` |
555 | `suppressionsToCompilerError` | Convert suppressions to compiler errors | `entrypoint/suppression.rs` |
556
557 ### From `Reanimated.ts`
558
559 | Function | What It Does | Rust Module |
560 |----------|-------------|-------------|
561 | `injectReanimatedFlag` | Set `enableCustomTypeDefinitionForReanimated` in environment config | Pre-resolved by JS; Rust receives `enableReanimated: bool` |
562 | `pipelineUsesReanimatedPlugin` | Check if reanimated babel plugin is present | Pre-resolved by JS |
563
564 ### From `Options.ts`
565
566 | Function | What It Does | Rust Module |
567 |----------|-------------|-------------|
568 | `parsePluginOptions` | Validate and parse plugin options | JS resolves, Rust re-validates serializable subset |
569 | Option types and schemas | Zod schemas for options | Rust serde types with validation |
570 | `LoggerEvent` types | Event type definitions | Rust enum (serialized back to JS) |
571
572 ---
573
574 ## NAPI Bridge Details
575
576 ### Technology: napi-rs
577
578 The bridge uses [napi-rs](https://napi.rs/) to expose the Rust `compile` function to Node.js. This is the same approach used by SWC (`@swc/core`), Biome, and other Rust-based JS tools.
579
580 ### Serialization: JSON Strings
581
582 The bridge passes JSON strings across the NAPI boundary. This is the simplest approach and provides several benefits:
583
584 - **Debuggable**: JSON can be logged, inspected, and round-trip tested
585 - **Consistent with existing infrastructure**: The `react_compiler_ast` crate already handles JSON serde with all 1714 test fixtures passing
586 - **No schema coupling**: The JS side doesn't need generated bindings — just `JSON.stringify`/`JSON.parse`
587 - **Adequate performance**: For file-level granularity (one call per file), JSON serialization overhead is negligible compared to compilation time
588
589 ### Performance Considerations
590
591 The JSON serialization adds overhead, but it is bounded:
592
593 - **Serialization**: `JSON.stringify` of a typical program AST: ~1-5ms
594 - **Deserialization in Rust**: `serde_json::from_str`: ~1-5ms
595 - **Re-serialization in Rust**: `serde_json::to_string` of result: ~1-5ms
596 - **Parse in JS**: `JSON.parse` of result: ~1-5ms
597 - **Total overhead**: ~4-20ms per file
598 - **Compilation time**: Typically 50-500ms per file
599
600 The serialization overhead is 2-10% of total time. If this becomes a bottleneck, a future optimization could use `Buffer` passing with a binary format, but JSON is the right starting point.
601
602 ### Native Module Structure
603
604 ```
605 compiler/packages/babel-plugin-react-compiler-rust/
606 native/
607 Cargo.toml # napi-rs crate
608 src/
609 lib.rs # #[napi] compile function
610 build.rs # napi-rs build script
611 npm/ # Platform-specific npm packages (generated by napi-rs)
612 darwin-arm64/
613 darwin-x64/
614 linux-x64-gnu/
615 win32-x64-msvc/
616 ...
617 ```
618
619 ---
620
621 ## What Stays in JS vs What Moves to Rust
622
623 ### JS Side (Thin Shim)
624
625 | Responsibility | Reason it stays in JS |
626 |---------------|----------------------|
627 | Pre-filter (name-based scan) | Avoids serialization for files with no React functions |
628 | Resolve `sources` filter | May be a JS function (not serializable) |
629 | Resolve Reanimated check | Requires `require.resolve` and Babel plugin list inspection |
630 | Resolve `isDev` | Requires `process.env` / `__DEV__` access |
631 | Extract scope info | Requires Babel scope API |
632 | Serialize AST/scope/options | Bridge responsibility |
633 | Forward logger events | Logger is a JS callback |
634 | Throw on fatal errors | JS exception mechanism |
635 | Replace program AST | Babel `path.replaceWith` API |
636 | Performance timing | `performance.mark/measure` API |
637
638 ### Rust Side (Everything Else)
639
640 | Responsibility | Current TS Location |
641 |---------------|-------------------|
642 | `shouldSkipCompilation` (non-sources checks) | `Program.ts:782-816` |
643 | `findFunctionsToCompile` | `Program.ts:495-559` |
644 | `getReactFunctionType` | `Program.ts:818-864` |
645 | `getComponentOrHookLike` | `Program.ts:1049-1078` |
646 | All name/param/return heuristics | `Program.ts:897-1164` |
647 | `forwardRef`/`memo` detection | `Program.ts:951-970` |
648 | Directive parsing (`use memo`, `use no memo`, `use memo if(...)`) | `Program.ts:47-144` |
649 | Suppression detection and filtering | `Suppression.ts` (all) |
650 | Per-function compilation (`compileFn`) | `Pipeline.ts` |
651 | Gating rewrites | `Gating.ts` (all) |
652 | Import generation and insertion | `Imports.ts:225-306` |
653 | Outlined function insertion | `Program.ts:283-329` |
654 | `ProgramContext` (uid gen, import tracking) | `Imports.ts:64-209` |
655 | Error handling / panicThreshold | `Program.ts:146-222` |
656 | Option validation | `Options.ts:324-403` |
657
658 ---
659
660 ## Cross-Tool Strategy (OXC, SWC)
661
662 This architecture is designed to support future OXC and SWC integrations with minimal per-tool code.
663
664 ### Common Boundary: Babel JSON AST
665
666 All integrations serialize to the same Babel JSON AST format that the `react_compiler_ast` crate expects. This means:
667
668 - **OXC integration**: A Rust transform that converts OXC's native AST → Babel JSON AST → calls `compile()` → converts result back to OXC AST. Since both are Rust, this can use the struct types directly (no JSON step needed for the Rust→Rust path — just type conversion).
669 - **SWC integration**: A Rust transform (native or WASM plugin) that converts SWC's AST → Babel JSON AST → calls `compile()` → converts result back.
670
671 ### Scope Abstraction
672
673 Each tool provides scope information differently:
674 - **Babel**: Scope tree object graph (extracted by JS, serialized to `ScopeInfo`)
675 - **OXC**: `ScopeTree` + `SymbolTable` from `oxc_semantic` (Rust-native, converted to `ScopeInfo`)
676 - **SWC**: Hygiene system (`SyntaxContext`/`Mark`) — requires building a scope tree equivalent
677
678 The `ScopeInfo` type from `rust-port-0002` serves as the common abstraction. Each integration extracts its tool's scope model into this format.
679
680 ### Integration Size Comparison
681
682 | Tool | Integration Code | Where Logic Lives |
683 |------|-----------------|-------------------|
684 | Babel (this doc) | ~50 lines JS + NAPI bridge | Rust |
685 | OXC (future) | ~100 lines Rust (AST conversion) | Rust |
686 | SWC (future) | ~100 lines Rust (AST conversion + scope extraction) | Rust |
687
688 ---
689
690 ## Differences from Current TS Plugin
691
692 ### Behavioral Equivalence
693
694 The Rust plugin must produce identical output to the TS plugin for all inputs. The existing test infrastructure (`yarn snap`) can be used to verify this by running both plugins on the same fixtures and comparing output.
695
696 ### Known Differences
697
698 1. **Timing events**: Handled on the JS side using `performance.mark/measure` (not sent to Rust). The JS shim wraps the Rust call with timing markers.
699
700 2. **`CompilerError` class**: Rust returns a plain JSON error object. The JS shim constructs a `CompilerError`-compatible exception for Babel's error reporting.
701
702 3. **`debugLogIRs` logger callback**: This optional callback receives intermediate compiler pipeline values. Rust would need to serialize these if supported. **Decision**: Defer to a follow-up; not needed for initial parity.
703
704 4. **Comments handling**: Babel stores comments separately on `file.ast.comments`, not attached to AST nodes. The JS shim attaches comments to the program AST before serializing. Rust uses them for suppression detection.