diff --git a/compiler/forget/packages/babel-plugin-react-forget/package.json b/compiler/forget/packages/babel-plugin-react-forget/package.json index 30c8a712dc..cafd7258e8 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/package.json +++ b/compiler/forget/packages/babel-plugin-react-forget/package.json @@ -55,8 +55,8 @@ "glob": "^7.1.6", "jest": "^29.0.3", "jest-environment-jsdom": "^29.0.3", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^0.0.0-experimental-493f72b0a-20230727", + "react-dom": "^0.0.0-experimental-493f72b0a-20230727", "rimraf": "^3.0.2", "test262-harness": "^8.0.0", "ts-jest": "^29.1.1", diff --git a/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/makeTransform.js b/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/makeTransform.js index af4e10a19f..db9d2caf2c 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/makeTransform.js +++ b/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/makeTransform.js @@ -5,9 +5,10 @@ * LICENSE file in the root directory of this source tree. */ -const ReactForgetBabelPlugin = require("../../dist").BabelPlugin; const babelJest = require("babel-jest"); -const { readFileSync } = require("fs"); +const { compile } = require("babel-plugin-react-forget"); +const { jsx } = require("@babel/plugin-syntax-jsx"); +const { execSync } = require("child_process"); module.exports = (useForget) => { function createTransformer() { @@ -17,18 +18,18 @@ module.exports = (useForget) => { "@babel/preset-typescript", { plugins: [ - "@babel/plugin-syntax-jsx", - ...(useForget + useForget ? [ - [ - ReactForgetBabelPlugin, - { - // Jest hashes the babel config as a cache breaker. - cacheBreaker: readFileSync("dist/HASH", "utf8"), - }, - ], + 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) + cacheKey: execSync( + "yarn --silent --cwd ../.. hash packages/babel-plugin-react-forget/dist" + ).toString(), + }, ] - : []), + : "@babel/plugin-syntax-jsx", ], }, "@babel/preset-react", @@ -68,3 +69,68 @@ module.exports = (useForget) => { createTransformer, }; }; + +// Copied from react/scripts/babel/transform-forget.js +function isReactComponentLike(fn) { + let isReactComponent = false; + let hasNoUseForgetDirective = false; + + // React components start with an upper case letter + if (fn.node.id.name[0].toUpperCase() !== fn.node.id.name[0]) { + 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" && + path.node.callee.name.startsWith("use") + ) { + isReactComponent = true; + } + }, + }); + + if (hasNoUseForgetDirective) { + return false; + } + + return isReactComponent; +} + +function ReactForgetFunctionTransform() { + const compiledFns = new Set(); + return { + name: "react-forget-e2e", + inherits: jsx, + visitor: { + FunctionDeclaration(fn) { + if (compiledFns.has(fn.node)) { + return; + } + + if (!isReactComponentLike(fn)) { + return; + } + + const compiled = compile(fn); + compiledFns.add(compiled); + fn.replaceWith(compiled); + }, + }, + }; +} diff --git a/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/setupEnvE2E.js b/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/setupEnvE2E.js index f48a0c241d..7f0ff80e6c 100644 --- a/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/setupEnvE2E.js +++ b/compiler/forget/packages/babel-plugin-react-forget/scripts/jest/setupEnvE2E.js @@ -6,6 +6,9 @@ */ const React = require("react"); -const ForgetRuntime = require("../../packages/react-forget-runtime"); -React.unstable_ForgetRuntime = ForgetRuntime; -React.unstable_useMemoCache = ForgetRuntime.unstable_useMemoCache; + +// Our e2e babel transform currently only compiles functions, not programs. +// As a result, our e2e transpiled code does not contain an import for `useMemoCache` +// This is a hack. +React.useMemoCache = React.unstable_useMemoCache; +globalThis.useMemoCache = React.unstable_useMemoCache; diff --git a/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/expectLogs.js b/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/expectLogs.js new file mode 100644 index 0000000000..7e00bd5014 --- /dev/null +++ b/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/expectLogs.js @@ -0,0 +1,17 @@ +/** + * 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. + */ + +const logs = []; + +export function log(message) { + logs.push(message); +} + +export function expectLogsAndClear(expected) { + expect(logs).toEqual(expected); + logs.length = 0; +} diff --git a/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/hello.e2e.js b/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/hello.e2e.js new file mode 100644 index 0000000000..34d0e34d80 --- /dev/null +++ b/compiler/forget/packages/babel-plugin-react-forget/src/__tests__/e2e/hello.e2e.js @@ -0,0 +1,75 @@ +/** + * 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 * as React from "react"; +import { render } from "@testing-library/react"; +import { expectLogsAndClear, log } from "./expectLogs"; + +function Hello({ name }) { + const items = [1, 2, 3].map((item) => { + log(`recomputing ${item}`); + return