diff --git a/compiler/yarn.lock b/compiler/yarn.lock index f480ff498cdf..a328b70efa72 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -10525,7 +10525,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10598,7 +10607,14 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11064,9 +11080,9 @@ undici-types@~6.19.2: integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== undici@^6.19.5: - version "6.21.2" - resolved "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz" - integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g== + version "6.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4" + integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g== unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.1" @@ -11375,7 +11391,7 @@ workerpool@^6.5.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11393,6 +11409,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" diff --git a/fixtures/packaging/webpack-alias/dev/yarn.lock b/fixtures/packaging/webpack-alias/dev/yarn.lock index 961a3a8208a0..2da73debceea 100644 --- a/fixtures/packaging/webpack-alias/dev/yarn.lock +++ b/fixtures/packaging/webpack-alias/dev/yarn.lock @@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + version "6.4.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6" querystring-es3@^0.2.0: version "0.2.1" diff --git a/fixtures/packaging/webpack/prod/yarn.lock b/fixtures/packaging/webpack/prod/yarn.lock index 961a3a8208a0..2da73debceea 100644 --- a/fixtures/packaging/webpack/prod/yarn.lock +++ b/fixtures/packaging/webpack/prod/yarn.lock @@ -936,8 +936,8 @@ punycode@^1.2.4, punycode@^1.4.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" qs@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + version "6.4.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.1.tgz#2bad97710a5b661c366b378b1e3a44a592ff45e6" querystring-es3@^0.2.0: version "0.2.1" diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 098a1a687e3a..fa89cf69ad67 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1040,6 +1040,8 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { // Initialize any debug info and block the initializing chunk on any // unresolved entries. initializeDebugChunk(response, chunk); + // TODO: The chunk might have transitioned to ERRORED now. + // Should we return early if that happens? } try { @@ -1075,6 +1077,7 @@ function initializeModelChunk(chunk: ResolvedModelChunk): void { const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; + initializedChunk.reason = null; if (__DEV__) { processChunkDebugInfo(response, initializedChunk, value); @@ -1097,6 +1100,7 @@ function initializeModuleChunk(chunk: ResolvedModuleChunk): void { const initializedChunk: InitializedChunk = (chunk: any); initializedChunk.status = INITIALIZED; initializedChunk.value = value; + initializedChunk.reason = null; } catch (error) { const erroredChunk: ErroredChunk = (chunk: any); erroredChunk.status = ERRORED; diff --git a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js index 51f233e284f0..7f92114c87e1 100644 --- a/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js +++ b/packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js @@ -1,8 +1,20 @@ +/** + * 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 + */ /* global chrome */ 'use strict'; +import type {ReactBuildType} from 'react-devtools-shared/src/backend/types'; -function setExtensionIconAndPopup(reactBuildType, tabId) { +function setExtensionIconAndPopup( + reactBuildType: ReactBuildType, + tabId: number, +) { chrome.action.setIcon({ tabId, path: { diff --git a/packages/react-devtools-extensions/src/contentScripts/installHook.js b/packages/react-devtools-extensions/src/contentScripts/installHook.js index 490232baf869..4a7cb44eaa31 100644 --- a/packages/react-devtools-extensions/src/contentScripts/installHook.js +++ b/packages/react-devtools-extensions/src/contentScripts/installHook.js @@ -10,6 +10,7 @@ import { getProfilingSettings, } from 'react-devtools-shared/src/utils'; import {postMessage} from './messages'; +import {createReactRendererListener} from './reactBuildType'; let resolveHookSettingsInjection: (settings: DevToolsHookSettings) => void; let resolveComponentFiltersInjection: (filters: Array) => void; @@ -67,17 +68,6 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) { // Detect React window.__REACT_DEVTOOLS_GLOBAL_HOOK__.on( 'renderer', - function ({reactBuildType}) { - window.postMessage( - { - source: 'react-devtools-hook', - payload: { - type: 'react-renderer-attached', - reactBuildType, - }, - }, - '*', - ); - }, + createReactRendererListener(window), ); } diff --git a/packages/react-devtools-extensions/src/contentScripts/reactBuildType.js b/packages/react-devtools-extensions/src/contentScripts/reactBuildType.js new file mode 100644 index 000000000000..f1d5326ea96a --- /dev/null +++ b/packages/react-devtools-extensions/src/contentScripts/reactBuildType.js @@ -0,0 +1,50 @@ +/** + * 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 {ReactBuildType} from 'react-devtools-shared/src/backend/types'; + +function reduceReactBuild( + currentReactBuildType: null | ReactBuildType, + nextReactBuildType: ReactBuildType, +): ReactBuildType { + if ( + currentReactBuildType === null || + currentReactBuildType === 'production' + ) { + return nextReactBuildType; + } + + // We only display the "worst" build type, so if we've already detected a non-production build, + // we ignore any future production builds. This way if a page has multiple renderers, + // and at least one of them is a non-production build, we'll display that instead of "production". + return nextReactBuildType === 'production' + ? currentReactBuildType + : nextReactBuildType; +} + +export function createReactRendererListener(target: { + postMessage: Function, + ... +}): ({reactBuildType: ReactBuildType}) => void { + let displayedReactBuild: null | ReactBuildType = null; + + return function ({reactBuildType}) { + displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType); + + target.postMessage( + { + source: 'react-devtools-hook', + payload: { + type: 'react-renderer-attached', + reactBuildType: displayedReactBuild, + }, + }, + '*', + ); + }; +} diff --git a/packages/react-devtools-shared/src/__tests__/storeForceError-test.js b/packages/react-devtools-shared/src/__tests__/storeForceError-test.js new file mode 100644 index 000000000000..16ae50d6f028 --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/storeForceError-test.js @@ -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. + * + * @flow + */ + +import type Store from 'react-devtools-shared/src/devtools/store'; + +import {getVersionedRenderImplementation} from './utils'; + +describe('Store forcing errors', () => { + let React; + let agent; + let store: Store; + let utils; + let actAsync; + + beforeEach(() => { + agent = global.agent; + store = global.store; + store.collapseNodesByDefault = false; + store.componentFilters = []; + store.recordChangeDescriptions = true; + + React = require('react'); + utils = require('./utils'); + + actAsync = utils.actAsync; + }); + + const {render} = getVersionedRenderImplementation(); + + // @reactVersion >= 18.0 + it('resets forced error and fallback states when filters are changed', async () => { + class AnyClassComponent extends React.Component { + render() { + return this.props.children; + } + } + + class ErrorBoundary extends React.Component { + state = {hasError: false}; + + static getDerivedStateFromError() { + return {hasError: true}; + } + + render() { + if (this.state.hasError) { + return ( + +
+ + ); + } + return this.props.children; + } + } + + function App() { + return ( + +
+ + ); + } + + await actAsync(async () => { + render(); + }); + const rendererID = utils.getRendererID(); + await actAsync(() => { + agent.overrideError({ + id: store.getElementIDAtIndex(2), + rendererID, + forceError: true, + }); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + ▾ +
+ `); + + await actAsync(() => { + agent.overrideError({ + id: store.getElementIDAtIndex(2), + rendererID, + forceError: false, + }); + }); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ +
+ `); + }); +}); diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 49e6192b467e..aa7fe9d6ee84 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -7898,7 +7898,7 @@ export function attach( // Map of Fiber and its force error status: true (error), false (toggled off) const forceErrorForFibers = new Map(); - function shouldErrorFiberAccordingToMap(fiber: any): boolean { + function shouldErrorFiberAccordingToMap(fiber: any): boolean | null { if (typeof setErrorHandler !== 'function') { throw new Error( 'Expected overrideError() to not get called for earlier React versions.', @@ -7934,7 +7934,7 @@ export function attach( } } if (status === undefined) { - return false; + return null; } return status; } diff --git a/packages/react-devtools-shared/src/backend/types.js b/packages/react-devtools-shared/src/backend/types.js index c68601f9b633..70ef8e4befef 100644 --- a/packages/react-devtools-shared/src/backend/types.js +++ b/packages/react-devtools-shared/src/backend/types.js @@ -603,3 +603,10 @@ export type DevToolsHookSettings = { export type DevToolsSettings = DevToolsHookSettings & { componentFilters: Array, }; + +export type ReactBuildType = + | 'deadcode' + | 'development' + | 'outdated' + | 'production' + | 'unminified'; 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 50186f47b215..ffcdd7df0ca8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspendedBy.js @@ -584,6 +584,9 @@ export default function InspectedElementSuspendedBy({ break; } + if (groups.length === 0) { + return null; + } return (
diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 5764a8bee4bf..f24ae48c3601 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -17,6 +17,7 @@ import type { DevToolsBackend, DevToolsHookSettings, ProfilingSettings, + ReactBuildType, } from './backend/types'; import type {ComponentFilter} from './frontend/types'; @@ -71,7 +72,7 @@ export function installHook( return null; } - function detectReactBuildType(renderer: ReactRenderer) { + function detectReactBuildType(renderer: ReactRenderer): ReactBuildType { try { if (typeof renderer.version === 'string') { // React DOM Fiber (16+) @@ -211,7 +212,7 @@ export function installHook( const id = ++uidCounter; renderers.set(id, renderer); - const reactBuildType = hasDetectedBadDCE + const reactBuildType: ReactBuildType = hasDetectedBadDCE ? 'deadcode' : detectReactBuildType(renderer); diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index 4cb4e8e4273a..727ec694bbf7 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -4520,18 +4520,30 @@ export function setFocusIfFocusable( // // We could compare the node to document.activeElement after focus, // but this would not handle the case where application code managed focus to automatically blur. + const element = ((node: any): HTMLElement); + + // If this element is already the active element, it's focusable and already + // focused. Calling .focus() on it would be a no-op (no focus event fires), + // so we short-circuit here. + if (element.ownerDocument.activeElement === element) { + return true; + } + let didFocus = false; const handleFocus = () => { didFocus = true; }; - const element = ((node: any): HTMLElement); try { - element.addEventListener('focus', handleFocus); + // Listen on the document in the capture phase so we detect focus even when + // it lands on a different element than the one we called .focus() on. This + // happens with