@samitouri / QOS-React-1 / commits / a3c48def1c

Add Babel plugin to annotate react components

This can be used to figure out all the components in the bundle by grepping the bundle for the typeof check.

Sathya Gunasekaran committed Dec 14, 2023 at 15:52 UTC a3c48def1c39af88f789ae74e7f6cb760d171b84
1 file changed +292
compiler/packages/babel-plugin-react-forget/scripts/babel-plugin-annotate-react-code.ts new
+292
@@ -0,0 +1,292 @@
1 +/*
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +import type * as BabelCore from "@babel/core";
9 +import { NodePath } from "@babel/core";
10 +import * as t from "@babel/types";
11 +
12 +export default function AnnotateReactCodeBabelPlugin(
13 + _babel: typeof BabelCore
14 +): BabelCore.PluginObj {
15 + return {
16 + name: "annotate-react-code",
17 + visitor: {
18 + Program(prog): void {
19 + annotate(prog);
20 + },
21 + },
22 + };
23 +}
24 +
25 +function annotate(program: NodePath<t.Program>): void {
26 + function traverseFn(fn: BabelFn): void {
27 + if (!shouldVisit(fn)) {
28 + return;
29 + }
30 +
31 + fn.skip();
32 +
33 + const body = fn.node.body;
34 + if (t.isBlockStatement(body)) {
35 + body.body.unshift(buildTypeOfReactForget());
36 + }
37 + }
38 +
39 + program.traverse({
40 + FunctionDeclaration: traverseFn,
41 + FunctionExpression: traverseFn,
42 + ArrowFunctionExpression: traverseFn,
43 + });
44 +}
45 +
46 +function shouldVisit(fn: BabelFn): boolean {
47 + return (
48 + // Component declarations are known components
49 + (fn.isFunctionDeclaration() && isComponentDeclaration(fn.node)) ||
50 + // Otherwise check if this is a component or hook-like function
51 + isComponentOrHookLike(fn)
52 + );
53 +}
54 +
55 +function buildTypeOfReactForget(): t.Statement {
56 + // typeof globalThis[Symbol.for("react_forget")]
57 + return t.expressionStatement(
58 + t.unaryExpression(
59 + "typeof",
60 + t.memberExpression(
61 + t.identifier("globalThis"),
62 + t.callExpression(
63 + t.memberExpression(
64 + t.identifier("Symbol"),
65 + t.identifier("for"),
66 + false,
67 + false
68 + ),
69 + [t.stringLiteral("react_forget")]
70 + ),
71 + true,
72 + false
73 + ),
74 + true
75 + )
76 + );
77 +}
78 +
79 +/**
80 + * COPIED FROM babel-plugin-react-forget/src/Entrypoint/BabelUtils.ts
81 + */
82 +type ComponentDeclaration = t.FunctionDeclaration & {
83 + __componentDeclaration: boolean;
84 +};
85 +
86 +type BabelFn =
87 + | NodePath<t.FunctionDeclaration>
88 + | NodePath<t.FunctionExpression>
89 + | NodePath<t.ArrowFunctionExpression>;
90 +
91 +export function isComponentDeclaration(
92 + node: t.FunctionDeclaration
93 +): node is ComponentDeclaration {
94 + return Object.prototype.hasOwnProperty.call(node, "__componentDeclaration");
95 +}
96 +
97 +/*
98 + * Adapted from the ESLint rule at
99 + * https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L90-L103
100 + */
101 +function isComponentOrHookLike(
102 + node: NodePath<
103 + t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
104 + >
105 +): boolean {
106 + const functionName = getFunctionName(node);
107 + // Check if the name is component or hook like:
108 + if (
109 + functionName !== null &&
110 + (isComponentName(functionName) || isHook(functionName))
111 + ) {
112 + return (
113 + // As an added check we also look for hook invocations or JSX
114 + callsHooksOrCreatesJsx(node) &&
115 + /*
116 + * and avoid helper functions that take more than one argument
117 + * helpers are _usually_ named with lowercase, but some code may
118 + * violate this rule
119 + */
120 + node.get("params").length <= 1
121 + );
122 + }
123 + /*
124 + * Otherwise for function or arrow function expressions, check if they
125 + * appear as the argument to React.forwardRef() or React.memo():
126 + */
127 + if (node.isFunctionExpression() || node.isArrowFunctionExpression()) {
128 + if (isForwardRefCallback(node) || isMemoCallback(node)) {
129 + // As an added check we also look for hook invocations or JSX
130 + return callsHooksOrCreatesJsx(node);
131 + } else {
132 + return false;
133 + }
134 + }
135 + return false;
136 +}
137 +
138 +function isHookName(s: string): boolean {
139 + return /^use[A-Z0-9]/.test(s);
140 +}
141 +
142 +/*
143 + * We consider hooks to be a hook name identifier or a member expression
144 + * containing a hook name.
145 + */
146 +
147 +function isHook(path: NodePath<t.Expression | t.PrivateName>): boolean {
148 + if (path.isIdentifier()) {
149 + return isHookName(path.node.name);
150 + } else if (
151 + path.isMemberExpression() &&
152 + !path.node.computed &&
153 + isHook(path.get("property"))
154 + ) {
155 + const obj = path.get("object").node;
156 + const isPascalCaseNameSpace = /^[A-Z].*/;
157 + return obj.type === "Identifier" && isPascalCaseNameSpace.test(obj.name);
158 + } else {
159 + return false;
160 + }
161 +}
162 +
163 +/*
164 + * Checks if the node is a React component name. React component names must
165 + * always start with an uppercase letter.
166 + */
167 +
168 +function isComponentName(path: NodePath<t.Expression>): boolean {
169 + return path.isIdentifier() && /^[A-Z]/.test(path.node.name);
170 +}
171 +/*
172 + * Checks if the node is a callback argument of forwardRef. This render function
173 + * should follow the rules of hooks.
174 + */
175 +
176 +function isForwardRefCallback(path: NodePath<t.Expression>): boolean {
177 + return !!(
178 + path.parentPath.isCallExpression() &&
179 + path.parentPath.get("callee").isExpression() &&
180 + isReactAPI(path.parentPath.get("callee"), "forwardRef")
181 + );
182 +}
183 +
184 +/*
185 + * Checks if the node is a callback argument of React.memo. This anonymous
186 + * functional component should follow the rules of hooks.
187 + */
188 +
189 +function isMemoCallback(path: NodePath<t.Expression>): boolean {
190 + return (
191 + path.parentPath.isCallExpression() &&
192 + path.parentPath.get("callee").isExpression() &&
193 + isReactAPI(path.parentPath.get("callee"), "memo")
194 + );
195 +}
196 +
197 +function isReactAPI(
198 + path: NodePath<t.Expression | t.PrivateName | t.V8IntrinsicIdentifier>,
199 + functionName: string
200 +): boolean {
201 + const node = path.node;
202 + return (
203 + (node.type === "Identifier" && node.name === functionName) ||
204 + (node.type === "MemberExpression" &&
205 + node.object.type === "Identifier" &&
206 + node.object.name === "React" &&
207 + node.property.type === "Identifier" &&
208 + node.property.name === functionName)
209 + );
210 +}
211 +
212 +function callsHooksOrCreatesJsx(node: NodePath<t.Node>): boolean {
213 + let invokesHooks = false;
214 + let createsJsx = false;
215 + node.traverse({
216 + JSX() {
217 + createsJsx = true;
218 + },
219 + CallExpression(call) {
220 + const callee = call.get("callee");
221 + if (callee.isExpression() && isHook(callee)) {
222 + invokesHooks = true;
223 + }
224 + },
225 + });
226 +
227 + return invokesHooks || createsJsx;
228 +}
229 +
230 +/*
231 + * Gets the static name of a function AST node. For function declarations it is
232 + * easy. For anonymous function expressions it is much harder. If you search for
233 + * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
234 + * where JS gives anonymous function expressions names. We roughly detect the
235 + * same AST nodes with some exceptions to better fit our use case.
236 + */
237 +
238 +function getFunctionName(
239 + path: NodePath<
240 + t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
241 + >
242 +): NodePath<t.Expression> | null {
243 + if (path.isFunctionDeclaration()) {
244 + const id = path.get("id");
245 + if (id.isIdentifier()) {
246 + return id;
247 + }
248 + return null;
249 + }
250 + let id: NodePath<t.LVal | t.Expression | t.PrivateName> | null = null;
251 + const parent = path.parentPath;
252 + if (parent.isVariableDeclarator() && parent.get("init").node === path.node) {
253 + // const useHook = () => {};
254 + id = parent.get("id");
255 + } else if (
256 + parent.isAssignmentExpression() &&
257 + parent.get("right").node === path.node &&
258 + parent.get("operator") === "="
259 + ) {
260 + // useHook = () => {};
261 + id = parent.get("left");
262 + } else if (
263 + parent.isProperty() &&
264 + parent.get("value").node === path.node &&
265 + !parent.get("computed") &&
266 + parent.get("key").isLVal()
267 + ) {
268 + /*
269 + * {useHook: () => {}}
270 + * {useHook() {}}
271 + */
272 + id = parent.get("key");
273 + } else if (
274 + parent.isAssignmentPattern() &&
275 + parent.get("right").node === path.node &&
276 + !parent.get("computed")
277 + ) {
278 + /*
279 + * const {useHook = () => {}} = {};
280 + * ({useHook = () => {}} = {});
281 + *
282 + * Kinda clowny, but we'd said we'd follow spec convention for
283 + * `IsAnonymousFunctionDefinition()` usage.
284 + */
285 + id = parent.get("left");
286 + }
287 + if (id !== null && (id.isIdentifier() || id.isMemberExpression())) {
288 + return id;
289 + } else {
290 + return null;
291 + }
292 +}