\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-declaration-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-declaration-output.txt
new file mode 100644
index 0000000000..51fb860de2
--- /dev/null
+++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-declaration-output.txt
@@ -0,0 +1,32 @@
+function Settings() {
+ "use memo";
+ const $ = _c(3);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = function Preferences() {
+ const handleSave = _temp;
+ return
Preferences Content
+ div>;
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const Preferences = t0;
+ let t1;
+ if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
+ t1 = function Notifications() {
+ return
Notifications Settings
;
+ };
+ $[1] = t1;
+ } else {
+ t1 = $[1];
+ }
+ const Notifications = t1;
+ let t2;
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
+ t2 = (
+
\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-expression-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-expression-output.txt
new file mode 100644
index 0000000000..2c31ad8e50
--- /dev/null
+++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/function-scope-use-no-memo-function-expression-output.txt
@@ -0,0 +1,18 @@
+function anonymous_1() {
+ "use no memo";
+ const Widget = function () {
+ const handleExpand = () => {
+ console.log("Widget expanded");
+ };
+ return
Widget Content
;
+ };
+ const Panel = function () {
+ return
;
+ };
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-memo-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-memo-output.txt
new file mode 100644
index 0000000000..3b26c022db
--- /dev/null
+++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-memo-output.txt
@@ -0,0 +1,15 @@
+function anonymous_1() {
+ const $ = _c(1);
+ const handleClick = _temp;
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 =
Welcome to the App! ;
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ return t0;
+}
+function _temp() {
+ console.log("Header clicked");
+}
\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-function-expression-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-function-expression-output.txt
new file mode 100644
index 0000000000..7af2442b5c
--- /dev/null
+++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-function-expression-output.txt
@@ -0,0 +1,3 @@
+function anonymous_1() {
+ return
;
+}
\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-output.txt
new file mode 100644
index 0000000000..3433dfeaae
--- /dev/null
+++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/module-scope-use-no-memo-output.txt
@@ -0,0 +1,7 @@
+function anonymous_1() {
+ const handleMouseOver = () => {
+ console.log("Footer hovered");
+ };
+ return
;
+}
\ No newline at end of file
diff --git a/compiler/apps/playground/__tests__/e2e/page.spec.ts b/compiler/apps/playground/__tests__/e2e/page.spec.ts
index bc6083f52d..1c2b2317d5 100644
--- a/compiler/apps/playground/__tests__/e2e/page.spec.ts
+++ b/compiler/apps/playground/__tests__/e2e/page.spec.ts
@@ -8,42 +8,245 @@
import {expect, test} from '@playwright/test';
import {encodeStore, type Store} from '../../lib/stores';
-const STORE: Store = {
- source: `export default function TestComponent({ x }) {
- return
{x} ;
-}
-`,
-};
-const HASH = encodeStore(STORE);
+test.describe.configure({mode: 'parallel'});
function concat(data: Array
): string {
return data.join('');
}
+const DIRECTIVE_TEST_CASES = [
+ {
+ name: 'module-scope-use-memo',
+ input: `'use memo';
-test('editor should compile successfully', async ({page}) => {
- await page.goto(`/#${HASH}`, {waitUntil: 'networkidle'});
+const Header = () => {
+ const handleClick = () => {
+ console.log('Header clicked');
+ };
+
+ return Welcome to the App! ;
+};`,
+ },
+ {
+ name: 'module-scope-use-no-memo',
+ input: `'use no memo';
+
+const Footer = () => {
+ const handleMouseOver = () => {
+ console.log('Footer hovered');
+ };
+
+ return ;
+};
+`,
+ },
+ {
+ name: 'function-scope-use-memo-function-declaration',
+ input: `function App() {
+ 'use memo';
+
+ function Sidebar() {
+ const handleToggle = () => {
+ console.log('Sidebar toggled');
+ };
+
+ return ;
+ }
+
+ const MemoizedSidebar = React.memo(Sidebar);
+
+ function Content() {
+ return Main Content ;
+ }
+
+ const MemoizedContent = React.memo(Content);
+
+ return (
+
+
+
+
+ );
+}`,
+ },
+ {
+ name: 'function-scope-use-no-memo-function-expression',
+ input: `const Dashboard = function() {
+ 'use no memo';
+ const Widget = function() {
+ const handleExpand = () => {
+ console.log('Widget expanded');
+ };
+
+ return Widget Content
;
+ };
+
+ const Panel = function() {
+ return ;
+ };
+
+ return (
+
+ );
+};`,
+ },
+ {
+ name: 'function-scope-use-memo-arrow-function-expression',
+ input: `const Analytics = () => {
+ 'use memo';
+
+ const Chart = () => {
+ const handleRefresh = () => {
+ console.log('Chart refreshed');
+ };
+
+ return Chart Content
;
+ };
+
+ const MemoizedChart = React.memo(Chart);
+
+ const Graph = () => {
+ return Graph Content
;
+ };
+
+ const MemoizedGraph = React.memo(Graph);
+
+ return (
+
+
+
+
+ );
+};`,
+ },
+ {
+ name: 'module-scope-use-no-memo-function-expression',
+ input: `'use no memo';
+
+const Sidebar = function() {
+ return ;
+};`,
+ },
+ {
+ name: 'function-scope-no-directive-arrow-function-expression',
+ input: `
+const Profile = () => {
+'use no memo';
+ const Avatar = () => {
+ return Avatar Content
;
+ };
+
+ const MemoizedAvatar = React.memo(Avatar);
+
+ const Bio = () => {
+ const handleBioUpdate = () => {
+ console.log('Bio updated');
+ };
+
+ return Bio Content
;
+ };
+
+ const MemoizedBio = React.memo(Bio);
+
+ return (
+
+
+
+
+ );
+};`,
+ },
+ {
+ name: 'function-scope-use-no-memo-function-declaration',
+ input: `'use no memo';
+
+function Settings() {
+ 'use memo';
+
+ function Preferences() {
+ const handleSave = () => {
+ console.log('Preferences saved');
+ };
+
+ return Preferences Content
;
+ }
+
+ function Notifications() {
+ return Notifications Settings
;
+ }
+
+ return (
+
+ );
+}`,
+ },
+];
+test('editor should open successfully', async ({page}) => {
+ await page.goto(`/`, {waitUntil: 'networkidle'});
await page.screenshot({
fullPage: true,
- path: 'test-results/00-on-networkidle.png',
+ path: 'test-results/00-fresh-page.png',
});
+});
+test('editor should compile from hash successfully', async ({page}) => {
+ const store: Store = {
+ source: `export default function TestComponent({ x }) {
+ return {x} ;
+ }
+ `,
+ };
+ const hash = encodeStore(store);
+ await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
// User input from hash compiles
await page.screenshot({
fullPage: true,
- path: 'test-results/01-show-js-before.png',
+ path: 'test-results/01-compiles-from-hash.png',
});
const userInput =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
- expect(concat(userInput)).toMatchSnapshot('user-input.txt');
+ expect(concat(userInput)).toMatchSnapshot('01-user-output.txt');
+});
+test('reset button works', async ({page}) => {
+ const store: Store = {
+ source: `export default function TestComponent({ x }) {
+ return {x} ;
+ }
+ `,
+ };
+ const hash = encodeStore(store);
+ await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
// Reset button works
page.on('dialog', dialog => dialog.accept());
await page.getByRole('button', {name: 'Reset'}).click();
await page.screenshot({
fullPage: true,
- path: 'test-results/02-show-js-after.png',
+ path: 'test-results/02-reset-button-works.png',
});
const defaultInput =
(await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
- expect(concat(defaultInput)).toMatchSnapshot('default-input.txt');
+ expect(concat(defaultInput)).toMatchSnapshot('02-default-output.txt');
});
+DIRECTIVE_TEST_CASES.forEach((t, idx) =>
+ test(`directives work: ${t.name}`, async ({page}) => {
+ const store: Store = {
+ source: t.input,
+ };
+ const hash = encodeStore(store);
+ await page.goto(`/#${hash}`, {waitUntil: 'networkidle'});
+ await page.screenshot({
+ fullPage: true,
+ path: `test-results/03-0${idx}-${t.name}.png`,
+ });
+
+ const useMemoOutput =
+ (await page.locator('.monaco-editor').nth(1).allInnerTexts()) ?? [];
+ expect(concat(useMemoOutput)).toMatchSnapshot(`${t.name}-output.txt`);
+ }),
+);
diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx
index 1bdd372ad2..82a40272bd 100644
--- a/compiler/apps/playground/components/Editor/EditorImpl.tsx
+++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx
@@ -18,6 +18,8 @@ import {
ValueKind,
runPlayground,
type Hook,
+ findDirectiveDisablingMemoization,
+ findDirectiveEnablingMemoization,
} from 'babel-plugin-react-compiler/src';
import {type ReactFunctionType} from 'babel-plugin-react-compiler/src/HIR/Environment';
import clsx from 'clsx';
@@ -43,6 +45,25 @@ import {
import {printFunctionWithOutlined} from 'babel-plugin-react-compiler/src/HIR/PrintHIR';
import {printReactiveFunctionWithOutlined} from 'babel-plugin-react-compiler/src/ReactiveScopes/PrintReactiveFunction';
+type FunctionLike =
+ | NodePath
+ | NodePath
+ | NodePath;
+enum MemoizeDirectiveState {
+ Enabled = 'Enabled',
+ Disabled = 'Disabled',
+ Undefined = 'Undefined',
+}
+
+const MEMOIZE_ENABLED_OR_UNDEFINED_STATES = new Set([
+ MemoizeDirectiveState.Enabled,
+ MemoizeDirectiveState.Undefined,
+]);
+
+const MEMOIZE_ENABLED_OR_DISABLED_STATES = new Set([
+ MemoizeDirectiveState.Enabled,
+ MemoizeDirectiveState.Disabled,
+]);
function parseInput(input: string, language: 'flow' | 'typescript'): any {
// Extract the first line to quickly check for custom test directives
if (language === 'flow') {
@@ -63,29 +84,36 @@ function parseInput(input: string, language: 'flow' | 'typescript'): any {
function parseFunctions(
source: string,
language: 'flow' | 'typescript',
-): Array<
- | NodePath
- | NodePath
- | NodePath
-> {
- const items: Array<
- | NodePath
- | NodePath
- | NodePath
- > = [];
+): Array<{
+ compilationEnabled: boolean;
+ fn: FunctionLike;
+}> {
+ const items: Array<{
+ compilationEnabled: boolean;
+ fn: FunctionLike;
+ }> = [];
try {
const ast = parseInput(source, language);
traverse(ast, {
FunctionDeclaration(nodePath) {
- items.push(nodePath);
+ items.push({
+ compilationEnabled: shouldCompile(nodePath),
+ fn: nodePath,
+ });
nodePath.skip();
},
ArrowFunctionExpression(nodePath) {
- items.push(nodePath);
+ items.push({
+ compilationEnabled: shouldCompile(nodePath),
+ fn: nodePath,
+ });
nodePath.skip();
},
FunctionExpression(nodePath) {
- items.push(nodePath);
+ items.push({
+ compilationEnabled: shouldCompile(nodePath),
+ fn: nodePath,
+ });
nodePath.skip();
},
});
@@ -98,9 +126,48 @@ function parseFunctions(
suggestions: null,
});
}
+
return items;
}
+function shouldCompile(fn: FunctionLike): boolean {
+ const {body} = fn.node;
+ if (t.isBlockStatement(body)) {
+ const selfCheck = checkExplicitMemoizeDirectives(body.directives);
+ if (selfCheck === MemoizeDirectiveState.Enabled) return true;
+ if (selfCheck === MemoizeDirectiveState.Disabled) return false;
+
+ const parentWithDirective = fn.findParent(parentPath => {
+ if (parentPath.isBlockStatement() || parentPath.isProgram()) {
+ const directiveCheck = checkExplicitMemoizeDirectives(
+ parentPath.node.directives,
+ );
+ return MEMOIZE_ENABLED_OR_DISABLED_STATES.has(directiveCheck);
+ }
+ return false;
+ });
+
+ if (!parentWithDirective) return true;
+ const parentDirectiveCheck = checkExplicitMemoizeDirectives(
+ (parentWithDirective.node as t.Program | t.BlockStatement).directives,
+ );
+ return MEMOIZE_ENABLED_OR_UNDEFINED_STATES.has(parentDirectiveCheck);
+ }
+ return false;
+}
+
+function checkExplicitMemoizeDirectives(
+ directives: Array,
+): MemoizeDirectiveState {
+ if (findDirectiveEnablingMemoization(directives).length) {
+ return MemoizeDirectiveState.Enabled;
+ }
+ if (findDirectiveDisablingMemoization(directives).length) {
+ return MemoizeDirectiveState.Disabled;
+ }
+ return MemoizeDirectiveState.Undefined;
+}
+
const COMMON_HOOKS: Array<[string, Hook]> = [
[
'useFragment',
@@ -209,18 +276,38 @@ function compile(source: string): [CompilerOutput, 'flow' | 'typescript'] {
// Extract the first line to quickly check for custom test directives
const pragma = source.substring(0, source.indexOf('\n'));
const config = parseConfigPragmaForTests(pragma);
-
- for (const fn of parseFunctions(source, language)) {
- const id = withIdentifier(getFunctionIdentifier(fn));
+ const parsedFunctions = parseFunctions(source, language);
+ for (const func of parsedFunctions) {
+ const id = withIdentifier(getFunctionIdentifier(func.fn));
+ const fnName = id.name;
+ if (!func.compilationEnabled) {
+ upsert({
+ kind: 'ast',
+ fnName,
+ name: 'CodeGen',
+ value: {
+ type: 'FunctionDeclaration',
+ id:
+ func.fn.isArrowFunctionExpression() ||
+ func.fn.isFunctionExpression()
+ ? withIdentifier(null)
+ : func.fn.node.id,
+ async: func.fn.node.async,
+ generator: !!func.fn.node.generator,
+ body: func.fn.node.body as t.BlockStatement,
+ params: func.fn.node.params,
+ },
+ });
+ continue;
+ }
for (const result of runPlayground(
- fn,
+ func.fn,
{
...config,
customHooks: new Map([...COMMON_HOOKS]),
},
getReactFunctionType(id),
)) {
- const fnName = id.name;
switch (result.kind) {
case 'ast': {
upsert({
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
index 1b1a82db0b..f5dbb324bf 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts
@@ -42,10 +42,10 @@ export type CompilerPass = {
comments: Array;
code: string | null;
};
-const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
+export const OPT_IN_DIRECTIVES = new Set(['use forget', 'use memo']);
export const OPT_OUT_DIRECTIVES = new Set(['use no forget', 'use no memo']);
-function findDirectiveEnablingMemoization(
+export function findDirectiveEnablingMemoization(
directives: Array,
): Array {
return directives.filter(directive =>
@@ -53,7 +53,7 @@ function findDirectiveEnablingMemoization(
);
}
-function findDirectiveDisablingMemoization(
+export function findDirectiveDisablingMemoization(
directives: Array,
): Array {
return directives.filter(directive =>
diff --git a/compiler/packages/babel-plugin-react-compiler/src/index.ts b/compiler/packages/babel-plugin-react-compiler/src/index.ts
index 60a7e7843c..150df26e45 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/index.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/index.ts
@@ -20,6 +20,9 @@ export {
run,
runPlayground,
OPT_OUT_DIRECTIVES,
+ OPT_IN_DIRECTIVES,
+ findDirectiveEnablingMemoization,
+ findDirectiveDisablingMemoization,
type CompilerPipelineValue,
type PluginOptions,
} from './Entrypoint';