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 0084911eec1..ab7b3ce58cf 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 00000000000..de3b01b0b05 --- /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 63522987db0..5f904960bac 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 d6a2bccc8ed..a90447c96b5 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 f4c64a14a05..206b98300be 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 ae8154f589e..22f908e51bb 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 55f9dbdd36c..582caebffb9 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 00000000000..1e9dfb69188 --- /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 4b01056f25b..49ff76543bb 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 2ef29293d41..10de19457ff 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 6cb21229c34..74a412d6f4c 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 3bde0d6db9a..f0d9c2e012e 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 10edd1e5c31..31bea759a0a 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 f2228233259..0b8d222e5cd 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 53fc8930524..6b71052d9ae 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 ef1a8553206..587306fe957 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 63ad3101d82..89076ea63da 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 35c10fe0f07..2ac6cb0a4b6 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 b865b6afd78..d13303f006a 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 c21c6469460..cf9e5d1d039 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 af5a9c40a5d..c2a856a0181 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 86db58a0983..01311217e24 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 3d059ab8e3e..a4ec2298174 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 de8adc7ce3e..f8f7fca0e09 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 b18573efa5b..90fdf21a428 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 f5fb8f81afa..ad07ce89589 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 77459530e22..5f74de7af0f 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 905aac9a785..c61336b4dd4 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 e594e4fbedb..ec88184d40a 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 4252fef65f3..14dcea33ffe 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 2e252acbf3b..4c100310855 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 ab4982ce020..42d3ee57a61 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 d851d72eb4c..0a9f1eb9baf 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 595135e606e..b3c3752ab0f 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 7f0d03a0b24..6bf0b8a6bb7 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 dfaeca747ed..a251b039492 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 fb637b799b0..3dba103e89d 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 cbeea0d28bb..aee4ac263de 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 80fc4d7cac7..d8b561541cb 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 1cb9de1e372..70b24eb0b24 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 b9b34e21882..12c3b6c4abf 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 ad885f0968e..4fe83248e2c 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 12b74039d53..e2d34cc516d 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 00000000000..bb498f00371 --- /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 8d047e69764..ae9930adf11 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: {}