Repository: facebook/react
Pull request: [compiler] Add support for commonjs


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.


Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Imports.ts
patch: @@ -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);

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.expect.md
patch: @@ -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>
\ No newline at end of file

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/script-source-type.js
patch: @@ -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'}],
+  },
+};

filename: compiler/packages/snap/src/compiler.ts
patch: @@ -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,21 +103,22 @@ 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') {
     return HermesParser.parse(input, {
       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;

filename: compiler/packages/snap/src/sprout/evaluator.ts
patch: @@ -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 {