Compare commits

...

3 Commits

Author SHA1 Message Date
Lauren Tan
269325c04f [eprh] Try to fix tests
Summary:

Test Plan:

Reviewers:

Subscribers:

Tasks:

Tags:
2025-03-19 16:17:26 -04:00
Lauren Tan
829e8918a6 [eprh] Remove __EXPERIMENTAL__
`__EXPERIMENTAL__` flag doesn't have any use outside of the React repo. Let's remove these flags for now.
2025-03-19 16:17:26 -04:00
Lauren Tan
5644e81f27 [eprh] Move to compiler directory
Moves the plugin into the compiler directory.

- Remove eslint-plugin-react-hooks from bundles.js
- Remove eslint-plugin-react-hooks from ReactVersions.js
- Remove jest.config.js
- Remove babel.config-react-compiler.js
- Replace babel.config.js with copy from eslint-plugin-react-compiler
- Add tsup.config.ts to eslint-plugin-react-hooks
- Add eslint-plugin-react-hooks to compiler release scripts
2025-03-19 16:17:24 -04:00
35 changed files with 1107 additions and 413 deletions

View File

@@ -336,11 +336,11 @@ module.exports = {
'packages/react-devtools-extensions/**/*.js',
'packages/react-devtools-timeline/**/*.js',
'packages/react-native-renderer/**/*.js',
'packages/eslint-plugin-react-hooks/**/*.js',
'packages/jest-react/**/*.js',
'packages/internal-test-utils/**/*.js',
'packages/**/__tests__/*.js',
'packages/**/npm/*.js',
'compiler/packages/eslint-plugin-react-hooks/**/*.js',
],
rules: {
'react-internal/prod-error-codes': OFF,
@@ -515,7 +515,7 @@ module.exports = {
},
},
{
files: ['packages/eslint-plugin-react-hooks/src/**/*'],
files: ['compiler/packages/eslint-plugin-react-hooks/src/**/*'],
extends: ['plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin'],

View File

@@ -33,7 +33,6 @@ const canaryChannelLabel = 'canary';
const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '5.2.0',
'jest-react': '0.17.0',
react: ReactVersion,
'react-art': ReactVersion,

View File

@@ -1,19 +0,0 @@
'use strict';
/**
* HACK: @poteto React Compiler inlines Zod in its build artifact. Zod spreads values passed to .map
* which causes issues in @babel/plugin-transform-spread in loose mode, as it will result in
* {undefined: undefined} which fails to parse.
*
* [@babel/plugin-transform-block-scoping', {throwIfClosureRequired: true}] also causes issues with
* the built version of the compiler. The minimal set of plugins needed for this file is reexported
* from babel.config-ts.
*
* I will remove this hack later when we move eslint-plugin-react-hooks into the compiler directory.
**/
const baseConfig = require('./babel.config-ts');
module.exports = {
plugins: baseConfig.plugins,
};

View File

@@ -16,7 +16,7 @@
"snap:build": "yarn workspace snap run build",
"snap:ci": "yarn snap:build && yarn snap",
"ts:analyze-trace": "scripts/ts-analyze-trace.sh",
"lint": "yarn eslint src",
"lint": "../../node_modules/eslint-v8/bin/eslint.js src",
"watch": "yarn build --watch"
},
"dependencies": {
@@ -43,7 +43,7 @@
"babel-jest": "^29.0.3",
"babel-plugin-fbt": "^1.0.0",
"babel-plugin-fbt-runtime": "^1.0.0",
"eslint": "^8.57.1",
"eslint-v8": "npm:eslint@^8.57.1",
"invariant": "^2.2.4",
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.0.3",

View File

@@ -9,6 +9,8 @@ import {ErrorSeverity} from 'babel-plugin-react-compiler/src';
import {RuleTester as ESLintTester} from 'eslint';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
const ESLintTesterV8 = require('eslint-v8').RuleTester;
/**
* A string template tag that removes padding from the left side of multi-line strings
* @param {Array} strings array of code strings (only one expected)
@@ -309,7 +311,7 @@ const tests: CompilerTestCases = {
],
};
const eslintTester = new ESLintTester({
const eslintTester = new ESLintTesterV8({
parser: require.resolve('hermes-eslint'),
parserOptions: {
ecmaVersion: 2015,

View File

@@ -8,6 +8,8 @@
import {RuleTester} from 'eslint';
import ReactCompilerRule from '../src/rules/ReactCompilerRule';
const ESLintTesterV8 = require('eslint-v8').RuleTester;
/**
* A string template tag that removes padding from the left side of multi-line strings
* @param {Array} strings array of code strings (only one expected)
@@ -70,7 +72,7 @@ const tests: CompilerTestCases = {
],
};
const eslintTester = new RuleTester({
const eslintTester = new ESLintTesterV8({
parser: require.resolve('@typescript-eslint/parser'),
});
eslintTester.run('react-compiler', ReactCompilerRule, tests);

View File

@@ -26,7 +26,7 @@
"@types/eslint": "^8.56.12",
"@types/node": "^20.2.5",
"babel-jest": "^29.0.3",
"eslint": "8.57.0",
"eslint-v8": "npm:eslint@^8.57.1",
"hermes-eslint": "^0.25.1",
"jest": "^29.5.0"
},

View File

@@ -7675,61 +7675,60 @@ const tests = {
],
};
if (__EXPERIMENTAL__) {
tests.valid = [
...tests.valid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, []);
}
`,
},
];
tests.valid = [
...tests.valid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, []);
}
`,
},
];
tests.invalid = [
...tests.invalid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, [onStuff]);
}
`,
errors: [
{
message:
'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
'Remove `onStuff` from the list.',
suggestions: [
{
desc: 'Remove the dependency `onStuff`',
output: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, []);
}
`,
},
],
},
],
},
];
}
// useEffectEvent
tests.invalid = [
...tests.invalid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, [onStuff]);
}
`,
errors: [
{
message:
'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
'Remove `onStuff` from the list.',
suggestions: [
{
desc: 'Remove the dependency `onStuff`',
output: normalizeIndent`
function MyComponent({ theme }) {
const onStuff = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onStuff();
}, []);
}
`,
},
],
},
],
},
];
// Tests that are only valid/invalid across parsers supporting Flow
const testsFlow = {
@@ -8370,16 +8369,16 @@ describe('rules-of-hooks/exhaustive-deps', () => {
testsTypescriptEslintParser
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('@typescript-eslint/parser-v3'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@3.x',
ReactHooksESLintRule,
testsTypescriptEslintParser
);
// new ESLintTesterV9({
// languageOptions: {
// ...languageOptionsV9,
// parser: require('@typescript-eslint/parser-v3'),
// },
// }).run(
// 'eslint: v9, parser: @typescript-eslint/parser@3.x',
// ReactHooksESLintRule,
// testsTypescriptEslintParser
// );
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v4'),

View File

@@ -1286,180 +1286,181 @@ const tests = {
],
};
if (__EXPERIMENTAL__) {
tests.valid = [
...tests.valid,
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in a useEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={() => onClick()}></Child>;
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = () => { onClick() };
const onClick3 = useCallback(() => onClick(), []);
return <>
<Child onClick={onClick2}></Child>
<Child onClick={onClick3}></Child>
</>;
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
// and useEffectEvent.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
});
useEffect(() => {
let id = setInterval(onClick, 100);
return () => clearInterval(onClick);
}, []);
return <Child onClick={() => onClick2()} />
}
`,
},
{
code: normalizeIndent`
const MyComponent = ({theme}) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={() => onClick()}></Child>;
};
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
const notificationService = useNotifications();
const showNotification = useEffectEvent((text) => {
notificationService.notify(theme, text);
});
const onClick = useEffectEvent((text) => {
showNotification(text);
});
return <Child onClick={(text) => onClick(text)} />
}
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
useEffect(() => {
onClick();
});
const onClick = useEffectEvent(() => {
showNotification(theme);
});
}
`,
},
];
tests.invalid = [
...tests.invalid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={onClick}></Child>;
}
`,
errors: [useEffectEventError('onClick')],
},
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
function MyComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={onClick} />
}
// useEffectEvent
tests.valid = [
...tests.valid,
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in a useEffect.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
useEffect(() => {
onClick();
});
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={() => onClick()}></Child>;
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be called in closures.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = () => { onClick() };
const onClick3 = useCallback(() => onClick(), []);
return <>
<Child onClick={onClick2}></Child>
<Child onClick={onClick3}></Child>
</>;
}
`,
},
{
code: normalizeIndent`
// Valid because functions created with useEffectEvent can be passed by reference in useEffect
// and useEffectEvent.
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
const onClick2 = useEffectEvent(() => {
debounce(onClick);
});
useEffect(() => {
let id = setInterval(onClick, 100);
return () => clearInterval(onClick);
}, []);
return <Child onClick={() => onClick2()} />
}
`,
},
{
code: normalizeIndent`
const MyComponent = ({theme}) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={() => onClick()}></Child>;
};
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
const notificationService = useNotifications();
const showNotification = useEffectEvent((text) => {
notificationService.notify(theme, text);
});
const onClick = useEffectEvent((text) => {
showNotification(text);
});
return <Child onClick={(text) => onClick(text)} />
}
`,
},
{
code: normalizeIndent`
function MyComponent({ theme }) {
useEffect(() => {
onClick();
});
const onClick = useEffectEvent(() => {
showNotification(theme);
});
}
`,
},
];
// The useEffectEvent function shares an identifier name with the above
function MyOtherComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={() => onClick()} />
}
`,
errors: [{...useEffectEventError('onClick'), line: 7}],
},
{
code: normalizeIndent`
const MyComponent = ({ theme }) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={onClick}></Child>;
}
`,
errors: [useEffectEventError('onClick')],
},
{
code: normalizeIndent`
// Invalid because onClick is being aliased to foo but not invoked
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
let foo = onClick;
return <Bar onClick={foo} />
}
`,
errors: [{...useEffectEventError('onClick'), line: 7}],
},
{
code: normalizeIndent`
// Should error because it's being passed down to JSX, although it's been referenced once
// in an effect
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(them);
});
useEffect(() => {
setTimeout(onClick, 100);
});
return <Child onClick={onClick} />
}
`,
errors: [useEffectEventError('onClick')],
},
];
}
// useEffectEvent
tests.invalid = [
...tests.invalid,
{
code: normalizeIndent`
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={onClick}></Child>;
}
`,
errors: [useEffectEventError('onClick')],
},
{
code: normalizeIndent`
// This should error even though it shares an identifier name with the below
function MyComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={onClick} />
}
// The useEffectEvent function shares an identifier name with the above
function MyOtherComponent({theme}) {
const onClick = useEffectEvent(() => {
showNotification(theme)
});
return <Child onClick={() => onClick()} />
}
`,
errors: [{...useEffectEventError('onClick'), line: 7}],
},
{
code: normalizeIndent`
const MyComponent = ({ theme }) => {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
return <Child onClick={onClick}></Child>;
}
`,
errors: [useEffectEventError('onClick')],
},
{
code: normalizeIndent`
// Invalid because onClick is being aliased to foo but not invoked
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(theme);
});
let foo = onClick;
return <Bar onClick={foo} />
}
`,
errors: [{...useEffectEventError('onClick'), line: 7}],
},
{
code: normalizeIndent`
// Should error because it's being passed down to JSX, although it's been referenced once
// in an effect
function MyComponent({ theme }) {
const onClick = useEffectEvent(() => {
showNotification(them);
});
useEffect(() => {
setTimeout(onClick, 100);
});
return <Child onClick={onClick} />
}
`,
errors: [useEffectEventError('onClick')],
},
];
function conditionalError(hook, hasPreviousFinalizer = false) {
return {
@@ -1623,16 +1624,16 @@ describe('rules-of-hooks/rules-of-hooks', () => {
tests
);
new ESLintTesterV9({
languageOptions: {
...languageOptionsV9,
parser: require('@typescript-eslint/parser-v3'),
},
}).run(
'eslint: v9, parser: @typescript-eslint/parser@3.x',
ReactHooksESLintRule,
tests
);
// new ESLintTesterV9({
// languageOptions: {
// ...languageOptionsV9,
// parser: require('@typescript-eslint/parser-v3'),
// },
// }).run(
// 'eslint: v9, parser: @typescript-eslint/parser@3.x',
// ReactHooksESLintRule,
// tests
// );
new ESLintTesterV7({
parser: require.resolve('@typescript-eslint/parser-v4'),

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
module.exports = {
presets: [
['@babel/preset-env', {targets: {esmodules: false, node: 'current'}}],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-syntax-jsx',
'@babel/plugin-transform-flow-strip-types',
['@babel/plugin-proposal-class-properties', {loose: true}],
'@babel/plugin-transform-classes',
['@babel/plugin-transform-private-property-in-object', {loose: true}],
['@babel/plugin-transform-private-methods', {loose: true}],
],
};

View File

@@ -0,0 +1,11 @@
/** @type {import('jest').Config} */
const config = {
transform: {
'\\.[jt]sx?$': [
'babel-jest',
{configFile: require.resolve('./babel.config.js')},
],
},
};
module.exports = config;

View File

@@ -5,7 +5,7 @@
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/eslint-plugin-react-hooks"
"directory": "compiler/packages/eslint-plugin-react-hooks"
},
"files": [
"LICENSE",
@@ -21,9 +21,11 @@
"react"
],
"scripts": {
"build:compiler": "cd ../../compiler && yarn workspace babel-plugin-react-compiler build",
"build:compiler": "yarn workspace babel-plugin-react-compiler build",
"build": "rimraf dist && tsup",
"test": "yarn build:compiler && jest",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"watch": "yarn build --watch"
},
"license": "MIT",
"bugs": {
@@ -48,6 +50,7 @@
},
"devDependencies": {
"@babel/eslint-parser": "^7.11.4",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-typescript": "^7.26.0",
"@babel/types": "^7.19.0",
"@tsconfig/strictest": "^2.0.5",
@@ -60,6 +63,7 @@
"@types/estree-jsx": "^1.0.5",
"@types/node": "^20.2.5",
"babel-eslint": "^10.0.3",
"babel-jest": "^29.7.0",
"eslint-v7": "npm:eslint@^7.7.0",
"eslint-v8": "npm:eslint@^8.57.1",
"eslint-v9": "npm:eslint@^9.0.0",

View File

@@ -2056,10 +2056,7 @@ function isAncestorNodeOf(a: Node, b: Node): boolean {
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
return false;
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
function getUnknownDependenciesMessage(reactiveHookName: string): string {

View File

@@ -109,10 +109,7 @@ function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement {
}
function isUseEffectEventIdentifier(node: Node): boolean {
if (__EXPERIMENTAL__) {
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
return false;
return node.type === 'Identifier' && node.name === 'useEffectEvent';
}
function isUseIdentifier(node: Node): boolean {

View File

@@ -9,7 +9,7 @@
"types": ["estree-jsx", "node"],
"downlevelIteration": true,
"paths": {
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"]
"babel-plugin-react-compiler": ["../babel-plugin-react-compiler/src"]
},
"jsx": "react-jsxdev",
"rootDir": "../..",

View File

@@ -0,0 +1,36 @@
import {defineConfig} from 'tsup';
export default defineConfig({
entry: ['./src/index.ts'],
outDir: './dist',
external: [
'@babel/core',
'@babel/parser',
'@babel/plugin-proposal-private-methods',
'hermes-parser',
'zod',
'zod-validation-error',
],
splitting: false,
sourcemap: false,
dts: false,
bundle: true,
format: 'cjs',
platform: 'node',
banner: {
js: `/**
* 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.
*
* @lightSyntaxTransform
* @noflow
* @nolint
* @preventMunge
* @preserve-invariant-messages
*/
"use no memo";`,
},
});

View File

@@ -1,6 +1,7 @@
const PUBLISHABLE_PACKAGES = [
'babel-plugin-react-compiler',
'eslint-plugin-react-compiler',
'eslint-plugin-react-hooks',
'react-compiler-healthcheck',
'react-compiler-runtime',
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
/**
* This file is purely being used for local jest runs, and doesn't participate in the build process.
*/
'use strict';
module.exports = {
extends: '../../babel.config-ts.js',
};

View File

@@ -1,8 +0,0 @@
'use strict';
process.env.NODE_ENV = 'development';
module.exports = {
setupFiles: [require.resolve('../../scripts/jest/setupEnvironment.js')],
moduleFileExtensions: ['ts', 'js', 'json'],
};

View File

@@ -7,12 +7,6 @@ module.exports = {
'<rootDir>/scripts/bench/',
],
transform: {
'^.+babel-plugin-react-compiler/dist/index.js$': [
'babel-jest',
{
configFile: require.resolve('../../babel.config-react-compiler.js'),
},
],
'^.+\\.ts$': [
'babel-jest',
{configFile: require.resolve('../../babel.config-ts.js')},

View File

@@ -1182,24 +1182,6 @@ const bundles = [
externals: ['react', 'scheduler', 'scheduler/unstable_mock'],
},
/******* ESLint Plugin for Hooks *******/
{
// TODO: we're building this from typescript source now, but there's really
// no reason to have both dev and prod for this package. It's
// currently required in order for the package to be copied over correctly.
// So, it would be worth improving that flow.
name: 'eslint-plugin-react-hooks',
bundleTypes: [NODE_DEV, NODE_PROD, CJS_DTS],
moduleType: ISOMORPHIC,
entry: 'eslint-plugin-react-hooks/src/index.ts',
global: 'ESLintPluginReactHooks',
minifyWithProdErrorCodes: false,
wrapWithModuleBoundaries: false,
externals: [],
tsconfig: './packages/eslint-plugin-react-hooks/tsconfig.json',
prebuild: `mkdir -p ./compiler/packages/babel-plugin-react-compiler/dist && echo "module.exports = require('../src/index.ts');" > ./compiler/packages/babel-plugin-react-compiler/dist/index.js`,
},
/******* React Fresh *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],