diff --git a/.eslintrc.js b/.eslintrc.js index 952149f30ca7..2cebe8698c7c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -507,7 +507,6 @@ module.exports = { __IS_FIREFOX__: 'readonly', __IS_EDGE__: 'readonly', __IS_NATIVE__: 'readonly', - __IS_INTERNAL_MCP_BUILD__: 'readonly', __IS_INTERNAL_VERSION__: 'readonly', chrome: 'readonly', }, diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index c1312fc6d8ec..32d4fadcb588 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -72,7 +72,6 @@ module.exports = { __IS_CHROME__: false, __IS_EDGE__: false, __IS_NATIVE__: true, - __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.GITHUB_URL': `"${GITHUB_URL}"`, diff --git a/packages/react-devtools-core/webpack.standalone.js b/packages/react-devtools-core/webpack.standalone.js index 6279ba7fb349..01437e7f0fb7 100644 --- a/packages/react-devtools-core/webpack.standalone.js +++ b/packages/react-devtools-core/webpack.standalone.js @@ -91,7 +91,6 @@ module.exports = { __IS_FIREFOX__: false, __IS_CHROME__: false, __IS_EDGE__: false, - __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-extensions/webpack.config.js b/packages/react-devtools-extensions/webpack.config.js index 7b5acca6cc28..129816e90749 100644 --- a/packages/react-devtools-extensions/webpack.config.js +++ b/packages/react-devtools-extensions/webpack.config.js @@ -34,8 +34,6 @@ const IS_FIREFOX = process.env.IS_FIREFOX === 'true'; const IS_EDGE = process.env.IS_EDGE === 'true'; const IS_INTERNAL_VERSION = process.env.FEATURE_FLAG_TARGET === 'extension-fb'; -const IS_INTERNAL_MCP_BUILD = process.env.IS_INTERNAL_MCP_BUILD === 'true'; - const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss'; let statsFileName = `webpack-stats.${featureFlagTarget}.${__DEV__ ? 'development' : 'production'}`; @@ -48,9 +46,6 @@ if (IS_FIREFOX) { if (IS_EDGE) { statsFileName += `.edge`; } -if (IS_INTERNAL_MCP_BUILD) { - statsFileName += `.mcp`; -} statsFileName += '.json'; const babelOptions = { @@ -139,7 +134,6 @@ module.exports = { __IS_FIREFOX__: IS_FIREFOX, __IS_EDGE__: IS_EDGE, __IS_NATIVE__: false, - __IS_INTERNAL_MCP_BUILD__: IS_INTERNAL_MCP_BUILD, __IS_INTERNAL_VERSION__: IS_INTERNAL_VERSION, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, diff --git a/packages/react-devtools-fusebox/webpack.config.frontend.js b/packages/react-devtools-fusebox/webpack.config.frontend.js index 0d56e81a34b6..a2e09f0e0adb 100644 --- a/packages/react-devtools-fusebox/webpack.config.frontend.js +++ b/packages/react-devtools-fusebox/webpack.config.frontend.js @@ -85,7 +85,6 @@ module.exports = { __IS_CHROME__: false, __IS_FIREFOX__: false, __IS_EDGE__: false, - __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-fusebox"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index a0300069bfa2..f2b5fd8a6f41 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -77,7 +77,6 @@ module.exports = { __IS_FIREFOX__: false, __IS_EDGE__: false, __IS_NATIVE__: false, - __IS_INTERNAL_MCP_BUILD__: false, 'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`, 'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`, 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null, diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 7a01d6c5984a..266346a6bbe7 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -8871,86 +8871,6 @@ export function attach( return unresolvedSource; } - type InternalMcpFunctions = { - __internal_only_getComponentTree?: Function, - }; - - const internalMcpFunctions: InternalMcpFunctions = {}; - if (__IS_INTERNAL_MCP_BUILD__) { - // eslint-disable-next-line no-inner-declarations - function __internal_only_getComponentTree(): string { - let treeString = ''; - - function buildTreeString( - instance: DevToolsInstance, - prefix: string = '', - isLastChild: boolean = true, - ): void { - if (!instance) return; - - const name = - (instance.kind !== VIRTUAL_INSTANCE - ? getDisplayNameForFiber(instance.data) - : instance.data.name) || 'Unknown'; - - const id = instance.id !== undefined ? instance.id : 'unknown'; - - if (name !== 'createRoot()') { - treeString += - prefix + - (isLastChild ? '└── ' : '├── ') + - name + - ' (id: ' + - id + - ')\n'; - } - - const childPrefix = prefix + (isLastChild ? ' ' : '│ '); - - let childCount = 0; - let tempChild = instance.firstChild; - while (tempChild !== null) { - childCount++; - tempChild = tempChild.nextSibling; - } - - let child = instance.firstChild; - let currentChildIndex = 0; - - while (child !== null) { - currentChildIndex++; - const isLastSibling = currentChildIndex === childCount; - buildTreeString(child, childPrefix, isLastSibling); - child = child.nextSibling; - } - } - - const rootInstances: Array = []; - idToDevToolsInstanceMap.forEach(instance => { - if (instance.parent === null || instance.parent.parent === null) { - rootInstances.push(instance); - } - }); - - if (rootInstances.length > 0) { - for (let i = 0; i < rootInstances.length; i++) { - const isLast = i === rootInstances.length - 1; - buildTreeString(rootInstances[i], '', isLast); - if (!isLast) { - treeString += '\n'; - } - } - } else { - treeString = 'No component tree found.'; - } - - return treeString; - } - - internalMcpFunctions.__internal_only_getComponentTree = - __internal_only_getComponentTree; - } - return { cleanup, clearErrorsAndWarnings, @@ -8994,6 +8914,5 @@ export function attach( supportsTogglingSuspense, updateComponentFilters, getEnvironmentNames, - ...internalMcpFunctions, }; } diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js index a03ccc161ad1..94d37cfc902d 100644 --- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js +++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js @@ -3056,13 +3056,16 @@ function indexOfEventListener( listener: EventListener, optionsOrUseCapture: void | EventListenerOptionsOrUseCapture, ): number { + if (eventListeners.length === 0) { + return -1; + } + const normalizedOptions = normalizeListenerOptions(optionsOrUseCapture); for (let i = 0; i < eventListeners.length; i++) { const item = eventListeners[i]; if ( item.type === type && item.listener === listener && - normalizeListenerOptions(item.optionsOrUseCapture) === - normalizeListenerOptions(optionsOrUseCapture) + normalizeListenerOptions(item.optionsOrUseCapture) === normalizedOptions ) { return i; } @@ -3154,18 +3157,34 @@ function collectChildren(child: Fiber, collection: Array): boolean { } // $FlowFixMe[prop-missing] FragmentInstance.prototype.blur = function (this: FragmentInstanceType): void { - // TODO: When we have a parent element reference, we can skip traversal if the fragment's parent - // does not contain document.activeElement + // Early exit if activeElement is not within the fragment's parent + const parentHostFiber = getFragmentParentHostFiber(this._fragmentFiber); + if (parentHostFiber === null) { + return; + } + const parentHostInstance = + getInstanceFromHostFiber(parentHostFiber); + const activeElement = parentHostInstance.ownerDocument.activeElement; + if (activeElement === null || !parentHostInstance.contains(activeElement)) { + return; + } + traverseFragmentInstance( this._fragmentFiber, blurActiveElementWithinFragment, + activeElement, ); }; -function blurActiveElementWithinFragment(child: Fiber): boolean { - // TODO: We can get the activeElement from the parent outside of the loop when we have a reference. +function blurActiveElementWithinFragment( + child: Fiber, + activeElement: Element, +): boolean { + // Skip text nodes - they can't be focused + if (enableFragmentRefsTextNodes && child.tag === HostText) { + return false; + } const instance = getInstanceFromHostFiber(child); - const ownerDocument = instance.ownerDocument; - if (instance === ownerDocument.activeElement) { + if (instance === activeElement) { // $FlowFixMe[prop-missing] instance.blur(); return true; @@ -3312,46 +3331,45 @@ FragmentInstance.prototype.compareDocumentPosition = function ( ); } - const firstElement = getInstanceFromHostFiber(children[0]); - const lastElement = getInstanceFromHostFiber( + const firstNode = getInstanceFromHostFiber(children[0]); + const lastNode = getInstanceFromHostFiber( children[children.length - 1], ); // If the fragment has been portaled into another host instance, we need to // our best guess is to use the parent of the child instance, rather than // the fiber tree host parent. - const firstInstance = getInstanceFromHostFiber(children[0]); const parentHostInstanceFromDOM = fiberIsPortaledIntoHost(this._fragmentFiber) - ? (firstInstance.parentElement: ?Instance) + ? (firstNode.parentElement: ?Instance) : parentHostInstance; if (parentHostInstanceFromDOM == null) { return Node.DOCUMENT_POSITION_DISCONNECTED; } - // Check if first and last element are actually in the expected document position - // before relying on them as source of truth for other contained elements - const firstElementIsContained = - parentHostInstanceFromDOM.compareDocumentPosition(firstElement) & + // Check if first and last node are actually in the expected document position + // before relying on them as source of truth for other contained nodes + const firstNodeIsContained = + parentHostInstanceFromDOM.compareDocumentPosition(firstNode) & Node.DOCUMENT_POSITION_CONTAINED_BY; - const lastElementIsContained = - parentHostInstanceFromDOM.compareDocumentPosition(lastElement) & + const lastNodeIsContained = + parentHostInstanceFromDOM.compareDocumentPosition(lastNode) & Node.DOCUMENT_POSITION_CONTAINED_BY; - const firstResult = firstElement.compareDocumentPosition(otherNode); - const lastResult = lastElement.compareDocumentPosition(otherNode); + const firstResult = firstNode.compareDocumentPosition(otherNode); + const lastResult = lastNode.compareDocumentPosition(otherNode); const otherNodeIsFirstOrLastChild = - (firstElementIsContained && firstElement === otherNode) || - (lastElementIsContained && lastElement === otherNode); + (firstNodeIsContained && firstNode === otherNode) || + (lastNodeIsContained && lastNode === otherNode); const otherNodeIsFirstOrLastChildDisconnected = - (!firstElementIsContained && firstElement === otherNode) || - (!lastElementIsContained && lastElement === otherNode); + (!firstNodeIsContained && firstNode === otherNode) || + (!lastNodeIsContained && lastNode === otherNode); const otherNodeIsWithinFirstOrLastChild = firstResult & Node.DOCUMENT_POSITION_CONTAINED_BY || lastResult & Node.DOCUMENT_POSITION_CONTAINED_BY; const otherNodeIsBetweenFirstAndLastChildren = - firstElementIsContained && - lastElementIsContained && + firstNodeIsContained && + lastNodeIsContained && firstResult & Node.DOCUMENT_POSITION_FOLLOWING && lastResult & Node.DOCUMENT_POSITION_PRECEDING; @@ -3544,40 +3562,48 @@ export function updateFragmentInstanceFiber( } export function commitNewChildToFragmentInstance( - childInstance: InstanceWithFragmentHandles, + childInstance: InstanceWithFragmentHandles | Text, fragmentInstance: FragmentInstanceType, ): void { + if (childInstance.nodeType === TEXT_NODE) { + return; + } + const instance: InstanceWithFragmentHandles = (childInstance: any); const eventListeners = fragmentInstance._eventListeners; if (eventListeners !== null) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; - childInstance.addEventListener(type, listener, optionsOrUseCapture); + instance.addEventListener(type, listener, optionsOrUseCapture); } } if (fragmentInstance._observers !== null) { fragmentInstance._observers.forEach(observer => { - observer.observe(childInstance); + observer.observe(instance); }); } if (enableFragmentRefsInstanceHandles) { - addFragmentHandleToInstance(childInstance, fragmentInstance); + addFragmentHandleToInstance(instance, fragmentInstance); } } export function deleteChildFromFragmentInstance( - childInstance: InstanceWithFragmentHandles, + childInstance: InstanceWithFragmentHandles | Text, fragmentInstance: FragmentInstanceType, ): void { + if (childInstance.nodeType === TEXT_NODE) { + return; + } + const instance: InstanceWithFragmentHandles = (childInstance: any); const eventListeners = fragmentInstance._eventListeners; if (eventListeners !== null) { for (let i = 0; i < eventListeners.length; i++) { const {type, listener, optionsOrUseCapture} = eventListeners[i]; - childInstance.removeEventListener(type, listener, optionsOrUseCapture); + instance.removeEventListener(type, listener, optionsOrUseCapture); } } if (enableFragmentRefsInstanceHandles) { - if (childInstance.unstable_reactFragments != null) { - childInstance.unstable_reactFragments.delete(fragmentInstance); + if (instance.unstable_reactFragments != null) { + instance.unstable_reactFragments.delete(fragmentInstance); } } } diff --git a/packages/react-native-renderer/src/ReactFiberConfigFabric.js b/packages/react-native-renderer/src/ReactFiberConfigFabric.js index f5a4361fd41d..cc7f0d0c36a2 100644 --- a/packages/react-native-renderer/src/ReactFiberConfigFabric.js +++ b/packages/react-native-renderer/src/ReactFiberConfigFabric.js @@ -40,7 +40,10 @@ import { type PublicTextInstance, type PublicRootInstance, } from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface'; -import {enableFragmentRefsInstanceHandles} from 'shared/ReactFeatureFlags'; +import { + enableFragmentRefsInstanceHandles, + enableFragmentRefsTextNodes, +} from 'shared/ReactFeatureFlags'; const { createNode, @@ -847,10 +850,15 @@ export function updateFragmentInstanceFiber( } export function commitNewChildToFragmentInstance( - childInstance: Instance, + childInstance: Instance | TextInstance, fragmentInstance: FragmentInstanceType, ): void { - const publicInstance = getPublicInstance(childInstance); + // Text nodes are not observable + if (enableFragmentRefsTextNodes && childInstance.canonical == null) { + return; + } + const instance: Instance = (childInstance: any); + const publicInstance = getPublicInstance(instance); if (fragmentInstance._observers !== null) { if (publicInstance == null) { throw new Error('Expected to find a host node. This is a bug in React.'); @@ -869,11 +877,16 @@ export function commitNewChildToFragmentInstance( } export function deleteChildFromFragmentInstance( - childInstance: Instance, + childInstance: Instance | TextInstance, fragmentInstance: FragmentInstanceType, ): void { + // Text nodes are not observable + if (enableFragmentRefsTextNodes && childInstance.canonical == null) { + return; + } + const instance: Instance = (childInstance: any); const publicInstance = ((getPublicInstance( - childInstance, + instance, ): any): PublicInstanceWithFragmentHandles); if (enableFragmentRefsInstanceHandles) { if (publicInstance.unstable_reactFragments != null) { diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js index 5c7ccf398787..3626561e1b49 100644 --- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js +++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js @@ -64,7 +64,10 @@ import {captureCommitPhaseError} from './ReactFiberWorkLoop'; import {trackHostMutation} from './ReactFiberMutationTracking'; import {runWithFiberInDEV} from './ReactCurrentFiber'; -import {enableFragmentRefs} from 'shared/ReactFeatureFlags'; +import { + enableFragmentRefs, + enableFragmentRefsTextNodes, +} from 'shared/ReactFeatureFlags'; export function commitHostMount(finishedWork: Fiber) { const type = finishedWork.type; @@ -258,7 +261,8 @@ export function commitNewChildToFragmentInstances( parentFragmentInstances: null | Array, ): void { if ( - fiber.tag !== HostComponent || + (fiber.tag !== HostComponent && + !(enableFragmentRefsTextNodes && fiber.tag === HostText)) || // Only run fragment insertion effects for initial insertions fiber.alternate !== null || parentFragmentInstances === null diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 08882a04766c..199a4a873127 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -62,6 +62,7 @@ import { enableFragmentRefs, enableEagerAlternateStateNodeCleanup, enableDefaultTransitionIndicator, + enableFragmentRefsTextNodes, } from 'shared/ReactFeatureFlags'; import { FunctionComponent, @@ -1533,7 +1534,11 @@ function commitDeletionEffectsOnFiber( if (!offscreenSubtreeWasHidden) { safelyDetachRef(deletedFiber, nearestMountedAncestor); } - if (enableFragmentRefs && deletedFiber.tag === HostComponent) { + if ( + enableFragmentRefs && + (deletedFiber.tag === HostComponent || + (enableFragmentRefsTextNodes && deletedFiber.tag === HostText)) + ) { commitFragmentInstanceDeletionEffects(deletedFiber); } // Intentional fallthrough to next branch @@ -3028,7 +3033,11 @@ export function disappearLayoutEffects(finishedWork: Fiber) { // TODO (Offscreen) Check: flags & RefStatic safelyDetachRef(finishedWork, finishedWork.return); - if (enableFragmentRefs && finishedWork.tag === HostComponent) { + if ( + enableFragmentRefs && + (finishedWork.tag === HostComponent || + (enableFragmentRefsTextNodes && finishedWork.tag === HostText)) + ) { commitFragmentInstanceDeletionEffects(finishedWork); } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js index 3dde9c75bf03..f759fc60ef35 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js @@ -213,6 +213,67 @@ describe('ReactIncrementalErrorLogging', () => { }).toThrow('logCapturedError error'); }); + it('does not report internal Offscreen component for errors thrown during reconciliation inside Suspense', async () => { + // When a child of Suspense throws during reconciliation (not render), + // a Throw fiber is created whose .return is the internal Offscreen fiber. + // We should skip Offscreen since it's an internal + // implementation detail and walk up to Suspense instead. + const lazyChild = React.lazy(() => { + throw new Error('lazy init error'); + }); + + await fakeAct(() => { + ReactNoop.render( + }>{lazyChild}, + ); + }); + expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1); + expect(uncaughtExceptionMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'lazy init error', + }), + ); + if (__DEV__) { + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.mock.calls[0]).toEqual([ + '%s\n\n%s\n', + 'An error occurred in the component.', + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' + + 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.', + ]); + } + }); + + it('does not report internal Offscreen component for errors thrown during reconciliation inside Activity', async () => { + // Same as the Suspense test above — Activity also wraps its children in + // an internal Offscreen fiber. The error message should show Activity, + // not Offscreen. + const lazyChild = React.lazy(() => { + throw new Error('lazy init error'); + }); + + await fakeAct(() => { + ReactNoop.render( + {lazyChild}, + ); + }); + expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1); + expect(uncaughtExceptionMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'lazy init error', + }), + ); + if (__DEV__) { + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.mock.calls[0]).toEqual([ + '%s\n\n%s\n', + 'An error occurred in the component.', + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' + + 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.', + ]); + } + }); + it('resets instance variables before unmounting failed node', async () => { class ErrorBoundary extends React.Component { state = {error: null}; diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 97124bbf5ba5..8719539cfc08 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -122,7 +122,10 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { } return 'Mode'; case OffscreenComponent: - return 'Offscreen'; + if (fiber.return !== null) { + return getComponentNameFromFiber(fiber.return); + } + return null; case Profiler: return 'Profiler'; case ScopeComponent: diff --git a/scripts/flow/react-devtools.js b/scripts/flow/react-devtools.js index 09a251bbe2f5..4e0f2a915ede 100644 --- a/scripts/flow/react-devtools.js +++ b/scripts/flow/react-devtools.js @@ -16,6 +16,5 @@ declare const __IS_FIREFOX__: boolean; declare const __IS_CHROME__: boolean; declare const __IS_EDGE__: boolean; declare const __IS_NATIVE__: boolean; -declare const __IS_INTERNAL_MCP_BUILD__: boolean; declare const chrome: any; diff --git a/scripts/jest/devtools/setupEnv.js b/scripts/jest/devtools/setupEnv.js index 32bf13e686c7..a797c0951435 100644 --- a/scripts/jest/devtools/setupEnv.js +++ b/scripts/jest/devtools/setupEnv.js @@ -15,7 +15,6 @@ global.__IS_FIREFOX__ = false; global.__IS_CHROME__ = false; global.__IS_EDGE__ = false; global.__IS_NATIVE__ = false; -global.__IS_INTERNAL_MCP_BUILD__ = false; const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion;