@samitouri / QOS-React-2 / commits / 50b0954279

[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 +});