diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md new file mode 100644 index 0000000000000..a1c64e50483f8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +import {useCallback, useRef} from 'react'; + +export default function useThunkDispatch(state, dispatch, extraArg) { + const stateRef = useRef(state); + stateRef.current = state; + + return useCallback( + function thunk(action) { + if (typeof action === 'function') { + return action(thunk, () => stateRef.current, extraArg); + } else { + dispatch(action); + return undefined; + } + }, + [dispatch, extraArg] + ); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: [InferMutationAliasingEffects] Expected value kind to be initialized + + thunk$14. + +error.bug-infer-mutation-aliasing-effects.ts:10:22 + 8 | function thunk(action) { + 9 | if (typeof action === 'function') { +> 10 | return action(thunk, () => stateRef.current, extraArg); + | ^^^^^ [InferMutationAliasingEffects] Expected value kind to be initialized + 11 | } else { + 12 | dispatch(action); + 13 | return undefined; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js new file mode 100644 index 0000000000000..3309406fc70ca --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-infer-mutation-aliasing-effects.js @@ -0,0 +1,18 @@ +import {useCallback, useRef} from 'react'; + +export default function useThunkDispatch(state, dispatch, extraArg) { + const stateRef = useRef(state); + stateRef.current = state; + + return useCallback( + function thunk(action) { + if (typeof action === 'function') { + return action(thunk, () => stateRef.current, extraArg); + } else { + dispatch(action); + return undefined; + } + }, + [dispatch, extraArg] + ); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md new file mode 100644 index 0000000000000..4ea831de8751e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.expect.md @@ -0,0 +1,31 @@ + +## Input + +```javascript +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` + +error.bug-invariant-codegen-methodcall.ts:3:17 + 1 | const YearsAndMonthsSince = () => { + 2 | const diff = foo(); +> 3 | const months = Math.floor(diff.bar()); + | ^^^^^^^^^^ [Codegen] Internal error: MethodCall::property must be an unpromoted + unmemoized MemberExpression. Got a `Identifier` + 4 | return <>{months}; + 5 | }; + 6 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js new file mode 100644 index 0000000000000..948182653cbe0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-codegen-methodcall.js @@ -0,0 +1,5 @@ +const YearsAndMonthsSince = () => { + const diff = foo(); + const months = Math.floor(diff.bar()); + return <>{months}; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md new file mode 100644 index 0000000000000..b50ad7035939e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useEffect} from 'react'; + +export function Foo() { + useEffect(() => { + try { + // do something + } catch ({status}) { + // do something + } + }, []); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: (BuildHIR::lowerAssignment) Could not find binding for declaration. + +error.bug-invariant-couldnt-find-binding-for-decl.ts:7:14 + 5 | try { + 6 | // do something +> 7 | } catch ({status}) { + | ^^^^^^ (BuildHIR::lowerAssignment) Could not find binding for declaration. + 8 | // do something + 9 | } + 10 | }, []); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js new file mode 100644 index 0000000000000..c005fec1bd5a5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-couldnt-find-binding-for-decl.js @@ -0,0 +1,11 @@ +import {useEffect} from 'react'; + +export function Foo() { + useEffect(() => { + try { + // do something + } catch ({status}) { + // do something + } + }, []); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md new file mode 100644 index 0000000000000..226ab20ac269b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +import {useMemo} from 'react'; + +export default function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + console.log('error'); + } + return formattedText || ''; + } catch (e) {} + }, [text]); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected a break target +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js new file mode 100644 index 0000000000000..4616e0232aafe --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-break-target.js @@ -0,0 +1,15 @@ +import {useMemo} from 'react'; + +export default function useFoo(text) { + return useMemo(() => { + try { + let formattedText = ''; + try { + formattedText = format(text); + } catch { + console.log('error'); + } + return formattedText || ''; + } catch (e) {} + }, [text]); +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md new file mode 100644 index 0000000000000..a30ccffcd7bd0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +import {useMemo} from 'react'; +import {useFoo, formatB, Baz} from './lib'; + +export const Example = ({data}) => { + let a; + let b; + + if (data) { + ({a, b} = data); + } + + const foo = useFoo(a); + const bar = useMemo(() => formatB(b), [b]); + + return ; +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected consistent kind for destructuring + +Other places were `Reassign` but 'mutate? #t8$46[7:9]{reactive}' is const. + +error.bug-invariant-expected-consistent-destructuring.ts:9:9 + 7 | + 8 | if (data) { +> 9 | ({a, b} = data); + | ^ Expected consistent kind for destructuring + 10 | } + 11 | + 12 | const foo = useFoo(a); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js new file mode 100644 index 0000000000000..c37b19314431b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-expected-consistent-destructuring.js @@ -0,0 +1,16 @@ +import {useMemo} from 'react'; +import {useFoo, formatB, Baz} from './lib'; + +export const Example = ({data}) => { + let a; + let b; + + if (data) { + ({a, b} = data); + } + + const foo = useFoo(a); + const bar = useMemo(() => formatB(b), [b]); + + return ; +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md new file mode 100644 index 0000000000000..bbf753f965091 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +import {useState} from 'react'; +import {bar} from './bar'; + +export const useFoot = () => { + const [, setState] = useState(null); + try { + const {data} = bar(); + setState({ + data, + error: null, + }); + } catch (err) { + setState(_prevState => ({ + loading: false, + error: err, + })); + } +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected all references to a variable to be consistently local or context references + +Identifier err$7 is referenced as a context variable, but was previously referenced as a [object Object] variable. + +error.bug-invariant-local-or-context-references.ts:15:13 + 13 | setState(_prevState => ({ + 14 | loading: false, +> 15 | error: err, + | ^^^ Expected all references to a variable to be consistently local or context references + 16 | })); + 17 | } + 18 | }; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js new file mode 100644 index 0000000000000..561bc25fb72ad --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-local-or-context-references.js @@ -0,0 +1,18 @@ +import {useState} from 'react'; +import {bar} from './bar'; + +export const useFoot = () => { + const [, setState] = useState(null); + try { + const {data} = bar(); + setState({ + data, + error: null, + }); + } catch (err) { + setState(_prevState => ({ + loading: false, + error: err, + })); + } +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md new file mode 100644 index 0000000000000..743d2b9071e6a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.expect.md @@ -0,0 +1,34 @@ + +## Input + +```javascript +const Foo = ({json}) => { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +}; + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Unexpected terminal in optional + +error.bug-invariant-unexpected-terminal-in-optional.ts:3:16 + 1 | const Foo = ({json}) => { + 2 | try { +> 3 | const foo = JSON.parse(json)?.foo; + | ^^^^ Unexpected terminal in optional + 4 | return {foo}; + 5 | } catch { + 6 | return null; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js new file mode 100644 index 0000000000000..961640bfbd387 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unexpected-terminal-in-optional.js @@ -0,0 +1,8 @@ +const Foo = ({json}) => { + try { + const foo = JSON.parse(json)?.foo; + return {foo}; + } catch { + return null; + } +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md new file mode 100644 index 0000000000000..f8c46659bf7c2 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.expect.md @@ -0,0 +1,30 @@ + +## Input + +```javascript +import Bar from './Bar'; + +export function Foo() { + return ( + { + return {displayValue}; + }} + /> + ); +} + +``` + + +## Error + +``` +Found 1 error: + +Invariant: Expected temporaries to be promoted to named identifiers in an earlier pass + +identifier 15 is unnamed. +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js new file mode 100644 index 0000000000000..4a06093d9f1ef --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-invariant-unnamed-temporary.js @@ -0,0 +1,11 @@ +import Bar from './Bar'; + +export function Foo() { + return ( + { + return {displayValue}; + }} + /> + ); +} diff --git a/packages/react-client/src/ReactFlightPerformanceTrack.js b/packages/react-client/src/ReactFlightPerformanceTrack.js index 2b35e82363e91..717d536dc94a1 100644 --- a/packages/react-client/src/ReactFlightPerformanceTrack.js +++ b/packages/react-client/src/ReactFlightPerformanceTrack.js @@ -490,7 +490,7 @@ export function logComponentAwait( if (typeof value === 'object' && value !== null) { addObjectToProperties(value, properties, 0, ''); } else if (value !== undefined) { - addValueToProperties('Resolved', value, properties, 0, ''); + addValueToProperties('awaited value', value, properties, 0, ''); } const tooltipText = getIOLongName( asyncInfo.awaited, @@ -547,7 +547,7 @@ export function logIOInfoErrored( String(error.message) : // eslint-disable-next-line react-internal/safe-string-coercion String(error); - const properties = [['Rejected', message]]; + const properties = [['rejected with', message]]; const tooltipText = getIOLongName(ioInfo, description, ioInfo.env, rootEnv) + ' Rejected'; debugTask.run( diff --git a/packages/react-devtools-inline/__tests__/__e2e__/components.test.js b/packages/react-devtools-inline/__tests__/__e2e__/components.test.js index 9a6b67d8ec65b..ae451d29587b0 100644 --- a/packages/react-devtools-inline/__tests__/__e2e__/components.test.js +++ b/packages/react-devtools-inline/__tests__/__e2e__/components.test.js @@ -52,7 +52,7 @@ test.describe('Components', () => { test('Should allow elements to be inspected', async () => { // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp'); + await devToolsUtils.selectElement(page, 'ListItem', '\n'); // Prop names/values may not be editable based on the React version. // If they're not editable, make sure they degrade gracefully @@ -119,7 +119,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp', true); + await devToolsUtils.selectElement(page, 'ListItem', '\n', true); // Then read the inspected values. const sourceText = await page.evaluate(() => { @@ -142,7 +142,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the first list item in DevTools. - await devToolsUtils.selectElement(page, 'ListItem', 'List\nApp'); + await devToolsUtils.selectElement(page, 'ListItem', '\n'); // Then edit the label prop. await page.evaluate(() => { @@ -177,7 +177,7 @@ test.describe('Components', () => { runOnlyForReactRange('>=16.8'); // Select the List component DevTools. - await devToolsUtils.selectElement(page, 'List', 'App'); + await devToolsUtils.selectElement(page, 'List', ''); // Then click to load and parse hook names. await devToolsUtils.clickButton(page, 'LoadHookNamesButton'); diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 57865f90f825d..83b31903e06a8 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -19,8 +19,8 @@ import { formatWithStyles, gt, gte, - parseSourceFromComponentStack, } from 'react-devtools-shared/src/backend/utils'; +import {extractLocationFromComponentStack} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { REACT_SUSPENSE_LIST_TYPE as SuspenseList, REACT_STRICT_MODE_TYPE as StrictMode, @@ -306,20 +306,20 @@ describe('utils', () => { }); }); - describe('parseSourceFromComponentStack', () => { + describe('extractLocationFromComponentStack', () => { it('should return null if passed empty string', () => { - expect(parseSourceFromComponentStack('')).toEqual(null); + expect(extractLocationFromComponentStack('')).toEqual(null); }); it('should construct the source from the first frame if available', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at l (https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js:1:10389)\n' + 'at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)\n' + 'at r (https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:498)\n', ), ).toEqual([ - '', + 'l', 'https://react.dev/_next/static/chunks/main-78a3b4c2aa4e4850.js', 1, 10389, @@ -328,7 +328,7 @@ describe('utils', () => { it('should construct the source from highest available frame', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at m (https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236)\n' + @@ -342,7 +342,7 @@ describe('utils', () => { ' at f (https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8519)', ), ).toEqual([ - '', + 'm', 'https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js', 5, 9236, @@ -351,7 +351,7 @@ describe('utils', () => { it('should construct the source from frame, which has only url specified', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( ' at Q\n' + ' at a\n' + ' at https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js:5:9236\n', @@ -366,13 +366,13 @@ describe('utils', () => { it('should parse sourceURL correctly if it includes parentheses', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'at HotReload (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:307:11)\n' + ' at Router (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/app-router.js:181:11)\n' + ' at ErrorBoundaryHandler (webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/error-boundary.js:114:9)', ), ).toEqual([ - '', + 'HotReload', 'webpack-internal:///(app-pages-browser)/./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js', 307, 11, @@ -381,13 +381,13 @@ describe('utils', () => { it('should support Firefox stack', () => { expect( - parseSourceFromComponentStack( + extractLocationFromComponentStack( 'tt@https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js:1:165558\n' + 'f@https://react.dev/_next/static/chunks/pages/%5B%5B...markdownPath%5D%5D-af2ed613aedf1d57.js:1:8535\n' + 'r@https://react.dev/_next/static/chunks/pages/_app-dd0b77ea7bd5b246.js:1:513', ), ).toEqual([ - '', + 'tt', 'https://react.dev/_next/static/chunks/363-3c5f1b553b6be118.js', 1, 165558, diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 5abfcc8c8b72a..b6bc24dd01b4c 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -54,10 +54,12 @@ import { formatDurationToMicrosecondsGranularity, gt, gte, - parseSourceFromComponentStack, - parseSourceFromOwnerStack, serializeToString, } from 'react-devtools-shared/src/backend/utils'; +import { + extractLocationFromComponentStack, + extractLocationFromOwnerStack, +} from 'react-devtools-shared/src/backend/utils/parseStackTrace'; import { cleanForBridge, copyWithDelete, @@ -969,7 +971,6 @@ export function attach( } = getInternalReactConstants(version); const { ActivityComponent, - CacheComponent, ClassComponent, ContextConsumer, DehydratedSuspenseComponent, @@ -2700,6 +2701,76 @@ export function attach( } } + function isChildOf( + parentInstance: DevToolsInstance, + childInstance: DevToolsInstance, + grandParent: DevToolsInstance, + ): boolean { + let instance = childInstance.parent; + while (instance !== null) { + if (parentInstance === instance) { + return true; + } + if (instance === parentInstance.parent || instance === grandParent) { + // This was a sibling but not inside the FiberInstance. We can bail out. + break; + } + instance = instance.parent; + } + return false; + } + + function consumeSuspenseNodesOfExistingInstance( + instance: DevToolsInstance, + ): void { + // We need to also consume any unchanged Suspense boundaries. + let suspenseNode = remainingReconcilingChildrenSuspenseNodes; + if (suspenseNode === null) { + return; + } + const parentSuspenseNode = reconcilingParentSuspenseNode; + if (parentSuspenseNode === null) { + throw new Error( + 'The should not be any remaining suspense node children if there is no parent.', + ); + } + let foundOne = false; + let previousSkippedSibling = null; + while (suspenseNode !== null) { + // Check if this SuspenseNode was a child of the bailed out FiberInstance. + if ( + isChildOf(instance, suspenseNode.instance, parentSuspenseNode.instance) + ) { + foundOne = true; + // The suspenseNode was child of the bailed out Fiber. + // First, remove it from the remaining children set. + const nextRemainingSibling = suspenseNode.nextSibling; + if (previousSkippedSibling === null) { + remainingReconcilingChildrenSuspenseNodes = nextRemainingSibling; + } else { + previousSkippedSibling.nextSibling = nextRemainingSibling; + } + suspenseNode.nextSibling = null; + // Then, re-insert it into the newly reconciled set. + if (previouslyReconciledSiblingSuspenseNode === null) { + parentSuspenseNode.firstChild = suspenseNode; + } else { + previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode; + } + previouslyReconciledSiblingSuspenseNode = suspenseNode; + // Continue + suspenseNode = nextRemainingSibling; + } else if (foundOne) { + // If we found one and then hit a miss, we assume that we're passed the sequence because + // they should've all been consecutive. + break; + } else { + previousSkippedSibling = suspenseNode; + suspenseNode = suspenseNode.nextSibling; + } + } + } + function mountVirtualInstanceRecursively( virtualInstance: VirtualInstance, firstChild: Fiber, @@ -3093,9 +3164,11 @@ export function attach( reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; - reconcilingParentSuspenseNode = stashedSuspenseParent; - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + if (instance.suspenseNode !== null) { + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + } } if (instance.kind === FIBER_INSTANCE) { recordUnmount(instance); @@ -3687,10 +3760,12 @@ export function attach( fiberInstance.firstChild = null; fiberInstance.suspendedBy = null; - if (fiberInstance.suspenseNode !== null) { - reconcilingParentSuspenseNode = fiberInstance.suspenseNode; + const suspenseNode = fiberInstance.suspenseNode; + if (suspenseNode !== null) { + reconcilingParentSuspenseNode = suspenseNode; previouslyReconciledSiblingSuspenseNode = null; - remainingReconcilingChildrenSuspenseNodes = null; + remainingReconcilingChildrenSuspenseNodes = suspenseNode.firstChild; + suspenseNode.firstChild = null; } } try { @@ -3848,6 +3923,8 @@ export function attach( fiberInstance.firstChild = remainingReconcilingChildren; remainingReconcilingChildren = null; + consumeSuspenseNodesOfExistingInstance(fiberInstance); + if (traceUpdatesEnabled) { // If we're tracing updates and we've bailed out before reaching a host node, // we should fall back to recursively marking the nearest host descendants for highlight. @@ -3918,9 +3995,11 @@ export function attach( reconcilingParent = stashedParent; previouslyReconciledSibling = stashedPrevious; remainingReconcilingChildren = stashedRemaining; - reconcilingParentSuspenseNode = stashedSuspenseParent; - previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; - remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + if (fiberInstance.suspenseNode !== null) { + reconcilingParentSuspenseNode = stashedSuspenseParent; + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious; + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining; + } } } } @@ -4618,7 +4697,8 @@ export function attach( // TODO Show custom UI for Cache like we do for Suspense // For now, just hide state data entirely since it's not meant to be inspected. - const showState = !usesHooks && tag !== CacheComponent; + const showState = + tag === ClassComponent || tag === IncompleteClassComponent; const typeSymbol = getTypeSymbol(type); @@ -6340,7 +6420,7 @@ export function attach( if (stackFrame === null) { return null; } - const source = parseSourceFromComponentStack(stackFrame); + const source = extractLocationFromComponentStack(stackFrame); fiberInstance.source = source; return source; } @@ -6369,7 +6449,7 @@ export function attach( // any intermediate utility functions. This won't point to the top of the component function // but it's at least somewhere within it. if (isError(unresolvedSource)) { - return (instance.source = parseSourceFromOwnerStack( + return (instance.source = extractLocationFromOwnerStack( (unresolvedSource: any), )); } @@ -6377,7 +6457,7 @@ export function attach( const idx = unresolvedSource.lastIndexOf('\n'); const lastLine = idx === -1 ? unresolvedSource : unresolvedSource.slice(idx + 1); - return (instance.source = parseSourceFromComponentStack(lastLine)); + return (instance.source = extractLocationFromComponentStack(lastLine)); } // $FlowFixMe: refined. diff --git a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js index fdd7bce2f8d9e..36102dcf963be 100644 --- a/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js +++ b/packages/react-devtools-shared/src/backend/shared/DevToolsOwnerStack.js @@ -13,12 +13,9 @@ export function formatOwnerStack(error: Error): string { const prevPrepareStackTrace = Error.prepareStackTrace; // $FlowFixMe[incompatible-type] It does accept undefined. Error.prepareStackTrace = undefined; - const stack = error.stack; + let stack = error.stack; Error.prepareStackTrace = prevPrepareStackTrace; - return formatOwnerStackString(stack); -} -export function formatOwnerStackString(stack: string): string { if (stack.startsWith('Error: react-stack-top-frame\n')) { // V8's default formatting prefixes with the error message which we // don't want/need. diff --git a/packages/react-devtools-shared/src/backend/utils/index.js b/packages/react-devtools-shared/src/backend/utils/index.js index 490790e89d9b8..fcb8d448a0c1e 100644 --- a/packages/react-devtools-shared/src/backend/utils/index.js +++ b/packages/react-devtools-shared/src/backend/utils/index.js @@ -12,14 +12,11 @@ import {compareVersions} from 'compare-versions'; import {dehydrate} from 'react-devtools-shared/src/hydration'; import isArray from 'shared/isArray'; -import type {ReactFunctionLocation} from 'shared/ReactTypes'; import type {DehydratedData} from 'react-devtools-shared/src/frontend/types'; export {default as formatWithStyles} from './formatWithStyles'; export {default as formatConsoleArguments} from './formatConsoleArguments'; -import {formatOwnerStackString} from '../shared/DevToolsOwnerStack'; - // TODO: update this to the first React version that has a corresponding DevTools backend const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9'; export function hasAssignedBackend(version?: string): boolean { @@ -258,186 +255,6 @@ export const isReactNativeEnvironment = (): boolean => { return window.document == null; }; -function extractLocation(url: string): null | { - functionName?: string, - sourceURL: string, - line?: string, - column?: string, -} { - if (url.indexOf(':') === -1) { - return null; - } - - // remove any parentheses from start and end - const withoutParentheses = url.replace(/^\(+/, '').replace(/\)+$/, ''); - const locationParts = /(at )?(.+?)(?::(\d+))?(?::(\d+))?$/.exec( - withoutParentheses, - ); - - if (locationParts == null) { - return null; - } - - const functionName = ''; // TODO: Parse this in the regexp. - const [, , sourceURL, line, column] = locationParts; - return {functionName, sourceURL, line, column}; -} - -const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; -function parseSourceFromChromeStack( - stack: string, -): ReactFunctionLocation | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - - const locationInParenthesesMatch = sanitizedFrame.match(/ (\(.+\)$)/); - const possibleLocation = locationInParenthesesMatch - ? locationInParenthesesMatch[1] - : sanitizedFrame; - - const location = extractLocation(possibleLocation); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {functionName, sourceURL, line = '1', column = '1'} = location; - - return [ - functionName || '', - sourceURL, - parseInt(line, 10), - parseInt(column, 10), - ]; - } - - return null; -} - -function parseSourceFromFirefoxStack( - stack: string, -): ReactFunctionLocation | null { - const frames = stack.split('\n'); - // eslint-disable-next-line no-for-of-loops/no-for-of-loops - for (const frame of frames) { - const sanitizedFrame = frame.trim(); - const frameWithoutFunctionName = sanitizedFrame.replace( - /((.*".+"[^@]*)?[^@]*)(?:@)/, - '', - ); - - const location = extractLocation(frameWithoutFunctionName); - // Continue the search until at least sourceURL is found - if (location == null) { - continue; - } - - const {functionName, sourceURL, line = '1', column = '1'} = location; - - return [ - functionName || '', - sourceURL, - parseInt(line, 10), - parseInt(column, 10), - ]; - } - - return null; -} - -export function parseSourceFromComponentStack( - componentStack: string, -): ReactFunctionLocation | null { - if (componentStack.match(CHROME_STACK_REGEXP)) { - return parseSourceFromChromeStack(componentStack); - } - - return parseSourceFromFirefoxStack(componentStack); -} - -let collectedLocation: ReactFunctionLocation | null = null; - -function collectStackTrace( - error: Error, - structuredStackTrace: CallSite[], -): string { - let result: null | ReactFunctionLocation = null; - // Collect structured stack traces from the callsites. - // We mirror how V8 serializes stack frames and how we later parse them. - for (let i = 0; i < structuredStackTrace.length; i++) { - const callSite = structuredStackTrace[i]; - const name = callSite.getFunctionName(); - if ( - name != null && - (name.includes('react_stack_bottom_frame') || - name.includes('react-stack-bottom-frame')) - ) { - // We pick the last frame that matches before the bottom frame since - // that will be immediately inside the component as opposed to some helper. - // If we don't find a bottom frame then we bail to string parsing. - collectedLocation = result; - // Skip everything after the bottom frame since it'll be internals. - break; - } else { - const sourceURL = callSite.getScriptNameOrSourceURL(); - const line = - // $FlowFixMe[prop-missing] - typeof callSite.getEnclosingLineNumber === 'function' - ? (callSite: any).getEnclosingLineNumber() - : callSite.getLineNumber(); - const col = - // $FlowFixMe[prop-missing] - typeof callSite.getEnclosingColumnNumber === 'function' - ? (callSite: any).getEnclosingColumnNumber() - : callSite.getColumnNumber(); - if (!sourceURL || !line || !col) { - // Skip eval etc. without source url. They don't have location. - continue; - } - result = [name, sourceURL, line, col]; - } - } - // At the same time we generate a string stack trace just in case someone - // else reads it. - const name = error.name || 'Error'; - const message = error.message || ''; - let stack = name + ': ' + message; - for (let i = 0; i < structuredStackTrace.length; i++) { - stack += '\n at ' + structuredStackTrace[i].toString(); - } - return stack; -} - -export function parseSourceFromOwnerStack( - error: Error, -): ReactFunctionLocation | null { - // First attempt to collected the structured data using prepareStackTrace. - collectedLocation = null; - const previousPrepare = Error.prepareStackTrace; - Error.prepareStackTrace = collectStackTrace; - let stack; - try { - stack = error.stack; - } catch (e) { - // $FlowFixMe[incompatible-type] It does accept undefined. - Error.prepareStackTrace = undefined; - stack = error.stack; - } finally { - Error.prepareStackTrace = previousPrepare; - } - if (collectedLocation !== null) { - return collectedLocation; - } - if (stack == null) { - return null; - } - // Fallback to parsing the string form. - const componentStack = formatOwnerStackString(stack); - return parseSourceFromComponentStack(componentStack); -} - // 0.123456789 => 0.123 // Expects high-resolution timestamp in milliseconds, like from performance.now() // Mainly used for optimizing the size of serialized profiling payload diff --git a/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js new file mode 100644 index 0000000000000..92b4156de7fa1 --- /dev/null +++ b/packages/react-devtools-shared/src/backend/utils/parseStackTrace.js @@ -0,0 +1,331 @@ +/** +/** + * 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. + * + * @flow + */ + +import type {ReactStackTrace, ReactFunctionLocation} from 'shared/ReactTypes'; + +function parseStackTraceFromChromeStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = chromeFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + let name = parsed[1] || ''; + let isAsync = parsed[8] === 'async '; + if (name === '') { + name = ''; + } else if (name.startsWith('async ')) { + name = name.slice(5); + isAsync = true; + } + let filename = parsed[2] || parsed[5] || ''; + if (filename === '') { + filename = ''; + } + const line = +(parsed[3] || parsed[6]); + const col = +(parsed[4] || parsed[7]); + parsedFrames.push([name, filename, line, col, 0, 0, isAsync]); + } + return parsedFrames; +} + +const firefoxFrameRegExp = /^((?:.*".+")?[^@]*)@(.+):(\d+):(\d+)$/; +function parseStackTraceFromFirefoxStack( + stack: string, + skipFrames: number, +): ReactStackTrace { + let idx = stack.indexOf('react_stack_bottom_frame'); + if (idx === -1) { + idx = stack.indexOf('react-stack-bottom-frame'); + } + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = firefoxFrameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + const name = parsed[1] || ''; + const filename = parsed[2] || ''; + const line = +parsed[3]; + const col = +parsed[4]; + parsedFrames.push([name, filename, line, col, 0, 0, false]); + } + return parsedFrames; +} + +const CHROME_STACK_REGEXP = /^\s*at .*(\S+:\d+|\(native\))/m; +export function parseStackTraceFromString( + stack: string, + skipFrames: number, +): ReactStackTrace { + if (stack.match(CHROME_STACK_REGEXP)) { + return parseStackTraceFromChromeStack(stack, skipFrames); + } + return parseStackTraceFromFirefoxStack(stack, skipFrames); +} + +let framesToSkip: number = 0; +let collectedStackTrace: null | ReactStackTrace = null; + +const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; + +function getMethodCallName(callSite: CallSite): string { + const typeName = callSite.getTypeName(); + const methodName = callSite.getMethodName(); + const functionName = callSite.getFunctionName(); + let result = ''; + if (functionName) { + if ( + typeName && + identifierRegExp.test(functionName) && + functionName !== typeName + ) { + result += typeName + '.'; + } + result += functionName; + if ( + methodName && + functionName !== methodName && + !functionName.endsWith('.' + methodName) && + !functionName.endsWith(' ' + methodName) + ) { + result += ' [as ' + methodName + ']'; + } + } else { + if (typeName) { + result += typeName + '.'; + } + if (methodName) { + result += methodName; + } else { + result += ''; + } + } + return result; +} + +function collectStackTrace( + error: Error, + structuredStackTrace: CallSite[], +): string { + const result: ReactStackTrace = []; + // Collect structured stack traces from the callsites. + // We mirror how V8 serializes stack frames and how we later parse them. + for (let i = framesToSkip; i < structuredStackTrace.length; i++) { + const callSite = structuredStackTrace[i]; + let name = callSite.getFunctionName() || ''; + if ( + name.includes('react_stack_bottom_frame') || + name.includes('react-stack-bottom-frame') + ) { + // Skip everything after the bottom frame since it'll be internals. + break; + } else if (callSite.isNative()) { + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([name, '', 0, 0, 0, 0, isAsync]); + } else { + // We encode complex function calls as if they're part of the function + // name since we cannot simulate the complex ones and they look the same + // as function names in UIs on the client as well as stacks. + if (callSite.isConstructor()) { + name = 'new ' + name; + } else if (!callSite.isToplevel()) { + name = getMethodCallName(callSite); + } + if (name === '') { + name = ''; + } + let filename = callSite.getScriptNameOrSourceURL() || ''; + if (filename === '') { + filename = ''; + if (callSite.isEval()) { + const origin = callSite.getEvalOrigin(); + if (origin) { + filename = origin.toString() + ', '; + } + } + } + const line = callSite.getLineNumber() || 0; + const col = callSite.getColumnNumber() || 0; + const enclosingLine: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingLineNumber === 'function' + ? (callSite: any).getEnclosingLineNumber() || 0 + : 0; + const enclosingCol: number = + // $FlowFixMe[prop-missing] + typeof callSite.getEnclosingColumnNumber === 'function' + ? (callSite: any).getEnclosingColumnNumber() || 0 + : 0; + // $FlowFixMe[prop-missing] + const isAsync = callSite.isAsync(); + result.push([ + name, + filename, + line, + col, + enclosingLine, + enclosingCol, + isAsync, + ]); + } + } + collectedStackTrace = result; + + // At the same time we generate a string stack trace just in case someone + // else reads it. Ideally, we'd call the previous prepareStackTrace to + // ensure it's in the expected format but it's common for that to be + // source mapped and since we do a lot of eager parsing of errors, it + // would be slow in those environments. We could maybe just rely on those + // environments having to disable source mapping globally to speed things up. + // For now, we just generate a default V8 formatted stack trace without + // source mapping as a fallback. + const name = error.name || 'Error'; + const message = error.message || ''; + let stack = name + ': ' + message; + for (let i = 0; i < structuredStackTrace.length; i++) { + stack += '\n at ' + structuredStackTrace[i].toString(); + } + return stack; +} + +// This matches either of these V8 formats. +// at name (filename:0:0) +// at filename:0:0 +// at async filename:0:0 +const chromeFrameRegExp = + /^ *at (?:(.+) \((?:(.+):(\d+):(\d+)|\)\)|(?:async )?(.+):(\d+):(\d+)|\)$/; + +const stackTraceCache: WeakMap = new WeakMap(); + +export function parseStackTrace( + error: Error, + skipFrames: number, +): ReactStackTrace { + // We can only get structured data out of error objects once. So we cache the information + // so we can get it again each time. It also helps performance when the same error is + // referenced more than once. + const existing = stackTraceCache.get(error); + if (existing !== undefined) { + return existing; + } + // We override Error.prepareStackTrace with our own version that collects + // the structured data. We need more information than the raw stack gives us + // and we need to ensure that we don't get the source mapped version. + collectedStackTrace = null; + framesToSkip = skipFrames; + const previousPrepare = Error.prepareStackTrace; + Error.prepareStackTrace = collectStackTrace; + let stack; + try { + stack = String(error.stack); + } finally { + Error.prepareStackTrace = previousPrepare; + } + + if (collectedStackTrace !== null) { + const result = collectedStackTrace; + collectedStackTrace = null; + stackTraceCache.set(error, result); + return result; + } + + // If the stack has already been read, or this is not actually a V8 compatible + // engine then we might not get a normalized stack and it might still have been + // source mapped. Regardless we try our best to parse it. + + const parsedFrames = parseStackTraceFromString(stack, skipFrames); + stackTraceCache.set(error, parsedFrames); + return parsedFrames; +} + +export function extractLocationFromOwnerStack( + error: Error, +): ReactFunctionLocation | null { + const stackTrace = parseStackTrace(error, 0); + const stack = error.stack; + if ( + !stack.includes('react_stack_bottom_frame') && + !stack.includes('react-stack-bottom-frame') + ) { + // This didn't have a bottom to it, we can't trust it. + return null; + } + // We start from the bottom since that will have the best location for the owner itself. + for (let i = stackTrace.length - 1; i >= 0; i--) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available, since that points to the start of the function. + encLine || line, + encCol || col, + ]; + } + } + return null; +} + +export function extractLocationFromComponentStack( + stack: string, +): ReactFunctionLocation | null { + const stackTrace = parseStackTraceFromString(stack, 0); + for (let i = 0; i < stackTrace.length; i++) { + const [functionName, fileName, line, col, encLine, encCol] = stackTrace[i]; + // Take the first match with a colon in the file name. + if (fileName.indexOf(':') !== -1) { + return [ + functionName, + fileName, + // Use enclosing line if available. (Never the case here because we parse from string.) + encLine || line, + encCol || col, + ]; + } + } + return null; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css index 41e510b7c181a..ded305bbc66ca 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css @@ -97,11 +97,11 @@ } .CollapsableContent { - padding: 0.25rem 0; + margin-top: -0.25rem; } .PreviewContainer { - padding: 0 0.25rem 0.25rem 0.25rem; + padding: 0.25rem; } .TimeBarContainer { diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js index 9deddef14b098..c7d0b39df3b83 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -107,11 +107,10 @@ function SuspendedByRow({ } const value: any = asyncInfo.awaited.value; - const isErrored = - value !== null && - typeof value === 'object' && - value[meta.name] === 'rejected Thenable'; - + const metaName = + value !== null && typeof value === 'object' ? value[meta.name] : null; + const isFulfilled = metaName === 'fulfilled Thenable'; + const isRejected = metaName === 'rejected Thenable'; return (
- {name} + + {name} + : + ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css index b4e5cd157f3c4..985fe4b9fb5d5 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.css @@ -2,6 +2,7 @@ color: var(--color-component-name); font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); + font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js index 561e8a6651362..b6e7a7bb1c9f4 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OwnerView.js @@ -59,7 +59,7 @@ export default function OwnerView({ - {displayName} + {'<' + displayName + '>'}