diff --git a/fixtures/flight/package.json b/fixtures/flight/package.json index 6d48c311cce3e..2aa38c85cabb1 100644 --- a/fixtures/flight/package.json +++ b/fixtures/flight/package.json @@ -71,10 +71,10 @@ "scripts": { "predev": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", "prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/ && rm -rf node_modules/.cache;", - "dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"", + "dev": "concurrently \"yarn run dev:region\" \"yarn run dev:global\"", "dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js --inspect=127.0.0.1:9230 server/global", "dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server --inspect=127.0.0.1:9229 server/region", - "start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"", + "start": "node scripts/build.js && concurrently \"yarn run start:region\" \"yarn run start:global\"", "start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global", "start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region", "build": "node scripts/build.js", diff --git a/packages/react-client/src/ReactFlightClientDevToolsHook.js b/packages/react-client/src/ReactFlightClientDevToolsHook.js index b8ca649d4de45..4f5a716eb32f4 100644 --- a/packages/react-client/src/ReactFlightClientDevToolsHook.js +++ b/packages/react-client/src/ReactFlightClientDevToolsHook.js @@ -30,7 +30,7 @@ export function injectInternals(internals: Object): boolean { } catch (err) { // Catch all errors because it is unsafe to throw during initialization. if (__DEV__) { - console.error('React instrumentation encountered an error: %s.', err); + console.error('React instrumentation encountered an error: %o.', err); } } if (hook.checkDCE) { diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index 50431b230ad87..610191fb2cdb8 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -141,7 +141,7 @@ function patchConsoleForTestingBeforeHookInstallation() { // if they use this code path. firstArg = firstArg.slice(9); } - if (firstArg === 'React instrumentation encountered an error: %s') { + if (firstArg === 'React instrumentation encountered an error: %o') { // Rethrow errors from React. throw args[1]; } else if ( diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index f5d202fe0164b..1eee4a3ad8a1c 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -367,6 +367,7 @@ export function getInternalReactConstants(version: string): { ReactPriorityLevels: ReactPriorityLevelsType, ReactTypeOfWork: WorkTagMap, StrictModeBits: number, + SuspenseyImagesMode: number, } { // ********************************************************** // The section below is copied from files in React repo. @@ -407,6 +408,8 @@ export function getInternalReactConstants(version: string): { StrictModeBits = 0b10; } + const SuspenseyImagesMode = 0b0100000; + let ReactTypeOfWork: WorkTagMap = ((null: any): WorkTagMap); // ********************************************************** @@ -820,6 +823,7 @@ export function getInternalReactConstants(version: string): { ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, + SuspenseyImagesMode, }; } @@ -988,6 +992,7 @@ export function attach( ReactPriorityLevels, ReactTypeOfWork, StrictModeBits, + SuspenseyImagesMode, } = getInternalReactConstants(version); const { ActivityComponent, @@ -2930,7 +2935,7 @@ export function attach( } if (suspenseNode.parent !== parentNode) { throw new Error( - 'Cannot remove a node from a different parent than is being reconciled.', + 'Cannot remove a Suspense node from a different parent than is being reconciled.', ); } let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes; @@ -3345,6 +3350,114 @@ export function attach( insertSuspendedBy(asyncInfo); } + function trackDebugInfoFromHostComponent( + devtoolsInstance: DevToolsInstance, + fiber: Fiber, + ): void { + if (fiber.tag !== HostComponent) { + return; + } + if ((fiber.mode & SuspenseyImagesMode) === 0) { + // In any released version, Suspensey Images are only enabled inside a ViewTransition + // subtree, which is enabled by the SuspenseyImagesMode. + // TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for + // all images and we'd need some other check for if the version of React has that enabled. + return; + } + + const type = fiber.type; + const props: { + src?: string, + onLoad?: (event: any) => void, + loading?: 'eager' | 'lazy', + ... + } = fiber.memoizedProps; + + const maySuspendCommit = + type === 'img' && + props.src != null && + props.src !== '' && + props.onLoad == null && + props.loading !== 'lazy'; + + // Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if + // it didn't suspend this particular update if it would've suspended if it mounted in this + // state, since we're tracking the dependencies inside the current state. + + if (!maySuspendCommit) { + return; + } + + const instance = fiber.stateNode; + if (instance == null) { + // Should never happen. + return; + } + + // Unlike props.src, currentSrc will be fully qualified which we need for comparison below. + // Unlike instance.src it will be resolved into the media queries currently matching which is + // the state we're inspecting. + const src = instance.currentSrc; + if (typeof src !== 'string' || src === '') { + return; + } + let start = -1; + let end = -1; + let fileSize = 0; + // $FlowFixMe[method-unbinding] + if (typeof performance.getEntriesByType === 'function') { + // We may be able to collect the start and end time of this resource from Performance Observer. + const resourceEntries = performance.getEntriesByType('resource'); + for (let i = 0; i < resourceEntries.length; i++) { + const resourceEntry = resourceEntries[i]; + if (resourceEntry.name === src) { + start = resourceEntry.startTime; + end = start + resourceEntry.duration; + // $FlowFixMe[prop-missing] + fileSize = (resourceEntry.encodedBodySize: any) || 0; + } + } + } + // A representation of the image data itself. + // TODO: We could render a little preview in the front end from the resource API. + const value: { + currentSrc: string, + naturalWidth?: number, + naturalHeight?: number, + fileSize?: number, + } = { + currentSrc: src, + }; + if (instance.naturalWidth > 0 && instance.naturalHeight > 0) { + // The intrinsic size of the file value itself, if it's loaded + value.naturalWidth = instance.naturalWidth; + value.naturalHeight = instance.naturalHeight; + } + if (fileSize > 0) { + // Cross-origin images won't have a file size that we can access. + value.fileSize = fileSize; + } + const promise = Promise.resolve(value); + (promise: any).status = 'fulfilled'; + (promise: any).value = value; + const ioInfo: ReactIOInfo = { + name: 'img', + start, + end, + value: promise, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber, // Allow linking to the if it's not filtered. + }; + const asyncInfo: ReactAsyncInfo = { + awaited: ioInfo, + // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file. + owner: fiber._debugOwner == null ? null : fiber._debugOwner, + debugStack: fiber._debugStack == null ? null : fiber._debugStack, + debugTask: fiber._debugTask == null ? null : fiber._debugTask, + }; + insertSuspendedBy(asyncInfo); + } + function mountVirtualChildrenRecursively( firstChild: Fiber, lastChild: null | Fiber, // non-inclusive @@ -3619,6 +3732,7 @@ export function attach( throw new Error('Did not expect a host hoistable to be the root'); } aquireHostInstance(nearestInstance, fiber.stateNode); + trackDebugInfoFromHostComponent(nearestInstance, fiber); } if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) { @@ -4447,20 +4561,22 @@ export function attach( aquireHostResource(nearestInstance, nextFiber.memoizedState); trackDebugInfoFromHostResource(nearestInstance, nextFiber); } else if ( - (nextFiber.tag === HostComponent || - nextFiber.tag === HostText || - nextFiber.tag === HostSingleton) && - prevFiber.stateNode !== nextFiber.stateNode + nextFiber.tag === HostComponent || + nextFiber.tag === HostText || + nextFiber.tag === HostSingleton ) { - // In persistent mode, it's possible for the stateNode to update with - // a new clone. In that case we need to release the old one and aquire - // new one instead. const nearestInstance = reconcilingParent; if (nearestInstance === null) { throw new Error('Did not expect a host hoistable to be the root'); } - releaseHostInstance(nearestInstance, prevFiber.stateNode); - aquireHostInstance(nearestInstance, nextFiber.stateNode); + if (prevFiber.stateNode !== nextFiber.stateNode) { + // In persistent mode, it's possible for the stateNode to update with + // a new clone. In that case we need to release the old one and aquire + // new one instead. + releaseHostInstance(nearestInstance, prevFiber.stateNode); + aquireHostInstance(nearestInstance, nextFiber.stateNode); + } + trackDebugInfoFromHostComponent(nearestInstance, nextFiber); } let updateFlags = NoUpdate; diff --git a/packages/react-reconciler/src/ReactFiberDevToolsHook.js b/packages/react-reconciler/src/ReactFiberDevToolsHook.js index dc357f1ac1aad..975c97d1b6554 100644 --- a/packages/react-reconciler/src/ReactFiberDevToolsHook.js +++ b/packages/react-reconciler/src/ReactFiberDevToolsHook.js @@ -78,7 +78,7 @@ export function injectInternals(internals: Object): boolean { } catch (err) { // Catch all errors because it is unsafe to throw during initialization. if (__DEV__) { - console.error('React instrumentation encountered an error: %s.', err); + console.error('React instrumentation encountered an error: %o.', err); } } if (hook.checkDCE) { @@ -101,7 +101,7 @@ export function onScheduleRoot(root: FiberRoot, children: ReactNodeList) { } catch (err) { if (__DEV__ && !hasLoggedError) { hasLoggedError = true; - console.error('React instrumentation encountered an error: %s', err); + console.error('React instrumentation encountered an error: %o', err); } } } @@ -144,7 +144,7 @@ export function onCommitRoot(root: FiberRoot, eventPriority: EventPriority) { if (__DEV__) { if (!hasLoggedError) { hasLoggedError = true; - console.error('React instrumentation encountered an error: %s', err); + console.error('React instrumentation encountered an error: %o', err); } } } @@ -162,7 +162,7 @@ export function onPostCommitRoot(root: FiberRoot) { if (__DEV__) { if (!hasLoggedError) { hasLoggedError = true; - console.error('React instrumentation encountered an error: %s', err); + console.error('React instrumentation encountered an error: %o', err); } } } @@ -177,7 +177,7 @@ export function onCommitUnmount(fiber: Fiber) { if (__DEV__) { if (!hasLoggedError) { hasLoggedError = true; - console.error('React instrumentation encountered an error: %s', err); + console.error('React instrumentation encountered an error: %o', err); } } } @@ -199,7 +199,7 @@ export function setIsStrictModeForDevtools(newIsStrictMode: boolean) { if (__DEV__) { if (!hasLoggedError) { hasLoggedError = true; - console.error('React instrumentation encountered an error: %s', err); + console.error('React instrumentation encountered an error: %o', err); } } } diff --git a/packages/shared/ReactIODescription.js b/packages/shared/ReactIODescription.js index 10c888213ddb7..7fa6bb243936f 100644 --- a/packages/shared/ReactIODescription.js +++ b/packages/shared/ReactIODescription.js @@ -26,6 +26,10 @@ export function getIODescription(value: any): string { return value.url; } else if (typeof value.href === 'string') { return value.href; + } else if (typeof value.src === 'string') { + return value.src; + } else if (typeof value.currentSrc === 'string') { + return value.currentSrc; } else if (typeof value.command === 'string') { return value.command; } else if (