From 1a27af36073d7dffcc7a4284ed569af6f804747a Mon Sep 17 00:00:00 2001 From: Eugene Choi <4eugenechoi@gmail.com> Date: Fri, 12 Sep 2025 11:43:04 -0400 Subject: [PATCH 1/5] [playground] Update the playground UI (#34468) ## Summary Updated the UI of the React compiler playground. The config, Input, and Output panels will now span the viewport width when "Show Internals" is not toggled on. When "Show Internals" is toggled on, the old vertical accordion tabs are still used. Going to add support for the "Applied Configs" tabs underneath the "Config Overrides" tab next. ## How did you test this change? https://github.com/user-attachments/assets/b8eab028-f58c-4cb9-a8b2-0f098f2cc262 --- .../compilationMode-all-output.txt | 3 +- .../playground/components/AccordionWindow.tsx | 106 +++++++ .../components/Editor/ConfigEditor.tsx | 145 ++++++---- .../components/Editor/EditorImpl.tsx | 263 ++++++++++-------- .../playground/components/Editor/Input.tsx | 69 +++-- .../playground/components/Editor/Output.tsx | 30 +- .../apps/playground/components/Header.tsx | 2 +- .../components/Icons/IconChevron.tsx | 41 +++ .../playground/components/TabbedWindow.tsx | 122 +++----- compiler/apps/playground/playwright.config.js | 6 +- 10 files changed, 495 insertions(+), 292 deletions(-) create mode 100644 compiler/apps/playground/components/AccordionWindow.tsx create mode 100644 compiler/apps/playground/components/Icons/IconChevron.tsx diff --git a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt index 0084911eec1b7..ab7b3ce58cf7b 100644 --- a/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt +++ b/compiler/apps/playground/__tests__/e2e/__snapshots__/page.spec.ts/compilationMode-all-output.txt @@ -1,5 +1,4 @@ -import { c as _c } from "react/compiler-runtime"; //  -@compilationMode:"all" +import { c as _c } from "react/compiler-runtime"; // @compilationMode:"all" function nonReactFn() {   const $ = _c(1);   let t0; diff --git a/compiler/apps/playground/components/AccordionWindow.tsx b/compiler/apps/playground/components/AccordionWindow.tsx new file mode 100644 index 0000000000000..de3b01b0b05d3 --- /dev/null +++ b/compiler/apps/playground/components/AccordionWindow.tsx @@ -0,0 +1,106 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {Resizable} from 're-resizable'; +import React, {useCallback} from 'react'; + +type TabsRecord = Map; + +export default function AccordionWindow(props: { + defaultTab: string | null; + tabs: TabsRecord; + tabsOpen: Set; + setTabsOpen: (newTab: Set) => void; + changedPasses: Set; +}): React.ReactElement { + if (props.tabs.size === 0) { + return ( +
+ No compiler output detected, see errors below +
+ ); + } + return ( +
+ {Array.from(props.tabs.keys()).map(name => { + return ( + + ); + })} +
+ ); +} + +function AccordionWindowItem({ + name, + tabs, + tabsOpen, + setTabsOpen, + hasChanged, +}: { + name: string; + tabs: TabsRecord; + tabsOpen: Set; + setTabsOpen: (newTab: Set) => void; + hasChanged: boolean; +}): React.ReactElement { + const isShow = tabsOpen.has(name); + + const toggleTabs = useCallback(() => { + const nextState = new Set(tabsOpen); + if (nextState.has(name)) { + nextState.delete(name); + } else { + nextState.add(name); + } + setTabsOpen(nextState); + }, [tabsOpen, name, setTabsOpen]); + + // Replace spaces with non-breaking spaces + const displayName = name.replace(/ /g, '\u00A0'); + + return ( +
+ {isShow ? ( + +

+ - {displayName} +

+ {tabs.get(name) ??
No output for {name}
} +
+ ) : ( +
+ +
+ )} +
+ ); +} diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index 63522987db052..5f904960bacbe 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -8,10 +8,11 @@ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; -import React, {useState, useCallback} from 'react'; +import React, {useState} from 'react'; import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; import {monacoOptions} from './monacoOptions'; +import {IconChevron} from '../Icons/IconChevron'; // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts'; @@ -20,13 +21,26 @@ loader.config({monaco}); export default function ConfigEditor(): React.ReactElement { const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+ {isExpanded ? ( + + ) : ( + + )} +
+ ); +} + +function ExpandedEditor({ + onToggle, +}: { + onToggle: (expanded: boolean) => void; +}): React.ReactElement { const store = useStore(); const dispatchStore = useStoreDispatch(); - const toggleExpanded = useCallback(() => { - setIsExpanded(prev => !prev); - }, []); - const handleChange: (value: string | undefined) => void = value => { if (value === undefined) return; @@ -68,57 +82,82 @@ export default function ConfigEditor(): React.ReactElement { }; return ( -
- {isExpanded ? ( - -

- - Config Overrides + +
+
+

+ Config Overrides

-
- -
- - ) : ( -
- + />
- )} +
+ + ); +} + +function CollapsedEditor({ + onToggle, +}: { + onToggle: (expanded: boolean) => void; +}): React.ReactElement { + return ( +
+
onToggle(true)} + style={{ + top: '50%', + marginTop: '-32px', + left: '-8px', + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }}> + +
); } diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index d6a2bccc8edef..a90447c96b50a 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -24,7 +24,6 @@ import BabelPluginReactCompiler, { printFunctionWithOutlined, type LoggerEvent, } from 'babel-plugin-react-compiler'; -import clsx from 'clsx'; import invariant from 'invariant'; import {useSnackbar} from 'notistack'; import {useDeferredValue, useMemo} from 'react'; @@ -47,7 +46,6 @@ import { PrintedCompilerPipelineValue, } from './Output'; import {transformFromAstSync} from '@babel/core'; -import {useSearchParams} from 'next/navigation'; function parseInput( input: string, @@ -144,6 +142,61 @@ const COMMON_HOOKS: Array<[string, Hook]> = [ ], ]; +function parseOptions( + source: string, + mode: 'compiler' | 'linter', + configOverrides: string, +): PluginOptions { + // Extract the first line to quickly check for custom test directives + const pragma = source.substring(0, source.indexOf('\n')); + + const parsedPragmaOptions = parseConfigPragmaForTests(pragma, { + compilationMode: 'infer', + environment: + mode === 'linter' + ? { + // enabled in compiler + validateRefAccessDuringRender: false, + // enabled in linter + validateNoSetStateInRender: true, + validateNoSetStateInEffects: true, + validateNoJSXInTryStatements: true, + validateNoImpureFunctionsInRender: true, + validateStaticComponents: true, + validateNoFreezingKnownMutableFunctions: true, + validateNoVoidUseMemo: true, + } + : { + /* use defaults for compiler mode */ + }, + }); + + // Parse config overrides from config editor + let configOverrideOptions: any = {}; + const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s); + // TODO: initialize store with URL params, not empty store + if (configOverrides.trim()) { + if (configMatch && configMatch[1]) { + const configString = configMatch[1].replace(/satisfies.*$/, '').trim(); + configOverrideOptions = new Function(`return (${configString})`)(); + } else { + throw new Error('Invalid override format'); + } + } + + const opts: PluginOptions = parsePluginOptions({ + ...parsedPragmaOptions, + ...configOverrideOptions, + environment: { + ...parsedPragmaOptions.environment, + ...configOverrideOptions.environment, + customHooks: new Map([...COMMON_HOOKS]), + }, + }); + + return opts; +} + function compile( source: string, mode: 'compiler' | 'linter', @@ -167,120 +220,94 @@ function compile( language = 'typescript'; } let transformOutput; + + let baseOpts: PluginOptions | null = null; try { - // Extract the first line to quickly check for custom test directives - const pragma = source.substring(0, source.indexOf('\n')); - const logIR = (result: CompilerPipelineValue): void => { - switch (result.kind) { - case 'ast': { - break; - } - case 'hir': { - upsert({ - kind: 'hir', - fnName: result.value.id, - name: result.name, - value: printFunctionWithOutlined(result.value), - }); - break; - } - case 'reactive': { - upsert({ - kind: 'reactive', - fnName: result.value.id, - name: result.name, - value: printReactiveFunctionWithOutlined(result.value), - }); - break; - } - case 'debug': { - upsert({ - kind: 'debug', - fnName: null, - name: result.name, - value: result.value, - }); - break; - } - default: { - const _: never = result; - throw new Error(`Unhandled result ${result}`); + baseOpts = parseOptions(source, mode, configOverrides); + } catch (err) { + error.details.push( + new CompilerErrorDetail({ + category: ErrorCategory.Config, + reason: `Unexpected failure when transforming configs! \n${err}`, + loc: null, + suggestions: null, + }), + ); + } + if (baseOpts) { + try { + const logIR = (result: CompilerPipelineValue): void => { + switch (result.kind) { + case 'ast': { + break; + } + case 'hir': { + upsert({ + kind: 'hir', + fnName: result.value.id, + name: result.name, + value: printFunctionWithOutlined(result.value), + }); + break; + } + case 'reactive': { + upsert({ + kind: 'reactive', + fnName: result.value.id, + name: result.name, + value: printReactiveFunctionWithOutlined(result.value), + }); + break; + } + case 'debug': { + upsert({ + kind: 'debug', + fnName: null, + name: result.name, + value: result.value, + }); + break; + } + default: { + const _: never = result; + throw new Error(`Unhandled result ${result}`); + } } - } - }; - const parsedPragmaOptions = parseConfigPragmaForTests(pragma, { - compilationMode: 'infer', - environment: - mode === 'linter' - ? { - // enabled in compiler - validateRefAccessDuringRender: false, - // enabled in linter - validateNoSetStateInRender: true, - validateNoSetStateInEffects: true, - validateNoJSXInTryStatements: true, - validateNoImpureFunctionsInRender: true, - validateStaticComponents: true, - validateNoFreezingKnownMutableFunctions: true, - validateNoVoidUseMemo: true, + }; + // Add logger options to the parsed options + const opts = { + ...baseOpts, + logger: { + debugLogIRs: logIR, + logEvent: (_filename: string | null, event: LoggerEvent) => { + if (event.kind === 'CompileError') { + otherErrors.push(event.detail); } - : { - /* use defaults for compiler mode */ - }, - }); - - // Parse config overrides from config editor - let configOverrideOptions: any = {}; - const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s); - // TODO: initialize store with URL params, not empty store - if (configOverrides.trim()) { - if (configMatch && configMatch[1]) { - const configString = configMatch[1].replace(/satisfies.*$/, '').trim(); - configOverrideOptions = new Function(`return (${configString})`)(); - } else { - throw new Error('Invalid config overrides'); - } - } - - const opts: PluginOptions = parsePluginOptions({ - ...parsedPragmaOptions, - ...configOverrideOptions, - environment: { - ...parsedPragmaOptions.environment, - ...configOverrideOptions.environment, - customHooks: new Map([...COMMON_HOOKS]), - }, - logger: { - debugLogIRs: logIR, - logEvent: (_filename: string | null, event: LoggerEvent) => { - if (event.kind === 'CompileError') { - otherErrors.push(event.detail); - } + }, }, - }, - }); - transformOutput = invokeCompiler(source, language, opts); - } catch (err) { - /** - * error might be an invariant violation or other runtime error - * (i.e. object shape that is not CompilerError) - */ - if (err instanceof CompilerError && err.details.length > 0) { - error.merge(err); - } else { + }; + transformOutput = invokeCompiler(source, language, opts); + } catch (err) { /** - * Handle unexpected failures by logging (to get a stack trace) - * and reporting + * error might be an invariant violation or other runtime error + * (i.e. object shape that is not CompilerError) */ - console.error(err); - error.details.push( - new CompilerErrorDetail({ - category: ErrorCategory.Invariant, - reason: `Unexpected failure when transforming input! ${err}`, - loc: null, - suggestions: null, - }), - ); + if (err instanceof CompilerError && err.details.length > 0) { + error.merge(err); + } else { + /** + * Handle unexpected failures by logging (to get a stack trace) + * and reporting + */ + error.details.push( + new CompilerErrorDetail({ + category: ErrorCategory.Invariant, + reason: `Unexpected failure when transforming input! \n${err}`, + loc: null, + suggestions: null, + }), + ); + } } } // Only include logger errors if there weren't other errors @@ -350,13 +377,17 @@ export default function Editor(): JSX.Element { } return ( <> -
- -
- +
+
+
-
- +
+
+ +
+
+ +
diff --git a/compiler/apps/playground/components/Editor/Input.tsx b/compiler/apps/playground/components/Editor/Input.tsx index f4c64a14a0501..206b98300be43 100644 --- a/compiler/apps/playground/components/Editor/Input.tsx +++ b/compiler/apps/playground/components/Editor/Input.tsx @@ -6,7 +6,10 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {CompilerErrorDetail} from 'babel-plugin-react-compiler'; +import { + CompilerErrorDetail, + CompilerDiagnostic, +} from 'babel-plugin-react-compiler'; import invariant from 'invariant'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; @@ -14,6 +17,7 @@ import {Resizable} from 're-resizable'; import {useEffect, useState} from 'react'; import {renderReactCompilerMarkers} from '../../lib/reactCompilerMonacoDiagnostics'; import {useStore, useStoreDispatch} from '../StoreContext'; +import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; // @ts-expect-error TODO: Make TS recognize .d.ts files, in addition to loading them with webpack. import React$Types from '../../node_modules/@types/react/index.d.ts'; @@ -21,7 +25,7 @@ import React$Types from '../../node_modules/@types/react/index.d.ts'; loader.config({monaco}); type Props = { - errors: Array; + errors: Array; language: 'flow' | 'typescript'; }; @@ -135,30 +139,51 @@ export default function Input({errors, language}: Props): JSX.Element { }); }; + const editorContent = ( + + ); + + const tabs = new Map([['Input', editorContent]]); + const [activeTab, setActiveTab] = useState('Input'); + + const tabbedContent = ( +
+ +
+ ); + return (
- - - + className="!h-[calc(100vh_-_3.5rem)]"> + {tabbedContent} + + ) : ( +
{tabbedContent}
+ )}
); } diff --git a/compiler/apps/playground/components/Editor/Output.tsx b/compiler/apps/playground/components/Editor/Output.tsx index ae8154f589efa..22f908e51bbdb 100644 --- a/compiler/apps/playground/components/Editor/Output.tsx +++ b/compiler/apps/playground/components/Editor/Output.tsx @@ -21,13 +21,17 @@ import * as prettierPluginEstree from 'prettier/plugins/estree'; import * as prettier from 'prettier/standalone'; import {memo, ReactNode, useEffect, useState} from 'react'; import {type Store} from '../../lib/stores'; +import AccordionWindow from '../AccordionWindow'; import TabbedWindow from '../TabbedWindow'; import {monacoOptions} from './monacoOptions'; import {BabelFileResult} from '@babel/core'; + const MemoizedOutput = memo(Output); export default MemoizedOutput; +export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap']; + export type PrintedCompilerPipelineValue = | { kind: 'hir'; @@ -71,7 +75,7 @@ async function tabify( 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') { + if (!showInternals && !BASIC_OUTPUT_TAB_NAMES.includes(passName)) { continue; } for (const result of results) { @@ -215,6 +219,7 @@ function Output({store, compilerOutput}: Props): JSX.Element { const [tabs, setTabs] = useState>( () => new Map(), ); + const [activeTab, setActiveTab] = useState('Output'); /* * Update the active tab back to the output or errors tab when the compilation state @@ -226,6 +231,7 @@ function Output({store, compilerOutput}: Props): JSX.Element { if (compilerOutput.kind !== previousOutputKind) { setPreviousOutputKind(compilerOutput.kind); setTabsOpen(new Set(['Output'])); + setActiveTab('Output'); } useEffect(() => { @@ -249,16 +255,24 @@ function Output({store, compilerOutput}: Props): JSX.Element { } } - return ( - <> + if (!store.showInternals) { + return ( - + ); + } + + return ( + ); } diff --git a/compiler/apps/playground/components/Header.tsx b/compiler/apps/playground/components/Header.tsx index 55f9dbdd36c33..582caebffb9c3 100644 --- a/compiler/apps/playground/components/Header.tsx +++ b/compiler/apps/playground/components/Header.tsx @@ -72,7 +72,7 @@ export default function Header(): JSX.Element { 'before:bg-white before:rounded-full before:transition-transform before:duration-250', 'focus-within:shadow-[0_0_1px_#2196F3]', store.showInternals - ? 'bg-blue-500 before:translate-x-3.5' + ? 'bg-link before:translate-x-3.5' : 'bg-gray-300', )}> diff --git a/compiler/apps/playground/components/Icons/IconChevron.tsx b/compiler/apps/playground/components/Icons/IconChevron.tsx new file mode 100644 index 0000000000000..1e9dfb69188a9 --- /dev/null +++ b/compiler/apps/playground/components/Icons/IconChevron.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {memo} from 'react'; + +export const IconChevron = memo< + JSX.IntrinsicElements['svg'] & { + /** + * The direction the arrow should point. + */ + displayDirection: 'right' | 'left'; + } +>(function IconChevron({className, displayDirection, ...props}) { + const rotationClass = + displayDirection === 'left' ? 'rotate-90' : '-rotate-90'; + const classes = className ? `${rotationClass} ${className}` : rotationClass; + + return ( + + + + + + + ); +}); diff --git a/compiler/apps/playground/components/TabbedWindow.tsx b/compiler/apps/playground/components/TabbedWindow.tsx index 4b01056f25bb7..49ff76543bb5c 100644 --- a/compiler/apps/playground/components/TabbedWindow.tsx +++ b/compiler/apps/playground/components/TabbedWindow.tsx @@ -4,103 +4,47 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ +import React from 'react'; +import clsx from 'clsx'; -import {Resizable} from 're-resizable'; -import React, {useCallback} from 'react'; - -type TabsRecord = Map; - -export default function TabbedWindow(props: { - defaultTab: string | null; - tabs: TabsRecord; - tabsOpen: Set; - setTabsOpen: (newTab: Set) => void; - changedPasses: Set; +export default function TabbedWindow({ + tabs, + activeTab, + onTabChange, +}: { + tabs: Map; + activeTab: string; + onTabChange: (tab: string) => void; }): React.ReactElement { - if (props.tabs.size === 0) { + if (tabs.size === 0) { return ( -
+
No compiler output detected, see errors below
); } return ( -
- {Array.from(props.tabs.keys()).map(name => { - return ( - - ); - })} -
- ); -} - -function TabbedWindowItem({ - name, - tabs, - tabsOpen, - setTabsOpen, - hasChanged, -}: { - name: string; - tabs: TabsRecord; - tabsOpen: Set; - setTabsOpen: (newTab: Set) => void; - hasChanged: boolean; -}): React.ReactElement { - const isShow = tabsOpen.has(name); - - const toggleTabs = useCallback(() => { - const nextState = new Set(tabsOpen); - if (nextState.has(name)) { - nextState.delete(name); - } else { - nextState.add(name); - } - setTabsOpen(nextState); - }, [tabsOpen, name, setTabsOpen]); - - // Replace spaces with non-breaking spaces - const displayName = name.replace(/ /g, '\u00A0'); - - return ( -
- {isShow ? ( - -

- - {displayName} -

- {tabs.get(name) ??
No output for {name}
} -
- ) : ( -
- -
- )} +
+
+ {Array.from(tabs.keys()).map(tab => { + const isActive = activeTab === tab; + return ( + + ); + })} +
+
+ {tabs.get(activeTab)} +
); } diff --git a/compiler/apps/playground/playwright.config.js b/compiler/apps/playground/playwright.config.js index 2ef29293d412b..10de19457ff0b 100644 --- a/compiler/apps/playground/playwright.config.js +++ b/compiler/apps/playground/playwright.config.js @@ -55,12 +55,16 @@ export default defineConfig({ // contextOptions: { // ignoreHTTPSErrors: true, // }, + viewport: {width: 1920, height: 1080}, }, projects: [ { name: 'chromium', - use: {...devices['Desktop Chrome']}, + use: { + ...devices['Desktop Chrome'], + viewport: {width: 1920, height: 1080}, + }, }, // { // name: 'Desktop Firefox', From 20e5431747347796b3be8312e56cef655b26ef4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 12 Sep 2025 11:55:07 -0400 Subject: [PATCH 2/5] [Flight][Fiber] Encode owner in the error payload in dev and use it as the Error's Task (#34460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When we report an error we typically log the owner stack of the thing that caught the error. Similarly we restore the `console.createTask` scope of the catching component when we call `reportError` or `console.error`. We also have a special case if something throws during reconciliation which uses the Server Component task as far as we got before we threw. https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactChildFiber.js#L1952-L1960 Chrome has since fixed it (on our request) that the Error constructor snapshots the Task at the time the constructor was created and logs that in `reportError`. This is a good thing since it means we get a coherent stack. Unfortunately, it means that the fake Errors that we create in Flight Client gets a snapshot of the task where they were created so when they're reported in the console they get the root Task instead of the Task of the handler of the error. Ideally we'd transfer the Task from the server and restore it. However, since we don't instrument the Error object to snapshot the owner and we can't read the native Task (if it's even enabled on the server) we don't actually have a correct snapshot to transfer for a Server Component Error. However, we can use the parent's task for where the error was observed by Flight Server and then encode that as a pseudo owner of the Error. Then we use this owner as the Task which the Error is created within. Now the client snapshots that Task which is reported by `reportError` so now we have an async stack for Server Component errors again. (Note that this owner may differ from the one observed by `captureOwnerStack` which gets the nearest Server Component from where it was caught. We could attach the owner to the Error object and use that owner when calling `onCaughtError`/`onUncaughtError`). Before: Screenshot 2025-09-10 at 10 57 54 AM After: Screenshot 2025-09-10 at 11 06 20 AM Similarly, there are Errors and warnings created by ChildFiber itself. Those execute in the scope of the general render of the parent Fiber. They used to get the scope of the nearest client component parent (e.g. div in this case) but that's the parent of the Server Component. It would be too expensive to run every level of reconciliation in its own task optimistically, so this does it only when we know that we'll throw or log an error that needs this context. Unfortunately this doesn't cover user space errors (such as if an iterable errors). Before: Screenshot 2025-09-10 at 11 31 55 AM After: Screenshot 2025-09-10 at 11 50
54 AM Screenshot 2025-09-10 at 11 52 46 AM --- .../react-client/src/ReactFlightClient.js | 24 +++++-- .../react-reconciler/src/ReactChildFiber.js | 69 ++++++++++++++++--- .../react-server/src/ReactFlightServer.js | 47 +++++++++---- packages/shared/ReactTypes.js | 1 + 4 files changed, 114 insertions(+), 27 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 6cb21229c34e8..74a412d6f4c0a 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -3181,11 +3181,27 @@ function resolveErrorDev( 'An error occurred in the Server Components render but no message was provided', ), ); - const rootTask = getRootTask(response, env); - if (rootTask != null) { - error = rootTask.run(callStack); + + let ownerTask: null | ConsoleTask = null; + if (errorInfo.owner != null) { + const ownerRef = errorInfo.owner.slice(1); + // TODO: This is not resilient to the owner loading later in an Error like a debug channel. + // The whole error serialization should probably go through the regular model at least for DEV. + const owner = getOutlinedModel(response, ownerRef, {}, '', createModel); + if (owner !== null) { + ownerTask = initializeFakeTask(response, owner); + } + } + + if (ownerTask === null) { + const rootTask = getRootTask(response, env); + if (rootTask != null) { + error = rootTask.run(callStack); + } else { + error = callStack(); + } } else { - error = callStack(); + error = ownerTask.run(callStack); } (error: any).name = name; diff --git a/packages/react-reconciler/src/ReactChildFiber.js b/packages/react-reconciler/src/ReactChildFiber.js index 3bde0d6db9ac3..f0d9c2e012e8e 100644 --- a/packages/react-reconciler/src/ReactChildFiber.js +++ b/packages/react-reconciler/src/ReactChildFiber.js @@ -13,6 +13,7 @@ import type { Thenable, ReactContext, ReactDebugInfo, + ReactComponentInfo, SuspenseListRevealOrder, } from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; @@ -101,6 +102,25 @@ function pushDebugInfo( return previousDebugInfo; } +function getCurrentDebugTask(): null | ConsoleTask { + // Get the debug task of the parent Server Component if there is one. + if (__DEV__) { + const debugInfo = currentDebugInfo; + if (debugInfo != null) { + for (let i = debugInfo.length - 1; i >= 0; i--) { + if (debugInfo[i].name != null) { + const componentInfo: ReactComponentInfo = debugInfo[i]; + const debugTask: ?ConsoleTask = componentInfo.debugTask; + if (debugTask != null) { + return debugTask; + } + } + } + } + } + return null; +} + let didWarnAboutMaps; let didWarnAboutGenerators; let ownerHasKeyUseWarning; @@ -274,7 +294,7 @@ function coerceRef(workInProgress: Fiber, element: ReactElement): void { workInProgress.ref = refProp !== undefined ? refProp : null; } -function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) { +function throwOnInvalidObjectTypeImpl(returnFiber: Fiber, newChild: Object) { if (newChild.$$typeof === REACT_LEGACY_ELEMENT_TYPE) { throw new Error( 'A React Element from an older version of React was rendered. ' + @@ -299,7 +319,18 @@ function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) { ); } -function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) { +function throwOnInvalidObjectType(returnFiber: Fiber, newChild: Object) { + const debugTask = getCurrentDebugTask(); + if (__DEV__ && debugTask !== null) { + debugTask.run( + throwOnInvalidObjectTypeImpl.bind(null, returnFiber, newChild), + ); + } else { + throwOnInvalidObjectTypeImpl(returnFiber, newChild); + } +} + +function warnOnFunctionTypeImpl(returnFiber: Fiber, invalidChild: Function) { if (__DEV__) { const parentName = getComponentNameFromFiber(returnFiber) || 'Component'; @@ -336,7 +367,16 @@ function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) { } } -function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) { +function warnOnFunctionType(returnFiber: Fiber, invalidChild: Function) { + const debugTask = getCurrentDebugTask(); + if (__DEV__ && debugTask !== null) { + debugTask.run(warnOnFunctionTypeImpl.bind(null, returnFiber, invalidChild)); + } else { + warnOnFunctionTypeImpl(returnFiber, invalidChild); + } +} + +function warnOnSymbolTypeImpl(returnFiber: Fiber, invalidChild: symbol) { if (__DEV__) { const parentName = getComponentNameFromFiber(returnFiber) || 'Component'; @@ -364,6 +404,15 @@ function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) { } } +function warnOnSymbolType(returnFiber: Fiber, invalidChild: symbol) { + const debugTask = getCurrentDebugTask(); + if (__DEV__ && debugTask !== null) { + debugTask.run(warnOnSymbolTypeImpl.bind(null, returnFiber, invalidChild)); + } else { + warnOnSymbolTypeImpl(returnFiber, invalidChild); + } +} + type ChildReconciler = ( returnFiber: Fiber, currentFirstChild: Fiber | null, @@ -1941,12 +1990,14 @@ function createChildReconciler( throwFiber.return = returnFiber; if (__DEV__) { const debugInfo = (throwFiber._debugInfo = currentDebugInfo); - // Conceptually the error's owner/task should ideally be captured when the - // Error constructor is called but neither console.createTask does this, - // nor do we override them to capture our `owner`. So instead, we use the - // nearest parent as the owner/task of the error. This is usually the same - // thing when it's thrown from the same async component but not if you await - // a promise started from a different component/task. + // Conceptually the error's owner should ideally be captured when the + // Error constructor is called but we don't override them to capture our + // `owner`. So instead, we use the nearest parent as the owner/task of the + // error. This is usually the same thing when it's thrown from the same + // async component but not if you await a promise started from a different + // component/task. + // In newer Chrome, Error constructor does capture the Task which is what + // is logged by reportError. In that case this debugTask isn't used. throwFiber._debugOwner = returnFiber._debugOwner; throwFiber._debugTask = returnFiber._debugTask; if (debugInfo != null) { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 10edd1e5c311d..31bea759a0a89 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -864,7 +864,7 @@ function serializeDebugThenable( const x = thenable.reason; // We don't log these errors since they didn't actually throw into Flight. const digest = ''; - emitErrorChunk(request, id, digest, x, true); + emitErrorChunk(request, id, digest, x, true, null); return ref; } } @@ -916,7 +916,7 @@ function serializeDebugThenable( } // We don't log these errors since they didn't actually throw into Flight. const digest = ''; - emitErrorChunk(request, id, digest, reason, true); + emitErrorChunk(request, id, digest, reason, true, null); enqueueFlush(request); }, ); @@ -964,7 +964,7 @@ function emitRequestedDebugThenable( } // We don't log these errors since they didn't actually throw into Flight. const digest = ''; - emitErrorChunk(request, id, digest, reason, true); + emitErrorChunk(request, id, digest, reason, true, null); enqueueFlush(request); }, ); @@ -2764,7 +2764,7 @@ function serializeClientReference( request.pendingChunks++; const errorId = request.nextChunkId++; const digest = logRecoverableError(request, x, null); - emitErrorChunk(request, errorId, digest, x, false); + emitErrorChunk(request, errorId, digest, x, false, null); return serializeByValueID(errorId); } } @@ -2813,7 +2813,7 @@ function serializeDebugClientReference( request.pendingDebugChunks++; const errorId = request.nextChunkId++; const digest = logRecoverableError(request, x, null); - emitErrorChunk(request, errorId, digest, x, true); + emitErrorChunk(request, errorId, digest, x, true, null); return serializeByValueID(errorId); } } @@ -3054,7 +3054,7 @@ function serializeDebugBlob(request: Request, blob: Blob): string { } function error(reason: mixed) { const digest = ''; - emitErrorChunk(request, id, digest, reason, true); + emitErrorChunk(request, id, digest, reason, true, null); enqueueFlush(request); // $FlowFixMe should be able to pass mixed reader.cancel(reason).then(noop, noop); @@ -3254,7 +3254,14 @@ function renderModel( emitPostponeChunk(request, errorId, postponeInstance); } else { const digest = logRecoverableError(request, x, task); - emitErrorChunk(request, errorId, digest, x, false); + emitErrorChunk( + request, + errorId, + digest, + x, + false, + __DEV__ ? task.debugOwner : null, + ); } if (wasReactNode) { // We'll replace this element with a lazy reference that throws on the client @@ -4072,7 +4079,8 @@ function emitErrorChunk( id: number, digest: string, error: mixed, - debug: boolean, + debug: boolean, // DEV-only + owner: ?ReactComponentInfo, // DEV-only ): void { let errorInfo: ReactErrorInfo; if (__DEV__) { @@ -4104,7 +4112,9 @@ function emitErrorChunk( message = 'An error occurred but serializing the error message failed.'; stack = []; } - errorInfo = {digest, name, message, stack, env}; + const ownerRef = + owner == null ? null : outlineComponentInfo(request, owner); + errorInfo = {digest, name, message, stack, env, owner: ownerRef}; } else { errorInfo = {digest}; } @@ -4204,7 +4214,7 @@ function emitDebugChunk( function outlineComponentInfo( request: Request, componentInfo: ReactComponentInfo, -): void { +): string { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes @@ -4213,9 +4223,10 @@ function outlineComponentInfo( ); } - if (request.writtenDebugObjects.has(componentInfo)) { + const existingRef = request.writtenDebugObjects.get(componentInfo); + if (existingRef !== undefined) { // Already written - return; + return existingRef; } if (componentInfo.owner != null) { @@ -4270,6 +4281,7 @@ function outlineComponentInfo( request.writtenDebugObjects.set(componentInfo, ref); // We also store this in the main dedupe set so that it can be referenced by inline React Elements. request.writtenObjects.set(componentInfo, ref); + return ref; } function emitIOInfoChunk( @@ -5465,7 +5477,14 @@ function erroredTask(request: Request, task: Task, error: mixed): void { emitPostponeChunk(request, task.id, postponeInstance); } else { const digest = logRecoverableError(request, error, task); - emitErrorChunk(request, task.id, digest, error, false); + emitErrorChunk( + request, + task.id, + digest, + error, + false, + __DEV__ ? task.debugOwner : null, + ); } request.abortableTasks.delete(task); callOnAllReadyIfReady(request); @@ -6040,7 +6059,7 @@ export function abort(request: Request, reason: mixed): void { const errorId = request.nextChunkId++; request.fatalError = errorId; request.pendingChunks++; - emitErrorChunk(request, errorId, digest, error, false); + emitErrorChunk(request, errorId, digest, error, false, null); abortableTasks.forEach(task => abortTask(task, request, errorId)); scheduleWork(() => finishAbort(request, abortableTasks, errorId)); } diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index f2228233259cd..0b8d222e5cdaa 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -228,6 +228,7 @@ export type ReactErrorInfoDev = { +message: string, +stack: ReactStackTrace, +env: string, + +owner?: null | string, }; export type ReactErrorInfo = ReactErrorInfoProd | ReactErrorInfoDev; From 93d7aa69b29c20529c40cf64b0afdb5d51c9ddd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 12 Sep 2025 11:55:25 -0400 Subject: [PATCH 3/5] [Fiber] Add context for the display: inline warning (#34461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This warning doesn't execute within any particular context so doesn't have a stack. Pick the fiber of the child if it exists, otherwise the parent. Screenshot 2025-09-10 at 12 38 28 PM --- .../src/client/ReactFiberConfigDOM.js | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 53fc89305245d..6b71052d9ae6e 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -1369,14 +1369,23 @@ function warnForBlockInsideInline(instance: HTMLElement) { node.nodeType === ELEMENT_NODE && getComputedStyle((node: any)).display === 'block' ) { - console.error( - "You're about to start a around a display: inline " + - 'element <%s>, which itself has a display: block element <%s> inside it. ' + - 'This might trigger a bug in Safari which causes the View Transition to ' + - 'be skipped with a duplicate name error.\n' + - 'https://bugs.webkit.org/show_bug.cgi?id=290923', - instance.tagName.toLocaleLowerCase(), - (node: any).tagName.toLocaleLowerCase(), + const fiber = + getInstanceFromNode(node) || getInstanceFromNode(instance); + runWithFiberInDEV( + fiber, + (parentTag: string, childTag: string) => { + console.error( + "You're about to start a around a display: inline " + + 'element <%s>, which itself has a display: block element <%s> inside it. ' + + 'This might trigger a bug in Safari which causes the View Transition to ' + + 'be skipped with a duplicate name error.\n' + + 'https://bugs.webkit.org/show_bug.cgi?id=290923', + parentTag.toLocaleLowerCase(), + childTag.toLocaleLowerCase(), + ); + }, + instance.tagName, + (node: any).tagName, ); break; } From 68f00c901c05e3a91f6cc77b660bc2334700f163 Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 12 Sep 2025 12:47:40 -0400 Subject: [PATCH 4/5] Release Activity in Canary (#34374) ## Overview This PR ships `` to the `react@canary` release channel for final feedback and prepare for semver stable release. ## What this means Shipping `` to canary means it has gone through extensive testing in production, we are confident in the stability of the feature, and we are preparing to release it in a future semver stable version. Libraries and frameworks following the [Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries) should begin implementing and testing the feature. ## Why we follow the Canary Workflow To prepare for semver stable, libraries should test canary features like `` with `react@canary` to confirm compatibility and prepare for the next semver release in a myriad of environments and configurations used throughout the React ecosystem. This provides libraries with ample time to catch any issues we missed before slamming them with problems in the wider semver release. Since these features have already gone through extensive production testing, and we are confident they are stable, frameworks following the [Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries) can also begin adopting canary features like ``. This adoption is similar to how different Browsers implement new proposed browser features before they are added to the standard. If a frameworks adopts a canary feature, they are committing to stability for their users by ensuring any API changes before a semver stable release are opaque and non-breaking to their users. Apps not using a framework are also free to adopt canary features like Activity as long as they follow the [Canary Workflow](https://react.dev/blog/2023/05/03/react-canaries), but we generally recommend waiting for a semver stable release unless you have the capacity to commit to following along with the canary changes and debugging library compatibility issues. Waiting for semver stable means you're able to benefit from libraries testing and confirming support, and use semver as signal for which version of a library you can use with support of the feature. ## Docs Check out the ["React Labs: View Transitions, Activity, and more"](https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more#activity) blog post, and [the new docs for ``](https://react.dev/reference/react/Activity) for more info. ## TODO - [x] Bump Activity docs to Canary https://github.com/reactjs/react.dev/pull/7974 --------- Co-authored-by: Sebastian Sebbie Silbermann --- .../view-transition/src/components/Page.js | 2 +- .../__tests__/storeComponentFilters-test.js | 2 +- .../src/__tests__/ReactDOMFragmentRefs-test.js | 2 +- ...tDOMServerPartialHydration-test.internal.js | 2 +- ...erPartialHydrationActivity-test.internal.js | 2 +- ...SelectiveHydrationActivity-test.internal.js | 2 +- .../src/__tests__/Activity-test.js | 4 ++-- .../__tests__/ActivityLegacySuspense-test.js | 2 +- .../src/__tests__/ActivityStrictMode-test.js | 2 +- .../src/__tests__/ActivitySuspense-test.js | 2 +- .../src/__tests__/ReactDeferredValue-test.js | 2 +- .../src/__tests__/ReactErrorStacks-test.js | 2 +- .../ReactHooksWithNoopRenderer-test.js | 2 +- .../src/__tests__/ReactLazy-test.internal.js | 4 ++-- .../__tests__/ReactSiblingPrerendering-test.js | 2 +- .../ReactSuspenseyCommitPhase-test.js | 2 +- .../__tests__/ReactTransitionTracing-test.js | 2 +- .../__tests__/ReactFreshIntegration-test.js | 18 +++++++++--------- packages/react/index.development.js | 2 +- .../react/index.experimental.development.js | 2 +- packages/react/index.experimental.js | 3 ++- packages/react/index.fb.js | 3 ++- packages/react/index.js | 2 +- packages/react/index.stable.development.js | 1 + packages/react/index.stable.js | 1 + packages/react/src/ReactClient.js | 2 +- packages/react/src/ReactServer.experimental.js | 2 +- scripts/jest/TestFlags.js | 3 ++- 28 files changed, 41 insertions(+), 36 deletions(-) diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index ef1a855320634..587306fe9578f 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -1,7 +1,7 @@ import React, { unstable_addTransitionType as addTransitionType, unstable_ViewTransition as ViewTransition, - unstable_Activity as Activity, + Activity, useLayoutEffect, useEffect, useState, diff --git a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js index 63ad3101d8246..89076ea63daa3 100644 --- a/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeComponentFilters-test.js @@ -199,7 +199,7 @@ describe('Store component filters', () => { }); it('should filter Activity', async () => { - const Activity = React.unstable_Activity; + const Activity = React.Activity || React.unstable_Activity; if (Activity != null) { await actAsync(async () => diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js index 35c10fe0f073e..2ac6cb0a4b603 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js @@ -31,7 +31,7 @@ describe('FragmentRefs', () => { jest.resetModules(); React = require('react'); Fragment = React.Fragment; - Activity = React.unstable_Activity; + Activity = React.Activity; ReactDOMClient = require('react-dom/client'); ReactDOM = require('react-dom'); createPortal = ReactDOM.createPortal; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index b865b6afd784f..d13303f006a8b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -113,7 +113,7 @@ describe('ReactDOMServerPartialHydration', () => { act = require('internal-test-utils').act; ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); - Activity = React.unstable_Activity; + Activity = React.Activity; Suspense = React.Suspense; useSyncExternalStore = React.useSyncExternalStore; if (gate(flags => flags.enableSuspenseList)) { diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js index c21c6469460ee..cf9e5d1d03992 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydrationActivity-test.internal.js @@ -100,7 +100,7 @@ describe('ReactDOMServerPartialHydrationActivity', () => { act = require('internal-test-utils').act; ReactDOMServer = require('react-dom/server'); Scheduler = require('scheduler'); - Activity = React.unstable_Activity; + Activity = React.Activity; Suspense = React.Suspense; useSyncExternalStore = React.useSyncExternalStore; diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js index af5a9c40a5da7..c2a856a018181 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydrationActivity-test.internal.js @@ -139,7 +139,7 @@ describe('ReactDOMServerSelectiveHydrationActivity', () => { ReactDOMServer = require('react-dom/server'); act = require('internal-test-utils').act; Scheduler = require('scheduler'); - Activity = React.unstable_Activity; + Activity = React.Activity; const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index 86db58a09834d..01311217e2444 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -24,7 +24,7 @@ describe('Activity', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; LegacyHidden = React.unstable_LegacyHidden; - Activity = React.unstable_Activity; + Activity = React.Activity; useState = React.useState; useInsertionEffect = React.useInsertionEffect; useLayoutEffect = React.useLayoutEffect; @@ -280,7 +280,7 @@ describe('Activity', () => { // @gate enableActivity it('nested offscreen does not call componentWillUnmount when hidden', async () => { - // This is a bug that appeared during production test of . + // This is a bug that appeared during production test of . // It is a very specific scenario with nested Offscreens. The inner offscreen // goes from visible to hidden in synchronous update. class ClassComponent extends React.Component { diff --git a/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js b/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js index 3d059ab8e3efa..a4ec2298174b9 100644 --- a/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js +++ b/packages/react-reconciler/src/__tests__/ActivityLegacySuspense-test.js @@ -22,7 +22,7 @@ describe('Activity Suspense', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; LegacyHidden = React.unstable_LegacyHidden; - Activity = React.unstable_Activity; + Activity = React.Activity; Suspense = React.Suspense; useState = React.useState; useEffect = React.useEffect; diff --git a/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js b/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js index de8adc7ce3ee4..f8f7fca0e09f0 100644 --- a/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js +++ b/packages/react-reconciler/src/__tests__/ActivityStrictMode-test.js @@ -10,7 +10,7 @@ describe('Activity StrictMode', () => { log = []; React = require('react'); - Activity = React.unstable_Activity; + Activity = React.Activity; ReactNoop = require('react-noop-renderer'); act = require('internal-test-utils').act; }); diff --git a/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js b/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js index b18573efa5b02..90fdf21a42860 100644 --- a/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js +++ b/packages/react-reconciler/src/__tests__/ActivitySuspense-test.js @@ -23,7 +23,7 @@ describe('Activity Suspense', () => { Scheduler = require('scheduler'); act = require('internal-test-utils').act; LegacyHidden = React.unstable_LegacyHidden; - Activity = React.unstable_Activity; + Activity = React.Activity; Suspense = React.Suspense; useState = React.useState; useEffect = React.useEffect; diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index f5fb8f81afa75..ad07ce89589f1 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -34,7 +34,7 @@ describe('ReactDeferredValue', () => { useMemo = React.useMemo; useState = React.useState; Suspense = React.Suspense; - Activity = React.unstable_Activity; + Activity = React.Activity; const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; diff --git a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js index 77459530e22f9..5f74de7af0ffc 100644 --- a/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js +++ b/packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js @@ -28,7 +28,7 @@ describe('ReactFragment', () => { React = require('react'); Suspense = React.Suspense; - Activity = React.unstable_Activity; + Activity = React.Activity; ViewTransition = React.unstable_ViewTransition; ReactNoop = require('react-noop-renderer'); const InternalTestUtils = require('internal-test-utils'); diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js index 905aac9a7857e..c61336b4dd4f2 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js @@ -68,7 +68,7 @@ describe('ReactHooksWithNoopRenderer', () => { useTransition = React.useTransition; useDeferredValue = React.useDeferredValue; Suspense = React.Suspense; - Activity = React.unstable_Activity; + Activity = React.Activity; ContinuousEventPriority = require('react-reconciler/constants').ContinuousEventPriority; if (gate(flags => flags.enableSuspenseList)) { diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index e594e4fbedb28..ec88184d40a06 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -968,7 +968,7 @@ describe('ReactLazy', () => { // @gate enableActivity it('throws with a useful error when wrapping Activity with lazy()', async () => { - const BadLazy = lazy(() => fakeImport(React.unstable_Activity)); + const BadLazy = lazy(() => fakeImport(React.Activity)); const root = ReactTestRenderer.create( }> @@ -981,7 +981,7 @@ describe('ReactLazy', () => { await waitForAll(['Loading...']); - await resolveFakeImport(React.unstable_Activity); + await resolveFakeImport(React.Activity); root.update( }> diff --git a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js index 4252fef65f33c..14dcea33ffe22 100644 --- a/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSiblingPrerendering-test.js @@ -25,7 +25,7 @@ describe('ReactSiblingPrerendering', () => { waitForAll = require('internal-test-utils').waitForAll; startTransition = React.startTransition; Suspense = React.Suspense; - Activity = React.unstable_Activity; + Activity = React.Activity; textCache = new Map(); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js index 2e252acbf3be7..4c10031085501 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js @@ -23,7 +23,7 @@ describe('ReactSuspenseyCommitPhase', () => { if (gate(flags => flags.enableSuspenseList)) { SuspenseList = React.unstable_SuspenseList; } - Activity = React.unstable_Activity; + Activity = React.Activity; useMemo = React.useMemo; startTransition = React.startTransition; resolveSuspenseyThing = ReactNoop.resolveSuspenseyThing; diff --git a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js index ab4982ce020d3..42d3ee57a6130 100644 --- a/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js +++ b/packages/react-reconciler/src/__tests__/ReactTransitionTracing-test.js @@ -53,7 +53,7 @@ describe('ReactInteractionTracing', () => { useState = React.useState; startTransition = React.startTransition; Suspense = React.Suspense; - Activity = React.unstable_Activity; + Activity = React.Activity; getCacheForType = React.unstable_getCacheForType; diff --git a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js index d851d72eb4c29..0a9f1eb9baf46 100644 --- a/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js +++ b/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js @@ -308,7 +308,7 @@ describe('ReactFreshIntegration', () => { // @gate __DEV__ && enableActivity it('ignores ref for class component in hidden subtree', async () => { const code = ` - import {unstable_Activity as Activity} from 'react'; + import {Activity} from 'react'; // Avoid creating a new class on Fast Refresh. global.A = global.A ?? class A extends React.Component { @@ -338,7 +338,7 @@ describe('ReactFreshIntegration', () => { // @gate __DEV__ && enableActivity it('ignores ref for hoistable resource in hidden subtree', async () => { const code = ` - import {unstable_Activity as Activity} from 'react'; + import {Activity} from 'react'; function hiddenRef() { throw new Error('Unexpected hiddenRef() invocation.'); @@ -360,7 +360,7 @@ describe('ReactFreshIntegration', () => { // @gate __DEV__ && enableActivity it('ignores ref for host component in hidden subtree', async () => { const code = ` - import {unstable_Activity as Activity} from 'react'; + import {Activity} from 'react'; function hiddenRef() { throw new Error('Unexpected hiddenRef() invocation.'); @@ -382,7 +382,7 @@ describe('ReactFreshIntegration', () => { // @gate __DEV__ && enableActivity it('ignores ref for Activity in hidden subtree', async () => { const code = ` - import {unstable_Activity as Activity} from 'react'; + import {Activity} from 'react'; function hiddenRef(value) { throw new Error('Unexpected hiddenRef() invocation.'); @@ -407,7 +407,7 @@ describe('ReactFreshIntegration', () => { it('ignores ref for Scope in hidden subtree', async () => { const code = ` import { - unstable_Activity as Activity, + Activity, unstable_Scope as Scope, } from 'react'; @@ -433,7 +433,7 @@ describe('ReactFreshIntegration', () => { // @gate __DEV__ && enableActivity it('ignores ref for functional component in hidden subtree', async () => { const code = ` - import {unstable_Activity as Activity} from 'react'; + import {Activity} from 'react'; // Avoid creating a new component on Fast Refresh. global.A = global.A ?? function A() { @@ -463,7 +463,7 @@ describe('ReactFreshIntegration', () => { const code = ` import { forwardRef, - unstable_Activity as Activity, + Activity, } from 'react'; // Avoid creating a new component on Fast Refresh. @@ -494,7 +494,7 @@ describe('ReactFreshIntegration', () => { const code = ` import { memo, - unstable_Activity as Activity, + Activity, } from 'react'; // Avoid creating a new component on Fast Refresh. @@ -526,7 +526,7 @@ describe('ReactFreshIntegration', () => { const code = ` import { memo, - unstable_Activity as Activity, + Activity, } from 'react'; // Avoid creating a new component on Fast Refresh. diff --git a/packages/react/index.development.js b/packages/react/index.development.js index 595135e606e64..b3c3752ab0f2a 100644 --- a/packages/react/index.development.js +++ b/packages/react/index.development.js @@ -44,7 +44,7 @@ export { cacheSignal, startTransition, unstable_LegacyHidden, - unstable_Activity, + Activity, unstable_Scope, unstable_SuspenseList, unstable_TracingMarker, diff --git a/packages/react/index.experimental.development.js b/packages/react/index.experimental.development.js index 7f0d03a0b2436..6bf0b8a6bb78c 100644 --- a/packages/react/index.experimental.development.js +++ b/packages/react/index.experimental.development.js @@ -29,7 +29,7 @@ export { cache, cacheSignal, startTransition, - unstable_Activity, + Activity, unstable_postpone, unstable_getCacheForType, unstable_SuspenseList, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index dfaeca747ed5a..a251b03949292 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -29,7 +29,8 @@ export { cache, cacheSignal, startTransition, - unstable_Activity, + Activity, + Activity as unstable_Activity, unstable_postpone, unstable_getCacheForType, unstable_SuspenseList, diff --git a/packages/react/index.fb.js b/packages/react/index.fb.js index fb637b799b0ca..3dba103e89d28 100644 --- a/packages/react/index.fb.js +++ b/packages/react/index.fb.js @@ -32,7 +32,8 @@ export { startTransition, StrictMode, Suspense, - unstable_Activity, + Activity, + Activity as unstable_Activity, unstable_getCacheForType, unstable_LegacyHidden, unstable_Scope, diff --git a/packages/react/index.js b/packages/react/index.js index cbeea0d28bb45..aee4ac263de2d 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -44,7 +44,7 @@ export { cacheSignal, startTransition, unstable_LegacyHidden, - unstable_Activity, + Activity, unstable_Scope, unstable_SuspenseList, unstable_TracingMarker, diff --git a/packages/react/index.stable.development.js b/packages/react/index.stable.development.js index 80fc4d7cac767..d8b561541cb0f 100644 --- a/packages/react/index.stable.development.js +++ b/packages/react/index.stable.development.js @@ -10,6 +10,7 @@ export { __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, __COMPILER_RUNTIME, + Activity, Children, Component, Fragment, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 1cb9de1e37233..70b24eb0b2408 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -10,6 +10,7 @@ export { __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE, __COMPILER_RUNTIME, + Activity, Children, Component, Fragment, diff --git a/packages/react/src/ReactClient.js b/packages/react/src/ReactClient.js index b9b34e218824d..12c3b6c4abf40 100644 --- a/packages/react/src/ReactClient.js +++ b/packages/react/src/ReactClient.js @@ -116,7 +116,7 @@ export { useDeferredValue, REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList, REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden, - REACT_ACTIVITY_TYPE as unstable_Activity, + REACT_ACTIVITY_TYPE as Activity, getCacheForType as unstable_getCacheForType, useCacheRefresh as unstable_useCacheRefresh, use, diff --git a/packages/react/src/ReactServer.experimental.js b/packages/react/src/ReactServer.experimental.js index ad885f0968e7c..4fe83248e2c77 100644 --- a/packages/react/src/ReactServer.experimental.js +++ b/packages/react/src/ReactServer.experimental.js @@ -82,5 +82,5 @@ export { // Experimental REACT_SUSPENSE_LIST_TYPE as unstable_SuspenseList, REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition, - REACT_ACTIVITY_TYPE as unstable_Activity, + REACT_ACTIVITY_TYPE as Activity, }; diff --git a/scripts/jest/TestFlags.js b/scripts/jest/TestFlags.js index 12b74039d5379..e2d34cc516d5c 100644 --- a/scripts/jest/TestFlags.js +++ b/scripts/jest/TestFlags.js @@ -81,7 +81,8 @@ function getTestFlags() { fb: www || xplat, // These aren't flags, just a useful aliases for tests. - enableActivity: releaseChannel === 'experimental' || www || xplat, + // TODO: Clean this up. + enableActivity: true, enableSuspenseList: releaseChannel === 'experimental' || www || xplat, enableLegacyHidden: www, // TODO: Suspending the work loop during the render phase is currently From 8a8e9a7edf16fabc1335c9910bddfef66737ee4e Mon Sep 17 00:00:00 2001 From: Ricky Date: Fri, 12 Sep 2025 14:14:25 -0400 Subject: [PATCH 5/5] move devtools notify to different channel (#34476) --- .github/workflows/devtools_discord_notify.yml | 49 +++++++++++++++++++ .github/workflows/runtime_discord_notify.yml | 2 + 2 files changed, 51 insertions(+) create mode 100644 .github/workflows/devtools_discord_notify.yml diff --git a/.github/workflows/devtools_discord_notify.yml b/.github/workflows/devtools_discord_notify.yml new file mode 100644 index 0000000000000..bb498f0037104 --- /dev/null +++ b/.github/workflows/devtools_discord_notify.yml @@ -0,0 +1,49 @@ +name: (DevTools) Discord Notify + +on: + pull_request_target: + types: [opened, ready_for_review] + paths: + - packages/react-devtools** + - .github/workflows/devtools_**.yml + +permissions: {} + +jobs: + check_access: + if: ${{ github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + outputs: + is_member_or_collaborator: ${{ steps.check_is_member_or_collaborator.outputs.is_member_or_collaborator }} + steps: + - run: echo ${{ github.event.pull_request.author_association }} + - name: Check is member or collaborator + id: check_is_member_or_collaborator + if: ${{ github.event.pull_request.author_association == 'MEMBER' || github.event.pull_request.author_association == 'COLLABORATOR' }} + run: echo "is_member_or_collaborator=true" >> "$GITHUB_OUTPUT" + + check_maintainer: + if: ${{ needs.check_access.outputs.is_member_or_collaborator == 'true' || needs.check_access.outputs.is_member_or_collaborator == true }} + needs: [check_access] + uses: facebook/react/.github/workflows/shared_check_maintainer.yml@main + permissions: + # Used by check_maintainer + contents: read + with: + actor: ${{ github.event.pull_request.user.login }} + + notify: + if: ${{ needs.check_maintainer.outputs.is_core_team == 'true' }} + needs: check_maintainer + runs-on: ubuntu-latest + steps: + - name: Discord Webhook Action + uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4 + with: + webhook-url: ${{ secrets.DEVTOOLS_DISCORD_WEBHOOK_URL }} + embed-author-name: ${{ github.event.pull_request.user.login }} + embed-author-url: ${{ github.event.pull_request.user.html_url }} + embed-author-icon-url: ${{ github.event.pull_request.user.avatar_url }} + embed-title: '#${{ github.event.number }} (+${{github.event.pull_request.additions}} -${{github.event.pull_request.deletions}}): ${{ github.event.pull_request.title }}' + embed-description: ${{ github.event.pull_request.body }} + embed-url: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/runtime_discord_notify.yml b/.github/workflows/runtime_discord_notify.yml index 8d047e697640d..ae9930adf114f 100644 --- a/.github/workflows/runtime_discord_notify.yml +++ b/.github/workflows/runtime_discord_notify.yml @@ -4,8 +4,10 @@ on: pull_request_target: types: [opened, ready_for_review] paths-ignore: + - packages/react-devtools** - compiler/** - .github/workflows/compiler_**.yml + - .github/workflows/devtools**.yml permissions: {}