@samitouri / QOS-React / commits / 3cb2c42013

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 <lauren@anysphere.co>

Jack Pope committed Mar 25, 2026 at 02:13 UTC 3cb2c42013eda273ac449126ab9fcc115a09d39d
15 files changed +125 -33
.github/workflows/runtime_commit_artifacts.yml
+4 -2
@@ -116,11 +116,13 @@ jobs:
116 run: |
117 sed -i -e 's/ @license React*//' \
118 build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
119 + build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
120 build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
121 - name: Insert @headers into eslint plugin and react-refresh
122 run: |
123 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/' \
124 build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
125 + build/facebook-www/ESLintPluginReactHooks-dev.modern.js \
126 build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js
127 - name: Move relevant files for React in www into compiled
128 run: |
@@ -132,9 +134,9 @@ jobs:
134 mkdir ./compiled/facebook-www/__test_utils__
135 mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js
136
135 - # Copy eslint-plugin-react-hooks
137 + # Copy eslint-plugin-react-hooks (www build with feature flags)
138 mkdir ./compiled/eslint-plugin-react-hooks
137 - cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \
139 + cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \
140 ./compiled/eslint-plugin-react-hooks/index.js
141
142 # Move unstable_server-external-runtime.js into facebook-www
packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts new
+15
@@ -0,0 +1,15 @@
1 +/**
2 + * Type declarations for shared/ReactFeatureFlags
3 + *
4 + * This allows importing from the Flow-typed ReactFeatureFlags.js file
5 + * without TypeScript errors.
6 + */
7 +declare module 'shared/ReactFeatureFlags' {
8 + export const eprh_enableUseKeyedStateCompilerLint: boolean;
9 + export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean;
10 + export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
11 + | 'off'
12 + | 'all'
13 + | 'extra-only'
14 + | 'missing-only';
15 +}
packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts
+17 -9
@@ -21,6 +21,11 @@ import type * as ESTree from 'estree';
21 import * as HermesParser from 'hermes-parser';
22 import {isDeepStrictEqual} from 'util';
23 import type {ParseResult} from '@babel/parser';
24 +import {
25 + eprh_enableUseKeyedStateCompilerLint,
26 + eprh_enableVerboseNoSetStateInEffectCompilerLint,
27 + eprh_enableExhaustiveEffectDependenciesCompilerLint,
28 +} from 'shared/ReactFeatureFlags';
29
30 // Pattern for component names: starts with uppercase letter
31 const COMPONENT_NAME_PATTERN = /^[A-Z]/;
@@ -81,10 +86,7 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
86 // Also handles Flow component/hook syntax transformed to FunctionDeclaration with flags
87 if (node.type === 'FunctionDeclaration') {
88 // Check for Hermes-added flags indicating Flow component/hook syntax
84 - if (
85 - '__componentDeclaration' in node ||
86 - '__hookDeclaration' in node
87 - ) {
89 + if ('__componentDeclaration' in node || '__hookDeclaration' in node) {
90 return true;
91 }
92 const id = (node as ESTree.FunctionDeclaration).id;
@@ -107,7 +109,10 @@ function checkTopLevelNode(node: ESTree.Node): boolean {
109 init.type === 'FunctionExpression')
110 ) {
111 const name = decl.id.name;
110 - if (COMPONENT_NAME_PATTERN.test(name) || HOOK_NAME_PATTERN.test(name)) {
112 + if (
113 + COMPONENT_NAME_PATTERN.test(name) ||
114 + HOOK_NAME_PATTERN.test(name)
115 + ) {
116 return true;
117 }
118 }
@@ -136,10 +141,13 @@ const COMPILER_OPTIONS: PluginOptions = {
141 validateNoCapitalizedCalls: [],
142 validateHooksUsage: true,
143 validateNoDerivedComputationsInEffects: true,
139 - // Temporarily enabled for internal testing
140 - enableUseKeyedState: true,
141 - enableVerboseNoSetStateInEffect: true,
142 - validateExhaustiveEffectDependencies: 'extra-only',
144 +
145 + // Experimental options controlled by ReactFeatureFlags
146 + enableUseKeyedState: eprh_enableUseKeyedStateCompilerLint,
147 + enableVerboseNoSetStateInEffect:
148 + eprh_enableVerboseNoSetStateInEffectCompilerLint,
149 + validateExhaustiveEffectDependencies:
150 + eprh_enableExhaustiveEffectDependenciesCompilerLint,
151 },
152 };
153
packages/eslint-plugin-react-hooks/tsconfig.json
+2 -1
@@ -9,7 +9,8 @@
9 "types": ["estree-jsx", "node"],
10 "downlevelIteration": true,
11 "paths": {
12 - "babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"]
12 + "babel-plugin-react-compiler": ["../../compiler/packages/babel-plugin-react-compiler/src"],
13 + "shared/*": ["../shared/*"]
14 },
15 "jsx": "react-jsxdev",
16 "rootDir": "../..",
packages/shared/ReactFeatureFlags.js
+11
@@ -252,3 +252,14 @@ export const enableAsyncDebugInfo: boolean = true;
252 export const enableUpdaterTracking = __PROFILE__;
253
254 export const ownerStackLimit = 1e4;
255 +
256 +// -----------------------------------------------------------------------------
257 +// eslint-plugin-react-hooks
258 +// -----------------------------------------------------------------------------
259 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
260 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
261 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
262 + | 'off'
263 + | 'all'
264 + | 'extra-only'
265 + | 'missing-only' = 'off';
packages/shared/forks/ReactFeatureFlags.native-fb.js
+8
@@ -85,5 +85,13 @@ export const enableInternalInstanceMap: boolean = false;
85 export const enableOptimisticKey: boolean = false;
86 export const enableParallelTransitions: boolean = false;
87
88 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
89 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
90 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
91 + | 'off'
92 + | 'all'
93 + | 'extra-only'
94 + | 'missing-only' = 'off';
95 +
96 // Flow magic to verify the exports of this file match the original version.
97 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+8
@@ -85,5 +85,13 @@ export const enableProfilerNestedUpdatePhase: boolean = __PROFILE__;
85 export const enableUpdaterTracking: boolean = __PROFILE__;
86 export const enableParallelTransitions: boolean = false;
87
88 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
89 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
90 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
91 + | 'off'
92 + | 'all'
93 + | 'extra-only'
94 + | 'missing-only' = 'off';
95 +
96 // Flow magic to verify the exports of this file match the original version.
97 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+8
@@ -94,5 +94,13 @@ export const enableObjectFiber: boolean = false;
94 export const enableOptimisticKey: boolean = false;
95 export const enableParallelTransitions: boolean = false;
96
97 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
98 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
99 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
100 + | 'off'
101 + | 'all'
102 + | 'extra-only'
103 + | 'missing-only' = 'off';
104 +
105 // Flow magic to verify the exports of this file match the original version.
106 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+8
@@ -71,5 +71,13 @@ export const ownerStackLimit = 1e4;
71 export const enableOptimisticKey = false;
72 export const enableParallelTransitions = false;
73
74 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
75 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
76 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
77 + | 'off'
78 + | 'all'
79 + | 'extra-only'
80 + | 'missing-only' = 'off';
81 +
82 // Flow magic to verify the exports of this file match the original version.
83 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+8
@@ -86,5 +86,13 @@ export const enableInternalInstanceMap: boolean = false;
86 export const enableOptimisticKey: boolean = false;
87 export const enableParallelTransitions: boolean = false;
88
89 +export const eprh_enableUseKeyedStateCompilerLint: boolean = false;
90 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = false;
91 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
92 + | 'off'
93 + | 'all'
94 + | 'extra-only'
95 + | 'missing-only' = 'off';
96 +
97 // Flow magic to verify the exports of this file match the original version.
98 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.www.js
+8
@@ -114,5 +114,13 @@ export const enableFragmentRefsInstanceHandles: boolean = true;
114
115 export const enableOptimisticKey: boolean = false;
116
117 +export const eprh_enableUseKeyedStateCompilerLint: boolean = true;
118 +export const eprh_enableVerboseNoSetStateInEffectCompilerLint: boolean = true;
119 +export const eprh_enableExhaustiveEffectDependenciesCompilerLint:
120 + | 'off'
121 + | 'all'
122 + | 'extra-only'
123 + | 'missing-only' = 'extra-only';
124 +
125 // Flow magic to verify the exports of this file match the original version.
126 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
scripts/flags/flags.js
+8 -8
@@ -190,7 +190,7 @@ function getNextMajorFlagValue(flag) {
190 return '📊';
191 } else if (value === 'dev') {
192 return '💻';
193 - } else if (typeof value === 'number') {
193 + } else if (typeof value === 'number' || typeof value === 'string') {
194 return value;
195 } else {
196 throw new Error(`Unexpected OSS Stable value ${value} for flag ${flag}`);
@@ -212,7 +212,7 @@ function getOSSCanaryFlagValue(flag) {
212 return '📊';
213 } else if (value === 'dev') {
214 return '💻';
215 - } else if (typeof value === 'number') {
215 + } else if (typeof value === 'number' || typeof value === 'string') {
216 return value;
217 } else {
218 throw new Error(`Unexpected OSS Canary value ${value} for flag ${flag}`);
@@ -229,7 +229,7 @@ function getOSSExperimentalFlagValue(flag) {
229 return '📊';
230 } else if (value === 'dev') {
231 return '💻';
232 - } else if (typeof value === 'number') {
232 + } else if (typeof value === 'number' || typeof value === 'string') {
233 return value;
234 } else {
235 throw new Error(
@@ -250,7 +250,7 @@ function getWWWModernFlagValue(flag) {
250 return '💻';
251 } else if (value === 'gk') {
252 return '🧪';
253 - } else if (typeof value === 'number') {
253 + } else if (typeof value === 'number' || typeof value === 'string') {
254 return value;
255 } else {
256 throw new Error(`Unexpected WWW Modern value ${value} for flag ${flag}`);
@@ -274,7 +274,7 @@ function getWWWClassicFlagValue(flag) {
274 return '💻';
275 } else if (value === 'gk') {
276 return '🧪';
277 - } else if (typeof value === 'number') {
277 + } else if (typeof value === 'number' || typeof value === 'string') {
278 return value;
279 } else {
280 throw new Error(`Unexpected WWW Classic value ${value} for flag ${flag}`);
@@ -295,7 +295,7 @@ function getRNNextMajorFlagValue(flag) {
295 return '💻';
296 } else if (value === 'gk') {
297 return '🧪';
298 - } else if (typeof value === 'number') {
298 + } else if (typeof value === 'number' || typeof value === 'string') {
299 return value;
300 } else {
301 throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
@@ -320,7 +320,7 @@ function getRNOSSFlagValue(flag) {
320 return '💻';
321 } else if (value === 'gk') {
322 return '🧪';
323 - } else if (typeof value === 'number') {
323 + } else if (typeof value === 'number' || typeof value === 'string') {
324 return value;
325 } else {
326 throw new Error(`Unexpected RN OSS value ${value} for flag ${flag}`);
@@ -344,7 +344,7 @@ function getRNFBFlagValue(flag) {
344 return '💻';
345 } else if (value === 'gk') {
346 return '🧪';
347 - } else if (typeof value === 'number') {
347 + } else if (typeof value === 'number' || typeof value === 'string') {
348 return value;
349 } else {
350 throw new Error(`Unexpected RN FB value ${value} for flag ${flag}`);
scripts/rollup/build.js
+14 -12
@@ -380,18 +380,20 @@ function getPlugins(
380 return [
381 // Keep dynamic imports as externals
382 dynamicImports(),
383 - bundle.tsconfig != null
384 - ? typescript({tsconfig: bundle.tsconfig})
385 - : {
386 - name: 'rollup-plugin-flow-remove-types',
387 - transform(code) {
388 - const transformed = flowRemoveTypes(code);
389 - return {
390 - code: transformed.toString(),
391 - map: null,
392 - };
393 - },
394 - },
383 + bundle.tsconfig != null ? typescript({tsconfig: bundle.tsconfig}) : false,
384 + {
385 + name: 'rollup-plugin-flow-remove-types',
386 + transform(code, id) {
387 + if (bundle.tsconfig != null && !id.endsWith('.js')) {
388 + return null;
389 + }
390 + const transformed = flowRemoveTypes(code);
391 + return {
392 + code: transformed.toString(),
393 + map: null,
394 + };
395 + },
396 + },
397 // See https://github.com/rollup/plugins/issues/1425
398 bundle.tsconfig != null ? commonjs({strictRequires: true}) : false,
399 // Shim any modules that need forking in this environment.
scripts/rollup/bundles.js
+1 -1
@@ -1235,7 +1235,7 @@ const bundles = [
1235 // currently required in order for the package to be copied over correctly.
1236 // So, it would be worth improving that flow.
1237 name: 'eslint-plugin-react-hooks',
1238 - bundleTypes: [NODE_DEV, NODE_PROD, CJS_DTS],
1238 + bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD, CJS_DTS],
1239 moduleType: ISOMORPHIC,
1240 entry: 'eslint-plugin-react-hooks/src/index.ts',
1241 global: 'ESLintPluginReactHooks',
scripts/rollup/validate/index.js
+5
@@ -17,6 +17,11 @@ function getFormat(filepath) {
17 // TODO: Should we lint them?
18 return null;
19 }
20 + if (filepath.includes('ESLintPluginReactHooks')) {
21 + // The ESLint plugin bundles compiler code with modern syntax that
22 + // doesn't need to conform to the ES5 www lint rules.
23 + return null;
24 + }
25 return 'fb';
26 }
27 if (filepath.includes('react-native')) {