Add ReactFeatureFlags support to eprh (#35951)

We're currently hardcoding experimental options to
`eslint-plugin-react-hooks`. This blocks the release on features that
might not be ready.

This PR extends the ReactFeatureFlag infra to support flags for
`eslint-plugin-react-hooks`. An alternative would be to create a
separate flag system for build tools, but for now we have a small number
of these and reusing existing infra seems like the simplest approach.

I ran a full `yarn build` and checked the output resolved the flag
values as expected:


_build/oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js_
```js
var eprh_enableUseKeyedStateCompilerLint = false;
var eprh_enableVerboseNoSetStateInEffectCompilerLint = false;
var eprh_enableExhaustiveEffectDependenciesCompilerLint = 'off';
```


_build/facebook-www/ESLintPluginReactHooks-dev.classic.js_

```js
var eprh_enableUseKeyedStateCompilerLint = true;
var eprh_enableVerboseNoSetStateInEffectCompilerLint = true;
var eprh_enableExhaustiveEffectDependenciesCompilerLint = 'extra-only';
```

---------

Co-authored-by: lauren <[email protected]>
This commit is contained in:
Jack Pope
2026-03-24 23:13:27 -07:00
committed by GitHub
co-authored by lauren
parent 5c3f8962b8
commit e2ed67eeee
15 changed files with 125 additions and 33 deletions
@@ -116,11 +116,13 @@ jobs:
run: |
sed -i -e 's/ @license React*//' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Insert @headers into eslint plugin and react-refresh
run: |
sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \
build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
- name: Move relevant files for React in www into compiled
run: |
@@ -132,9 +134,9 @@ jobs:
mkdir ./compiled/facebook-www/__test_utils__
mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
# Copy eslint-plugin-react-hooks
# Copy eslint-plugin-react-hooks (www build with feature flags)
mkdir ./compiled/eslint-plugin-react-hooks
cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
./compiled/eslint-plugin-react-hooks/index.js
# Move unstable_server-external-runtime.js into facebook-www
@@ -0,0 +1,15 @@
/**
* Type declarations for shared/ReactFeatureFlags
*
* This allows importing from the Flow-typed ReactFeatureFlags.js file
* without TypeScript errors.
*/
declare module 'shared/ReactFeatureFlags' {
export const eprh_enableUseKeyedStateCompilerLint: boolean;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only';
}
@@ -21,6 +21,11 @@ import type * as ESTree from 'estree';
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
import {
eprh_enableUseKeyedStateCompilerLint,
eprh_enableVerboseNoSetStateInEffectCompilerLint,
eprh_enableExhaustiveEffectDependenciesCompilerLint,
} from 'shared/ReactFeatureFlags';
// Pattern for component names: starts with uppercase letter
const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -81,10 +86,7 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
// Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
if (node.type === 'FunctionDeclaration') {
// Check for Hermes-added flags indicating Flow component/hook syntax
if (
'__componentDeclaration' in node ||
'__hookDeclaration' in node
) {
if ('__componentDeclaration' in node || '__hookDeclaration' in node) {
return true;
}
const id = (node as ESTree.FunctionDeclaration).id;
@@ -107,7 +109,10 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
init.type === 'FunctionExpression')
) {
const name = decl.id.name;
if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
if (
COMPONENT_NAME_PATTERN.test(name) ||
HOOK_NAME_PATTERN.test(name)
) {
return true;
}
}
@@ -136,10 +141,13 @@ const COMPILER_OPTIONS: PluginOptions = {
validateNoCapitalizedCalls: [],
validateHooksUsage: true,
validateNoDerivedComputationsInEffects: true,
// Temporarily enabled for internal testing
enableUseKeyedState: true,
enableVerboseNoSetStateInEffect: true,
validateExhaustiveEffectDependencies: 'extra-only',
// Experimental options controlled by ReactFeatureFlags
enableUseKeyedState: eprh_enableUseKeyedStateCompilerLint,
enableVerboseNoSetStateInEffect:
eprh_enableVerboseNoSetStateInEffectCompilerLint,
validateExhaustiveEffectDependencies:
eprh_enableExhaustiveEffectDependenciesCompilerLint,
},
};
@@ -9,7 +9,8 @@
"types": ["estree-jsx", "node"],
"downlevelIteration": true,
"paths": {
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"]
"babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"],
"shared/*": ["../shared/*"]
},
"jsx": "react-jsxdev",
"rootDir": "../..",
+11
View File
@@ -252,3 +252,14 @@ export const enableAsyncDebugInfo: boolean = true;
export const enableUpdaterTracking = __PROFILE__;
export const ownerStackLimit = 1e4;
// -----------------------------------------------------------------------------
// eslint-plugin-react-hooks
// -----------------------------------------------------------------------------
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
@@ -85,5 +85,13 @@ export const enableInternalInstanceMap: boolean = false;
export const enableOptimisticKey: boolean = false;
export const enableParallelTransitions: boolean = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -85,5 +85,13 @@ export const enableProfilerNestedUpdatePhase: boolean = __PROFILE__;
export const enableUpdaterTracking: boolean = __PROFILE__;
export const enableParallelTransitions: boolean = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -94,5 +94,13 @@ export const enableObjectFiber: boolean = false;
export const enableOptimisticKey: boolean = false;
export const enableParallelTransitions: boolean = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -71,5 +71,13 @@ export const ownerStackLimit = 1e4;
export const enableOptimisticKey = false;
export const enableParallelTransitions = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -86,5 +86,13 @@ export const enableInternalInstanceMap: boolean = false;
export const enableOptimisticKey: boolean = false;
export const enableParallelTransitions: boolean = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'off';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -114,5 +114,13 @@ export const enableFragmentRefsInstanceHandles: boolean = true;
export const enableOptimisticKey: boolean = false;
export const eprh_enableUseKeyedStateCompilerLint: boolean = true;
export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = true;
export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
| 'off'
| 'all'
| 'extra-only'
| 'missing-only' = 'extra-only';
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
+8 -8
View File
@@ -190,7 +190,7 @@ function getNextMajorFlagValue(flag) {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected OSS Stable value ${value} for flag ${flag}`);
@@ -212,7 +212,7 @@ function getOSSCanaryFlagValue(flag) {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected OSS Canary value ${value} for flag ${flag}`);
@@ -229,7 +229,7 @@ function getOSSExperimentalFlagValue(flag) {
return '📊';
} else if (value === 'dev') {
return '💻';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(
@@ -250,7 +250,7 @@ function getWWWModernFlagValue(flag) {
return '💻';
} else if (value === 'gk') {
return '🧪';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected WWW Modern value ${value} for flag ${flag}`);
@@ -274,7 +274,7 @@ function getWWWClassicFlagValue(flag) {
return '💻';
} else if (value === 'gk') {
return '🧪';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected WWW Classic value ${value} for flag ${flag}`);
@@ -295,7 +295,7 @@ function getRNNextMajorFlagValue(flag) {
return '💻';
} else if (value === 'gk') {
return '🧪';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
@@ -320,7 +320,7 @@ function getRNOSSFlagValue(flag) {
return '💻';
} else if (value === 'gk') {
return '🧪';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
@@ -344,7 +344,7 @@ function getRNFBFlagValue(flag) {
return '💻';
} else if (value === 'gk') {
return '🧪';
} else if (typeof value === 'number') {
} else if (typeof value === 'number' || typeof value === 'string') {
return value;
} else {
throw new Error(`Unexpected RN FB value ${value} for flag ${flag}`);
+14 -12
View File
@@ -380,18 +380,20 @@ function getPlugins(
return [
// Keep dynamic imports as externals
dynamicImports(),
bundle.tsconfig != null
? typescript({tsconfig: bundle.tsconfig})
: {
name: 'rollup-plugin-flow-remove-types',
transform(code) {
const transformed = flowRemoveTypes(code);
return {
code: transformed.toString(),
map: null,
};
},
},
bundle.tsconfig != null ? typescript({tsconfig: bundle.tsconfig}) : false,
{
name: 'rollup-plugin-flow-remove-types',
transform(code, id) {
if (bundle.tsconfig != null && !id.endsWith('.js')) {
return null;
}
const transformed = flowRemoveTypes(code);
return {
code: transformed.toString(),
map: null,
};
},
},
// See https://github.com/rollup/plugins/issues/1425
bundle.tsconfig != null ? commonjs({strictRequires: true}) : false,
// Shim any modules that need forking in this environment.
+1 -1
View File
@@ -1235,7 +1235,7 @@ const bundles = [
// 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],
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD, CJS_DTS],
moduleType: ISOMORPHIC,
entry: 'eslint-plugin-react-hooks/src/index.ts',
global: 'ESLintPluginReactHooks',
+5
View File
@@ -17,6 +17,11 @@ function getFormat(filepath) {
// TODO: Should we lint them?
return null;
}
if (filepath.includes('ESLintPluginReactHooks')) {
// The ESLint plugin bundles compiler code with modern syntax that
// doesn't need to conform to the ES5 www lint rules.
return null;
}
return 'fb';
}
if (filepath.includes('react-native')) {