@samitouri / QOS-React / commits / 8ad773b1f3

[compiler] Add support for commonjs (#34589)

We previously always generated import statements for any modules that had to be required, notably the `import {c} from 'react/compiler-runtime'` for the memo cache function. However, this obviously doesn't work when the source is using commonjs. Now we check the sourceType of the module and generate require() statements if the source type is 'script'. I initially explored using https://babeljs.io/docs/babel-helper-module-imports, but the API design was unfortunately not flexible enough for our use-case. Specifically, our pipeline is as follows: * Compile individual functions. Generate candidate imports, pre-allocating the local names for those imports. * If the file is compiled successfully, actually add the imports to the program. Ie we need to pre-allocate identifier names for the imports before we add them to the program — but that isn't supported by babel-helper-module-imports. So instead we generate our own require() calls if the sourceType is script.

Joseph Savona committed Sep 24, 2025 at 11:17 UTC 8ad773b1f342d20e4773c8d086028c6927445a22
5 files changed +104 -8
compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
+24 -4
@@ -240,7 +240,7 @@ export function addImportsToProgram(
240 programContext: ProgramContext,
241 ): void {
242 const existingImports = getExistingImports(path);
243 - const stmts: Array<t.ImportDeclaration> = [];
243 + const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
244 const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
245 a.localeCompare(b),
246 );
@@ -303,9 +303,29 @@ export function addImportsToProgram(
303 if (maybeExistingImports != null) {
304 maybeExistingImports.pushContainer('specifiers', importSpecifiers);
305 } else {
306 - stmts.push(
307 - t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
308 - );
306 + if (path.node.sourceType === 'module') {
307 + stmts.push(
308 + t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
309 + );
310 + } else {
311 + stmts.push(
312 + t.variableDeclaration('const', [
313 + t.variableDeclarator(
314 + t.objectPattern(
315 + sortedImport.map(specifier => {
316 + return t.objectProperty(
317 + t.identifier(specifier.imported),
318 + t.identifier(specifier.name),
319 + );
320 + }),
321 + ),
322 + t.callExpression(t.identifier('require'), [
323 + t.stringLiteral(moduleName),
324 + ]),
325 + ),
326 + ]),
327 + );
328 + }
329 }
330 }
331 path.unshiftContainer('body', stmts);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md new
+52
@@ -0,0 +1,52 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @script
6 +const React = require('react');
7 +
8 +function Component(props) {
9 + return <div>{props.name}</div>;
10 +}
11 +
12 +// To work with snap evaluator
13 +exports = {
14 + FIXTURE_ENTRYPOINT: {
15 + fn: Component,
16 + params: [{name: 'React Compiler'}],
17 + },
18 +};
19 +
20 +```
21 +
22 +## Code
23 +
24 +```javascript
25 +const { c: _c } = require("react/compiler-runtime"); // @script
26 +const React = require("react");
27 +
28 +function Component(props) {
29 + const $ = _c(2);
30 + let t0;
31 + if ($[0] !== props.name) {
32 + t0 = <div>{props.name}</div>;
33 + $[0] = props.name;
34 + $[1] = t0;
35 + } else {
36 + t0 = $[1];
37 + }
38 + return t0;
39 +}
40 +
41 +// To work with snap evaluator
42 +exports = {
43 + FIXTURE_ENTRYPOINT: {
44 + fn: Component,
45 + params: [{ name: "React Compiler" }],
46 + },
47 +};
48 +
49 +```
50 +
51 +### Eval output
52 +(kind: ok) <div>React Compiler</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js new
+14
@@ -0,0 +1,14 @@
1 +// @script
2 +const React = require('react');
3 +
4 +function Component(props) {
5 + return <div>{props.name}</div>;
6 +}
7 +
8 +// To work with snap evaluator
9 +exports = {
10 + FIXTURE_ENTRYPOINT: {
11 + fn: Component,
12 + params: [{name: 'React Compiler'}],
13 + },
14 +};
compiler/packages/snap/src/compiler.ts
+10 -3
@@ -31,10 +31,15 @@ import prettier from 'prettier';
31 import SproutTodoFilter from './SproutTodoFilter';
32 import {isExpectError} from './fixture-utils';
33 import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';
34 +
35 export function parseLanguage(source: string): 'flow' | 'typescript' {
36 return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
37 }
38
39 +export function parseSourceType(source: string): 'script' | 'module' {
40 + return source.indexOf('@script') !== -1 ? 'script' : 'module';
41 +}
42 +
43 /**
44 * Parse react compiler plugin + environment options from test fixture. Note
45 * that although this primarily uses `Environment:parseConfigPragma`, it also
@@ -98,6 +103,7 @@ export function parseInput(
103 input: string,
104 filename: string,
105 language: 'flow' | 'typescript',
106 + sourceType: 'module' | 'script',
107 ): BabelCore.types.File {
108 // Extract the first line to quickly check for custom test directives
109 if (language === 'flow') {
@@ -105,14 +111,14 @@ export function parseInput(
111 babel: true,
112 flow: 'all',
113 sourceFilename: filename,
108 - sourceType: 'module',
114 + sourceType,
115 enableExperimentalComponentSyntax: true,
116 });
117 } else {
118 return BabelParser.parse(input, {
119 sourceFilename: filename,
120 plugins: ['typescript', 'jsx'],
115 - sourceType: 'module',
121 + sourceType,
122 });
123 }
124 }
@@ -221,11 +227,12 @@ export async function transformFixtureInput(
227 const firstLine = input.substring(0, input.indexOf('\n'));
228
229 const language = parseLanguage(firstLine);
230 + const sourceType = parseSourceType(firstLine);
231 // Preserve file extension as it determines typescript's babel transform
232 // mode (e.g. stripping types, parsing rules for brackets)
233 const filename =
234 path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');
228 - const inputAst = parseInput(input, filename, language);
235 + const inputAst = parseInput(input, filename, language, sourceType);
236 // Give babel transforms an absolute path as relative paths get prefixed
237 // with `cwd`, which is different across machines
238 const virtualFilepath = '/' + filename;
compiler/packages/snap/src/sprout/evaluator.ts
+4 -1
@@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult {
298 return {
299 kind: 'UnexpectedError',
300 value:
301 - 'Unexpected error during eval, possible syntax error?\n' + e.message,
301 + 'Unexpected error during eval, possible syntax error?\n' +
302 + e.message +
303 + '\n\nsource:\n' +
304 + source,
305 logs,
306 };
307 } finally {