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: {} 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', 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-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-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-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; } 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/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-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-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/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/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; 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