1# Rust Port Step 5: Babel Plugin (`babel-plugin-react-compiler-rust`)23## Goal45Create 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:671. **Pre-filter**: Quick name-based scan for potential React functions (capitalized or hook-like names)82. **Invoke Rust**: Serialize the Babel AST, scope info, and resolved options to JSON; call the Rust compiler via NAPI93. **Apply result**: Replace the program AST with the Rust-returned AST and forward logger events1011All 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.1213**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.1415**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).1617---1819## Architecture Overview2021```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```5960### Why This Split6162The 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:6364- **Babel shim**: ~50 lines of JS65- **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)6768Each 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.6970---7172## Rust Public API7374The Rust compiler exposes a single entry point. This extends the existing planned API from `rust-port-notes.md` with structured results:7576```rust77/// 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]83pub 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)?;9192 let result = react_compiler::compile_program(ast, scope, opts);9394 Ok(serde_json::to_string(&result)?)95}96```9798### Result Type99100```rust101#[derive(Serialize)]102#[serde(tag = "kind")]103pub 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}117118#[derive(Serialize)]119pub struct CompilerErrorInfo {120 pub reason: String,121 pub description: Option<String>,122 pub details: Vec<CompilerErrorDetail>,123}124```125126### Logger Events127128Rust returns the same structured events as the current TS compiler. The JS shim forwards them to the user-provided logger:129130```rust131#[derive(Serialize)]132#[serde(tag = "kind")]133pub 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```163164---165166## Resolved Options167168Options 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:169170### JS-Side Resolution171172| 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) |178179### Serializable Options (Passed Directly to Rust)180181```typescript182// Options that serialize directly to Rust183interface RustPluginOptions {184 // Pre-resolved by JS185 shouldCompile: boolean;186 enableReanimated: boolean;187 isDev: boolean;188 filename: string | null;189190 // Passed through as-is191 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```205206---207208## JS Shim: `babel-plugin-react-compiler-rust`209210### Package Structure211212```213compiler/packages/babel-plugin-react-compiler-rust/214 package.json215 tsconfig.json216 src/217 index.ts # Babel plugin entry point (main export)218 BabelPlugin.ts # Program visitor, pre-filter, bridge call219 prefilter.ts # Name-based React function detection220 bridge.ts # NAPI invocation, JSON serialization221 scope.ts # Babel scope → ScopeInfo extraction (from rust-port-0002)222 options.ts # Option resolution (pre-resolve JS-only options)223```224225### `BabelPlugin.ts` — Babel Plugin Entry Point226227```typescript228import type * as BabelCore from '@babel/core';229import {hasReactLikeFunctions} from './prefilter';230import {compileWithRust} from './bridge';231import {extractScopeInfo} from './scope';232import {resolveOptions, type PluginOptions} from './options';233234export 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;243244 // Step 1: Resolve options (pre-resolve JS-only values)245 const opts = resolveOptions(pass.opts, pass.file, filename);246247 // Step 2: Quick bail — should we compile this file at all?248 if (!opts.shouldCompile) {249 return;250 }251252 // Step 3: Pre-filter — any potential React functions?253 if (!hasReactLikeFunctions(prog)) {254 return;255 }256257 // Step 4: Extract scope info258 const scopeInfo = extractScopeInfo(prog);259260 // Step 5: Call Rust compiler261 const result = compileWithRust(262 prog.node,263 scopeInfo,264 opts,265 pass.file.ast.comments ?? [],266 );267268 // Step 6: Forward logger events269 if (pass.opts.logger && result.events) {270 for (const event of result.events) {271 pass.opts.logger.logEvent(filename, event);272 }273 }274275 // Step 7: Handle result276 if (result.kind === 'error') {277 // panicThreshold triggered — throw278 const err = new Error(result.error.reason);279 // Attach details for CompilerError compatibility280 (err as any).details = result.error.details;281 throw err;282 }283284 if (result.ast != null) {285 // Replace the entire program body with Rust's output286 prog.replaceWith(result.ast);287 prog.skip(); // Don't re-traverse288 }289 },290 },291 },292 };293}294```295296### `prefilter.ts` — Name-Based Pre-Filter297298The 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`.299300```typescript301import {NodePath} from '@babel/core';302import * as t from '@babel/types';303304/**305 * Quick check: does this program contain any functions with names that306 * 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 no310 * React functions (e.g., pure utility modules, CSS-in-JS, configs).311 */312export function hasReactLikeFunctions(313 program: NodePath<t.Program>,314): boolean {315 let found = false;316 program.traverse({317 // Skip classes — their methods are not compiled318 ClassDeclaration(path) { path.skip(); },319 ClassExpression(path) { path.skip(); },320321 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}348349function isReactLikeName(name: string): boolean {350 return /^[A-Z]/.test(name) || /^use[A-Z0-9]/.test(name);351}352353/**354 * Infer the name of an anonymous function expression from its parent355 * (e.g., `const Foo = () => {}` → 'Foo').356 */357function 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```378379### `bridge.ts` — NAPI Bridge380381```typescript382// The napi-rs generated binding383import {compile as rustCompile} from '../native';384385import type {ResolvedOptions} from './options';386import type {ScopeInfo} from './scope';387import type * as t from '@babel/types';388389export interface CompileSuccess {390 kind: 'success';391 ast: t.Program | null;392 events: Array<LoggerEvent>;393}394395export interface CompileError {396 kind: 'error';397 error: {398 reason: string;399 description?: string;400 details: Array<unknown>;401 };402 events: Array<LoggerEvent>;403}404405export type CompileResult = CompileSuccess | CompileError;406407export type LoggerEvent = {408 kind: string;409 [key: string]: unknown;410};411412export 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};420421 const resultJson = rustCompile(422 JSON.stringify(astWithComments),423 JSON.stringify(scopeInfo),424 JSON.stringify(options),425 );426427 return JSON.parse(resultJson) as CompileResult;428}429```430431### `options.ts` — Option Resolution432433```typescript434import type * as BabelCore from '@babel/core';435import {436 pipelineUsesReanimatedPlugin,437 injectReanimatedFlag,438} from './reanimated'; // Thin copy or import from existing439440export interface ResolvedOptions {441 // Pre-resolved by JS442 shouldCompile: boolean;443 enableReanimated: boolean;444 isDev: boolean;445 filename: string | null;446447 // Pass-through448 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}461462export type PluginOptions = Partial<ResolvedOptions> & Record<string, unknown>;463464export 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 filename481 }482483 // Resolve reanimated check484 const enableReanimated =485 (rawOpts.enableReanimatedCheck !== false) &&486 pipelineUsesReanimatedPlugin(file.opts.plugins);487488 // Resolve isDev489 const isDev =490 (typeof __DEV__ !== 'undefined' && __DEV__ === true) ||491 process.env['NODE_ENV'] === 'development';492493 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```513514---515516## What Rust Implements (from `Program.ts` and friends)517518The 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.519520### From `Program.ts`521522| 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` |533534### From `Imports.ts`535536| 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` |541542### From `Gating.ts`543544| 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` |548549### From `Suppression.ts`550551| 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` |556557### From `Reanimated.ts`558559| 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 |563564### From `Options.ts`565566| 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) |571572---573574## NAPI Bridge Details575576### Technology: napi-rs577578The 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.579580### Serialization: JSON Strings581582The bridge passes JSON strings across the NAPI boundary. This is the simplest approach and provides several benefits:583584- **Debuggable**: JSON can be logged, inspected, and round-trip tested585- **Consistent with existing infrastructure**: The `react_compiler_ast` crate already handles JSON serde with all 1714 test fixtures passing586- **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 time588589### Performance Considerations590591The JSON serialization adds overhead, but it is bounded:592593- **Serialization**: `JSON.stringify` of a typical program AST: ~1-5ms594- **Deserialization in Rust**: `serde_json::from_str`: ~1-5ms595- **Re-serialization in Rust**: `serde_json::to_string` of result: ~1-5ms596- **Parse in JS**: `JSON.parse` of result: ~1-5ms597- **Total overhead**: ~4-20ms per file598- **Compilation time**: Typically 50-500ms per file599600The 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.601602### Native Module Structure603604```605compiler/packages/babel-plugin-react-compiler-rust/606 native/607 Cargo.toml # napi-rs crate608 src/609 lib.rs # #[napi] compile function610 build.rs # napi-rs build script611 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```618619---620621## What Stays in JS vs What Moves to Rust622623### JS Side (Thin Shim)624625| 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 |637638### Rust Side (Everything Else)639640| 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` |657658---659660## Cross-Tool Strategy (OXC, SWC)661662This architecture is designed to support future OXC and SWC integrations with minimal per-tool code.663664### Common Boundary: Babel JSON AST665666All integrations serialize to the same Babel JSON AST format that the `react_compiler_ast` crate expects. This means:667668- **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.670671### Scope Abstraction672673Each 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 equivalent677678The `ScopeInfo` type from `rust-port-0002` serves as the common abstraction. Each integration extracts its tool's scope model into this format.679680### Integration Size Comparison681682| 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 |687688---689690## Differences from Current TS Plugin691692### Behavioral Equivalence693694The 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.695696### Known Differences6976981. **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.6997002. **`CompilerError` class**: Rust returns a plain JSON error object. The JS shim constructs a `CompilerError`-compatible exception for Babel's error reporting.7017023. **`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.7037044. **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.
Findings
✓ No findings reported for this file.