[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.
This commit is contained in:
Joseph Savona
2025-09-24 11:17:42 -07:00
committed by GitHub
parent 58d17912e8
commit 8ad773b1f3
5 changed files with 104 additions and 8 deletions

View File

@@ -240,7 +240,7 @@ export function addImportsToProgram(
programContext: ProgramContext,
): void {
const existingImports = getExistingImports(path);
const stmts: Array<t.ImportDeclaration> = [];
const stmts: Array<t.ImportDeclaration | t.VariableDeclaration> = [];
const sortedModules = [...programContext.imports.entries()].sort(([a], [b]) =>
a.localeCompare(b),
);
@@ -303,9 +303,29 @@ export function addImportsToProgram(
if (maybeExistingImports != null) {
maybeExistingImports.pushContainer('specifiers', importSpecifiers);
} else {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
if (path.node.sourceType === 'module') {
stmts.push(
t.importDeclaration(importSpecifiers, t.stringLiteral(moduleName)),
);
} else {
stmts.push(
t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern(
sortedImport.map(specifier => {
return t.objectProperty(
t.identifier(specifier.imported),
t.identifier(specifier.name),
);
}),
),
t.callExpression(t.identifier('require'), [
t.stringLiteral(moduleName),
]),
),
]),
);
}
}
}
path.unshiftContainer('body', stmts);

View File

@@ -0,0 +1,52 @@
## Input
```javascript
// @script
const React = require('react');
function Component(props) {
return <div>{props.name}</div>;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{name: 'React Compiler'}],
},
};
```
## Code
```javascript
const { c: _c } = require("react/compiler-runtime"); // @script
const React = require("react");
function Component(props) {
const $ = _c(2);
let t0;
if ($[0] !== props.name) {
t0 = <div>{props.name}</div>;
$[0] = props.name;
$[1] = t0;
} else {
t0 = $[1];
}
return t0;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{ name: "React Compiler" }],
},
};
```
### Eval output
(kind: ok) <div>React Compiler</div>

View File

@@ -0,0 +1,14 @@
// @script
const React = require('react');
function Component(props) {
return <div>{props.name}</div>;
}
// To work with snap evaluator
exports = {
FIXTURE_ENTRYPOINT: {
fn: Component,
params: [{name: 'React Compiler'}],
},
};

View File

@@ -31,10 +31,15 @@ import prettier from 'prettier';
import SproutTodoFilter from './SproutTodoFilter';
import {isExpectError} from './fixture-utils';
import {makeSharedRuntimeTypeProvider} from './sprout/shared-runtime-type-provider';
export function parseLanguage(source: string): 'flow' | 'typescript' {
return source.indexOf('@flow') !== -1 ? 'flow' : 'typescript';
}
export function parseSourceType(source: string): 'script' | 'module' {
return source.indexOf('@script') !== -1 ? 'script' : 'module';
}
/**
* Parse react compiler plugin + environment options from test fixture. Note
* that although this primarily uses `Environment:parseConfigPragma`, it also
@@ -98,6 +103,7 @@ export function parseInput(
input: string,
filename: string,
language: 'flow' | 'typescript',
sourceType: 'module' | 'script',
): BabelCore.types.File {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
@@ -105,14 +111,14 @@ export function parseInput(
babel: true,
flow: 'all',
sourceFilename: filename,
sourceType: 'module',
sourceType,
enableExperimentalComponentSyntax: true,
});
} else {
return BabelParser.parse(input, {
sourceFilename: filename,
plugins: ['typescript', 'jsx'],
sourceType: 'module',
sourceType,
});
}
}
@@ -221,11 +227,12 @@ export async function transformFixtureInput(
const firstLine = input.substring(0, input.indexOf('\n'));
const language = parseLanguage(firstLine);
const sourceType = parseSourceType(firstLine);
// Preserve file extension as it determines typescript's babel transform
// mode (e.g. stripping types, parsing rules for brackets)
const filename =
path.basename(fixturePath) + (language === 'typescript' ? '.ts' : '');
const inputAst = parseInput(input, filename, language);
const inputAst = parseInput(input, filename, language, sourceType);
// Give babel transforms an absolute path as relative paths get prefixed
// with `cwd`, which is different across machines
const virtualFilepath = '/' + filename;

View File

@@ -298,7 +298,10 @@ export function doEval(source: string): EvaluatorResult {
return {
kind: 'UnexpectedError',
value:
'Unexpected error during eval, possible syntax error?\n' + e.message,
'Unexpected error during eval, possible syntax error?\n' +
e.message +
'\n\nsource:\n' +
source,
logs,
};
} finally {