import { jsx } from "@babel/plugin-syntax-jsx";
import babelJest from "babel-jest";
import { compile } from "babel-plugin-react-compiler";
import { execSync } from "child_process";
import type { NodePath, Visitor } from "@babel/traverse";
import type { CallExpression, FunctionDeclaration } from "@babel/types";
import * as t from "@babel/types";
import {
EnvironmentConfig,
validateEnvironmentConfig,
} from "babel-plugin-react-compiler";
import { basename } from "path";
const e2eTransformerCacheKey = 1;
const forgetOptions: EnvironmentConfig = validateEnvironmentConfig({
enableAssumeHooksFollowRulesOfReact: true,
});
const debugMode = process.env["DEBUG_FORGET_COMPILER"] != null;
module.exports = (useForget: boolean) => {
function createTransformer() {
return babelJest.createTransformer({
passPerPreset: true,
presets: [
"@babel/preset-typescript",
{
plugins: [
useForget
? [
ReactForgetFunctionTransform,
{
compilerCacheKey: execSync(
"yarn --silent --cwd ../.. hash packages/babel-plugin-react-compiler/dist"
).toString(),
transformOptionsCacheKey: forgetOptions,
e2eTransformerCacheKey,
},
]
: "@babel/plugin-syntax-jsx",
],
},
"@babel/preset-react",
{
plugins: [
[
function BabelPluginRewriteRequirePath(): { visitor: Visitor } {
return {
visitor: {
CallExpression(path: NodePath<CallExpression>): void {
const { callee } = path.node;
if (
callee.type === "Identifier" &&
callee.name === "require"
) {
const arg = path.node.arguments[0];
if (arg.type === "StringLiteral") {
if (arg.value === "React") {
arg.value = "react";
}
}
}
},
},
};
},
],
"@babel/plugin-transform-modules-commonjs",
],
},
],
targets: {
esmodules: true,
},
} as any);
}
return {
createTransformer,
};
};
function isReactComponentLike(fn: NodePath<FunctionDeclaration>): boolean {
let isReactComponent = false;
let hasNoUseForgetDirective = false;
if (
fn.node.id == null ||
(fn.node.id.name[0].toUpperCase() !== fn.node.id.name[0] &&
!/^use[A-Z0-9]/.test(fn.node.id.name))
) {
return false;
}
fn.traverse({
DirectiveLiteral(path) {
if (path.node.value === "use no forget") {
hasNoUseForgetDirective = true;
}
},
JSX(path) {
if (path.scope.getFunctionParent()?.path.node === fn.node) {
isReactComponent = true;
}
},
CallExpression(path) {
if (
path.node.callee.type === "Identifier" &&
!/^use[A-Z0-9]/.test(path.node.callee.name)
) {
isReactComponent = true;
}
},
});
if (hasNoUseForgetDirective) {
return false;
}
return isReactComponent;
}
function ReactForgetFunctionTransform() {
const compiledFns = new Set();
const visitor = {
FunctionDeclaration(fn: NodePath<FunctionDeclaration>, state: any): void {
if (compiledFns.has(fn.node)) {
return;
}
if (!isReactComponentLike(fn)) {
return;
}
if (debugMode) {
const filename = basename(state.file.opts.filename);
if (fn.node.loc && fn.node.id) {
console.log(
` Compiling ${filename}:${fn.node.loc.start.line}:${fn.node.loc.start.column} ${fn.node.id.name}`
);
} else {
console.log(` Compiling ${filename} ${fn.node.id?.name}`);
}
}
const compiled = compile(
fn,
forgetOptions,
"Other",
"_c",
null,
null,
null
);
compiledFns.add(compiled);
const fun = t.functionDeclaration(
compiled.id,
compiled.params,
compiled.body,
compiled.generator,
compiled.async
);
fn.replaceWith(fun);
fn.skip();
},
};
return {
name: "react-forget-e2e",
inherits: jsx,
visitor,
};
}