diff --git a/packages/plugin-react/src/reactCompilerPreset.test.ts b/packages/plugin-react/src/reactCompilerPreset.test.ts new file mode 100644 index 000000000..8cc1a8f1b --- /dev/null +++ b/packages/plugin-react/src/reactCompilerPreset.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, test } from 'vitest' +import { defaultCodeFilter } from './reactCompilerPreset' + +describe('defaultCodeFilter', () => { + const cases: Record = { + directive: ['"use memo";', true], + + 'component declaration': ['function App() { return <> }', true], + 'component declaration with types': [ + 'function App(): Type { return <> }', + true, + ], + 'component arrow expression': [ + 'const MyComponent = () => { return <> }', + true, + ], + 'component arrow expression with types (1)': [ + 'const MyComponent = (): Type => { return <> }', + true, + ], + 'component arrow expression with types (2)': [ + 'const MyComponent: Type = () => { return <> }', + true, + ], + 'component arrow expression with types (3)': [ + 'const MyComponent: SomeComplexType = () => { return <> }', + true, + ], + 'component arrow expressions': [ + 'const a = 0, MyComponent = () => { return <> }', + true, + ], + 'component arrow expressions (let)': [ + 'let a = 0, MyComponent = () => { return <> }', + true, + ], + 'component function expression': [ + 'const MyComponent = function() { return <> }', + true, + ], + 'component function expression with types (1)': [ + 'const MyComponent = function(): Type { return <> }', + true, + ], + 'component function expression with types (2)': [ + 'const MyComponent: Type = function() { return <> }', + true, + ], + 'component function expression with types (3)': [ + 'const MyComponent: SomeComplexType = function() { return <> }', + true, + ], + 'component function expression (let)': [ + 'let MyComponent = function() { return <> }', + true, + ], + 'component function expression (var)': [ + 'var MyComponent = function() { return <> }', + true, + ], + 'exported component declaration': [ + 'export default function Page() { return <> }', + true, + ], + 'exported component declaration with types': [ + 'export default function Page(): Type { return <> }', + true, + ], + 'component assignment': [ + 'let MyComponent; MyComponent = function() { return <> }', + true, + ], + 'component default declaration': [ + 'const { MyComponent = function() { return <> } } = {}', + true, + ], + 'component default assignment': [ + 'let MyComponent; ({ MyComponent = function() { return <> } }) = {}', + true, + ], + 'component property function expression': [ + 'const components = { MyComponent: function() { return <> } }', + true, + ], + 'component property arrow function expression': [ + 'const components = { MyComponent: () => <> }', + true, + ], + 'component method': [ + 'const components = { MyComponent() { return <> } }', + true, + ], + + 'hook declaration': ['function useEffect() { return <> }', true], + 'hook arrow expression': ['const useMyHook = () => { return <> }', true], + 'hook function expression': [ + 'const useMyHook = function() { return <> }', + true, + ], + 'hook with digit': ['function use0() { return <> }', true], + 'hook using hooks': [ + 'function useMyHook() { return useOtherHook() }', + true, + ], + 'hook using nested hooks': [ + 'function useMyHook() { return Foo.useOtherHook() }', + true, + ], + + 'React.forwardRef': ['React.forwardRef(() => <>)', true], + 'React.memo': ['React.memo(() => <>)', true], + forwardRef: [ + 'import { forwardRef } from "react"; forwardRef(() => <>)', + true, + ], + memo: ['import { memo } from "react"; memo(() => <>)', true], + + 'edge case: memo callback with hooks': [ + `import React, { useState } from "react"; +import { jsx } from "react/jsx-runtime" + +export const components = { + A: React.memo(() => { + const [state, setState] = useState(0); + + return jsx("div", { children: state }) + }) +}`, + true, + ], + 'edge case: memo without namespace': [ + `import { memo, useState } from "react"; + +export default memo(() => { + const [count, setCount] = useState(0); + return
{count}
+})`, + true, + ], + 'edge case: memo without namespace from re-export': [ + `import { memo, useState } from "my-react"; + +export default memo(() => { + const [count, setCount] = useState(0); + return
{count}
+})`, + true, + ], + + 'simple variable': ['const foo = 1', false], + 'lowercase function': ['function bar() {}', false], + 'lowercase arrow function': ['let baz = () => {}', false], + 'non assignments (1)': ['(0,useState)()', false], + 'non assignments (2)': ['[useState][0]()', false], + 'non assignments (3)': ['useState;s()', false], + 'non assignments (4)': ['useState,s()', false], + 'object without methods (1)': ['const obj = { useState: 1 }', false], + 'object without methods (2)': ['const obj = { Foo: 1 }', false], + } + + for (const [name, [code, expected]] of Object.entries(cases)) { + test(name, () => { + expect(defaultCodeFilter.test(code)).toBe(expected) + }) + } +}) diff --git a/packages/plugin-react/src/reactCompilerPreset.ts b/packages/plugin-react/src/reactCompilerPreset.ts index 8d750f806..cc8bb42af 100644 --- a/packages/plugin-react/src/reactCompilerPreset.ts +++ b/packages/plugin-react/src/reactCompilerPreset.ts @@ -3,6 +3,9 @@ import type { RolldownBabelPreset, } from '#optionalTypes' +export const defaultCodeFilter = + /forwardRef|memo|(?:const|let|var|function)\s+(?:[A-Z]|use[A-Z0-9])|(?:[A-Z]|use[A-Z0-9])[^\s:=(){}[\],;]*\s*(?:\(|[:=]\s*(?:function|\())/ + export const reactCompilerPreset = ( options: Pick< ReactCompilerBabelPluginOptions, @@ -18,7 +21,7 @@ export const reactCompilerPreset = ( code: options.compilationMode === 'annotation' ? /['"]use memo['"]/ - : /\b[A-Z]|\buse/, + : defaultCodeFilter, }, applyToEnvironmentHook: (env) => env.config.consumer === 'client', optimizeDeps: {