From b91119f0d1b423fb0f25edd4b12eb6e8863108a4 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 24 Sep 2025 09:59:48 -0700 Subject: [PATCH] [commit] Add support for commonjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/Entrypoint/Imports.ts | 28 ++++++++-- .../compiler/script-source-type.expect.md | 52 +++++++++++++++++++ .../fixtures/compiler/script-source-type.js | 14 +++++ compiler/packages/snap/src/compiler.ts | 13 +++-- .../packages/snap/src/sprout/evaluator.ts | 5 +- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts index 9b0db75912..3849051b2b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts @@ -240,7 +240,7 @@ export function addImportsToProgram( programContext: ProgramContext, ): void { const existingImports = getExistingImports(path); - const stmts: Array = []; + const stmts: Array = []; 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); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md new file mode 100644 index 0000000000..891a0fb0dd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +// @script +const React = require('react'); + +function Component(props) { + return
{props.name}
; +} + +// 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 =
{props.name}
; + $[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)
React Compiler
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js new file mode 100644 index 0000000000..604f0d9618 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js @@ -0,0 +1,14 @@ +// @script +const React = require('react'); + +function Component(props) { + return
{props.name}
; +} + +// To work with snap evaluator +exports = { + FIXTURE_ENTRYPOINT: { + fn: Component, + params: [{name: 'React Compiler'}], + }, +}; diff --git a/compiler/packages/snap/src/compiler.ts b/compiler/packages/snap/src/compiler.ts index a6041bd5cc..cafe869244 100644 --- a/compiler/packages/snap/src/compiler.ts +++ b/compiler/packages/snap/src/compiler.ts @@ -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; diff --git a/compiler/packages/snap/src/sprout/evaluator.ts b/compiler/packages/snap/src/sprout/evaluator.ts index 60da5dc53c..8af8487d01 100644 --- a/compiler/packages/snap/src/sprout/evaluator.ts +++ b/compiler/packages/snap/src/sprout/evaluator.ts @@ -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 {