[tests] Add typed CompilerOptions for e2e tests
Mofei Zhang committed
Aug 2, 2023 at 17:30 UTC
50b09542790824f8d973ad265e49aab41cf8e954
3 files changed
+138
-30
compiler/forget/packages/babel-plugin-react-forget/package.json
+1
-1
@@ -10,7 +10,7 @@
10
"scripts": {
11
"build": "rimraf dist && tsc",
12
"test": "yarn jest && yarn snap:build && yarn snap",
13
- "jest": "tsc && jest",
13
+ "jest": "tsc && ts-node \"$(yarn --silent which jest)\"",
14
"snap": "node ../snap/dist/main.js",
15
"snap:build": "yarn workspace snap run build",
16
"ts:analyze-trace": "scripts/ts-analyze-trace.sh",
compiler/forget/packages/babel-plugin-react-forget/scripts/jest/makeTransform.ts
renamed
+70
-29
@@ -5,12 +5,29 @@
5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-const babelJest = require("babel-jest");
9
-const { compile } = require("babel-plugin-react-forget");
10
-const { jsx } = require("@babel/plugin-syntax-jsx");
11
-const { execSync } = require("child_process");
8
+import { jsx } from "@babel/plugin-syntax-jsx";
9
+import babelJest from "babel-jest";
10
+import { compile } from "babel-plugin-react-forget";
11
+import { execSync } from "child_process";
12
13
-module.exports = (useForget) => {
13
+import type { NodePath } from "@babel/traverse";
14
+import type { CallExpression, FunctionDeclaration } from "@babel/types";
15
+import type { PluginOptions } from "babel-plugin-react-forget";
16
+import path from "path";
17
+
18
+/**
19
+ * -- IMPORTANT --
20
+ * When making changes to any babel plugins defined this file
21
+ * (e.g. `ReactForgetFunctionTransform`), make sure to bump e2eTransformerCacheKey
22
+ * as our script files are currently not used for babel cache breaking!!
23
+ */
24
+const e2eTransformerCacheKey = 1;
25
+const forgetOptions: Partial<PluginOptions["environment"]> = {
26
+ enableAssumeHooksFollowRulesOfReact: true,
27
+};
28
+const debugMode = process.env["DEBUG_FORGET_COMPILER"] != null;
29
+
30
+module.exports = (useForget: boolean) => {
31
function createTransformer() {
32
return babelJest.createTransformer({
33
passPerPreset: true,
@@ -24,9 +41,11 @@ module.exports = (useForget) => {
41
{
42
// Jest hashes the babel config as a cache breaker.
43
// (see https://github.com/jestjs/jest/blob/v29.6.2/packages/babel-jest/src/index.ts#L84)
27
- cacheKey: execSync(
44
+ compilerCacheKey: execSync(
45
"yarn --silent --cwd ../.. hash packages/babel-plugin-react-forget/dist"
46
).toString(),
47
+ transformOptionsCacheKey: forgetOptions,
48
+ e2eTransformerCacheKey,
49
},
50
]
51
: "@babel/plugin-syntax-jsx",
@@ -36,11 +55,15 @@ module.exports = (useForget) => {
55
{
56
plugins: [
57
[
39
- function BabelPluginRewriteRequirePath(babel) {
58
+ function BabelPluginRewriteRequirePath() {
59
return {
60
visitor: {
42
- CallExpression(path) {
43
- if (path.node.callee.name === "require") {
61
+ CallExpression(path: NodePath<CallExpression>) {
62
+ const { callee } = path.node;
63
+ if (
64
+ callee.type === "Identifier" &&
65
+ callee.name === "require"
66
+ ) {
67
const arg = path.node.arguments[0];
68
if (arg.type === "StringLiteral") {
69
// The compiler adds requires of "React", which is expected to be a wrapper
@@ -62,7 +85,9 @@ module.exports = (useForget) => {
85
targets: {
86
esmodules: true,
87
},
65
- });
88
+ } as any);
89
+ // typecast needed as DefinitelyTyped does not have updated Babel configs types yet
90
+ // (missing passPerPreset and targets).
91
}
92
93
return {
@@ -70,13 +95,18 @@ module.exports = (useForget) => {
95
};
96
};
97
73
-// Copied from react/scripts/babel/transform-forget.js
74
-function isReactComponentLike(fn) {
98
+// Mostly copied from react/scripts/babel/transform-forget.js
99
+function isReactComponentLike(fn: NodePath<FunctionDeclaration>) {
100
let isReactComponent = false;
101
let hasNoUseForgetDirective = false;
102
78
- // React components start with an upper case letter
79
- if (fn.node.id.name[0].toUpperCase() !== fn.node.id.name[0]) {
103
+ // React components start with an upper case letter,
104
+ // React hooks start with `use`
105
+ if (
106
+ fn.node.id == null ||
107
+ (fn.node.id.name[0].toUpperCase() !== fn.node.id.name[0] &&
108
+ !/^use[A-Z0-9]/.test(fn.node.id.name))
109
+ ) {
110
return false;
111
}
112
@@ -98,7 +128,7 @@ function isReactComponentLike(fn) {
128
// Is there hook usage?
129
if (
130
path.node.callee.type === "Identifier" &&
101
- path.node.callee.name.startsWith("use")
131
+ !/^use[A-Z0-9]/.test(path.node.callee.name)
132
) {
133
isReactComponent = true;
134
}
@@ -114,23 +144,34 @@ function isReactComponentLike(fn) {
144
145
function ReactForgetFunctionTransform() {
146
const compiledFns = new Set();
117
- return {
118
- name: "react-forget-e2e",
119
- inherits: jsx,
120
- visitor: {
121
- FunctionDeclaration(fn) {
122
- if (compiledFns.has(fn.node)) {
123
- return;
124
- }
147
+ const visitor = {
148
+ FunctionDeclaration(fn: NodePath<FunctionDeclaration>, state: any) {
149
+ if (compiledFns.has(fn.node)) {
150
+ return;
151
+ }
152
126
- if (!isReactComponentLike(fn)) {
127
- return;
153
+ if (!isReactComponentLike(fn)) {
154
+ return;
155
+ }
156
+ if (debugMode) {
157
+ const filename = path.basename(state.file.opts.filename);
158
+ if (fn.node.loc && fn.node.id) {
159
+ console.log(
160
+ ` Compiling ${filename}:${fn.node.loc.start.line}:${fn.node.loc.start.column} ${fn.node.id.name}`
161
+ );
162
+ } else {
163
+ console.log(` Compiling ${filename} ${fn.node.id?.name}`);
164
}
165
+ }
166
130
- const compiled = compile(fn);
131
- compiledFns.add(compiled);
132
- fn.replaceWith(compiled);
133
- },
167
+ const compiled = compile(fn, forgetOptions);
168
+ compiledFns.add(compiled);
169
+ fn.replaceWith(compiled);
170
},
171
};
172
+ return {
173
+ name: "react-forget-e2e",
174
+ inherits: jsx,
175
+ visitor,
176
+ };
177
}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/update-button.e2e.js
new
+67
@@ -0,0 +1,67 @@
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 { render } from "@testing-library/react";
9
+import * as React from "react";
10
+
11
+function Button({ label }) {
12
+ const theme = useTheme();
13
+ const style = computeStyle(theme);
14
+ return <button color={style}>{label}</button>;
15
+}
16
+
17
+let currentTheme = "light";
18
+function useTheme() {
19
+ return currentTheme;
20
+}
21
+
22
+let styleComputations = 0;
23
+function computeStyle(theme) {
24
+ styleComputations++;
25
+ return theme === "light" ? "white" : "black";
26
+}
27
+
28
+test("update-button", () => {
29
+ const { asFragment, rerender } = render(<Button label="Click me" />);
30
+ expect(asFragment()).toMatchInlineSnapshot(`
31
+ <DocumentFragment>
32
+ <button
33
+ color="white"
34
+ >
35
+ Click me
36
+ </button>
37
+ </DocumentFragment>
38
+ `);
39
+
40
+ // Update the label, but not the theme
41
+ rerender(<Button label="Click again" />);
42
+ // `computeStyle` should not be called again when Forget is enabled
43
+ expect(styleComputations).toBe(__FORGET__ ? 1 : 2);
44
+ expect(asFragment()).toMatchInlineSnapshot(`
45
+ <DocumentFragment>
46
+ <button
47
+ color="white"
48
+ >
49
+ Click again
50
+ </button>
51
+ </DocumentFragment>
52
+ `);
53
+
54
+ currentTheme = "dark";
55
+ rerender(<Button label="Click again" />);
56
+ expect(asFragment()).toMatchInlineSnapshot(`
57
+ <DocumentFragment>
58
+ <button
59
+ color="black"
60
+ >
61
+ Click again
62
+ </button>
63
+ </DocumentFragment>
64
+ `);
65
+
66
+ expect(styleComputations).toBe(__FORGET__ ? 2 : 3);
67
+});