main
ts 285 lines 10.1 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import type * as BabelCore from '@babel/core';
9 import {hasReactLikeFunctions} from './prefilter';
10 import {compileWithRust, type BindingRenameInfo} from './bridge';
11 import {extractScopeInfo} from './scope';
12 import {resolveOptions, type PluginOptions} from './options';
13
14 export default function BabelPluginReactCompilerRust(
15 _babel: typeof BabelCore,
16 ): BabelCore.PluginObj {
17 let compiledProgram = false;
18 return {
19 name: 'react-compiler-rust',
20 visitor: {
21 Program: {
22 enter(prog, pass): void {
23 // Guard against re-entry: replaceWith() below causes Babel
24 // to re-traverse the new Program, which would re-trigger this
25 // handler. Skip if we've already compiled.
26 if (compiledProgram) {
27 return;
28 }
29 compiledProgram = true;
30 const filename = pass.filename ?? null;
31
32 // Step 1: Resolve options (pre-resolve JS-only values)
33 const opts = resolveOptions(
34 pass.opts as PluginOptions,
35 pass.file,
36 filename,
37 pass.file.ast,
38 );
39
40 // Step 2: Quick bail — should we compile this file at all?
41 if (!opts.shouldCompile) {
42 return;
43 }
44
45 // Step 3: Pre-filter — any potential React functions?
46 // Skip prefilter when compilationMode is 'all' (compiles all functions)
47 if (opts.compilationMode !== 'all' && !hasReactLikeFunctions(prog)) {
48 return;
49 }
50
51 // Step 4: Extract scope info
52 const logger = (pass.opts as PluginOptions).logger;
53 let scopeInfo;
54 try {
55 scopeInfo = extractScopeInfo(prog);
56 } catch (e) {
57 // Scope extraction can fail on unsupported syntax (e.g., reserved
58 // word as binding name). Report as CompileUnexpectedThrow +
59 // CompileError, matching TS compiler behavior.
60 const errMsg = e instanceof Error ? e.message : String(e);
61 const dotIdx = errMsg.indexOf('. ');
62 const reason = dotIdx >= 0 ? errMsg.substring(0, dotIdx) : errMsg;
63 let description: string | undefined =
64 dotIdx >= 0 ? errMsg.substring(dotIdx + 2) : undefined;
65 if (description?.endsWith('.')) {
66 description = description.slice(0, -1);
67 }
68 if (logger) {
69 logger.logEvent(filename, {
70 kind: 'CompileUnexpectedThrow',
71 data: `Error: ${errMsg}`,
72 });
73 logger.logEvent(filename, {
74 kind: 'CompileError',
75 detail: {
76 reason,
77 severity: 'Error',
78 category: 'Syntax',
79 description: description ?? null,
80 suggestions: null,
81 details: [
82 {
83 kind: 'error',
84 loc: null,
85 message: 'reserved word',
86 },
87 ],
88 },
89 });
90 }
91 const panicThreshold = (pass.opts as PluginOptions).panicThreshold;
92 if (
93 panicThreshold === 'all_errors' ||
94 panicThreshold === 'critical_errors'
95 ) {
96 const heading = 'Error';
97 const parts = [`${heading}: ${reason}`];
98 if (description != null) {
99 parts.push(`\n\n${description}.`);
100 }
101 const formatted = `Found 1 error:\n\n${parts.join('')}`;
102 const err = new Error(formatted);
103 (err as any).details = [];
104 throw err;
105 }
106 return;
107 }
108
109 // Step 5: Call Rust compiler
110 const optsForRust =
111 (logger as any)?.debugLogIRs != null
112 ? {...opts, __debug: true}
113 : opts;
114 const result = compileWithRust(
115 pass.file.ast,
116 scopeInfo,
117 optsForRust,
118 pass.file.code ?? null,
119 );
120
121 // Step 6: Forward logger events and debug logs via orderedLog
122 if (logger && result.orderedLog && result.orderedLog.length > 0) {
123 for (const item of result.orderedLog) {
124 if (item.type === 'event') {
125 logger.logEvent(filename, item.event);
126 } else if (item.type === 'debug' && logger.debugLogIRs) {
127 logger.debugLogIRs(item.entry);
128 }
129 }
130 } else if (logger && result.events) {
131 for (const event of result.events) {
132 logger.logEvent(filename, event);
133 }
134 }
135
136 // Step 7: Handle result
137 if (result.kind === 'error') {
138 const message =
139 (result.error as any).rawMessage ??
140 (result.error as any).formattedMessage ??
141 'Unexpected compiler error';
142 const err = new Error(message);
143 (err as any).details = result.error.details;
144 throw err;
145 }
146
147 if (result.ast != null) {
148 // Replace the program with Rust's compiled output.
149 const newFile = result.ast as any;
150 const newProgram = newFile.program ?? newFile;
151
152 // After JSON round-tripping through Rust, comment objects that were
153 // shared by reference in Babel's AST (e.g., a comment between two
154 // statements appears as trailingComments on stmt A and leadingComments
155 // on stmt B, sharing the same JS object) become separate objects.
156 // Babel's generator uses reference identity to avoid printing the
157 // same comment twice. We restore sharing by deduplicating: for each
158 // unique comment position, we keep one canonical object and replace
159 // all duplicates with references to it.
160 deduplicateComments(newProgram);
161
162 // Use Babel's replaceWith() API so that subsequent plugins
163 // (babel-plugin-fbt, babel-plugin-fbt-runtime, babel-plugin-idx)
164 // properly traverse the new AST. Direct assignment to
165 // pass.file.ast.program bypasses Babel's traversal tracking,
166 // and prog.skip() would prevent all merged plugin visitors from
167 // running on the new children.
168 pass.file.ast.comments = [];
169 prog.replaceWith(newProgram);
170 }
171
172 // Apply variable renames from lowering to the Babel AST.
173 // Must run AFTER the AST replacement so that scope.rename()
174 // operates on the compiled output, not the original (discarded) AST.
175 if (result.renames != null && result.renames.length > 0) {
176 applyRenames(prog, result.renames);
177 }
178 },
179 },
180 },
181 };
182 }
183
184 /**
185 * Deduplicate comments across AST nodes after JSON round-tripping.
186 *
187 * Babel's parser attaches the same comment object to multiple nodes
188 * (e.g., as trailingComments on node A and leadingComments on node B).
189 * The code generator uses reference identity (`===`) to avoid printing
190 * a comment twice. After JSON serialization/deserialization through Rust,
191 * these shared references become separate objects with identical content.
192 *
193 * This function walks the AST, finds comments with the same (start, end)
194 * position, and replaces duplicates with references to a single canonical
195 * object, restoring the sharing that Babel expects.
196 */
197 /**
198 * Apply variable renames from the Rust compiler's lowering phase to the Babel AST.
199 *
200 * During lowering, the Rust compiler renames variables that shadow outer bindings
201 * (e.g., an inner function parameter `ref` that shadows an outer `ref` becomes `ref_0`).
202 * In the TS compiler, this is done via Babel's `scope.rename()` during HIRBuilder.
203 * Since the Rust compiler doesn't have access to Babel's scope API, it records the
204 * renames and returns them here for the Babel plugin to apply.
205 */
206 function applyRenames(
207 prog: BabelCore.NodePath<BabelCore.types.Program>,
208 renames: Array<BindingRenameInfo>,
209 ): void {
210 // Build a map from declaration start position to rename info
211 const renamesByPos = new Map<number, BindingRenameInfo>();
212 for (const rename of renames) {
213 renamesByPos.set(rename.declarationStart, rename);
214 }
215
216 // Traverse all scopes to find bindings that match by position
217 prog.traverse({
218 Scope(path: BabelCore.NodePath) {
219 const scope = path.scope;
220 for (const [name, binding] of Object.entries(
221 scope.bindings as Record<string, any>,
222 )) {
223 const start = binding.identifier.start;
224 if (start != null) {
225 const rename = renamesByPos.get(start);
226 if (rename != null && name === rename.original) {
227 scope.rename(rename.original, rename.renamed);
228 renamesByPos.delete(start);
229 }
230 }
231 }
232 },
233 } as BabelCore.Visitor);
234 }
235
236 function deduplicateComments(node: any): void {
237 // Map from "start:end" to canonical comment object
238 const canonical = new Map<string, any>();
239
240 function dedup(comments: any[]): any[] {
241 return comments.map(c => {
242 const key = `${c.start}:${c.end}`;
243 const existing = canonical.get(key);
244 if (existing != null) {
245 return existing;
246 }
247 canonical.set(key, c);
248 return c;
249 });
250 }
251
252 function visit(n: any): void {
253 if (n == null || typeof n !== 'object') return;
254 if (Array.isArray(n)) {
255 for (const item of n) {
256 visit(item);
257 }
258 return;
259 }
260 if (n.leadingComments) {
261 n.leadingComments = dedup(n.leadingComments);
262 }
263 if (n.trailingComments) {
264 n.trailingComments = dedup(n.trailingComments);
265 }
266 if (n.innerComments) {
267 n.innerComments = dedup(n.innerComments);
268 }
269 for (const key of Object.keys(n)) {
270 if (
271 key === 'leadingComments' ||
272 key === 'trailingComments' ||
273 key === 'innerComments' ||
274 key === 'start' ||
275 key === 'end' ||
276 key === 'loc'
277 ) {
278 continue;
279 }
280 visit(n[key]);
281 }
282 }
283
284 visit(node);
285 }