From 294c33f34da0b5f908946c9add86f58426e7da5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 8 Sep 2025 12:28:14 -0400 Subject: [PATCH 1/3] [Flight] Always initialize a debug info array for each Chunk (#34419) I'm about to add info for pretty much all of these anyway since they all depend on the data stream itself. --- .../react-client/src/ReactFlightClient.js | 65 +++++++++---------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index fc59a91fb2fac..c7cb641cd242b 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -169,7 +169,7 @@ type PendingChunk = { reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null | SomeChunk, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type BlockedChunk = { @@ -178,7 +178,7 @@ type BlockedChunk = { reason: null | Array mixed)>, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModelChunk = { @@ -187,7 +187,7 @@ type ResolvedModelChunk = { reason: Response, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null | SomeChunk, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type ResolvedModuleChunk = { @@ -196,7 +196,7 @@ type ResolvedModuleChunk = { reason: null, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedChunk = { @@ -205,7 +205,7 @@ type InitializedChunk = { reason: null | FlightStreamController, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type InitializedStreamChunk< @@ -216,7 +216,7 @@ type InitializedStreamChunk< reason: FlightStreamController, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (ReadableStream) => mixed, reject?: (mixed) => mixed): void, }; type ErroredChunk = { @@ -225,7 +225,7 @@ type ErroredChunk = { reason: mixed, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type HaltedChunk = { @@ -234,7 +234,7 @@ type HaltedChunk = { reason: null, _children: Array> | ProfilingResult, // Profiling-only _debugChunk: null, // DEV-only - _debugInfo: null | ReactDebugInfo, // DEV-only + _debugInfo: ReactDebugInfo, // DEV-only then(resolve: (T) => mixed, reject?: (mixed) => mixed): void, }; type SomeChunk = @@ -256,7 +256,7 @@ function ReactPromise(status: any, value: any, reason: any) { } if (__DEV__) { this._debugChunk = null; - this._debugInfo = null; + this._debugInfo = []; } } // We subclass Promise.prototype so that we get other methods like .catch @@ -798,12 +798,10 @@ function resolveModuleChunk( resolvedChunk.value = value; if (__DEV__) { const debugInfo = getModuleDebugInfo(value); - if (debugInfo !== null && resolvedChunk._debugInfo != null) { + if (debugInfo !== null) { // Add to the live set if it was already initialized. // $FlowFixMe[method-unbinding] resolvedChunk._debugInfo.push.apply(resolvedChunk._debugInfo, debugInfo); - } else { - resolvedChunk._debugInfo = debugInfo; } } if (resolveListeners !== null) { @@ -842,7 +840,7 @@ function initializeDebugChunk( ): void { const debugChunk = chunk._debugChunk; if (debugChunk !== null) { - const debugInfo = chunk._debugInfo || (chunk._debugInfo = []); + const debugInfo = chunk._debugInfo; try { if (debugChunk.status === RESOLVED_MODEL) { // Find the index of this debug info by walking the linked list. @@ -1303,10 +1301,8 @@ function createLazyChunkWrapper( _init: readChunk, }; if (__DEV__) { - // Ensure we have a live array to track future debug info. - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo)); - lazyType._debugInfo = chunkDebugInfo; + // Forward the live array + lazyType._debugInfo = chunk._debugInfo; // Initialize a store for key validation by the JSX runtime. lazyType._store = {validated: validated}; } @@ -1508,9 +1504,7 @@ function rejectReference( // $FlowFixMe[cannot-write] erroredComponent.debugTask = element._debugTask; } - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); - chunkDebugInfo.push(erroredComponent); + chunk._debugInfo.push(erroredComponent); } } @@ -1750,9 +1744,7 @@ function loadServerReference, T>( // $FlowFixMe[cannot-write] erroredComponent.debugTask = element._debugTask; } - const chunkDebugInfo: ReactDebugInfo = - chunk._debugInfo || (chunk._debugInfo = []); - chunkDebugInfo.push(erroredComponent); + chunk._debugInfo.push(erroredComponent); } } @@ -1770,7 +1762,7 @@ function transferReferencedDebugInfo( referencedChunk: SomeChunk, referencedValue: mixed, ): void { - if (__DEV__ && referencedChunk._debugInfo) { + if (__DEV__) { const referencedDebugInfo = referencedChunk._debugInfo; // If we have a direct reference to an object that was rendered by a synchronous // server component, it might have some debug info about how it was rendered. @@ -1784,24 +1776,29 @@ function transferReferencedDebugInfo( referencedValue !== null && (isArray(referencedValue) || typeof referencedValue[ASYNC_ITERATOR] === 'function' || - referencedValue.$$typeof === REACT_ELEMENT_TYPE) && - !referencedValue._debugInfo + referencedValue.$$typeof === REACT_ELEMENT_TYPE) ) { // We should maybe use a unique symbol for arrays but this is a React owned array. // $FlowFixMe[prop-missing]: This should be added to elements. - Object.defineProperty((referencedValue: any), '_debugInfo', { - configurable: false, - enumerable: false, - writable: true, - value: referencedDebugInfo, - }); + const existingDebugInfo: ?ReactDebugInfo = + (referencedValue._debugInfo: any); + if (existingDebugInfo == null) { + Object.defineProperty((referencedValue: any), '_debugInfo', { + configurable: false, + enumerable: false, + writable: true, + value: referencedDebugInfo.slice(0), // Clone so that pushing later isn't going into the original + }); + } else { + // $FlowFixMe[method-unbinding] + existingDebugInfo.push.apply(existingDebugInfo, referencedDebugInfo); + } } // We also add it to the initializing chunk since the resolution of that promise is // also blocked by these. By adding it to both we can track it even if the array/element // is extracted, or if the root is rendered as is. if (parentChunk !== null) { - const parentDebugInfo = - parentChunk._debugInfo || (parentChunk._debugInfo = []); + const parentDebugInfo = parentChunk._debugInfo; // $FlowFixMe[method-unbinding] parentDebugInfo.push.apply(parentDebugInfo, referencedDebugInfo); } From 3f2a42a5decc88551d34c96f3d031c316ac34f6a Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:33:10 -0700 Subject: [PATCH 2/3] [compiler] Handle empty list of eslint suppression rules (#34323) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34323). * #34276 * __->__ #34323 --- .../src/Entrypoint/Options.ts | 5 ++ .../src/Entrypoint/Suppression.ts | 27 +++++++--- ...empty-eslint-suppressions-config.expect.md | 53 +++++++++++++++++++ .../empty-eslint-suppressions-config.js | 15 ++++++ 4 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts index c13940ed10a21..1ebdb68f9501e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Options.ts @@ -135,7 +135,12 @@ export type PluginOptions = { */ eslintSuppressionRules: Array | null | undefined; + /** + * Whether to report "suppression" errors for Flow suppressions. If false, suppression errors + * are only emitted for ESLint suppressions + */ flowSuppressions: boolean; + /* * Ignore 'use no forget' annotations. Helpful during testing but should not be used in production. */ diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts index 509ed72966758..24a9bccf4264b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Suppression.ts @@ -86,12 +86,18 @@ export function findProgramSuppressions( let enableComment: t.Comment | null = null; let source: SuppressionSource | null = null; - const rulePattern = `(${ruleNames.join('|')})`; - const disableNextLinePattern = new RegExp( - `eslint-disable-next-line ${rulePattern}`, - ); - const disablePattern = new RegExp(`eslint-disable ${rulePattern}`); - const enablePattern = new RegExp(`eslint-enable ${rulePattern}`); + let disableNextLinePattern: RegExp | null = null; + let disablePattern: RegExp | null = null; + let enablePattern: RegExp | null = null; + if (ruleNames.length !== 0) { + const rulePattern = `(${ruleNames.join('|')})`; + disableNextLinePattern = new RegExp( + `eslint-disable-next-line ${rulePattern}`, + ); + disablePattern = new RegExp(`eslint-disable ${rulePattern}`); + enablePattern = new RegExp(`eslint-enable ${rulePattern}`); + } + const flowSuppressionPattern = new RegExp( '\\$(FlowFixMe\\w*|FlowExpectedError|FlowIssue)\\[react\\-rule', ); @@ -107,6 +113,7 @@ export function findProgramSuppressions( * CommentLine within the block. */ disableComment == null && + disableNextLinePattern != null && disableNextLinePattern.test(comment.value) ) { disableComment = comment; @@ -124,12 +131,16 @@ export function findProgramSuppressions( source = 'Flow'; } - if (disablePattern.test(comment.value)) { + if (disablePattern != null && disablePattern.test(comment.value)) { disableComment = comment; source = 'Eslint'; } - if (enablePattern.test(comment.value) && source === 'Eslint') { + if ( + enablePattern != null && + enablePattern.test(comment.value) && + source === 'Eslint' + ) { enableComment = comment; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.expect.md new file mode 100644 index 0000000000000..eeb0ba6c96db9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @eslintSuppressionRules:[] + +// The suppression here shouldn't cause compilation to get skipped +// Previously we had a bug where an empty list of suppressions would +// create a regexp that matched any suppression +function Component(props) { + 'use forget'; + // eslint-disable-next-line foo/not-react-related + return
{props.text}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{text: 'Hello'}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @eslintSuppressionRules:[] + +// The suppression here shouldn't cause compilation to get skipped +// Previously we had a bug where an empty list of suppressions would +// create a regexp that matched any suppression +function Component(props) { + "use forget"; + const $ = _c(2); + let t0; + if ($[0] !== props.text) { + t0 =
{props.text}
; + $[0] = props.text; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ text: "Hello" }], +}; + +``` + +### Eval output +(kind: ok)
Hello
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.js new file mode 100644 index 0000000000000..b81132d3b8269 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/empty-eslint-suppressions-config.js @@ -0,0 +1,15 @@ +// @eslintSuppressionRules:[] + +// The suppression here shouldn't cause compilation to get skipped +// Previously we had a bug where an empty list of suppressions would +// create a regexp that matched any suppression +function Component(props) { + 'use forget'; + // eslint-disable-next-line foo/not-react-related + return
{props.text}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{text: 'Hello'}], +}; From d4374b3ae37eb972af6fc492de294a06edd6d325 Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Mon, 8 Sep 2025 14:21:03 -0400 Subject: [PATCH 3/3] [compiler] [playground] Show internals toggle (#34399) ## Summary Added a "Show Internals" toggle switch to either show only the Config, Input, Output, and Source Map tabs, or these tabs + all the additional compiler options. The open/close state of these tabs will be preserved (unless on page refresh, which is the same as the currently functionality). ## How did you test this change? https://github.com/user-attachments/assets/8eb0f69e-360c-4e9b-9155-7aa185a0c018 --- .../playground/components/Editor/Output.tsx | 10 +++++--- .../apps/playground/components/Header.tsx | 24 ++++++++++++++++++- .../playground/components/StoreContext.tsx | 12 +++++++++- compiler/apps/playground/lib/defaultStore.ts | 2 ++ compiler/apps/playground/lib/stores/store.ts | 22 ++++++++--------- 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index bf7bd3eb65078..ae8154f589efa 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -64,12 +64,16 @@ type Props = { async function tabify( source: string, compilerOutput: CompilerOutput, + showInternals: boolean, ): Promise> { const tabs = new Map(); const reorderedTabs = new Map(); const concattedResults = new Map(); // Concat all top level function declaration results into a single tab for each pass for (const [passName, results] of compilerOutput.results) { + if (!showInternals && passName !== 'Output' && passName !== 'SourceMap') { + continue; + } for (const result of results) { switch (result.kind) { case 'hir': { @@ -225,10 +229,10 @@ function Output({store, compilerOutput}: Props): JSX.Element { } useEffect(() => { - tabify(store.source, compilerOutput).then(tabs => { + tabify(store.source, compilerOutput, store.showInternals).then(tabs => { setTabs(tabs); }); - }, [store.source, compilerOutput]); + }, [store.source, compilerOutput, store.showInternals]); const changedPasses: Set = new Set(['Output', 'HIR']); // Initial and final passes should always be bold let lastResult: string = ''; @@ -248,7 +252,7 @@ function Output({store, compilerOutput}: Props): JSX.Element { return ( <> React Compiler Playground

+
+ + Show Internals +