import type * as BabelCore from '@babel/core';
import {NodePath} from '@babel/core';
import * as t from '@babel/types';
export default function AnnotateReactCodeBabelPlugin(
_babel: typeof BabelCore,
): BabelCore.PluginObj {
return {
name: 'annotate-react-code',
visitor: {
Program(prog): void {
annotate(prog);
},
},
};
}
function annotate(program: NodePath<t.Program>): void {
function traverseFn(fn: BabelFn): void {
if (!shouldVisit(fn)) {
return;
}
fn.skip();
const body = fn.node.body;
if (t.isBlockStatement(body)) {
body.body.unshift(buildTypeOfReactForget());
}
}
program.traverse({
FunctionDeclaration: traverseFn,
FunctionExpression: traverseFn,
ArrowFunctionExpression: traverseFn,
});
}
function shouldVisit(fn: BabelFn): boolean {
return (
(fn.isFunctionDeclaration() && isComponentDeclaration(fn.node)) ||
isComponentOrHookLike(fn)
);
}
function buildTypeOfReactForget(): t.Statement {
return t.expressionStatement(
t.unaryExpression(
'typeof',
t.memberExpression(
t.identifier('globalThis'),
t.callExpression(
t.memberExpression(
t.identifier('Symbol'),
t.identifier('for'),
false,
false,
),
[t.stringLiteral('react_forget')],
),
true,
false,
),
true,
),
);
}
type ComponentDeclaration = t.FunctionDeclaration & {
__componentDeclaration: boolean;
};
type BabelFn =
| NodePath<t.FunctionDeclaration>
| NodePath<t.FunctionExpression>
| NodePath<t.ArrowFunctionExpression>;
export function isComponentDeclaration(
node: t.FunctionDeclaration,
): node is ComponentDeclaration {
return Object.prototype.hasOwnProperty.call(node, '__componentDeclaration');
}
function isComponentOrHookLike(
node: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
): boolean {
const functionName = getFunctionName(node);
if (functionName !== null && isComponentName(functionName)) {
return (
callsHooksOrCreatesJsx(node) &&
node.get('params').length <= 1
);
} else if (functionName !== null && isHook(functionName)) {
return callsHooksOrCreatesJsx(node);
}
if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {
if (isForwardRefCallback(node) || isMemoCallback(node)) {
return callsHooksOrCreatesJsx(node);
} else {
return false;
}
}
return false;
}
function isHookName(s: string): boolean {
return /^use[A-Z0-9]/.test(s);
}
function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
if (path.isIdentifier()) {
return isHookName(path.node.name);
} else if (
path.isMemberExpression() &&
!path.node.computed &&
isHook(path.get('property'))
) {
const obj = path.get('object').node;
const isPascalCaseNameSpace = /^[A-Z].*/;
return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);
} else {
return false;
}
}
function isComponentName(path: NodePath<t.Expression>): boolean {
return path.isIdentifier() && /^[A-Z]/.test(path.node.name);
}
function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
return !!(
path.parentPath.isCallExpression() &&
path.parentPath.get('callee').isExpression() &&
isReactAPI(path.parentPath.get('callee'), 'forwardRef')
);
}
function isMemoCallback(path: NodePath<t.Expression>): boolean {
return (
path.parentPath.isCallExpression() &&
path.parentPath.get('callee').isExpression() &&
isReactAPI(path.parentPath.get('callee'), 'memo')
);
}
function isReactAPI(
path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
functionName: string,
): boolean {
const node = path.node;
return (
(node.type === 'Identifier' && node.name === functionName) ||
(node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.object.name === 'React' &&
node.property.type === 'Identifier' &&
node.property.name === functionName)
);
}
function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {
let invokesHooks = false;
let createsJsx = false;
node.traverse({
JSX() {
createsJsx = true;
},
CallExpression(call) {
const callee = call.get('callee');
if (callee.isExpression() && isHook(callee)) {
invokesHooks = true;
}
},
});
return invokesHooks || createsJsx;
}
function getFunctionName(
path: NodePath<
t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
>,
): NodePath<t.Expression> | null {
if (path.isFunctionDeclaration()) {
const id = path.get('id');
if (id.isIdentifier()) {
return id;
}
return null;
}
let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
const parent = path.parentPath;
if (parent.isVariableDeclarator() && parent.get('init').node === path.node) {
id = parent.get('id');
} else if (
parent.isAssignmentExpression() &&
parent.get('right').node === path.node &&
parent.get('operator') === '='
) {
id = parent.get('left');
} else if (
parent.isProperty() &&
parent.get('value').node === path.node &&
!parent.get('computed') &&
parent.get('key').isLVal()
) {
id = parent.get('key');
} else if (
parent.isAssignmentPattern() &&
parent.get('right').node === path.node &&
!parent.get('computed')
) {
id = parent.get('left');
}
if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
return id;
} else {
return null;
}
}