mirror of
https://github.com/facebook/react.git
synced 2026-02-26 02:44:59 +00:00
This PR changes the way we compile ArrowFunctionExpression to allow compiling more cases, such as within `React.forwardRef()`. We no longer convert arrow functions to function declarations. Instead, CodeGenerator emits a generic `CodegenFunction` type, and `Program.ts` is responsible for converting that to the appropriate type. The rule is basically: * Retain the original node type by default. Function declaration in, function declaration out. Arrow function in, arrow function out. * When gating is enabled, we emit a ConditionalExpression instead of creating a temporary variable. If the original (and hence compiled) functions are function declarations, we force them into FunctionExpressions only here, since we need an expression for each branch of the conditional. Then the rules are: * If this is a `export function Foo` ie a named export, replace it with a variable declaration with the conditional expression as the initializer, and the function name as the variable name. * Else, just replace the original function node with the conditional. This works for all other cases. I'm open to feedback but this seems like a pretty robust approach and will allow us to support a lot of real-world cases that we didn't yet, so i think we need _something_ in this direction.
188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import { jsx } from "@babel/plugin-syntax-jsx";
|
|
import babelJest from "babel-jest";
|
|
import { compile } from "babel-plugin-react-forget";
|
|
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 type { PluginOptions } from "babel-plugin-react-forget";
|
|
import { basename } from "path";
|
|
|
|
/**
|
|
* -- IMPORTANT --
|
|
* When making changes to any babel plugins defined this file
|
|
* (e.g. `ReactForgetFunctionTransform`), make sure to bump e2eTransformerCacheKey
|
|
* as our script files are currently not used for babel cache breaking!!
|
|
*/
|
|
const e2eTransformerCacheKey = 1;
|
|
const forgetOptions: Partial<PluginOptions["environment"]> = {
|
|
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,
|
|
{
|
|
// Jest hashes the babel config as a cache breaker.
|
|
// (see https://github.com/jestjs/jest/blob/v29.6.2/packages/babel-jest/src/index.ts#L84)
|
|
compilerCacheKey: execSync(
|
|
"yarn --silent --cwd ../.. hash packages/babel-plugin-react-forget/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") {
|
|
// The compiler adds requires of "React", which is expected to be a wrapper
|
|
// around the "react" package. For tests, we just rewrite the require.
|
|
if (arg.value === "React") {
|
|
arg.value = "react";
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
};
|
|
},
|
|
],
|
|
"@babel/plugin-transform-modules-commonjs",
|
|
],
|
|
},
|
|
],
|
|
targets: {
|
|
esmodules: true,
|
|
},
|
|
} as any);
|
|
// typecast needed as DefinitelyTyped does not have updated Babel configs types yet
|
|
// (missing passPerPreset and targets).
|
|
}
|
|
|
|
return {
|
|
createTransformer,
|
|
};
|
|
};
|
|
|
|
// Mostly copied from react/scripts/babel/transform-forget.js
|
|
function isReactComponentLike(fn: NodePath<FunctionDeclaration>): boolean {
|
|
let isReactComponent = false;
|
|
let hasNoUseForgetDirective = false;
|
|
|
|
// React components start with an upper case letter,
|
|
// React hooks start with `use`
|
|
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) {
|
|
// Is there is a JSX node created in the current function context?
|
|
if (path.scope.getFunctionParent()?.path.node === fn.node) {
|
|
isReactComponent = true;
|
|
}
|
|
},
|
|
|
|
CallExpression(path) {
|
|
// Is there hook usage?
|
|
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);
|
|
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,
|
|
};
|
|
}
|