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