From c0c29e89066df60c8340d4e853d38b5843c165ab Mon Sep 17 00:00:00 2001 From: Rahul salunke <137675576+rahul24salunke@users.noreply.github.com> Date: Wed, 25 Mar 2026 04:39:59 +0530 Subject: [PATCH 1/3] Fix typos in the documentation (#35439) ## Summary So in this PR the typo mistakes in the docs are corrected such as the 1. **Ie** it should be **"i.e"**. 2. **errros** should be the **"errors"**. 3. **consdier** should be the **"consider"**. 4. **CreatFrom** should be **"CreateForm"**. ## How did you test this change? I verified the fixes by reviewing the updated files locally to ensure the corrected terms appear consistently and accurately in the documentation. --------- Co-authored-by: Yummy_Bacon5 <68166338+YummyBacon5@users.noreply.github.com> --- compiler/docs/DESIGN_GOALS.md | 2 +- .../src/Inference/MUTABILITY_ALIASING_MODEL.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compiler/docs/DESIGN_GOALS.md b/compiler/docs/DESIGN_GOALS.md index 76d19e25c084..d07bd2b30189 100644 --- a/compiler/docs/DESIGN_GOALS.md +++ b/compiler/docs/DESIGN_GOALS.md @@ -8,7 +8,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar * Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast performance by default. * Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup. -* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts. +* Retain React's familiar declarative, component-oriented programming model. i.e, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts. * "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc). * Support typical debugging and profiling tools and workflows. * Be predictable and understandable enough by React developers โ€” i.e. developers should be able to quickly develop a rough intuition of how React Compiler works. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md index ab327c255b10..dfff673cabc4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -24,7 +24,7 @@ The goal of mutability and aliasing inference is to understand the set of instru In code, the mutability and aliasing model is compromised of the following phases: -* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. +* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. * `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values. * `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values. @@ -69,7 +69,7 @@ Describes the creation of new function value, capturing the given set of mutable kind: 'Apply'; receiver: Place; function: Place; // same as receiver for function calls - mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default + mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default args: Array; into: Place; // where result is stored signature: FunctionSignature | null; @@ -526,7 +526,7 @@ Capture c <- a Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations: -Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. +Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. ```js const b = [a]; // capture From 3cb2c42013eda273ac449126ab9fcc115a09d39d Mon Sep 17 00:00:00 2001 From: Jack Pope Date: Wed, 25 Mar 2026 02:13:27 -0400 Subject: [PATCH 2/3] 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 --- .../workflows/runtime_commit_artifacts.yml | 6 +++-- .../src/shared/ReactFeatureFlags.d.ts | 15 +++++++++++ .../src/shared/RunReactCompiler.ts | 26 ++++++++++++------- .../eslint-plugin-react-hooks/tsconfig.json | 3 ++- packages/shared/ReactFeatureFlags.js | 11 ++++++++ .../forks/ReactFeatureFlags.native-fb.js | 8 ++++++ .../forks/ReactFeatureFlags.native-oss.js | 8 ++++++ .../forks/ReactFeatureFlags.test-renderer.js | 8 ++++++ ...actFeatureFlags.test-renderer.native-fb.js | 8 ++++++ .../ReactFeatureFlags.test-renderer.www.js | 8 ++++++ .../shared/forks/ReactFeatureFlags.www.js | 8 ++++++ scripts/flags/flags.js | 16 ++++++------ scripts/rollup/build.js | 26 ++++++++++--------- scripts/rollup/bundles.js | 2 +- scripts/rollup/validate/index.js | 5 ++++ 15 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 1b98673cd4dd..11a22e6c2a4c 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -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 diff --git a/packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts b/packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts new file mode 100644 index 000000000000..6d135b41bde2 --- /dev/null +++ b/packages/eslint-plugin-react-hooks/src/shared/ReactFeatureFlags.d.ts @@ -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'; +} diff --git a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts index 9aaddb07e656..34151fe8c27e 100644 --- a/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts +++ b/packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts @@ -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, }, }; diff --git a/packages/eslint-plugin-react-hooks/tsconfig.json b/packages/eslint-plugin-react-hooks/tsconfig.json index c5d8847f1ec4..22c36e50eb6e 100644 --- a/packages/eslint-plugin-react-hooks/tsconfig.json +++ b/packages/eslint-plugin-react-hooks/tsconfig.json @@ -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": "../..", diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index ba4a7c52e3d3..c1d12dfcfbd4 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -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'; diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0e43e34d0009..e420b5443cc4 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -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); diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 035bf2a75dd0..d37dff7f34e9 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -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); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 76597e0cbb01..537448d3904a 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -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); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index 8022dd8e2254..8d97f9dd9ddd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -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); diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 271c464daa60..53b8b487df3b 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -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); diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index a07f34414217..c38c32a9e865 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -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); diff --git a/scripts/flags/flags.js b/scripts/flags/flags.js index a02b4d84b341..7a5c9730f22a 100644 --- a/scripts/flags/flags.js +++ b/scripts/flags/flags.js @@ -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}`); diff --git a/scripts/rollup/build.js b/scripts/rollup/build.js index 630391822803..66b74b436349 100644 --- a/scripts/rollup/build.js +++ b/scripts/rollup/build.js @@ -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. diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 8a551d2a1549..dbf6160a847e 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -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', diff --git a/scripts/rollup/validate/index.js b/scripts/rollup/validate/index.js index 08d356190aa8..429a9bbc6b3e 100644 --- a/scripts/rollup/validate/index.js +++ b/scripts/rollup/validate/index.js @@ -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')) { From c5362ecafaf3056a8940b4748f8fd291d2e13f3f Mon Sep 17 00:00:00 2001 From: Daniel Saewitz Date: Wed, 25 Mar 2026 08:34:33 -0400 Subject: [PATCH 3/3] TIM-471: Fizz internals analysis for fused renderer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive analysis of ReactFizzServer.js covering: - renderElement() dispatch table and insertion points - Request object shape and lifecycle - Task/Segment model and how they interact - Suspense boundary suspend โ†’ resolve โ†’ stream flow - The hydration data TODO at line 5944 (exact insertion point) - How components are called via renderWithHooks - Concrete entry points for fused renderer changes --- design/fused-renderer-fizz-analysis.md | 306 +++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 design/fused-renderer-fizz-analysis.md diff --git a/design/fused-renderer-fizz-analysis.md b/design/fused-renderer-fizz-analysis.md new file mode 100644 index 000000000000..536dd6fa9d61 --- /dev/null +++ b/design/fused-renderer-fizz-analysis.md @@ -0,0 +1,306 @@ +# Fizz Internals Analysis for Fused Renderer + +Analysis of `packages/react-server/src/ReactFizzServer.js` and +`packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js` to identify +insertion points for the fused single-pass renderer. + +## 1. renderElement() Dispatch Table + +Location: `ReactFizzServer.js:2917` + +`renderElement(request, task, keyPath, type, props, ref)` dispatches on `type`: + +| Type check | Handler | Notes | +|---|---|---| +| `typeof type === 'function'` + `shouldConstruct(type)` | `renderClassComponent()` | Class components with `isReactComponent` | +| `typeof type === 'function'` (else) | `renderFunctionComponent()` | **All function components go here โ€” server components would too** | +| `typeof type === 'string'` | `renderHostElement()` | DOM elements (`div`, `span`, etc.) | +| `type === REACT_FRAGMENT_TYPE` | `renderNodeDestructive(props.children)` | Fragments, also StrictMode, Profiler, LegacyHidden | +| `type === REACT_ACTIVITY_TYPE` | `renderActivity()` | Activity (offscreen) | +| `type === REACT_SUSPENSE_LIST_TYPE` | `renderSuspenseList()` | SuspenseList ordering | +| `type === REACT_VIEW_TRANSITION_TYPE` | `renderViewTransition()` | View transitions (feature-flagged) | +| `type === REACT_SCOPE_TYPE` | `renderNodeDestructive(props.children)` | Scope API (feature-flagged) | +| `type === REACT_SUSPENSE_TYPE` | `renderSuspenseBoundary()` | **Suspense โ€” key for async server components** | +| `type.$$typeof === REACT_FORWARD_REF_TYPE` | `renderForwardRef()` | ForwardRef wrappers | +| `type.$$typeof === REACT_MEMO_TYPE` | `renderMemo()` | Memo wrappers | +| `type.$$typeof === REACT_CONTEXT_TYPE` | `renderContextProvider()` | Context.Provider | +| `type.$$typeof === REACT_CONSUMER_TYPE` | `renderContextConsumer()` | Context.Consumer | +| `type.$$typeof === REACT_LAZY_TYPE` | `renderLazyComponent()` | Lazy loading | +| Otherwise | **throw** | "Element type is invalid" | + +### Key observation for fused renderer + +Today, `typeof type === 'function'` catches **all** function components. Fizz has no concept of "server component" vs "client component" โ€” Flight resolves everything before Fizz sees it. In fused mode, we need to distinguish: + +1. **Server component**: function without `'use client'` module reference โ†’ call it inline +2. **Client component**: function with module reference โ†’ render to HTML + emit hydration data +3. **Plain function component**: regular component (neither server nor client) โ†’ render as today + +The detection mechanism will depend on how the bundler marks client components (typically via `$$typeof` on the module reference or a special property). See TIM-472 flight analysis for details. + +## 2. Request Object Shape + +Location: `ReactFizzServer.js:366` (opaque type), `ReactFizzServer.js:517` (RequestInstance constructor) + +### Fields + +``` +Request { + // Output + destination: null | Destination // The writable stream we flush to + flushScheduled: boolean // Whether a flush is pending + + // Configuration (immutable after creation) + resumableState: ResumableState // Tracks what instructions have been sent (deduplication) + renderState: RenderState // Precomputed chunks, script prefixes, boundary prefixes + rootFormatContext: FormatContext // HTML/SVG/MathML context for the root + progressiveChunkSize: number // Target chunk size for streaming (~12.8KB default) + + // Lifecycle state + status: OPENING | OPEN | ABORTING | CLOSING | CLOSED | STALLED_DEV + fatalError: mixed // Set when a fatal error occurs + + // Task tracking + nextSegmentId: number // Auto-incrementing ID for new segments + allPendingTasks: number // Total pending tasks (when 0, connection can close) + pendingRootTasks: number // Pending tasks in the root/shell (when 0, shell is done) + abortableTasks: Set // All tasks that can be aborted + pingedTasks: Array // High-priority tasks to work on next (the work queue) + + // Output queues (flushed in priority order) + completedRootSegment: null | Segment // The root segment, once completed + completedPreambleSegments: null | Array> // Preamble segments (head content) + byteSize: number // Accumulated shell bytes + clientRenderedBoundaries: Array // Errored boundaries โ†’ client render + completedBoundaries: Array // Done boundaries โ†’ stream replacement + partialBoundaries: Array // Partially done โ†’ stream segments + + // Prerender tracking + trackedPostpones: null | PostponedHoles // Non-null during prerender to track holes + + // Callbacks + onError: (error, errorInfo) => ?string + onAllReady: () => void // All tasks done (good for static generation) + onShellReady: () => void // Shell/root done (good for streaming start) + onShellError: (error) => void // Shell failed + onFatalError: (error) => void // Unrecoverable + + // Form state + formState: null | ReactFormState // For MPA form submission hydration +} +``` + +### Lifecycle + +1. **OPENING** (10): Created, initial work scheduled via `scheduleMicrotask` +2. **OPEN** (11): First `scheduleWork` callback fired +3. **ABORTING** (12): `abort()` called, tasks are being torn down +4. **CLOSING** (13): All tasks done, flushing final output +5. **CLOSED** (14): Done, destination closed + +### Where to add fused renderer state + +A `fusedMode: boolean` flag on Request would gate the new behavior. Additionally: +- `clientBoundaryQueue: Array<{id, moduleRef, serializedProps}>` for hydration data +- `nextClientBoundaryId: number` for generating boundary IDs + +These would be added to RequestInstance constructor and the Request opaque type. + +## 3. Task/Segment Model + +### Task types + +Two task types share the `Task` union: + +**RenderTask** โ€” produces HTML into a Segment: +``` +RenderTask { + replay: null // null = render mode (not replay) + node: ReactNodeList // The React node being rendered + childIndex: number // Position within parent's children + ping: () => void // Callback to re-queue this task + blockedBoundary: Root | SuspenseBoundary // The Suspense boundary this renders into + blockedSegment: Segment // The segment being written to + blockedPreamble: null | PreambleState // Preamble state for head content + hoistableState: null | HoistableState // Hoistable resources (stylesheets, etc.) + abortSet: Set // Which abort set this belongs to + keyPath: Root | KeyNode // React element key path + formatContext: FormatContext // HTML/SVG/MathML rendering context + context: ContextSnapshot // React context state + treeContext: TreeContext // ID generation tree context + row: null | SuspenseListRow // SuspenseList row tracking + componentStack: ComponentStackNode // For error messages + thenableState: null | ThenableState // Saved thenable state for resumption + legacyContext: LegacyContext // Legacy context (being removed) +} +``` + +**ReplayTask** โ€” replays a prerendered tree (used for resume): +- Same shape but `replay: ReplaySet` (non-null) and `blockedSegment: null` + +### Segment + +``` +Segment { + status: PENDING | COMPLETED | FLUSHED | ABORTED | ERRORED | POSTPONED | RENDERING + parentFlushed: boolean // Can this be flushed (parent already sent)? + id: number // Lazily assigned segment ID + index: number // Position within parent's chunks + chunks: Array // THE HTML OUTPUT BUFFER + children: Array // Child segments (from Suspense boundaries) + preambleChildren: Array // Preamble child segments + parentFormatContext: FormatContext // Context when this segment was created + boundary: null | SuspenseBoundary // Associated boundary (for fallback segments) + lastPushedText: boolean // Text separator tracking + textEmbedded: boolean // Text separator tracking +} +``` + +### How they interact + +1. `createRequest()` creates a root Segment and a root RenderTask pointing at it +2. The task is pushed to `request.pingedTasks` +3. `performWork()` iterates `pingedTasks`, calling `retryTask()` on each +4. `retryTask()` โ†’ `retryRenderTask()` โ†’ `retryNode()` โ†’ `renderNodeDestructive()` โ†’ `renderElement()` +5. HTML chunks are pushed into `segment.chunks` via `pushStartInstance()` / `pushEndInstance()` +6. When a Suspense boundary is encountered, new child Segments and Tasks are created +7. When a Task completes, `finishedSegment()` and `finishedTask()` update counters +8. When all tasks for a boundary complete, it moves to `completedBoundaries` +9. `flushCompletedQueues()` writes segments to the destination stream + +## 4. Suspense Boundary Flow (suspend โ†’ resolve โ†’ stream) + +Location: `ReactFizzServer.js:1337` (`renderSuspenseBoundary`) + +### Normal render path (not prerender) + +1. **Create boundary**: `createSuspenseBoundary()` โ€” tracks pending tasks, completed segments +2. **Create segments**: + - `boundarySegment` โ€” holds the fallback content (child of parent segment) + - `contentRootSegment` โ€” holds the actual content (independent) +3. **Try to render content synchronously**: + - Temporarily swap `task.blockedBoundary` and `task.blockedSegment` to the new boundary/segment + - Call `renderNode(request, task, content, -1)` + - If it succeeds without suspending โ†’ boundary is COMPLETED immediately + - If the boundary is small enough, skip creating the fallback entirely (early return) +4. **If content throws a thenable** (suspends): + - Caught in `renderNode()` catch block (~line 4268) + - `spawnNewSuspendedRenderTask()` creates a new task for the suspended subtree + - New task gets a new child segment, registered with the boundary + - The thenable's `.then(ping, ping)` ensures the task is re-queued when resolved +5. **Create fallback task**: Always created (unless early return), queued to `pingedTasks` +6. **When suspended content resolves**: + - `ping()` โ†’ `pingTask()` โ†’ pushes task to `pingedTasks` + - Next `performWork()` calls `retryRenderTask()` on it + - Rendering continues from where it left off (same node, same context) + - On completion: `finishedSegment()` + `finishedTask()` +7. **When boundary completes**: + - `finishedTask()` decrements `boundary.pendingTasks` + - When it reaches 0: boundary moves to `completedBoundaries` queue + - `flushCompletedQueues()` writes a streaming `