From 6d2a97a7113dfac2ad45067001b7e49a98718324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 1 Jul 2024 15:30:00 -0400 Subject: [PATCH 01/85] [Fizz] Gate legacyContext field on disableLegacyContext (#30173) We're running out of fields and this one we can avoid at runtime in any modern builds. --- packages/react-server/src/ReactFizzServer.js | 58 ++++++++++++-------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 6c21a1828d..91907a7ffd 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -243,12 +243,12 @@ type RenderTask = { abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) - legacyContext: LegacyContext, // the current legacy context that this task is executing in context: ContextSnapshot, // the current new context that this task is executing in treeContext: TreeContext, // the current tree context that this task is executing in componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component thenableState: null | ThenableState, isFallback: boolean, // whether this task is rendering inside a fallback tree + legacyContext: LegacyContext, // the current legacy context that this task is executing in // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor. // Consider splitting into multiple objects or consolidating some fields. }; @@ -272,12 +272,12 @@ type ReplayTask = { abortSet: Set, // the abortable set that this task belongs to keyPath: Root | KeyNode, // the path of all parent keys currently rendering formatContext: FormatContext, // the format's specific context (e.g. HTML/SVG/MathML) - legacyContext: LegacyContext, // the current legacy context that this task is executing in context: ContextSnapshot, // the current new context that this task is executing in treeContext: TreeContext, // the current tree context that this task is executing in componentStack: null | ComponentStackNode, // stack frame description of the currently rendering component thenableState: null | ThenableState, isFallback: boolean, // whether this task is rendering inside a fallback tree + legacyContext: LegacyContext, // the current legacy context that this task is executing in // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor. // Consider splitting into multiple objects or consolidating some fields. }; @@ -462,11 +462,11 @@ function RequestInstance( abortSet, null, rootFormatContext, - emptyContextObject, rootContextSnapshot, emptyTreeContext, null, false, + emptyContextObject, ); pingedTasks.push(rootTask); } @@ -604,11 +604,11 @@ export function resumeRequest( abortSet, null, postponedState.rootFormatContext, - emptyContextObject, rootContextSnapshot, emptyTreeContext, null, false, + emptyContextObject, ); pingedTasks.push(rootTask); return request; @@ -630,11 +630,11 @@ export function resumeRequest( abortSet, null, postponedState.rootFormatContext, - emptyContextObject, rootContextSnapshot, emptyTreeContext, null, false, + emptyContextObject, ); pingedTasks.push(rootTask); return request; @@ -698,11 +698,11 @@ function createRenderTask( abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, - legacyContext: LegacyContext, context: ContextSnapshot, treeContext: TreeContext, componentStack: null | ComponentStackNode, isFallback: boolean, + legacyContext: LegacyContext, ): RenderTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -710,7 +710,7 @@ function createRenderTask( } else { blockedBoundary.pendingTasks++; } - const task: RenderTask = { + const task: RenderTask = ({ replay: null, node, childIndex, @@ -721,13 +721,15 @@ function createRenderTask( abortSet, keyPath, formatContext, - legacyContext, context, treeContext, componentStack, thenableState, isFallback, - }; + }: any); + if (!disableLegacyContext) { + task.legacyContext = legacyContext; + } abortSet.add(task); return task; } @@ -743,11 +745,11 @@ function createReplayTask( abortSet: Set, keyPath: Root | KeyNode, formatContext: FormatContext, - legacyContext: LegacyContext, context: ContextSnapshot, treeContext: TreeContext, componentStack: null | ComponentStackNode, isFallback: boolean, + legacyContext: LegacyContext, ): ReplayTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -756,7 +758,7 @@ function createReplayTask( blockedBoundary.pendingTasks++; } replay.pendingTasks++; - const task: ReplayTask = { + const task: ReplayTask = ({ replay, node, childIndex, @@ -767,13 +769,15 @@ function createReplayTask( abortSet, keyPath, formatContext, - legacyContext, context, treeContext, componentStack, thenableState, isFallback, - }; + }: any); + if (!disableLegacyContext) { + task.legacyContext = legacyContext; + } abortSet.add(task); return task; } @@ -1188,13 +1192,13 @@ function renderSuspenseBoundary( fallbackAbortSet, fallbackKeyPath, task.formatContext, - task.legacyContext, task.context, task.treeContext, // This stack should be the Suspense boundary stack because while the fallback is actually a child segment // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself suspenseComponentStack, true, + !disableLegacyContext ? task.legacyContext : emptyContextObject, ); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. @@ -1328,13 +1332,13 @@ function replaySuspenseBoundary( fallbackAbortSet, fallbackKeyPath, task.formatContext, - task.legacyContext, task.context, task.treeContext, // This stack should be the Suspense boundary stack because while the fallback is actually a child segment // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself suspenseComponentStack, true, + !disableLegacyContext ? task.legacyContext : emptyContextObject, ); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. @@ -3271,13 +3275,13 @@ function spawnNewSuspendedReplayTask( task.abortSet, task.keyPath, task.formatContext, - task.legacyContext, task.context, task.treeContext, // We pop one task off the stack because the node that suspended will be tried again, // which will add it back onto the stack. task.componentStack !== null ? task.componentStack.parent : null, task.isFallback, + !disableLegacyContext ? task.legacyContext : emptyContextObject, ); const ping = newTask.ping; @@ -3317,13 +3321,13 @@ function spawnNewSuspendedRenderTask( task.abortSet, task.keyPath, task.formatContext, - task.legacyContext, task.context, task.treeContext, // We pop one task off the stack because the node that suspended will be tried again, // which will add it back onto the stack. task.componentStack !== null ? task.componentStack.parent : null, task.isFallback, + !disableLegacyContext ? task.legacyContext : emptyContextObject, ); const ping = newTask.ping; @@ -3341,7 +3345,9 @@ function renderNode( // Snapshot the current context in case something throws to interrupt the // process. const previousFormatContext = task.formatContext; - const previousLegacyContext = task.legacyContext; + const previousLegacyContext = !disableLegacyContext + ? task.legacyContext + : emptyContextObject; const previousContext = task.context; const previousKeyPath = task.keyPath; const previousTreeContext = task.treeContext; @@ -3383,7 +3389,9 @@ function renderNode( // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; - task.legacyContext = previousLegacyContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; @@ -3435,7 +3443,9 @@ function renderNode( // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; - task.legacyContext = previousLegacyContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; @@ -3469,7 +3479,9 @@ function renderNode( // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; - task.legacyContext = previousLegacyContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; @@ -3485,7 +3497,9 @@ function renderNode( // Restore the context. We assume that this will be restored by the inner // functions in case nothing throws so we don't use "finally" here. task.formatContext = previousFormatContext; - task.legacyContext = previousLegacyContext; + if (!disableLegacyContext) { + task.legacyContext = previousLegacyContext; + } task.context = previousContext; task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; From e60063d9e7d346e92a5af42975a2fe7dd306f86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 2 Jul 2024 16:05:20 -0400 Subject: [PATCH 02/85] [Fizz] Include a component stack in prod but only lazily generate it (#30132) When we added component stacks to Fizz in prod it severely slowed down common cases where you intentionally are throwing error for purposes of client rendering. Our parent component stack generation is very slow since call components with fake errors to generate them. Therefore we disabled them in prod but included them in prerenders. https://github.com/facebook/react/pull/27850 However, we still kept generating data structures for them and the code still exists there for the prerenders. We could stop generating the data structures which are not completely free but also not crazy bad. What we can do instead is just lazily generate the component stacks. This is in fact what plain stacks do anyway. This doesn't work as well in Fiber because the data structures are live but on the server they're immutable so it's fine to do it later as well. That way you can choose to not read this getter for intentionally thrown errors - after inspecting the Error object - yet still get component stacks in prod for other errors. --- packages/react-server/src/ReactFizzServer.js | 54 +++++++++----------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 91907a7ffd..2c5dd0b1c3 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -908,27 +908,23 @@ type ThrownInfo = { export type ErrorInfo = ThrownInfo; export type PostponeInfo = ThrownInfo; -// While we track component stacks in prod all the time we only produce a reified stack in dev and -// during prerender in Prod. The reason for this is that the stack is useful for prerender where the timeliness -// of the request is less critical than the observability of the execution. For renders and resumes however we -// prioritize speed of the request. -function getThrownInfo( - request: Request, - node: null | ComponentStackNode, -): ThrownInfo { - if ( - node && - // Always produce a stack in dev - (__DEV__ || - // Produce a stack in prod if we're in a prerender - request.trackedPostpones !== null) - ) { - return { - componentStack: getStackFromNode(node), - }; - } else { - return {}; +function getThrownInfo(node: null | ComponentStackNode): ThrownInfo { + const errorInfo: ThrownInfo = {}; + if (node) { + Object.defineProperty(errorInfo, 'componentStack', { + configurable: true, + enumerable: true, + get() { + // Lazyily generate the stack since it's expensive. + const stack = getStackFromNode(node); + Object.defineProperty(errorInfo, 'componentStack', { + value: stack, + }); + return stack; + }, + }); } + return errorInfo; } function encodeErrorForBoundary( @@ -1127,7 +1123,7 @@ function renderSuspenseBoundary( } catch (error: mixed) { contentRootSegment.status = ERRORED; newBoundary.status = CLIENT_RENDERED; - const thrownInfo = getThrownInfo(request, task.componentStack); + const thrownInfo = getThrownInfo(task.componentStack); let errorDigest; if ( enablePostpone && @@ -1273,7 +1269,7 @@ function replaySuspenseBoundary( } } catch (error: mixed) { resumedBoundary.status = CLIENT_RENDERED; - const thrownInfo = getThrownInfo(request, task.componentStack); + const thrownInfo = getThrownInfo(task.componentStack); let errorDigest; if ( enablePostpone && @@ -2337,7 +2333,7 @@ function replayElement( // in the original prerender. What's unable to complete is the child // replay nodes which might be Suspense boundaries which are able to // absorb the error and we can still continue with siblings. - const thrownInfo = getThrownInfo(request, task.componentStack); + const thrownInfo = getThrownInfo(task.componentStack); erroredReplay( request, task.blockedBoundary, @@ -2868,7 +2864,7 @@ function replayFragment( // replay nodes which might be Suspense boundaries which are able to // absorb the error and we can still continue with siblings. // This is an error, stash the component stack if it is null. - const thrownInfo = getThrownInfo(request, task.componentStack); + const thrownInfo = getThrownInfo(task.componentStack); erroredReplay( request, task.blockedBoundary, @@ -3467,7 +3463,7 @@ function renderNode( const trackedPostpones = request.trackedPostpones; const postponeInstance: Postpone = (x: any); - const thrownInfo = getThrownInfo(request, task.componentStack); + const thrownInfo = getThrownInfo(task.componentStack); const postponedSegment = injectPostponedHole( request, ((task: any): RenderTask), // We don't use ReplayTasks in prerenders. @@ -3782,7 +3778,7 @@ function abortTask(task: Task, request: Request, error: mixed): void { boundary.status = CLIENT_RENDERED; // We construct an errorInfo from the boundary's componentStack so the error in dev will indicate which // boundary the message is referring to - const errorInfo = getThrownInfo(request, task.componentStack); + const errorInfo = getThrownInfo(task.componentStack); let errorDigest; if ( enablePostpone && @@ -4074,7 +4070,7 @@ function retryRenderTask( task.abortSet.delete(task); const postponeInstance: Postpone = (x: any); - const postponeInfo = getThrownInfo(request, task.componentStack); + const postponeInfo = getThrownInfo(task.componentStack); logPostpone(request, postponeInstance.message, postponeInfo); trackPostpone(request, trackedPostpones, task, segment); finishedTask(request, task.blockedBoundary, segment); @@ -4082,7 +4078,7 @@ function retryRenderTask( } } - const errorInfo = getThrownInfo(request, task.componentStack); + const errorInfo = getThrownInfo(task.componentStack); task.abortSet.delete(task); segment.status = ERRORED; erroredTask(request, task.blockedBoundary, x, errorInfo); @@ -4156,7 +4152,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { } task.replay.pendingTasks--; task.abortSet.delete(task); - const errorInfo = getThrownInfo(request, task.componentStack); + const errorInfo = getThrownInfo(task.componentStack); erroredReplay( request, task.blockedBoundary, From 309e146193c7b84d1c7a60d1a2ab2d6c836ba515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 2 Jul 2024 16:31:35 -0400 Subject: [PATCH 03/85] Implement onError signature for renderToMarkup (#30170) Stacked on #30132. This way we can get parent and owner stacks from the error. This forces us to confront multiple errors and whether or not a Flight error that ends up being unobservable needs to really reject the render. This implements stashing of Flight errors with a digest and only errors if they end up erroring the Fizz render too. At this point they'll have parent stacks so we can surface those. --- packages/react-html/src/ReactHTMLClient.js | 11 +++- packages/react-html/src/ReactHTMLServer.js | 54 +++++++++++++++- .../src/__tests__/ReactHTMLClient-test.js | 62 +++++++++++++++++++ .../src/__tests__/ReactHTMLServer-test.js | 58 +++++++++++++++++ 4 files changed, 180 insertions(+), 5 deletions(-) diff --git a/packages/react-html/src/ReactHTMLClient.js b/packages/react-html/src/ReactHTMLClient.js index 533ae7a3c3..e12b76356e 100644 --- a/packages/react-html/src/ReactHTMLClient.js +++ b/packages/react-html/src/ReactHTMLClient.js @@ -8,6 +8,7 @@ */ import type {ReactNodeList} from 'shared/ReactTypes'; +import type {ErrorInfo} from 'react-server/src/ReactFizzServer'; import ReactVersion from 'shared/ReactVersion'; @@ -27,6 +28,7 @@ import { type MarkupOptions = { identifierPrefix?: string, signal?: AbortSignal, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, }; export function renderToMarkup( @@ -49,11 +51,16 @@ export function renderToMarkup( reject(error); }, }; - function onError(error: mixed) { + function handleError(error: mixed, errorInfo: ErrorInfo) { // Any error rejects the promise, regardless of where it happened. // Unlike other React SSR we don't want to put Suspense boundaries into // client rendering mode because there's no client rendering here. reject(error); + + const onError = options && options.onError; + if (onError) { + onError(error, errorInfo); + } } const resumableState = createResumableState( options ? options.identifierPrefix : undefined, @@ -72,7 +79,7 @@ export function renderToMarkup( ), createRootFormatContext(), Infinity, - onError, + handleError, undefined, undefined, undefined, diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js index 923881e4da..8937001633 100644 --- a/packages/react-html/src/ReactHTMLServer.js +++ b/packages/react-html/src/ReactHTMLServer.js @@ -9,9 +9,13 @@ import type {ReactNodeList} from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; +import type {ErrorInfo} from 'react-server/src/ReactFizzServer'; import ReactVersion from 'shared/ReactVersion'; +import ReactSharedInternalsServer from 'react-server/src/ReactSharedInternalsServer'; +import ReactSharedInternalsClient from 'shared/ReactSharedInternals'; + import { createRequest as createFlightRequest, startWork as startFlightWork, @@ -62,6 +66,7 @@ type ReactMarkupNodeList = type MarkupOptions = { identifierPrefix?: string, signal?: AbortSignal, + onError?: (error: mixed, errorInfo: ErrorInfo) => ?string, }; function noServerCallOrFormAction() { @@ -109,17 +114,60 @@ export function renderToMarkup( reject(error); }, }; - function onError(error: mixed) { + + let stashErrorIdx = 1; + const stashedErrors: Map = new Map(); + + function handleFlightError(error: mixed): string { + // For Flight errors we don't immediately reject, because they might not matter + // to the output of the HTML. We stash the error with a digest in case we need + // to get to the original error from the Fizz render. + const id = '' + stashErrorIdx++; + stashedErrors.set(id, error); + return id; + } + + function handleError(error: mixed, errorInfo: ErrorInfo) { + if (typeof error === 'object' && error !== null) { + const id = error.digest; + // Note that the original error might be `undefined` so we need a has check. + if (typeof id === 'string' && stashedErrors.has(id)) { + // Get the original error thrown inside Flight. + error = stashedErrors.get(id); + } + } + // Any error rejects the promise, regardless of where it happened. // Unlike other React SSR we don't want to put Suspense boundaries into // client rendering mode because there's no client rendering here. reject(error); + + const onError = options && options.onError; + if (onError) { + if (__DEV__) { + const prevGetCurrentStackImpl = + ReactSharedInternalsServer.getCurrentStack; + // We're inside a "client" callback from Fizz but we only have access to the + // "server" runtime so to get access to a stack trace within this callback we + // need to override it to get it from the client runtime. + ReactSharedInternalsServer.getCurrentStack = + ReactSharedInternalsClient.getCurrentStack; + try { + onError(error, errorInfo); + } finally { + ReactSharedInternalsServer.getCurrentStack = + prevGetCurrentStackImpl; + } + } else { + onError(error, errorInfo); + } + } } const flightRequest = createFlightRequest( // $FlowFixMe: This should be a subtype but not everything is typed covariant. children, null, - onError, + handleFlightError, options ? options.identifierPrefix : undefined, undefined, 'Markup', @@ -153,7 +201,7 @@ export function renderToMarkup( ), createRootFormatContext(), Infinity, - onError, + handleError, undefined, undefined, undefined, diff --git a/packages/react-html/src/__tests__/ReactHTMLClient-test.js b/packages/react-html/src/__tests__/ReactHTMLClient-test.js index 02cef97c2d..5fb0a55acc 100644 --- a/packages/react-html/src/__tests__/ReactHTMLClient-test.js +++ b/packages/react-html/src/__tests__/ReactHTMLClient-test.js @@ -12,6 +12,15 @@ let React; let ReactHTML; +function normalizeCodeLocInfo(str) { + return ( + str && + String(str).replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)'; + }) + ); +} + if (!__EXPERIMENTAL__) { it('should not be built in stable', () => { try { @@ -170,5 +179,58 @@ if (!__EXPERIMENTAL__) { const html = await ReactHTML.renderToMarkup(); expect(html).toBe('
01
'); }); + + it('can get the component owner stacks for onError in dev', async () => { + const thrownError = new Error('hi'); + const caughtErrors = []; + + function Foo() { + return ; + } + function Bar() { + return ( +
+ +
+ ); + } + function Baz({unused}) { + throw thrownError; + } + + await expect(async () => { + await ReactHTML.renderToMarkup( +
+ +
, + { + onError(error, errorInfo) { + caughtErrors.push({ + error: error, + parentStack: errorInfo.componentStack, + ownerStack: React.captureOwnerStack + ? React.captureOwnerStack() + : null, + }); + }, + }, + ); + }).rejects.toThrow(thrownError); + + expect(caughtErrors.length).toBe(1); + expect(caughtErrors[0].error).toBe(thrownError); + expect(normalizeCodeLocInfo(caughtErrors[0].parentStack)).toBe( + '\n in Baz (at **)' + + '\n in div (at **)' + + '\n in Bar (at **)' + + '\n in Foo (at **)' + + '\n in div (at **)', + ); + expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe( + __DEV__ && gate(flags => flags.enableOwnerStacks) + ? '\n in Bar (at **)' + '\n in Foo (at **)' + : null, + ); + }); }); } diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js index 236c2b0f40..0d7b7c8da2 100644 --- a/packages/react-html/src/__tests__/ReactHTMLServer-test.js +++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js @@ -15,6 +15,15 @@ global.TextEncoder = require('util').TextEncoder; let React; let ReactHTML; +function normalizeCodeLocInfo(str) { + return ( + str && + String(str).replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + return '\n in ' + name + ' (at **)'; + }) + ); +} + if (!__EXPERIMENTAL__) { it('should not be built in stable', () => { try { @@ -200,5 +209,54 @@ if (!__EXPERIMENTAL__) { ); expect(html).toBe('
00
'); }); + + it('can get the component owner stacks for onError in dev', async () => { + const thrownError = new Error('hi'); + const caughtErrors = []; + + function Foo() { + return React.createElement(Bar); + } + function Bar() { + return React.createElement('div', null, React.createElement(Baz)); + } + function Baz({unused}) { + throw thrownError; + } + + await expect(async () => { + await ReactHTML.renderToMarkup( + React.createElement('div', null, React.createElement(Foo)), + { + onError(error, errorInfo) { + caughtErrors.push({ + error: error, + parentStack: errorInfo.componentStack, + ownerStack: React.captureOwnerStack + ? React.captureOwnerStack() + : null, + }); + }, + }, + ); + }).rejects.toThrow(thrownError); + + expect(caughtErrors.length).toBe(1); + expect(caughtErrors[0].error).toBe(thrownError); + expect(normalizeCodeLocInfo(caughtErrors[0].parentStack)).toBe( + // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks + // it doesn't have the Server Components in the parent stacks. + '\n in Lazy (at **)' + + '\n in div (at **)' + + '\n in div (at **)', + ); + expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe( + __DEV__ && gate(flags => flags.enableOwnerStacks) + ? // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks + // it doesn't have the Server Components in the parent stacks. + '\n in Lazy (at **)' + : null, + ); + }); }); } From cfb8945f511add040e1d5427d9961337f98f7618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 2 Jul 2024 18:26:32 -0400 Subject: [PATCH 04/85] [Fizz] Implement debugInfo (#30174) Stacked on #30170. This lets us track Server Component parent stacks in Fizz which also lets us track the correct owner stack for lazy. In Fiber we're careful not to make any DEV only fibers but since the ReactFizzComponentStack data structures just exist for debug meta data anyway we can just expand on that. --- .../src/__tests__/ReactHTMLServer-test.js | 18 +- .../src/ReactFiberComponentStack.js | 8 +- .../src/ReactFizzComponentStack.js | 29 ++- packages/react-server/src/ReactFizzServer.js | 219 +++++++++++++----- 4 files changed, 194 insertions(+), 80 deletions(-) diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js index 0d7b7c8da2..01c5873b56 100644 --- a/packages/react-html/src/__tests__/ReactHTMLServer-test.js +++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js @@ -244,17 +244,19 @@ if (!__EXPERIMENTAL__) { expect(caughtErrors.length).toBe(1); expect(caughtErrors[0].error).toBe(thrownError); expect(normalizeCodeLocInfo(caughtErrors[0].parentStack)).toBe( - // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks - // it doesn't have the Server Components in the parent stacks. - '\n in Lazy (at **)' + - '\n in div (at **)' + - '\n in div (at **)', + __DEV__ + ? '\n in Baz (at **)' + + '\n in div (at **)' + + '\n in Bar (at **)' + + '\n in Foo (at **)' + + '\n in div (at **)' + : '\n in Lazy (at **)' + + '\n in div (at **)' + + '\n in div (at **)', ); expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe( __DEV__ && gate(flags => flags.enableOwnerStacks) - ? // TODO: Because Fizz doesn't yet implement debugInfo for parent stacks - // it doesn't have the Server Components in the parent stacks. - '\n in Lazy (at **)' + ? '\n in Bar (at **)' + '\n in Foo (at **)' : null, ); }); diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index 8a69ba9ddf..18f65530ea 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -185,11 +185,11 @@ export function getOwnerStackByFiberInDev( // another code path anyway. I.e. this is likely NOT a V8 based browser. // This will cause some of the stack to have different formatting. // TODO: Normalize server component stacks to the client formatting. - if (owner.stack !== '') { - info += '\n' + owner.stack; + const ownerStack: string = owner.stack; + owner = owner.owner; + if (owner && ownerStack !== '') { + info += '\n' + ownerStack; } - const componentInfo: ReactComponentInfo = (owner: any); - owner = componentInfo.owner; } else { break; } diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index a16b2c5f91..6e3a31b284 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -41,10 +41,19 @@ type ClassComponentStackNode = { owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only stack?: null | string | Error, // DEV only }; +type ServerComponentStackNode = { + // DEV only + tag: 3, + parent: null | ComponentStackNode, + type: string, // name + env + owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only + stack?: null | string | Error, // DEV only +}; export type ComponentStackNode = | BuiltInComponentStackNode | FunctionComponentStackNode - | ClassComponentStackNode; + | ClassComponentStackNode + | ServerComponentStackNode; export function getStackByComponentStackNode( componentStack: ComponentStackNode, @@ -63,6 +72,11 @@ export function getStackByComponentStackNode( case 2: info += describeClassComponentFrame(node.type); break; + case 3: + if (__DEV__) { + info += describeBuiltInComponentFrame(node.type); + break; + } } // $FlowFixMe[incompatible-type] we bail out when we get a null node = node.parent; @@ -110,6 +124,11 @@ export function getOwnerStackByComponentStackNodeInDev( ); } break; + case 3: + if (!componentStack.owner) { + info += describeBuiltInComponentFrame(componentStack.type); + } + break; } let owner: void | null | ComponentStackNode | ReactComponentInfo = @@ -137,11 +156,11 @@ export function getOwnerStackByComponentStackNodeInDev( } } else if (typeof owner.stack === 'string') { // Server Component - if (owner.stack !== '') { - info += '\n' + owner.stack; + const ownerStack: string = owner.stack; + owner = owner.owner; + if (owner && ownerStack !== '') { + info += '\n' + ownerStack; } - const componentInfo: ReactComponentInfo = (owner: any); - owner = componentInfo.owner; } else { break; } diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 2c5dd0b1c3..1fc905fe20 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -21,6 +21,7 @@ import type { Thenable, ReactFormState, ReactComponentInfo, + ReactDebugInfo, } from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type { @@ -886,6 +887,41 @@ function createClassComponentStack( type, }; } +function createServerComponentStack( + task: Task, + debugInfo: void | null | ReactDebugInfo, +): null | ComponentStackNode { + // Build a Server Component parent stack from the debugInfo. + if (__DEV__) { + let node = task.componentStack; + if (debugInfo != null) { + const stack: ReactDebugInfo = debugInfo; + for (let i = 0; i < stack.length; i++) { + const componentInfo: ReactComponentInfo = (stack[i]: any); + if (typeof componentInfo.name !== 'string') { + continue; + } + let name = componentInfo.name; + const env = componentInfo.env; + if (env) { + name += ' (' + env + ')'; + } + node = { + tag: 3, + parent: node, + type: name, + owner: componentInfo.owner, + stack: componentInfo.stack, + }; + } + } + return node; + } + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'createServerComponentStack should never be called in production. This is a bug in React.', + ); +} function createComponentStackFromType( task: Task, @@ -1982,6 +2018,7 @@ function renderLazyComponent( stack: null | Error, // DEV only ): void { const previousComponentStack = task.componentStack; + // TODO: Do we really need this stack frame? We don't on the client. task.componentStack = createBuiltInComponentStack(task, 'Lazy', owner, stack); let Component; if (__DEV__) { @@ -2533,72 +2570,90 @@ function renderNodeDestructive( const owner = __DEV__ ? element._owner : null; const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null; + const previousComponentStack = task.componentStack; + if (__DEV__) { + task.componentStack = createServerComponentStack( + task, + element._debugInfo, + ); + } + const name = getComponentNameFromType(type); const keyOrIndex = key == null ? (childIndex === -1 ? 0 : childIndex) : key; const keyPath = [task.keyPath, name, keyOrIndex]; if (task.replay !== null) { - if (__DEV__ && enableOwnerStacks) { - const debugTask: null | ConsoleTask = element._debugTask; - if (debugTask) { - debugTask.run( - replayElement.bind( - null, - request, - task, - keyPath, - name, - keyOrIndex, - childIndex, - type, - props, - ref, - task.replay, - owner, - stack, - ), - ); - return; - } + const debugTask: null | ConsoleTask = + __DEV__ && enableOwnerStacks ? element._debugTask : null; + if (debugTask) { + debugTask.run( + replayElement.bind( + null, + request, + task, + keyPath, + name, + keyOrIndex, + childIndex, + type, + props, + ref, + task.replay, + owner, + stack, + ), + ); + } else { + replayElement( + request, + task, + keyPath, + name, + keyOrIndex, + childIndex, + type, + props, + ref, + task.replay, + owner, + stack, + ); } - replayElement( - request, - task, - keyPath, - name, - keyOrIndex, - childIndex, - type, - props, - ref, - task.replay, - owner, - stack, - ); // No matches found for this node. We assume it's already emitted in the // prelude and skip it during the replay. } else { // We're doing a plain render. - if (__DEV__ && enableOwnerStacks) { - const debugTask: null | ConsoleTask = element._debugTask; - if (debugTask) { - debugTask.run( - renderElement.bind( - null, - request, - task, - keyPath, - type, - props, - ref, - owner, - stack, - ), - ); - return; - } + const debugTask: null | ConsoleTask = + __DEV__ && enableOwnerStacks ? element._debugTask : null; + if (debugTask) { + debugTask.run( + renderElement.bind( + null, + request, + task, + keyPath, + type, + props, + ref, + owner, + stack, + ), + ); + } else { + renderElement( + request, + task, + keyPath, + type, + props, + ref, + owner, + stack, + ); } - renderElement(request, task, keyPath, type, props, ref, owner, stack); + } + if (__DEV__) { + task.componentStack = previousComponentStack; } return; } @@ -2608,14 +2663,23 @@ function renderNodeDestructive( 'Render them conditionally so that they only appear on the client render.', ); case REACT_LAZY_TYPE: { - const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack( - task, - 'Lazy', - null, - null, - ); const lazyNode: LazyComponentType = (node: any); + const previousComponentStack = task.componentStack; + if (__DEV__) { + task.componentStack = createServerComponentStack( + task, + lazyNode._debugInfo, + ); + } + if (!__DEV__ || task.componentStack === previousComponentStack) { + // TODO: Do we really need this stack frame? We don't on the client. + task.componentStack = createBuiltInComponentStack( + task, + 'Lazy', + null, + null, + ); + } let resolvedNode; if (__DEV__) { resolvedNode = callLazyInitInDEV(lazyNode); @@ -2746,12 +2810,23 @@ function renderNodeDestructive( // Clear any previous thenable state that was created by the unwrapping. task.thenableState = null; const thenable: Thenable = (maybeUsable: any); - return renderNodeDestructive( + const previousComponentStack = task.componentStack; + if (__DEV__) { + task.componentStack = createServerComponentStack( + task, + thenable._debugInfo, + ); + } + const result = renderNodeDestructive( request, task, unwrapThenable(thenable), childIndex, ); + if (__DEV__) { + task.componentStack = previousComponentStack; + } + return result; } if (maybeUsable.$$typeof === REACT_CONTEXT_TYPE) { @@ -2982,6 +3057,15 @@ function renderChildrenArray( childIndex: number, ): void { const prevKeyPath = task.keyPath; + const previousComponentStack = task.componentStack; + if (__DEV__) { + // We read debugInfo from task.node instead of children because it might have been an + // unwrapped iterable so we read from the original node. + task.componentStack = createServerComponentStack( + task, + (task.node: any)._debugInfo, + ); + } if (childIndex !== -1) { task.keyPath = [task.keyPath, 'Fragment', childIndex]; if (task.replay !== null) { @@ -2993,6 +3077,9 @@ function renderChildrenArray( childIndex, ); task.keyPath = prevKeyPath; + if (__DEV__) { + task.componentStack = previousComponentStack; + } return; } } @@ -3023,6 +3110,9 @@ function renderChildrenArray( } task.treeContext = prevTreeContext; task.keyPath = prevKeyPath; + if (__DEV__) { + task.componentStack = previousComponentStack; + } return; } } @@ -3042,6 +3132,9 @@ function renderChildrenArray( // only need to reset it to the previous value at the very end. task.treeContext = prevTreeContext; task.keyPath = prevKeyPath; + if (__DEV__) { + task.componentStack = previousComponentStack; + } } function trackPostpone( From 3db98c917701d59f62cf1fbe45cbf01b0b61c704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 2 Jul 2024 19:46:18 -0400 Subject: [PATCH 05/85] [Fizz] Track Current debugTask and run it for onError Callbacks (#30182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30174. This tracks the current debugTask on the Task so that when an error is thrown we can use it to run the `onError` (and `onShellError` and `onFatalError`) callbacks within the Context of that task. Ideally it would be associated with the error object but neither console.error [nor reportError](https://crbug.com/350426235) reports this as the async stack so we have to manually restore it. That way when you inspect Fizz using node `--inspect` we show the right async stack. Screenshot 2024-07-01 at 10 52 29 PM This is equivalent to how we track the task that created the parent server component or the Fiber: https://github.com/facebook/react/blob/6d2a97a7113dfac2ad45067001b7e49a98718324/packages/react-reconciler/src/ReactChildFiber.js#L1985 Then use them when invoking the error callbacks: https://github.com/facebook/react/blob/6d2a97a7113dfac2ad45067001b7e49a98718324/packages/react-reconciler/src/ReactFiberThrow.js#L104-L108 --------- Co-authored-by: Sebastian Silbermann --- packages/react-server/src/ReactFizzServer.js | 258 +++++++++++++------ 1 file changed, 178 insertions(+), 80 deletions(-) diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 1fc905fe20..e1e180f2f7 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -250,6 +250,7 @@ type RenderTask = { thenableState: null | ThenableState, isFallback: boolean, // whether this task is rendering inside a fallback tree legacyContext: LegacyContext, // the current legacy context that this task is executing in + debugTask: null | ConsoleTask, // DEV only // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor. // Consider splitting into multiple objects or consolidating some fields. }; @@ -279,6 +280,7 @@ type ReplayTask = { thenableState: null | ThenableState, isFallback: boolean, // whether this task is rendering inside a fallback tree legacyContext: LegacyContext, // the current legacy context that this task is executing in + debugTask: null | ConsoleTask, // DEV only // DON'T ANY MORE FIELDS. We at 16 already which otherwise requires converting to a constructor. // Consider splitting into multiple objects or consolidating some fields. }; @@ -468,6 +470,7 @@ function RequestInstance( null, false, emptyContextObject, + null, ); pingedTasks.push(rootTask); } @@ -610,6 +613,7 @@ export function resumeRequest( null, false, emptyContextObject, + null, ); pingedTasks.push(rootTask); return request; @@ -636,6 +640,7 @@ export function resumeRequest( null, false, emptyContextObject, + null, ); pingedTasks.push(rootTask); return request; @@ -704,6 +709,7 @@ function createRenderTask( componentStack: null | ComponentStackNode, isFallback: boolean, legacyContext: LegacyContext, + debugTask: null | ConsoleTask, ): RenderTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -731,6 +737,9 @@ function createRenderTask( if (!disableLegacyContext) { task.legacyContext = legacyContext; } + if (__DEV__ && enableOwnerStacks) { + task.debugTask = debugTask; + } abortSet.add(task); return task; } @@ -751,6 +760,7 @@ function createReplayTask( componentStack: null | ComponentStackNode, isFallback: boolean, legacyContext: LegacyContext, + debugTask: null | ConsoleTask, ): ReplayTask { request.allPendingTasks++; if (blockedBoundary === null) { @@ -779,6 +789,9 @@ function createReplayTask( if (!disableLegacyContext) { task.legacyContext = legacyContext; } + if (__DEV__ && enableOwnerStacks) { + task.debugTask = debugTask; + } abortSet.add(task); return task; } @@ -887,40 +900,44 @@ function createClassComponentStack( type, }; } -function createServerComponentStack( +function pushServerComponentStack( task: Task, debugInfo: void | null | ReactDebugInfo, -): null | ComponentStackNode { +): void { + if (!__DEV__) { + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'pushServerComponentStack should never be called in production. This is a bug in React.', + ); + } // Build a Server Component parent stack from the debugInfo. - if (__DEV__) { - let node = task.componentStack; - if (debugInfo != null) { - const stack: ReactDebugInfo = debugInfo; - for (let i = 0; i < stack.length; i++) { - const componentInfo: ReactComponentInfo = (stack[i]: any); - if (typeof componentInfo.name !== 'string') { - continue; - } - let name = componentInfo.name; - const env = componentInfo.env; - if (env) { - name += ' (' + env + ')'; - } - node = { - tag: 3, - parent: node, - type: name, - owner: componentInfo.owner, - stack: componentInfo.stack, - }; + if (debugInfo != null) { + const stack: ReactDebugInfo = debugInfo; + for (let i = 0; i < stack.length; i++) { + const componentInfo: ReactComponentInfo = (stack[i]: any); + if (typeof componentInfo.name !== 'string') { + continue; + } + if (enableOwnerStacks && componentInfo.stack === undefined) { + continue; + } + let name = componentInfo.name; + const env = componentInfo.env; + if (env) { + name += ' (' + env + ')'; + } + task.componentStack = { + tag: 3, + parent: task.componentStack, + type: name, + owner: componentInfo.owner, + stack: componentInfo.stack, + }; + if (enableOwnerStacks) { + task.debugTask = (componentInfo.task: any); } } - return node; } - // eslint-disable-next-line react-internal/prod-error-codes - throw new Error( - 'createServerComponentStack should never be called in production. This is a bug in React.', - ); } function createComponentStackFromType( @@ -1000,20 +1017,31 @@ function logPostpone( request: Request, reason: string, postponeInfo: ThrownInfo, + debugTask: null | ConsoleTask, ): void { // If this callback errors, we intentionally let that error bubble up to become a fatal error // so that someone fixes the error reporting instead of hiding it. - request.onPostpone(reason, postponeInfo); + const onPostpone = request.onPostpone; + if (__DEV__ && enableOwnerStacks && debugTask) { + debugTask.run(onPostpone.bind(null, reason, postponeInfo)); + } else { + onPostpone(reason, postponeInfo); + } } function logRecoverableError( request: Request, error: any, errorInfo: ThrownInfo, + debugTask: null | ConsoleTask, ): ?string { // If this callback errors, we intentionally let that error bubble up to become a fatal error // so that someone fixes the error reporting instead of hiding it. - const errorDigest = request.onError(error, errorInfo); + const onError = request.onError; + const errorDigest = + __DEV__ && enableOwnerStacks && debugTask + ? debugTask.run(onError.bind(null, error, errorInfo)) + : onError(error, errorInfo); if (errorDigest != null && typeof errorDigest !== 'string') { // We used to throw here but since this gets called from a variety of unprotected places it // seems better to just warn and discard the returned value. @@ -1028,14 +1056,24 @@ function logRecoverableError( return errorDigest; } -function fatalError(request: Request, error: mixed): void { +function fatalError( + request: Request, + error: mixed, + errorInfo: ThrownInfo, + debugTask: null | ConsoleTask, +): void { // This is called outside error handling code such as if the root errors outside // a suspense boundary or if the root suspense boundary's fallback errors. // It's also called if React itself or its host configs errors. const onShellError = request.onShellError; - onShellError(error); const onFatalError = request.onFatalError; - onFatalError(error); + if (__DEV__ && enableOwnerStacks && debugTask) { + debugTask.run(onShellError.bind(null, error)); + debugTask.run(onFatalError.bind(null, error)); + } else { + onShellError(error); + onFatalError(error); + } if (request.destination !== null) { request.status = CLOSED; closeWithError(request.destination, error); @@ -1168,11 +1206,21 @@ function renderSuspenseBoundary( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, thrownInfo); + logPostpone( + request, + postponeInstance.message, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, thrownInfo); + errorDigest = logRecoverableError( + request, + error, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); } encodeErrorForBoundary(newBoundary, errorDigest, error, thrownInfo, false); @@ -1231,6 +1279,7 @@ function renderSuspenseBoundary( suspenseComponentStack, true, !disableLegacyContext ? task.legacyContext : emptyContextObject, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. @@ -1314,11 +1363,21 @@ function replaySuspenseBoundary( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, thrownInfo); + logPostpone( + request, + postponeInstance.message, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, thrownInfo); + errorDigest = logRecoverableError( + request, + error, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); } encodeErrorForBoundary( resumedBoundary, @@ -1371,6 +1430,7 @@ function replaySuspenseBoundary( suspenseComponentStack, true, !disableLegacyContext ? task.legacyContext : emptyContextObject, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. @@ -2378,6 +2438,7 @@ function replayElement( thrownInfo, childNodes, childSlots, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); } task.replay = replay; @@ -2570,12 +2631,17 @@ function renderNodeDestructive( const owner = __DEV__ ? element._owner : null; const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null; + let previousDebugTask: null | ConsoleTask = null; const previousComponentStack = task.componentStack; + let debugTask: null | ConsoleTask; if (__DEV__) { - task.componentStack = createServerComponentStack( - task, - element._debugInfo, - ); + if (enableOwnerStacks) { + previousDebugTask = task.debugTask; + } + pushServerComponentStack(task, element._debugInfo); + if (enableOwnerStacks) { + task.debugTask = debugTask = element._debugTask; + } } const name = getComponentNameFromType(type); @@ -2583,8 +2649,6 @@ function renderNodeDestructive( key == null ? (childIndex === -1 ? 0 : childIndex) : key; const keyPath = [task.keyPath, name, keyOrIndex]; if (task.replay !== null) { - const debugTask: null | ConsoleTask = - __DEV__ && enableOwnerStacks ? element._debugTask : null; if (debugTask) { debugTask.run( replayElement.bind( @@ -2623,8 +2687,6 @@ function renderNodeDestructive( // prelude and skip it during the replay. } else { // We're doing a plain render. - const debugTask: null | ConsoleTask = - __DEV__ && enableOwnerStacks ? element._debugTask : null; if (debugTask) { debugTask.run( renderElement.bind( @@ -2654,6 +2716,9 @@ function renderNodeDestructive( } if (__DEV__) { task.componentStack = previousComponentStack; + if (enableOwnerStacks) { + task.debugTask = previousDebugTask; + } } return; } @@ -2665,11 +2730,12 @@ function renderNodeDestructive( case REACT_LAZY_TYPE: { const lazyNode: LazyComponentType = (node: any); const previousComponentStack = task.componentStack; + let previousDebugTask = null; if (__DEV__) { - task.componentStack = createServerComponentStack( - task, - lazyNode._debugInfo, - ); + if (enableOwnerStacks) { + previousDebugTask = task.debugTask; + } + pushServerComponentStack(task, lazyNode._debugInfo); } if (!__DEV__ || task.componentStack === previousComponentStack) { // TODO: Do we really need this stack frame? We don't on the client. @@ -2692,6 +2758,9 @@ function renderNodeDestructive( // We restore the stack before rendering the resolved node because once the Lazy // has resolved any future errors task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } // Now we render the resolved node renderNodeDestructive(request, task, resolvedNode, childIndex); @@ -2812,10 +2881,7 @@ function renderNodeDestructive( const thenable: Thenable = (maybeUsable: any); const previousComponentStack = task.componentStack; if (__DEV__) { - task.componentStack = createServerComponentStack( - task, - thenable._debugInfo, - ); + pushServerComponentStack(task, thenable._debugInfo); } const result = renderNodeDestructive( request, @@ -2947,6 +3013,7 @@ function replayFragment( thrownInfo, childNodes, childSlots, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); } task.replay = replay; @@ -3058,13 +3125,14 @@ function renderChildrenArray( ): void { const prevKeyPath = task.keyPath; const previousComponentStack = task.componentStack; + let previousDebugTask = null; if (__DEV__) { + if (enableOwnerStacks) { + previousDebugTask = task.debugTask; + } // We read debugInfo from task.node instead of children because it might have been an // unwrapped iterable so we read from the original node. - task.componentStack = createServerComponentStack( - task, - (task.node: any)._debugInfo, - ); + pushServerComponentStack(task, (task.node: any)._debugInfo); } if (childIndex !== -1) { task.keyPath = [task.keyPath, 'Fragment', childIndex]; @@ -3079,6 +3147,9 @@ function renderChildrenArray( task.keyPath = prevKeyPath; if (__DEV__) { task.componentStack = previousComponentStack; + if (enableOwnerStacks) { + task.debugTask = previousDebugTask; + } } return; } @@ -3112,6 +3183,9 @@ function renderChildrenArray( task.keyPath = prevKeyPath; if (__DEV__) { task.componentStack = previousComponentStack; + if (enableOwnerStacks) { + task.debugTask = previousDebugTask; + } } return; } @@ -3134,6 +3208,9 @@ function renderChildrenArray( task.keyPath = prevKeyPath; if (__DEV__) { task.componentStack = previousComponentStack; + if (enableOwnerStacks) { + task.debugTask = previousDebugTask; + } } } @@ -3327,7 +3404,12 @@ function injectPostponedHole( reason: string, thrownInfo: ThrownInfo, ): Segment { - logPostpone(request, reason, thrownInfo); + logPostpone( + request, + reason, + thrownInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); // Something suspended, we'll need to create a new segment and resolve it later. const segment = task.blockedSegment; const insertionIndex = segment.chunks.length; @@ -3371,6 +3453,7 @@ function spawnNewSuspendedReplayTask( task.componentStack !== null ? task.componentStack.parent : null, task.isFallback, !disableLegacyContext ? task.legacyContext : emptyContextObject, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); const ping = newTask.ping; @@ -3417,6 +3500,7 @@ function spawnNewSuspendedRenderTask( task.componentStack !== null ? task.componentStack.parent : null, task.isFallback, !disableLegacyContext ? task.legacyContext : emptyContextObject, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); const ping = newTask.ping; @@ -3609,6 +3693,7 @@ function erroredReplay( errorInfo: ThrownInfo, replayNodes: ReplayNode[], resumeSlots: ResumeSlots, + debugTask: null | ConsoleTask, ): void { // Erroring during a replay doesn't actually cause an error by itself because // that component has already rendered. What causes the error is the resumable @@ -3625,11 +3710,11 @@ function erroredReplay( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo); + logPostpone(request, postponeInstance.message, errorInfo, debugTask); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, errorInfo); + errorDigest = logRecoverableError(request, error, errorInfo, debugTask); } abortRemainingReplayNodes( request, @@ -3648,6 +3733,7 @@ function erroredTask( boundary: Root | SuspenseBoundary, error: mixed, errorInfo: ThrownInfo, + debugTask: null | ConsoleTask, ) { // Report the error to a global handler. let errorDigest; @@ -3658,14 +3744,14 @@ function erroredTask( error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo); + logPostpone(request, postponeInstance.message, errorInfo, debugTask); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, errorInfo); + errorDigest = logRecoverableError(request, error, errorInfo, debugTask); } if (boundary === null) { - fatalError(request, error); + fatalError(request, error, errorInfo, debugTask); } else { boundary.pendingTasks--; if (boundary.status !== CLIENT_RENDERED) { @@ -3821,11 +3907,11 @@ function abortTask(task: Task, request: Request, error: mixed): void { 'The render was aborted with postpone when the shell is incomplete. Reason: ' + postponeInstance.message, ); - logRecoverableError(request, fatal, errorInfo); - fatalError(request, fatal); + logRecoverableError(request, fatal, errorInfo, null); + fatalError(request, fatal, errorInfo, null); } else { - logRecoverableError(request, error, errorInfo); - fatalError(request, error); + logRecoverableError(request, error, errorInfo, null); + fatalError(request, error, errorInfo, null); } return; } else { @@ -3842,11 +3928,11 @@ function abortTask(task: Task, request: Request, error: mixed): void { error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo); + logPostpone(request, postponeInstance.message, errorInfo, null); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, errorInfo); + errorDigest = logRecoverableError(request, error, errorInfo, null); } abortRemainingReplayNodes( request, @@ -3880,11 +3966,11 @@ function abortTask(task: Task, request: Request, error: mixed): void { error.$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (error: any); - logPostpone(request, postponeInstance.message, errorInfo); + logPostpone(request, postponeInstance.message, errorInfo, null); // TODO: Figure out a better signal than a magic digest value. errorDigest = 'POSTPONE'; } else { - errorDigest = logRecoverableError(request, error, errorInfo); + errorDigest = logRecoverableError(request, error, errorInfo, null); } encodeErrorForBoundary(boundary, errorDigest, error, errorInfo, true); @@ -3922,7 +4008,7 @@ function safelyEmitEarlyPreloads( } catch (error) { // We assume preloads are optimistic and thus non-fatal if errored. const errorInfo: ThrownInfo = {}; - logRecoverableError(request, error, errorInfo); + logRecoverableError(request, error, errorInfo, null); } } @@ -4164,7 +4250,12 @@ function retryRenderTask( const postponeInstance: Postpone = (x: any); const postponeInfo = getThrownInfo(task.componentStack); - logPostpone(request, postponeInstance.message, postponeInfo); + logPostpone( + request, + postponeInstance.message, + postponeInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); trackPostpone(request, trackedPostpones, task, segment); finishedTask(request, task.blockedBoundary, segment); return; @@ -4174,7 +4265,13 @@ function retryRenderTask( const errorInfo = getThrownInfo(task.componentStack); task.abortSet.delete(task); segment.status = ERRORED; - erroredTask(request, task.blockedBoundary, x, errorInfo); + erroredTask( + request, + task.blockedBoundary, + x, + errorInfo, + __DEV__ && enableOwnerStacks ? task.debugTask : null, + ); return; } finally { if (__DEV__) { @@ -4253,6 +4350,7 @@ function retryReplayTask(request: Request, task: ReplayTask): void { errorInfo, task.replay.nodes, task.replay.slots, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); request.pendingRootTasks--; if (request.pendingRootTasks === 0) { @@ -4306,8 +4404,8 @@ export function performWork(request: Request): void { } } catch (error) { const errorInfo: ThrownInfo = {}; - logRecoverableError(request, error, errorInfo); - fatalError(request, error); + logRecoverableError(request, error, errorInfo, null); + fatalError(request, error, errorInfo, null); } finally { setCurrentResumableState(prevResumableState); ReactSharedInternals.H = prevDispatcher; @@ -4891,8 +4989,8 @@ export function startFlowing(request: Request, destination: Destination): void { flushCompletedQueues(request, destination); } catch (error) { const errorInfo: ThrownInfo = {}; - logRecoverableError(request, error, errorInfo); - fatalError(request, error); + logRecoverableError(request, error, errorInfo, null); + fatalError(request, error, errorInfo, null); } } @@ -4917,8 +5015,8 @@ export function abort(request: Request, reason: mixed): void { } } catch (error) { const errorInfo: ThrownInfo = {}; - logRecoverableError(request, error, errorInfo); - fatalError(request, error); + logRecoverableError(request, error, errorInfo, null); + fatalError(request, error, errorInfo, null); } } From 572ded3762fa7332e2d062e4373369cd7008b7b0 Mon Sep 17 00:00:00 2001 From: Ruslan Lesiutin Date: Wed, 3 Jul 2024 11:46:46 +0100 Subject: [PATCH 06/85] React DevTools 5.3.0 -> 5.3.1 (#30199) ## Summary Full list of changes, mostly fixes: * chore[react-devtools/renderer]: dont show strict mode warning for prod renderer builds ([hoxyq](https://github.com/hoxyq) in [#30158](https://github.com/facebook/react/pull/30158)) * chore[react-devtools/ui]: fix strict mode badge styles ([hoxyq](https://github.com/hoxyq) in [#30159](https://github.com/facebook/react/pull/30159)) * fix[react-devtools]: restore original args when recording errors ([hoxyq](https://github.com/hoxyq) in [#30091](https://github.com/facebook/react/pull/30091)) * Read constructor name more carefully ([LoganDark](https://github.com/LoganDark) in [#29954](https://github.com/facebook/react/pull/29954)) * refactor[react-devtools/extensions]: dont debounce cleanup logic on navigation ([hoxyq](https://github.com/hoxyq) in [#30027](https://github.com/facebook/react/pull/30027)) * lint: enable reportUnusedDisableDirectives and remove unused suppressions ([kassens](https://github.com/kassens) in [#28721](https://github.com/facebook/react/pull/28721)) * fix[react-devtools/extensions]: propagate globals from env ([hoxyq](https://github.com/hoxyq) in [#29963](https://github.com/facebook/react/pull/29963)) * refactor[react-devtools/tests]: use registered marks instead of cleared in tests ([hoxyq](https://github.com/hoxyq) in [#29929](https://github.com/facebook/react/pull/29929)) --- packages/react-devtools-core/package.json | 2 +- .../react-devtools-extensions/chrome/manifest.json | 4 ++-- packages/react-devtools-extensions/edge/manifest.json | 4 ++-- .../react-devtools-extensions/firefox/manifest.json | 2 +- packages/react-devtools-inline/package.json | 2 +- packages/react-devtools-timeline/package.json | 2 +- packages/react-devtools/CHANGELOG.md | 11 +++++++++++ packages/react-devtools/package.json | 4 ++-- 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/react-devtools-core/package.json b/packages/react-devtools-core/package.json index 3670512f04..4c7f54775f 100644 --- a/packages/react-devtools-core/package.json +++ b/packages/react-devtools-core/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-core", - "version": "5.3.0", + "version": "5.3.1", "description": "Use react-devtools outside of the browser", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-extensions/chrome/manifest.json b/packages/react-devtools-extensions/chrome/manifest.json index 99c9d47fd0..4dcd951a48 100644 --- a/packages/react-devtools-extensions/chrome/manifest.json +++ b/packages/react-devtools-extensions/chrome/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Chrome Developer Tools.", - "version": "5.3.0", - "version_name": "5.3.0", + "version": "5.3.1", + "version_name": "5.3.1", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/edge/manifest.json b/packages/react-devtools-extensions/edge/manifest.json index 77c4f059eb..fd19f1c5df 100644 --- a/packages/react-devtools-extensions/edge/manifest.json +++ b/packages/react-devtools-extensions/edge/manifest.json @@ -2,8 +2,8 @@ "manifest_version": 3, "name": "React Developer Tools", "description": "Adds React debugging tools to the Microsoft Edge Developer Tools.", - "version": "5.3.0", - "version_name": "5.3.0", + "version": "5.3.1", + "version_name": "5.3.1", "minimum_chrome_version": "102", "icons": { "16": "icons/16-production.png", diff --git a/packages/react-devtools-extensions/firefox/manifest.json b/packages/react-devtools-extensions/firefox/manifest.json index 70639fae88..ffa48634e0 100644 --- a/packages/react-devtools-extensions/firefox/manifest.json +++ b/packages/react-devtools-extensions/firefox/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "React Developer Tools", "description": "Adds React debugging tools to the Firefox Developer Tools.", - "version": "5.3.0", + "version": "5.3.1", "applications": { "gecko": { "id": "@react-devtools", diff --git a/packages/react-devtools-inline/package.json b/packages/react-devtools-inline/package.json index b6c0bd0c9b..ead4c18380 100644 --- a/packages/react-devtools-inline/package.json +++ b/packages/react-devtools-inline/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools-inline", - "version": "5.3.0", + "version": "5.3.1", "description": "Embed react-devtools within a website", "license": "MIT", "main": "./dist/backend.js", diff --git a/packages/react-devtools-timeline/package.json b/packages/react-devtools-timeline/package.json index da673cd03b..b9a8aab901 100644 --- a/packages/react-devtools-timeline/package.json +++ b/packages/react-devtools-timeline/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "react-devtools-timeline", - "version": "5.3.0", + "version": "5.3.1", "license": "MIT", "dependencies": { "@elg/speedscope": "1.9.0-a6f84db", diff --git a/packages/react-devtools/CHANGELOG.md b/packages/react-devtools/CHANGELOG.md index e6d5525fd6..bfebd3ea0a 100644 --- a/packages/react-devtools/CHANGELOG.md +++ b/packages/react-devtools/CHANGELOG.md @@ -4,6 +4,17 @@ --- +### 5.3.1 +July 3, 2024 + +* chore[react-devtools/renderer]: dont show strict mode warning for prod renderer builds ([hoxyq](https://github.com/hoxyq) in [#30158](https://github.com/facebook/react/pull/30158)) +* chore[react-devtools/ui]: fix strict mode badge styles ([hoxyq](https://github.com/hoxyq) in [#30159](https://github.com/facebook/react/pull/30159)) +* fix[react-devtools]: restore original args when recording errors ([hoxyq](https://github.com/hoxyq) in [#30091](https://github.com/facebook/react/pull/30091)) +* Read constructor name more carefully ([LoganDark](https://github.com/LoganDark) in [#29954](https://github.com/facebook/react/pull/29954)) +* refactor[react-devtools/extensions]: dont debounce cleanup logic on navigation ([hoxyq](https://github.com/hoxyq) in [#30027](https://github.com/facebook/react/pull/30027)) + +--- + ### 5.3.0 June 17, 2024 diff --git a/packages/react-devtools/package.json b/packages/react-devtools/package.json index 37ed26fe64..d2a9ec6614 100644 --- a/packages/react-devtools/package.json +++ b/packages/react-devtools/package.json @@ -1,6 +1,6 @@ { "name": "react-devtools", - "version": "5.3.0", + "version": "5.3.1", "description": "Use react-devtools outside of the browser", "license": "MIT", "repository": { @@ -27,7 +27,7 @@ "electron": "^23.1.2", "internal-ip": "^6.2.0", "minimist": "^1.2.3", - "react-devtools-core": "5.3.0", + "react-devtools-core": "5.3.1", "update-notifier": "^2.1.0" } } From 9c6806964f453cb5e8a530881dcc9f33480e7388 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Wed, 3 Jul 2024 13:10:23 +0200 Subject: [PATCH 07/85] Add regression test for #30172 (#30198) The issue reported in #30172 was fixed with #29823. The PR also added the test [`should resolve deduped objects that are themselves blocked`](https://github.com/facebook/react/blob/6d2a97a7113dfac2ad45067001b7e49a98718324/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js#L348-L393), which tests a similar scenario. However, the existing test would have also succeeded before applying the changes from #29823. Therefore, I believe it makes sense to add an additional test `should resolve deduped objects in nested children of blocked models`, which does not succeed without #29823, to prevent regressions. --- .../__tests__/ReactFlightDOMBrowser-test.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 90393e435e..49f2823c8b 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -392,6 +392,70 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('
1234512345
'); }); + it('should resolve deduped objects in nested children of blocked models', async () => { + let resolveOuterClientComponentChunk; + let resolveInnerClientComponentChunk; + + const ClientOuter = clientExports( + function ClientOuter({children, value}) { + return children; + }, + '1', + '/outer.js', + new Promise(resolve => (resolveOuterClientComponentChunk = resolve)), + ); + + function PassthroughServerComponent({children}) { + return children; + } + + const ClientInner = clientExports( + function ClientInner({children}) { + return JSON.stringify(children); + }, + '2', + '/inner.js', + new Promise(resolve => (resolveInnerClientComponentChunk = resolve)), + ); + + const value = {}; + + function Server() { + return ( + + + {value} + + + ); + } + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream(, webpackMap), + ); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(stream); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + expect(container.innerHTML).toBe(''); + + await act(() => { + resolveInnerClientComponentChunk(); + resolveOuterClientComponentChunk(); + }); + + expect(container.innerHTML).toBe('{}'); + }); + it('should progressively reveal server components', async () => { let reportedErrors = []; From 6e169fc65da097629588132f8fec82d8a836afdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 3 Jul 2024 13:25:04 -0400 Subject: [PATCH 08/85] [Flight] Allow String Chunks to Passthrough in Node streams and renderToMarkup (#30131) It can be efficient to accept raw string chunks to pass through a stream instead of encoding them into a binary copy first. Previously our Flight parsers didn't accept receiving string chunks. That's partly because we sometimes need to encode binary chunks anyway so string only transport isn't enough but some chunks can be strings. This adds a partial ability for chunks to be received as strings. However, accepting strings comes with some downsides. E.g. if the strings are split up we need to buffer it which compromises the perf for the common case. If the chunk represents binary data, then we'd need to encode it back into a typed array which would require a TextEncoder dependency in the parser. If the string chunk represents a byte length encoded string we don't know how many unicode characters to read without measuring them in terms of binary - also requiring a TextEncoder. This PR is mainly intended for use for pass-through within the same memory. We can simplify the implementation by assuming that any string chunk is passed as the original chunk. This requires that the server stream config doesn't arbitrarily concatenate strings (e.g. large strings should not be concatenated which is probably a good heuristic anyway). It also means that this is not suitable to be used with for example receiving string chunks on the client by passing them through SSR hydration data - except if the encoding that way was only used with chunks that were already encoded as strings by Flight. Web streams mostly just work on binary data anyway so they can't use this. In Node.js streams we concatenate precomputed and small strings into larger buffers. It might make sense to do that using string ropes instead. However, in the meantime we can at least pass large strings that are outside our buffer view size as raw strings. There's no benefit to us eagerly encoding those. Also, let Node accept string chunks as long as they're following our expected constraints. This lets us test the mixed protocol using pass-throughs. This can also be useful when the RSC server is in the same environment as the SSR server as they don't have to go from strings to typed arrays back to strings. Now we can also use this in the pass-through used in renderToMarkup. This lets us avoid the dependency on TextDecoder/TextEncoder in that package. --- .../react-client/src/ReactFlightClient.js | 158 +++++++++++++++++- .../src/ReactHTMLLegacyClientStreamConfig.js | 14 +- packages/react-html/src/ReactHTMLServer.js | 6 +- .../src/ReactFlightDOMClientNode.js | 7 +- .../src/__tests__/ReactFlightDOMNode-test.js | 19 ++- .../src/ReactServerStreamConfigNode.js | 3 +- scripts/error-codes/codes.json | 4 +- 7 files changed, 186 insertions(+), 25 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 7d421f0422..3cb918d6c8 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -2121,7 +2121,7 @@ function resolveTypedArray( resolveBuffer(response, id, view); } -function processFullRow( +function processFullBinaryRow( response: Response, id: number, tag: number, @@ -2183,6 +2183,15 @@ function processFullRow( row += readPartialStringChunk(stringDecoder, buffer[i]); } row += readFinalStringChunk(stringDecoder, chunk); + processFullStringRow(response, id, tag, row); +} + +function processFullStringRow( + response: Response, + id: number, + tag: number, + row: string, +): void { switch (tag) { case 73 /* "I" */: { resolveModule(response, id, row); @@ -2385,7 +2394,7 @@ export function processBinaryChunk( // We found the last chunk of the row const length = lastIdx - i; const lastChunk = new Uint8Array(chunk.buffer, offset, length); - processFullRow(response, rowID, rowTag, buffer, lastChunk); + processFullBinaryRow(response, rowID, rowTag, buffer, lastChunk); // Reset state machine for a new row i = lastIdx; if (rowState === ROW_CHUNK_BY_NEWLINE) { @@ -2415,6 +2424,151 @@ export function processBinaryChunk( response._rowLength = rowLength; } +export function processStringChunk(response: Response, chunk: string): void { + // This is a fork of processBinaryChunk that takes a string as input. + // This can't be just any binary chunk coverted to a string. It needs to be + // in the same offsets given from the Flight Server. E.g. if it's shifted by + // one byte then it won't line up to the UCS-2 encoding. It also needs to + // be valid Unicode. Also binary chunks cannot use this even if they're + // value Unicode. Large strings are encoded as binary and cannot be passed + // here. Basically, only if Flight Server gave you this string as a chunk, + // you can use it here. + let i = 0; + let rowState = response._rowState; + let rowID = response._rowID; + let rowTag = response._rowTag; + let rowLength = response._rowLength; + const buffer = response._buffer; + const chunkLength = chunk.length; + while (i < chunkLength) { + let lastIdx = -1; + switch (rowState) { + case ROW_ID: { + const byte = chunk.charCodeAt(i++); + if (byte === 58 /* ":" */) { + // Finished the rowID, next we'll parse the tag. + rowState = ROW_TAG; + } else { + rowID = (rowID << 4) | (byte > 96 ? byte - 87 : byte - 48); + } + continue; + } + case ROW_TAG: { + const resolvedRowTag = chunk.charCodeAt(i); + if ( + resolvedRowTag === 84 /* "T" */ || + (enableBinaryFlight && + (resolvedRowTag === 65 /* "A" */ || + resolvedRowTag === 79 /* "O" */ || + resolvedRowTag === 111 /* "o" */ || + resolvedRowTag === 85 /* "U" */ || + resolvedRowTag === 83 /* "S" */ || + resolvedRowTag === 115 /* "s" */ || + resolvedRowTag === 76 /* "L" */ || + resolvedRowTag === 108 /* "l" */ || + resolvedRowTag === 71 /* "G" */ || + resolvedRowTag === 103 /* "g" */ || + resolvedRowTag === 77 /* "M" */ || + resolvedRowTag === 109 /* "m" */ || + resolvedRowTag === 86)) /* "V" */ + ) { + rowTag = resolvedRowTag; + rowState = ROW_LENGTH; + i++; + } else if ( + (resolvedRowTag > 64 && resolvedRowTag < 91) /* "A"-"Z" */ || + resolvedRowTag === 114 /* "r" */ || + resolvedRowTag === 120 /* "x" */ + ) { + rowTag = resolvedRowTag; + rowState = ROW_CHUNK_BY_NEWLINE; + i++; + } else { + rowTag = 0; + rowState = ROW_CHUNK_BY_NEWLINE; + // This was an unknown tag so it was probably part of the data. + } + continue; + } + case ROW_LENGTH: { + const byte = chunk.charCodeAt(i++); + if (byte === 44 /* "," */) { + // Finished the rowLength, next we'll buffer up to that length. + rowState = ROW_CHUNK_BY_LENGTH; + } else { + rowLength = (rowLength << 4) | (byte > 96 ? byte - 87 : byte - 48); + } + continue; + } + case ROW_CHUNK_BY_NEWLINE: { + // We're looking for a newline + lastIdx = chunk.indexOf('\n', i); + break; + } + case ROW_CHUNK_BY_LENGTH: { + if (rowTag !== 84) { + throw new Error( + 'Binary RSC chunks cannot be encoded as strings. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + // For a large string by length, we don't know how many unicode characters + // we are looking for but we can assume that the raw string will be its own + // chunk. We add extra validation that the length is at least within the + // possible byte range it could possibly be to catch mistakes. + if (rowLength < chunk.length || chunk.length > rowLength * 3) { + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + lastIdx = chunk.length; + break; + } + } + if (lastIdx > -1) { + // We found the last chunk of the row + if (buffer.length > 0) { + // If we had a buffer already, it means that this chunk was split up into + // binary chunks preceeding it. + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + const lastChunk = chunk.slice(i, lastIdx); + processFullStringRow(response, rowID, rowTag, lastChunk); + // Reset state machine for a new row + i = lastIdx; + if (rowState === ROW_CHUNK_BY_NEWLINE) { + // If we're trailing by a newline we need to skip it. + i++; + } + rowState = ROW_ID; + rowTag = 0; + rowID = 0; + rowLength = 0; + buffer.length = 0; + } else if (chunk.length !== i) { + // The rest of this row is in a future chunk. We only support passing the + // string from chunks in their entirety. Not split up into smaller string chunks. + // We could support this by buffering them but we shouldn't need to for + // this use case. + throw new Error( + 'String chunks need to be passed in their original shape. ' + + 'Not split into smaller string chunks. ' + + 'This is a bug in the wiring of the React streams.', + ); + } + } + response._rowState = rowState; + response._rowID = rowID; + response._rowTag = rowTag; + response._rowLength = rowLength; +} + function parseModel(response: Response, json: UninitializedModel): T { return JSON.parse(json, response._fromJSON); } diff --git a/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js index 74b0503590..eaee8d4593 100644 --- a/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js +++ b/packages/react-html/src/ReactHTMLLegacyClientStreamConfig.js @@ -7,26 +7,22 @@ * @flow */ -// TODO: The legacy one should not use binary. +export type StringDecoder = null; -export type StringDecoder = TextDecoder; - -export function createStringDecoder(): StringDecoder { - return new TextDecoder(); +export function createStringDecoder(): null { + return null; } -const decoderOptions = {stream: true}; - export function readPartialStringChunk( decoder: StringDecoder, buffer: Uint8Array, ): string { - return decoder.decode(buffer, decoderOptions); + throw new Error('Not implemented.'); } export function readFinalStringChunk( decoder: StringDecoder, buffer: Uint8Array, ): string { - return decoder.decode(buffer); + throw new Error('Not implemented.'); } diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js index 8937001633..6bea571338 100644 --- a/packages/react-html/src/ReactHTMLServer.js +++ b/packages/react-html/src/ReactHTMLServer.js @@ -26,7 +26,7 @@ import { import { createResponse as createFlightResponse, getRoot as getFlightRoot, - processBinaryChunk as processFlightBinaryChunk, + processStringChunk as processFlightStringChunk, close as closeFlight, } from 'react-client/src/ReactFlightClient'; @@ -80,12 +80,10 @@ export function renderToMarkup( options?: MarkupOptions, ): Promise { return new Promise((resolve, reject) => { - const textEncoder = new TextEncoder(); const flightDestination = { push(chunk: string | null): boolean { if (chunk !== null) { - // TODO: Legacy should not use binary streams. - processFlightBinaryChunk(flightResponse, textEncoder.encode(chunk)); + processFlightStringChunk(flightResponse, chunk); } else { closeFlight(flightResponse); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index d0fb59c51e..e7beb9586a 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -30,6 +30,7 @@ import { createResponse, getRoot, reportGlobalError, + processStringChunk, processBinaryChunk, close, } from 'react-client/src/ReactFlightClient'; @@ -79,7 +80,11 @@ function createFromNodeStream( : undefined, ); stream.on('data', chunk => { - processBinaryChunk(response, chunk); + if (typeof chunk === 'string') { + processStringChunk(response, chunk); + } else { + processBinaryChunk(response, chunk); + } }); stream.on('error', error => { reportGlobalError(response, error); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js index 6f6a825e5e..2de34cc1c4 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js @@ -27,6 +27,11 @@ let use; let ReactServerScheduler; let reactServerAct; +// We test pass-through without encoding strings but it should work without it too. +const streamOptions = { + objectMode: true, +}; + describe('ReactFlightDOMNode', () => { beforeEach(() => { jest.resetModules(); @@ -76,7 +81,7 @@ describe('ReactFlightDOMNode', () => { function readResult(stream) { return new Promise((resolve, reject) => { let buffer = ''; - const writable = new Stream.PassThrough(); + const writable = new Stream.PassThrough(streamOptions); writable.setEncoding('utf8'); writable.on('data', chunk => { buffer += chunk; @@ -128,7 +133,7 @@ describe('ReactFlightDOMNode', () => { const stream = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); - const readable = new Stream.PassThrough(); + const readable = new Stream.PassThrough(streamOptions); let response; stream.pipe(readable); @@ -160,7 +165,7 @@ describe('ReactFlightDOMNode', () => { }), ); - const readable = new Stream.PassThrough(); + const readable = new Stream.PassThrough(streamOptions); const stringResult = readResult(readable); const parsedResult = ReactServerDOMClient.createFromNodeStream(readable, { @@ -206,7 +211,7 @@ describe('ReactFlightDOMNode', () => { const stream = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(buffers), ); - const readable = new Stream.PassThrough(); + const readable = new Stream.PassThrough(streamOptions); const promise = ReactServerDOMClient.createFromNodeStream(readable, { moduleMap: {}, moduleLoading: webpackModuleLoading, @@ -253,7 +258,7 @@ describe('ReactFlightDOMNode', () => { const stream = await serverAct(() => ReactServerDOMServer.renderToPipeableStream(, webpackMap), ); - const readable = new Stream.PassThrough(); + const readable = new Stream.PassThrough(streamOptions); let response; stream.pipe(readable); @@ -304,7 +309,7 @@ describe('ReactFlightDOMNode', () => { ), ); - const writable = new Stream.PassThrough(); + const writable = new Stream.PassThrough(streamOptions); rscStream.pipe(writable); controller.enqueue('hi'); @@ -349,7 +354,7 @@ describe('ReactFlightDOMNode', () => { ), ); - const readable = new Stream.PassThrough(); + const readable = new Stream.PassThrough(streamOptions); rscStream.pipe(readable); const result = await ReactServerDOMClient.createFromNodeStream(readable, { diff --git a/packages/react-server/src/ReactServerStreamConfigNode.js b/packages/react-server/src/ReactServerStreamConfigNode.js index 773c998610..fe03332618 100644 --- a/packages/react-server/src/ReactServerStreamConfigNode.js +++ b/packages/react-server/src/ReactServerStreamConfigNode.js @@ -63,7 +63,8 @@ function writeStringChunk(destination: Destination, stringChunk: string) { currentView = new Uint8Array(VIEW_SIZE); writtenBytes = 0; } - writeToDestination(destination, textEncoder.encode(stringChunk)); + // Write the raw string chunk and let the consumer handle the encoding. + writeToDestination(destination, stringChunk); return; } diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 46600256b0..088bd5b33b 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -523,5 +523,7 @@ "535": "renderToMarkup should not have emitted Server References. This is a bug in React.", "536": "Cannot pass ref in renderToMarkup because they will never be hydrated.", "537": "Cannot pass event handlers (%s) in renderToMarkup because the HTML will never be hydrated so they can never get called.", - "538": "Cannot use state or effect Hooks in renderToMarkup because this component will never be hydrated." + "538": "Cannot use state or effect Hooks in renderToMarkup because this component will never be hydrated.", + "539": "Binary RSC chunks cannot be encoded as strings. This is a bug in the wiring of the React streams.", + "540": "String chunks need to be passed in their original shape. Not split into smaller string chunks. This is a bug in the wiring of the React streams." } From 15ca8b6bad9c2e51f1a3b6e943c9af494b62a1a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 3 Jul 2024 16:57:05 -0400 Subject: [PATCH 09/85] Don't strip out component stack in assertConsole helpers (#30204) Use the same normalizeCodeLocInfo that we use everywhere else. We should actually test the component stack itself. Not just that it exists. This was causing false passes. However, the logic was also wrong before because it wouldn't always strip out the last line so wouldn't accurately normalize it. Leading to false failures as well. --- .../__tests__/ReactInternalTestUtils-test.js | 100 ++++++++++-------- packages/internal-test-utils/consoleMock.js | 17 +-- 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index e2a8f34cc9..ad3a7cdce7 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -1002,8 +1002,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Wow - + Bye " + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1025,8 +1025,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Bye " + + Hi in div (at **) + + Bye in div (at **)" `); }); @@ -1048,8 +1048,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Wow " + + Hi in div (at **) + + Wow in div (at **)" `); }); @@ -1071,9 +1071,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Wow - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1095,9 +1095,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1119,9 +1119,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1297,7 +1297,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1318,10 +1319,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1444,7 +1447,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1477,10 +1481,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleWarnDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this warning should include a component stack, remove {withoutStack: true} from this warning. If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." @@ -1934,8 +1940,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Wow - + Bye " + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -1957,8 +1963,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Bye " + + Hi in div (at **) + + Bye in div (at **)" `); }); @@ -1980,8 +1986,8 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - Bye - + Hi - + Wow " + + Hi in div (at **) + + Wow in div (at **)" `); }); @@ -2003,9 +2009,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Wow - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -2027,9 +2033,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Bye - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); @@ -2051,9 +2057,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Hi - Wow - + Hi - + Wow - + Bye " + + Hi in div (at **) + + Wow in div (at **) + + Bye in div (at **)" `); }); // @gate __DEV__ @@ -2170,7 +2176,7 @@ describe('ReactInternalTestUtils console assertions', () => { + Received errors - This is a completely different message that happens to start with "T" - + Message that happens to contain a "T" " + + Message that happens to contain a "T" in div (at **)" `); }); @@ -2247,7 +2253,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2268,10 +2275,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2394,7 +2403,8 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2427,10 +2437,12 @@ describe('ReactInternalTestUtils console assertions', () => { "assertConsoleErrorDev(expected) Unexpected component stack for: - "Hello " + "Hello + in div (at **)" Unexpected component stack for: - "Bye " + "Bye + in div (at **)" If this error should include a component stack, remove {withoutStack: true} from this error. If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." @@ -2459,7 +2471,7 @@ describe('ReactInternalTestUtils console assertions', () => { + Received errors - Hello - + Bye " + + Bye in div (at **)" `); }); }); diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 328cf3d90d..3f35d9e122 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -228,7 +228,7 @@ export function assertConsoleLogsCleared() { } } -function replaceComponentStack(str) { +function normalizeCodeLocInfo(str) { if (typeof str !== 'string') { return str; } @@ -239,8 +239,13 @@ function replaceComponentStack(str) { // at Component (/path/filename.js:123:45) // React format: // in Component (at filename.js:123) - return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*.*/, function (m, name) { - return chalk.dim(' '); + return str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { + if (name.endsWith('.render')) { + // Class components will have the `render` method as part of their stack trace. + // We strip that out in our normalization to make it look more like component stacks. + name = name.slice(0, name.length - 7); + } + return '\n in ' + name + ' (at **)'; }); } @@ -382,11 +387,11 @@ export function createLogAssertion( ); } - expectedMessage = replaceComponentStack(currentExpectedMessage); + expectedMessage = normalizeCodeLocInfo(currentExpectedMessage); expectedWithoutStack = expectedMessageOrArray[1].withoutStack; } else if (typeof expectedMessageOrArray === 'string') { // Should be in the form assert(['log']) or assert(['log'], {withoutStack: true}) - expectedMessage = replaceComponentStack(expectedMessageOrArray); + expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray); if (consoleMethod === 'log') { expectedWithoutStack = true; } else { @@ -410,7 +415,7 @@ export function createLogAssertion( ); } - const normalizedMessage = replaceComponentStack(message); + const normalizedMessage = normalizeCodeLocInfo(message); receivedLogs.push(normalizedMessage); // Check the number of %s interpolations. From 3da26163a35969b32b5d7876a3f0c95bea61bc2b Mon Sep 17 00:00:00 2001 From: Jack Works <5390719+Jack-Works@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:34:48 +0800 Subject: [PATCH 10/85] fix: path handling in react devtools (#29199) ## Summary Fix how devtools handles URLs. It - cannot handle relative source map URLs `//# sourceMappingURL=x.map` - cannot recognize Windows style URLs ## How did you test this change? works on my side --- .../src/__tests__/utils-test.js | 30 +++++++++++++++++ .../src/hooks/SourceMapConsumer.js | 15 +++------ .../parseHookNames/parseSourceAndMetadata.js | 5 +++ .../src/symbolicateSource.js | 33 +++++++++++-------- packages/react-devtools-shared/src/utils.js | 2 +- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/packages/react-devtools-shared/src/__tests__/utils-test.js b/packages/react-devtools-shared/src/__tests__/utils-test.js index 9e781e072b..f35cacc733 100644 --- a/packages/react-devtools-shared/src/__tests__/utils-test.js +++ b/packages/react-devtools-shared/src/__tests__/utils-test.js @@ -26,6 +26,7 @@ import { REACT_STRICT_MODE_TYPE as StrictMode, } from 'shared/ReactSymbols'; import {createElement} from 'react'; +import {symbolicateSource} from '../symbolicateSource'; describe('utils', () => { describe('getDisplayName', () => { @@ -385,6 +386,35 @@ describe('utils', () => { }); }); + describe('symbolicateSource', () => { + const source = `"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.f = f; +function f() { } +//# sourceMappingURL=`; + const result = { + column: 16, + line: 1, + sourceURL: 'http://test/a.mts', + }; + const fs = { + 'http://test/a.mts': `export function f() {}`, + 'http://test/a.mjs.map': `{"version":3,"file":"a.mjs","sourceRoot":"","sources":["a.mts"],"names":[],"mappings":";;AAAA,cAAsB;AAAtB,SAAgB,CAAC,KAAI,CAAC"}`, + 'http://test/a.mjs': `${source}a.mjs.map`, + 'http://test/b.mjs': `${source}./a.mjs.map`, + 'http://test/c.mjs': `${source}http://test/a.mjs.map`, + 'http://test/d.mjs': `${source}/a.mjs.map`, + }; + const fetchFileWithCaching = async (url: string) => fs[url] || null; + it('should parse source map urls', async () => { + const run = url => symbolicateSource(fetchFileWithCaching, url, 4, 10); + await expect(run('http://test/a.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/b.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/c.mjs')).resolves.toStrictEqual(result); + await expect(run('http://test/d.mjs')).resolves.toStrictEqual(result); + }); + }); + describe('formatConsoleArguments', () => { it('works with empty arguments list', () => { expect(formatConsoleArguments(...[])).toEqual([]); diff --git a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js index 779cbb3b2f..8e6503685b 100644 --- a/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js +++ b/packages/react-devtools-shared/src/hooks/SourceMapConsumer.js @@ -24,8 +24,8 @@ type SearchPosition = { type ResultPosition = { column: number, line: number, - sourceContent: string, - sourceURL: string, + sourceContent: string | null, + sourceURL: string | null, }; export type SourceMapConsumerType = { @@ -118,18 +118,11 @@ function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) { const line = nearestEntry[2] + 1; const column = nearestEntry[3]; - if (sourceContent === null || sourceURL === null) { - // TODO maybe fall back to the runtime source instead of throwing? - throw Error( - `Could not find original source for line:${lineNumber} and column:${columnNumber}`, - ); - } - return { column, line, - sourceContent: ((sourceContent: any): string), - sourceURL: ((sourceURL: any): string), + sourceContent: ((sourceContent: any): string | null), + sourceURL: ((sourceURL: any): string | null), }; } diff --git a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js index 40bfed48ba..15423c1241 100644 --- a/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js +++ b/packages/react-devtools-shared/src/hooks/parseHookNames/parseSourceAndMetadata.js @@ -276,6 +276,11 @@ function parseSourceAST( columnNumber, lineNumber, }); + if (sourceContent === null || sourceURL === null) { + throw Error( + `Could not find original source for line:${lineNumber} and column:${columnNumber}`, + ); + } originalSourceColumnNumber = column; originalSourceLineNumber = line; diff --git a/packages/react-devtools-shared/src/symbolicateSource.js b/packages/react-devtools-shared/src/symbolicateSource.js index d28ed42e59..9430e88b3f 100644 --- a/packages/react-devtools-shared/src/symbolicateSource.js +++ b/packages/react-devtools-shared/src/symbolicateSource.js @@ -39,7 +39,7 @@ export async function symbolicateSourceWithCache( } const SOURCE_MAP_ANNOTATION_PREFIX = 'sourceMappingURL='; -async function symbolicateSource( +export async function symbolicateSource( fetchFileWithCaching: FetchFileWithCaching, sourceURL: string, lineNumber: number, // 1-based @@ -63,11 +63,12 @@ async function symbolicateSource( const sourceMapAnnotationStartIndex = resourceLine.indexOf( SOURCE_MAP_ANNOTATION_PREFIX, ); - const sourceMapURL = resourceLine.slice( + const sourceMapAt = resourceLine.slice( sourceMapAnnotationStartIndex + SOURCE_MAP_ANNOTATION_PREFIX.length, resourceLine.length, ); + const sourceMapURL = new URL(sourceMapAt, sourceURL).toString(); const sourceMap = await fetchFileWithCaching(sourceMapURL).catch( () => null, ); @@ -84,29 +85,33 @@ async function symbolicateSource( columnNumber, // 1-based }); + if (possiblyURL === null) { + return null; + } try { - void new URL(possiblyURL); // This is a valid URL + // sourceMapURL = https://react.dev/script.js.map + void new URL(possiblyURL); // test if it is a valid URL const normalizedURL = normalizeUrl(possiblyURL); return {sourceURL: normalizedURL, line, column}; } catch (e) { // This is not valid URL - if (possiblyURL.startsWith('/')) { + if ( + // sourceMapURL = /file + possiblyURL.startsWith('/') || + // sourceMapURL = C:\\... + possiblyURL.slice(1).startsWith(':\\\\') + ) { // This is an absolute path return {sourceURL: possiblyURL, line, column}; } // This is a relative path - const [sourceMapAbsolutePathWithoutQueryParameters] = - sourceMapURL.split(/[?#&]/); - - const absoluteSourcePath = - sourceMapAbsolutePathWithoutQueryParameters + - (sourceMapAbsolutePathWithoutQueryParameters.endsWith('/') - ? '' - : '/') + - possiblyURL; - + // possiblyURL = x.js.map, sourceMapURL = https://react.dev/script.js.map + const absoluteSourcePath = new URL( + possiblyURL, + sourceMapURL, + ).toString(); return {sourceURL: absoluteSourcePath, line, column}; } } catch (e) { diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 5b0903883c..ffbc9e390d 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -1017,7 +1017,7 @@ export function backendToFrontendSerializedElementMapper( }; } -// This is a hacky one to just support this exact case. +// Chrome normalizes urls like webpack-internals:// but new URL don't, so cannot use new URL here. export function normalizeUrl(url: string): string { return url.replace('/./', '/'); } From 8e9de898d3a8a0945fcdc1a02959560bce2bbc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 4 Jul 2024 12:15:35 -0400 Subject: [PATCH 11/85] [Flight] Add option to replay console logs or not (#30207) Defaults to true in browser builds, otherwise defaults to false. The assumption is that the server logs will already contain a log from the original Flight server. We currently always replay console logs but this leads to duplicates on the server by default when you use SSR, because the Flight Client on the server replays the logs. This can be nice since those logs gets badged. It can also be nice if they're running in separate servers but when they're logging to the same stream it's annoying. Which is really the typical set up so we should just make that the default but leave it configurable. --- packages/react-client/src/ReactFlightClient.js | 9 +++++++++ packages/react-html/src/ReactHTMLServer.js | 1 + .../react-noop-renderer/src/ReactNoopFlightClient.js | 11 ++++++++++- .../src/ReactFlightDOMClientBrowser.js | 2 ++ .../src/ReactFlightDOMClientNode.js | 2 ++ .../src/ReactFlightDOMClientBrowser.js | 2 ++ .../src/ReactFlightDOMClientEdge.js | 2 ++ .../src/ReactFlightDOMClientNode.js | 2 ++ .../src/ReactFlightDOMClientBrowser.js | 2 ++ .../src/ReactFlightDOMClientEdge.js | 2 ++ .../src/ReactFlightDOMClientNode.js | 2 ++ 11 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 3cb918d6c8..da835412f1 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -250,6 +250,7 @@ export type Response = { _tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from _debugRootTask?: null | ConsoleTask, // DEV-only _debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only + _replayConsole: boolean, // DEV-only }; function readChunk(chunk: SomeChunk): T { @@ -1278,6 +1279,7 @@ function ResponseInstance( nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, findSourceMapURL: void | FindSourceMapURLCallback, + replayConsole: boolean, ) { const chunks: Map> = new Map(); this._bundlerConfig = bundlerConfig; @@ -1304,6 +1306,7 @@ function ResponseInstance( } if (__DEV__) { this._debugFindSourceMapURL = findSourceMapURL; + this._replayConsole = replayConsole; } // Don't inline this call because it causes closure to outline the call above. this._fromJSON = createFromJSONCallback(this); @@ -1317,6 +1320,7 @@ export function createResponse( nonce: void | string, temporaryReferences: void | TemporaryReferenceSet, findSourceMapURL: void | FindSourceMapURLCallback, + replayConsole: boolean, ): Response { // $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors return new ResponseInstance( @@ -1327,6 +1331,7 @@ export function createResponse( nonce, temporaryReferences, findSourceMapURL, + replayConsole, ); } @@ -2034,6 +2039,10 @@ function resolveConsoleEntry( ); } + if (!response._replayConsole) { + return; + } + const payload: [string, string, null | ReactComponentInfo, string, mixed] = parseModel(response, value); const methodName = payload[0]; diff --git a/packages/react-html/src/ReactHTMLServer.js b/packages/react-html/src/ReactHTMLServer.js index 6bea571338..8f62075124 100644 --- a/packages/react-html/src/ReactHTMLServer.js +++ b/packages/react-html/src/ReactHTMLServer.js @@ -179,6 +179,7 @@ export function renderToMarkup( undefined, undefined, undefined, + false, ); const resumableState = createResumableState( options ? options.identifierPrefix : undefined, diff --git a/packages/react-noop-renderer/src/ReactNoopFlightClient.js b/packages/react-noop-renderer/src/ReactNoopFlightClient.js index afb3eb4760..4065bd4957 100644 --- a/packages/react-noop-renderer/src/ReactNoopFlightClient.js +++ b/packages/react-noop-renderer/src/ReactNoopFlightClient.js @@ -50,7 +50,16 @@ const {createResponse, processBinaryChunk, getRoot, close} = ReactFlightClient({ }); function read(source: Source): Thenable { - const response = createResponse(source, null); + const response = createResponse( + source, + null, + undefined, + undefined, + undefined, + undefined, + undefined, + true, + ); for (let i = 0; i < source.length; i++) { processBinaryChunk(response, source[i], 0); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js index 56d98e6517..e9ad4d4925 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js @@ -42,6 +42,7 @@ export type Options = { callServer?: CallServerCallback, temporaryReferences?: TemporaryReferenceSet, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createResponseFromOptions(options: void | Options) { @@ -57,6 +58,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true ); } diff --git a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js index 7bcc12d94b..0a66776830 100644 --- a/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js @@ -50,6 +50,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createFromNodeStream( @@ -68,6 +69,7 @@ function createFromNodeStream( __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false ); stream.on('data', chunk => { processBinaryChunk(response, chunk); diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js index 1aac84fde6..99bebcb87f 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js @@ -41,6 +41,7 @@ export type Options = { callServer?: CallServerCallback, temporaryReferences?: TemporaryReferenceSet, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createResponseFromOptions(options: void | Options) { @@ -56,6 +57,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true ); } diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js index c6336f7e42..13b1daaa56 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientEdge.js @@ -71,6 +71,7 @@ export type Options = { encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createResponseFromOptions(options: Options) { @@ -86,6 +87,7 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false ); } diff --git a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js index d0fb59c51e..da335e3df4 100644 --- a/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-turbopack/src/ReactFlightDOMClientNode.js @@ -60,6 +60,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createFromNodeStream( @@ -77,6 +78,7 @@ function createFromNodeStream( __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false ); stream.on('data', chunk => { processBinaryChunk(response, chunk); diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js index 1aac84fde6..99bebcb87f 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js @@ -41,6 +41,7 @@ export type Options = { callServer?: CallServerCallback, temporaryReferences?: TemporaryReferenceSet, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createResponseFromOptions(options: void | Options) { @@ -56,6 +57,7 @@ function createResponseFromOptions(options: void | Options) { __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js index c6336f7e42..13b1daaa56 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientEdge.js @@ -71,6 +71,7 @@ export type Options = { encodeFormAction?: EncodeFormActionCallback, temporaryReferences?: TemporaryReferenceSet, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createResponseFromOptions(options: Options) { @@ -86,6 +87,7 @@ function createResponseFromOptions(options: Options) { __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false ); } diff --git a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js index e7beb9586a..38bd585aad 100644 --- a/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js +++ b/packages/react-server-dom-webpack/src/ReactFlightDOMClientNode.js @@ -61,6 +61,7 @@ export type Options = { nonce?: string, encodeFormAction?: EncodeFormActionCallback, findSourceMapURL?: FindSourceMapURLCallback, + replayConsoleLogs?: boolean, }; function createFromNodeStream( @@ -78,6 +79,7 @@ function createFromNodeStream( __DEV__ && options && options.findSourceMapURL ? options.findSourceMapURL : undefined, + __DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false ); stream.on('data', chunk => { if (typeof chunk === 'string') { From 0b5835a46f6e19a0a0ead0f4cd30b0db14bc72f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 4 Jul 2024 12:15:51 -0400 Subject: [PATCH 12/85] [Flight] Implement captureStackTrace and owner stacks on the Server (#30197) Wire up owner stacks in Flight to the shared internals. This exposes it to `captureOwnerStack()`. In this case we install it permanently as we only allow one RSC renderer which then supports async contexts. Same thing we do for owner. This also ends up adding it to errors logged by React through `consoleWithStackDev`. The plan is to eventually remove that but this is inline with what we do in Fizz and Fiber already. However, at the same time we've instrumented the console so we need to strip them back out before sending to the client. This lets the client control whether to add the stack back in or allowing `console.createTask` to control it. This is another reason we shouldn't append them from React but for now we hack it by removing them after the fact. --- .../src/__tests__/ReactFlight-test.js | 78 ++++++++++++++++++- .../src/__tests__/ReactFlightDOMEdge-test.js | 47 +++++++++++ .../react-server/src/ReactFlightServer.js | 45 ++++++++++- .../src/flight/ReactFlightComponentStack.js | 52 +++++++++++++ packages/shared/consoleWithStackDev.js | 4 + 5 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 packages/react-server/src/flight/ReactFlightComponentStack.js diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 2e3857a90d..2293e9e3a7 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -81,6 +81,7 @@ let ErrorBoundary; let NoErrorExpected; let Scheduler; let assertLog; +let assertConsoleErrorDev; describe('ReactFlight', () => { beforeEach(() => { @@ -102,6 +103,7 @@ describe('ReactFlight', () => { Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; ErrorBoundary = class extends React.Component { state = {hasError: false, error: null}; @@ -1441,9 +1443,7 @@ describe('ReactFlight', () => {
{Array(6).fill()}
, ); ReactNoopFlightClient.read(transport); - }).toErrorDev('Each child in a list should have a unique "key" prop.', { - withoutStack: gate(flags => flags.enableOwnerStacks), - }); + }).toErrorDev('Each child in a list should have a unique "key" prop.'); }); it('should warn in DEV a child is missing keys in client component', async () => { @@ -2728,4 +2728,76 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(Hello, Seb); }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks during rendering in dev', () => { + let stack; + + function Foo() { + return ReactServer.createElement(Bar, null); + } + function Bar() { + return ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Baz, null), + ); + } + + function Baz() { + stack = ReactServer.captureOwnerStack(); + return ReactServer.createElement('span', null, 'hi'); + } + ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + ); + + expect(normalizeCodeLocInfo(stack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); + + // @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__ + it('should not include component stacks in replayed logs (unless DevTools add them)', () => { + function Foo() { + return 'hi'; + } + + function Bar() { + const array = []; + // Trigger key warning + array.push(ReactServer.createElement(Foo)); + return ReactServer.createElement('div', null, array); + } + + function App() { + return ReactServer.createElement(Bar); + } + + const transport = ReactNoopFlightServer.render( + ReactServer.createElement(App), + ); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + ' in Bar (at **)\n' + + ' in App (at **)', + ]); + + // Replay logs on the client + ReactNoopFlightClient.read(transport); + assertConsoleErrorDev( + [ + 'Each child in a list should have a unique "key" prop.' + + ' See https://react.dev/link/warning-keys for more information.', + ], + // We should not have a stack in the replay because that should be added either by console.createTask + // or React DevTools on the client. Neither of which we do here. + {withoutStack: true}, + ); + }); }); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 47418276db..4997184078 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -39,6 +39,15 @@ let ReactServerDOMServer; let ReactServerDOMClient; let use; +function normalizeCodeLocInfo(str) { + return ( + str && + str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) { + return ' in ' + name + (/\d/.test(m) ? ' (at **)' : ''); + }) + ); +} + describe('ReactFlightDOMEdge', () => { beforeEach(() => { jest.resetModules(); @@ -883,4 +892,42 @@ describe('ReactFlightDOMEdge', () => { ); } }); + + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks asynchronously', async () => { + let stack; + + function Foo() { + return ReactServer.createElement(Bar, null); + } + function Bar() { + return ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Baz, null), + ); + } + + const promise = Promise.resolve(0); + + async function Baz() { + await promise; + stack = ReactServer.captureOwnerStack(); + return ReactServer.createElement('span', null, 'hi'); + } + + const stream = ReactServerDOMServer.renderToReadableStream( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + webpackMap, + ); + await readResult(stream); + + expect(normalizeCodeLocInfo(stack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index cb0d5ee395..44873a927e 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -97,6 +97,10 @@ import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher'; import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner'; +import {getOwnerStackByComponentInfoInDev} from './flight/ReactFlightComponentStack'; + +import {isWritingAppendedStack} from 'shared/consoleWithStackDev'; + import { getIteratorFn, REACT_ELEMENT_TYPE, @@ -263,8 +267,9 @@ function patchConsole(consoleInst: typeof console, methodName: string) { 'name', ); const wrapperMethod = function (this: typeof console) { + let args = arguments; const request = resolveRequest(); - if (methodName === 'assert' && arguments[0]) { + if (methodName === 'assert' && args[0]) { // assert doesn't emit anything unless first argument is falsy so we can skip it. } else if (request !== null) { // Extract the stack. Not all console logs print the full stack but they have at @@ -276,7 +281,22 @@ function patchConsole(consoleInst: typeof console, methodName: string) { // refer to previous logs in debug info to associate them with a component. const id = request.nextChunkId++; const owner: null | ReactComponentInfo = resolveOwner(); - emitConsoleChunk(request, id, methodName, owner, stack, arguments); + if ( + isWritingAppendedStack && + (methodName === 'error' || methodName === 'warn') && + args.length > 1 && + typeof args[0] === 'string' && + args[0].endsWith('%s') + ) { + // This looks like we've appended the component stack to the error from our own logs. + // We don't want those added to the replayed logs since those have the opportunity to add + // their own stacks or use console.createTask on the client as needed. + // TODO: Remove this special case once we remove consoleWithStackDev. + // $FlowFixMe[method-unbinding] + args = Array.prototype.slice.call(args, 0, args.length - 1); + args[0] = args[0].slice(0, args[0].length - 2); + } + emitConsoleChunk(request, id, methodName, owner, stack, args); } // $FlowFixMe[prop-missing] return originalMethod.apply(this, arguments); @@ -317,6 +337,21 @@ if ( patchConsole(console, 'warn'); } +function getCurrentStackInDEV(): string { + if (__DEV__) { + if (enableOwnerStacks) { + const owner: null | ReactComponentInfo = resolveOwner(); + if (owner === null) { + return ''; + } + return getOwnerStackByComponentInfoInDev(owner); + } + // We don't have Parent Stacks in Flight. + return ''; + } + return ''; +} + const ObjectPrototype = Object.prototype; type JSONValue = @@ -491,6 +526,12 @@ function RequestInstance( ); } ReactSharedInternals.A = DefaultAsyncDispatcher; + if (__DEV__) { + // Unlike Fizz or Fiber, we don't reset this and just keep it on permanently. + // This lets it act more like the AsyncDispatcher so that we can get the + // stack asynchronously too. + ReactSharedInternals.getCurrentStack = getCurrentStackInDEV; + } const abortSet: Set = new Set(); const pingedTasks: Array = []; diff --git a/packages/react-server/src/flight/ReactFlightComponentStack.js b/packages/react-server/src/flight/ReactFlightComponentStack.js new file mode 100644 index 0000000000..0dd41477cd --- /dev/null +++ b/packages/react-server/src/flight/ReactFlightComponentStack.js @@ -0,0 +1,52 @@ +/** + * 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 {ReactComponentInfo} from 'shared/ReactTypes'; + +import {describeBuiltInComponentFrame} from 'shared/ReactComponentStackFrame'; + +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; + +export function getOwnerStackByComponentInfoInDev( + componentInfo: ReactComponentInfo, +): string { + if (!enableOwnerStacks || !__DEV__) { + return ''; + } + try { + let info = ''; + + // The owner stack of the current component will be where it was created, i.e. inside its owner. + // There's no actual name of the currently executing component. Instead, that is available + // on the regular stack that's currently executing. However, if there is no owner at all, then + // there's no stack frame so we add the name of the root component to the stack to know which + // component is currently executing. + if (!componentInfo.owner && typeof componentInfo.name === 'string') { + return describeBuiltInComponentFrame(componentInfo.name); + } + + let owner: void | null | ReactComponentInfo = componentInfo; + + while (owner) { + if (typeof owner.stack === 'string') { + // Server Component + const ownerStack: string = owner.stack; + owner = owner.owner; + if (owner && ownerStack !== '') { + info += '\n' + ownerStack; + } + } else { + break; + } + } + return info; + } catch (x) { + return '\nError generating stack: ' + x.message + '\n' + x.stack; + } +} diff --git a/packages/shared/consoleWithStackDev.js b/packages/shared/consoleWithStackDev.js index 3fa2de383a..462788e490 100644 --- a/packages/shared/consoleWithStackDev.js +++ b/packages/shared/consoleWithStackDev.js @@ -40,6 +40,8 @@ export function error(format, ...args) { // eslint-disable-next-line react-internal/no-production-logging const supportsCreateTask = __DEV__ && enableOwnerStacks && !!console.createTask; +export let isWritingAppendedStack = false; + function printWarning(level, format, args, currentStack) { // When changing this logic, you might want to also // update consoleWithStackDev.www.js as well. @@ -50,6 +52,7 @@ function printWarning(level, format, args, currentStack) { // can be lost while DevTools isn't open but we can't detect this. const stack = ReactSharedInternals.getCurrentStack(currentStack); if (stack !== '') { + isWritingAppendedStack = true; format += '%s'; args = args.concat([stack]); } @@ -60,5 +63,6 @@ function printWarning(level, format, args, currentStack) { // breaks IE9: https://github.com/facebook/react/issues/13610 // eslint-disable-next-line react-internal/no-production-logging Function.prototype.apply.call(console[level], console, args); + isWritingAppendedStack = false; } } From f38c22b244086f62ae5ed851b6ed17029ec44be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 4 Jul 2024 12:31:23 -0400 Subject: [PATCH 13/85] [Flight] Set Current Owner / Task When Calling console.error or invoking onError/onPostpone (#30206) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #30197. This is similar to #30182 and #21610 in Fizz. Track the current owner/stack/task on the task. This tracks it for attribution when serializing child properties. This lets us provide the right owner and createTask when we console.error from inside Flight itself. This also affects the way we print those logs on the client since we need the owner and stack. Now console.errors that originate on the server gets the right stack on the client: Screenshot 2024-07-03 at 6 03 13 PM Unfortunately, because we don't track the stack we never pop it so it'll keep tracking for serializing sibling properties. We rely on "children" typically being the last property in the common case anyway. However, this can lead to wrong attribution in some cases where the invalid property is a next property (without a wrapping element) and there's a previous element that doesn't. E.g. `} invalid={nonSerializable} />` would use the div as the attribution instead of ClientComponent. I also wrap all of our own console.error, onError and onPostpone in the context of the parent component. It's annoying to have to remember to do this though. We could always wrap the whole rendering in such as context but it would add more overhead since this rarely actually happens. It might make sense to track the whole current task instead to lower the overhead. That's what we do in Fizz. We'd still have to remember to restore the debug task though. I realize now Fizz doesn't do that neither so the debug task isn't wrapping the console.errors that Fizz itself logs. There's something off about that Flight and Fizz implementations don't perfectly align. --- .../src/__tests__/ReactFlight-test.js | 63 +++- .../react-server/src/ReactFlightServer.js | 336 ++++++++++++------ 2 files changed, 288 insertions(+), 111 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 2293e9e3a7..11969871a9 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2761,10 +2761,61 @@ describe('ReactFlight', () => { ); }); + // @gate __DEV__ && enableOwnerStacks + it('can get the component owner stacks for onError in dev', async () => { + const thrownError = new Error('hi'); + let caughtError; + let ownerStack; + + function Foo() { + return ReactServer.createElement(Bar, null); + } + function Bar() { + return ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Baz, null), + ); + } + function Baz() { + throw thrownError; + } + + ReactNoopFlightServer.render( + ReactServer.createElement( + 'div', + null, + ReactServer.createElement(Foo, null), + ), + { + onError(error, errorInfo) { + caughtError = error; + ownerStack = ReactServer.captureOwnerStack + ? ReactServer.captureOwnerStack() + : null; + }, + }, + ); + + expect(caughtError).toBe(thrownError); + expect(normalizeCodeLocInfo(ownerStack)).toBe( + '\n in Bar (at **)' + '\n in Foo (at **)', + ); + }); + // @gate (enableOwnerStacks && enableServerComponentLogs) || !__DEV__ it('should not include component stacks in replayed logs (unless DevTools add them)', () => { + class MyError extends Error { + toJSON() { + return 123; + } + } + function Foo() { - return 'hi'; + return ReactServer.createElement('div', null, [ + 'Womp womp: ', + new MyError('spaghetti'), + ]); } function Bar() { @@ -2781,11 +2832,18 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render( ReactServer.createElement(App), ); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + ' See https://react.dev/link/warning-keys for more information.\n' + ' in Bar (at **)\n' + ' in App (at **)', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^\n' + + ' in Foo (at **)\n' + + ' in Bar (at **)\n' + + ' in App (at **)', ]); // Replay logs on the client @@ -2794,6 +2852,9 @@ describe('ReactFlight', () => { [ 'Each child in a list should have a unique "key" prop.' + ' See https://react.dev/link/warning-keys for more information.', + 'Error objects cannot be rendered as text children. Try formatting it using toString().\n' + + '
Womp womp: {Error}
\n' + + ' ^^^^^^^', ], // We should not have a stack in the replay because that should be added either by console.createTask // or React DevTools on the client. Neither of which we do here. diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 44873a927e..30a8ece0ad 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -426,6 +426,9 @@ type Task = { implicitSlot: boolean, // true if the root server component of this sequence had a null key thenableState: ThenableState | null, environmentName: string, // DEV-only. Used to track if the environment for this task changed. + debugOwner: null | ReactComponentInfo, // DEV-only + debugStack: null | string, // DEV-only + debugTask: null | ConsoleTask, // DEV-only }; interface Reference {} @@ -577,7 +580,16 @@ function RequestInstance( : environmentName; this.didWarnForKey = null; } - const rootTask = createTask(this, model, null, false, abortSet); + const rootTask = createTask( + this, + model, + null, + false, + abortSet, + null, + null, + null, + ); pingedTasks.push(rootTask); } @@ -624,6 +636,9 @@ function serializeThenable( task.keyPath, // the server component sequence continues through Promise-as-a-child. task.implicitSlot, request.abortableTasks, + __DEV__ && enableOwnerStacks ? task.debugOwner : null, + __DEV__ && enableOwnerStacks ? task.debugStack : null, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); if (__DEV__) { // If this came from Flight, forward any debug info into this new row. @@ -649,10 +664,10 @@ function serializeThenable( (x: any).$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (x: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, newTask); emitPostponeChunk(request, newTask.id, postponeInstance); } else { - const digest = logRecoverableError(request, x); + const digest = logRecoverableError(request, x, null); emitErrorChunk(request, newTask.id, digest, x); } return newTask.id; @@ -708,11 +723,11 @@ function serializeThenable( (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (reason: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, newTask); emitPostponeChunk(request, newTask.id, postponeInstance); } else { newTask.status = ERRORED; - const digest = logRecoverableError(request, reason); + const digest = logRecoverableError(request, reason, newTask); emitErrorChunk(request, newTask.id, digest, reason); } request.abortableTasks.delete(newTask); @@ -753,6 +768,9 @@ function serializeReadableStream( task.keyPath, task.implicitSlot, request.abortableTasks, + __DEV__ && enableOwnerStacks ? task.debugOwner : null, + __DEV__ && enableOwnerStacks ? task.debugStack : null, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); request.abortableTasks.delete(streamTask); @@ -801,10 +819,10 @@ function serializeReadableStream( (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (reason: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, streamTask); emitPostponeChunk(request, streamTask.id, postponeInstance); } else { - const digest = logRecoverableError(request, reason); + const digest = logRecoverableError(request, reason, streamTask); emitErrorChunk(request, streamTask.id, digest, reason); } enqueueFlush(request); @@ -849,6 +867,9 @@ function serializeAsyncIterable( task.keyPath, task.implicitSlot, request.abortableTasks, + __DEV__ && enableOwnerStacks ? task.debugOwner : null, + __DEV__ && enableOwnerStacks ? task.debugStack : null, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); request.abortableTasks.delete(streamTask); @@ -930,10 +951,10 @@ function serializeAsyncIterable( (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (reason: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, streamTask); emitPostponeChunk(request, streamTask.id, postponeInstance); } else { - const digest = logRecoverableError(request, reason); + const digest = logRecoverableError(request, reason, streamTask); emitErrorChunk(request, streamTask.id, digest, reason); } enqueueFlush(request); @@ -1077,15 +1098,43 @@ function callLazyInitInDEV(lazy: LazyComponent): any { return init(payload); } +function callWithDebugContextInDEV( + task: Task, + callback: A => T, + arg: A, +): T { + // We don't have a Server Component instance associated with this callback and + // the nearest context is likely a Client Component being serialized. We create + // a fake owner during this callback so we can get the stack trace from it. + // This also gets sent to the client as the owner for the replaying log. + const componentDebugInfo: ReactComponentInfo = { + env: task.environmentName, + owner: task.debugOwner, + }; + if (enableOwnerStacks) { + // $FlowFixMe[cannot-write] + componentDebugInfo.stack = task.debugStack; + } + const debugTask = task.debugTask; + // We don't need the async component storage context here so we only set the + // synchronous tracking of owner. + setCurrentOwner(componentDebugInfo); + try { + if (enableOwnerStacks && debugTask) { + return debugTask.run(callback.bind(null, arg)); + } + return callback(arg); + } finally { + setCurrentOwner(null); + } +} + function renderFunctionComponent( request: Request, task: Task, key: null | string, Component: (p: Props, arg: void) => any, props: Props, - owner: null | ReactComponentInfo, // DEV-only - stack: null | string, // DEV-only - debugTask: null | ConsoleTask, // DEV-only validated: number, // DEV-only ): ReactJSONValue { // Reset the task's thenable state before continuing, so that if a later @@ -1117,11 +1166,11 @@ function renderFunctionComponent( componentDebugInfo = ({ name: componentName, env: componentEnv, - owner: owner, + owner: task.debugOwner, }: ReactComponentInfo); if (enableOwnerStacks) { // $FlowFixMe[cannot-write] - componentDebugInfo.stack = stack; + componentDebugInfo.stack = task.debugStack; } // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up @@ -1138,7 +1187,7 @@ function renderFunctionComponent( key, validated, componentDebugInfo, - debugTask, + task.debugTask, ); } } @@ -1147,7 +1196,7 @@ function renderFunctionComponent( Component, props, componentDebugInfo, - debugTask, + task.debugTask, ); } else { prepareToUseHooksForComponent(prevThenableState, null); @@ -1222,10 +1271,12 @@ function renderFunctionComponent( Object.prototype.toString.call(iterableChild) === '[object Generator]'; if (!isGeneratorComponent) { - console.error( - 'Returning an Iterator from a Server Component is not supported ' + - 'since it cannot be looped over more than once. ', - ); + callWithDebugContextInDEV(task, () => { + console.error( + 'Returning an Iterator from a Server Component is not supported ' + + 'since it cannot be looped over more than once. ', + ); + }); } } } @@ -1259,10 +1310,12 @@ function renderFunctionComponent( Object.prototype.toString.call(iterableChild) === '[object AsyncGenerator]'; if (!isGeneratorComponent) { - console.error( - 'Returning an AsyncIterator from a Server Component is not supported ' + - 'since it cannot be looped over more than once. ', - ); + callWithDebugContextInDEV(task, () => { + console.error( + 'Returning an AsyncIterator from a Server Component is not supported ' + + 'since it cannot be looped over more than once. ', + ); + }); } } } @@ -1489,8 +1542,6 @@ function renderClientElement( type: any, key: null | string, props: any, - owner: null | ReactComponentInfo, // DEV-only - stack: null | string, // DEV-only validated: number, // DEV-only ): ReactJSONValue { // We prepend the terminal client element that actually gets serialized with @@ -1503,8 +1554,16 @@ function renderClientElement( } const element = __DEV__ ? enableOwnerStacks - ? [REACT_ELEMENT_TYPE, type, key, props, owner, stack, validated] - : [REACT_ELEMENT_TYPE, type, key, props, owner] + ? [ + REACT_ELEMENT_TYPE, + type, + key, + props, + task.debugOwner, + task.debugStack, + validated, + ] + : [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner] : [REACT_ELEMENT_TYPE, type, key, props]; if (task.implicitSlot && key !== null) { // The root Server Component had no key so it was in an implicit slot. @@ -1531,6 +1590,9 @@ function outlineTask(request: Request, task: Task): ReactJSONValue { task.keyPath, // unlike outlineModel this one carries along context task.implicitSlot, request.abortableTasks, + __DEV__ && enableOwnerStacks ? task.debugOwner : null, + __DEV__ && enableOwnerStacks ? task.debugStack : null, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); retryTask(request, newTask); @@ -1551,9 +1613,6 @@ function renderElement( key: null | string, ref: mixed, props: any, - owner: null | ReactComponentInfo, // DEV only - stack: null | string, // DEV only - debugTask: null | ConsoleTask, // DEV only validated: number, // DEV only ): ReactJSONValue { if (ref !== null && ref !== undefined) { @@ -1578,17 +1637,7 @@ function renderElement( !isOpaqueTemporaryReference(type) ) { // This is a Server Component. - return renderFunctionComponent( - request, - task, - key, - type, - props, - owner, - stack, - debugTask, - validated, - ); + return renderFunctionComponent(request, task, key, type, props, validated); } else if (type === REACT_FRAGMENT_TYPE && key === null) { // For key-less fragments, we add a small optimization to avoid serializing // it as a wrapper. @@ -1633,9 +1682,6 @@ function renderElement( key, ref, props, - owner, - stack, - debugTask, validated, ); } @@ -1646,9 +1692,6 @@ function renderElement( key, type.render, props, - owner, - stack, - debugTask, validated, ); } @@ -1660,9 +1703,6 @@ function renderElement( key, ref, props, - owner, - stack, - debugTask, validated, ); } @@ -1680,7 +1720,7 @@ function renderElement( // We don't know if the client will support it or not. This might error on the // client or error during serialization but the stack will point back to the // server. - return renderClientElement(task, type, key, props, owner, stack, validated); + return renderClientElement(task, type, key, props, validated); } function pingTask(request: Request, task: Task): void { @@ -1698,6 +1738,9 @@ function createTask( keyPath: null | string, implicitSlot: boolean, abortSet: Set, + debugOwner: null | ReactComponentInfo, // DEV-only + debugStack: null | string, // DEV-only + debugTask: null | ConsoleTask, // DEV-only ): Task { request.pendingChunks++; const id = request.nextChunkId++; @@ -1735,38 +1778,50 @@ function createTask( originalValue !== value && !(originalValue instanceof Date) ) { - if (objectName(originalValue) !== 'Object') { - const jsxParentType = jsxChildrenParents.get(parent); - if (typeof jsxParentType === 'string') { - console.error( - '%s objects cannot be rendered as text children. Try formatting it using toString().%s', - objectName(originalValue), - describeObjectForErrorMessage(parent, parentPropertyName), - ); + // Call with the server component as the currently rendering component + // for context. + callWithDebugContextInDEV(task, () => { + if (objectName(originalValue) !== 'Object') { + const jsxParentType = jsxChildrenParents.get(parent); + if (typeof jsxParentType === 'string') { + console.error( + '%s objects cannot be rendered as text children. Try formatting it using toString().%s', + objectName(originalValue), + describeObjectForErrorMessage(parent, parentPropertyName), + ); + } else { + console.error( + 'Only plain objects can be passed to Client Components from Server Components. ' + + '%s objects are not supported.%s', + objectName(originalValue), + describeObjectForErrorMessage(parent, parentPropertyName), + ); + } } else { console.error( 'Only plain objects can be passed to Client Components from Server Components. ' + - '%s objects are not supported.%s', - objectName(originalValue), + 'Objects with toJSON methods are not supported. Convert it manually ' + + 'to a simple value before passing it to props.%s', describeObjectForErrorMessage(parent, parentPropertyName), ); } - } else { - console.error( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with toJSON methods are not supported. Convert it manually ' + - 'to a simple value before passing it to props.%s', - describeObjectForErrorMessage(parent, parentPropertyName), - ); - } + }); } } return renderModel(request, task, parent, parentPropertyName, value); }, thenableState: null, - }: Omit): any); + }: Omit< + Task, + 'environmentName' | 'debugOwner' | 'debugStack' | 'debugTask', + >): any); if (__DEV__) { task.environmentName = request.environmentName(); + if (enableOwnerStacks) { + task.debugOwner = debugOwner; + task.debugStack = debugStack; + task.debugTask = debugTask; + } } abortSet.add(task); return task; @@ -1884,7 +1939,7 @@ function serializeClientReference( } catch (x) { request.pendingChunks++; const errorId = request.nextChunkId++; - const digest = logRecoverableError(request, x); + const digest = logRecoverableError(request, x, null); emitErrorChunk(request, errorId, digest, x); return serializeByValueID(errorId); } @@ -1897,6 +1952,9 @@ function outlineModel(request: Request, value: ReactClientValue): number { null, // The way we use outlining is for reusing an object. false, // It makes no sense for that use case to be contextual. request.abortableTasks, + null, // TODO: Currently we don't associate any debug information with + null, // this object on the server. If it ends up erroring, it won't + null, // have any context on the server but can on the client. ); retryTask(request, newTask); return newTask.id; @@ -1990,6 +2048,9 @@ function serializeBlob(request: Request, blob: Blob): string { null, false, request.abortableTasks, + null, // TODO: Currently we don't associate any debug information with + null, // this object on the server. If it ends up erroring, it won't + null, // have any context on the server but can on the client. ); const reader = blob.stream().getReader(); @@ -2019,7 +2080,7 @@ function serializeBlob(request: Request, blob: Blob): string { } aborted = true; request.abortListeners.delete(error); - const digest = logRecoverableError(request, reason); + const digest = logRecoverableError(request, reason, newTask); emitErrorChunk(request, newTask.id, digest, reason); request.abortableTasks.delete(newTask); enqueueFlush(request); @@ -2098,6 +2159,9 @@ function renderModel( task.keyPath, task.implicitSlot, request.abortableTasks, + __DEV__ && enableOwnerStacks ? task.debugOwner : null, + __DEV__ && enableOwnerStacks ? task.debugStack : null, + __DEV__ && enableOwnerStacks ? task.debugTask : null, ); const ping = newTask.ping; (x: any).then(ping, ping); @@ -2118,7 +2182,7 @@ function renderModel( const postponeInstance: Postpone = (x: any); request.pendingChunks++; const postponeId = request.nextChunkId++; - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, task); emitPostponeChunk(request, postponeId, postponeInstance); // Restore the context. We assume that this will be restored by the inner @@ -2150,7 +2214,7 @@ function renderModel( // Something errored. We'll still send everything we have up until this point. request.pendingChunks++; const errorId = request.nextChunkId++; - const digest = logRecoverableError(request, x); + const digest = logRecoverableError(request, x, task); emitErrorChunk(request, errorId, digest, x); if (wasReactNode) { // We'll replace this element with a lazy reference that throws on the client @@ -2252,6 +2316,23 @@ function renderModelDestructive( } // Attempt to render the Server Component. + + if (__DEV__) { + task.debugOwner = element._owner; + if (enableOwnerStacks) { + task.debugStack = + !element._debugStack || typeof element._debugStack === 'string' + ? element._debugStack + : filterDebugStack(element._debugStack); + task.debugTask = element._debugTask; + } + // TODO: Pop this. Since we currently don't have a point where we can pop the stack + // this debug information will be used for errors inside sibling properties that + // are not elements. Leading to the wrong attribution on the server. We could fix + // that if we switch to a proper stack instead of JSON.stringify's trampoline. + // Attribution on the client is still correct since it has a pop. + } + const newChild = renderElement( request, task, @@ -2260,13 +2341,6 @@ function renderModelDestructive( element.key, ref, props, - __DEV__ ? element._owner : null, - __DEV__ && enableOwnerStacks - ? !element._debugStack || typeof element._debugStack === 'string' - ? element._debugStack - : filterDebugStack(element._debugStack) - : null, - __DEV__ && enableOwnerStacks ? element._debugTask : null, __DEV__ && enableOwnerStacks ? element._store.validated : 0, ); if ( @@ -2573,27 +2647,33 @@ function renderModelDestructive( } if (objectName(value) !== 'Object') { - console.error( - 'Only plain objects can be passed to Client Components from Server Components. ' + - '%s objects are not supported.%s', - objectName(value), - describeObjectForErrorMessage(parent, parentPropertyName), - ); + callWithDebugContextInDEV(task, () => { + console.error( + 'Only plain objects can be passed to Client Components from Server Components. ' + + '%s objects are not supported.%s', + objectName(value), + describeObjectForErrorMessage(parent, parentPropertyName), + ); + }); } else if (!isSimpleObject(value)) { - console.error( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Classes or other objects with methods are not supported.%s', - describeObjectForErrorMessage(parent, parentPropertyName), - ); + callWithDebugContextInDEV(task, () => { + console.error( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Classes or other objects with methods are not supported.%s', + describeObjectForErrorMessage(parent, parentPropertyName), + ); + }); } else if (Object.getOwnPropertySymbols) { const symbols = Object.getOwnPropertySymbols(value); if (symbols.length > 0) { - console.error( - 'Only plain objects can be passed to Client Components from Server Components. ' + - 'Objects with symbol properties like %s are not supported.%s', - symbols[0].description, - describeObjectForErrorMessage(parent, parentPropertyName), - ); + callWithDebugContextInDEV(task, () => { + console.error( + 'Only plain objects can be passed to Client Components from Server Components. ' + + 'Objects with symbol properties like %s are not supported.%s', + symbols[0].description, + describeObjectForErrorMessage(parent, parentPropertyName), + ); + }); } } } @@ -2749,12 +2829,30 @@ function renderModelDestructive( ); } -function logPostpone(request: Request, reason: string): void { +function logPostpone( + request: Request, + reason: string, + task: Task | null, // DEV-only +): void { const prevRequest = currentRequest; + // We clear the request context so that console.logs inside the callback doesn't + // get forwarded to the client. currentRequest = null; try { const onPostpone = request.onPostpone; - if (supportsRequestStorage) { + if (__DEV__ && task !== null) { + if (supportsRequestStorage) { + requestStorage.run( + undefined, + callWithDebugContextInDEV, + task, + onPostpone, + reason, + ); + } else { + callWithDebugContextInDEV(task, onPostpone, reason); + } + } else if (supportsRequestStorage) { // Exit the request context while running callbacks. requestStorage.run(undefined, onPostpone, reason); } else { @@ -2765,13 +2863,31 @@ function logPostpone(request: Request, reason: string): void { } } -function logRecoverableError(request: Request, error: mixed): string { +function logRecoverableError( + request: Request, + error: mixed, + task: Task | null, // DEV-only +): string { const prevRequest = currentRequest; + // We clear the request context so that console.logs inside the callback doesn't + // get forwarded to the client. currentRequest = null; let errorDigest; try { const onError = request.onError; - if (supportsRequestStorage) { + if (__DEV__ && task !== null) { + if (supportsRequestStorage) { + errorDigest = requestStorage.run( + undefined, + callWithDebugContextInDEV, + task, + onError, + error, + ); + } else { + errorDigest = callWithDebugContextInDEV(task, onError, error); + } + } else if (supportsRequestStorage) { // Exit the request context while running callbacks. errorDigest = requestStorage.run(undefined, onError, error); } else { @@ -3567,7 +3683,7 @@ function retryTask(request: Request, task: Task): void { request.abortableTasks.delete(task); task.status = ERRORED; const postponeInstance: Postpone = (x: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, task); emitPostponeChunk(request, task.id, postponeInstance); return; } @@ -3584,7 +3700,7 @@ function retryTask(request: Request, task: Task): void { request.abortableTasks.delete(task); task.status = ERRORED; - const digest = logRecoverableError(request, x); + const digest = logRecoverableError(request, x, task); emitErrorChunk(request, task.id, digest, x); } finally { if (__DEV__) { @@ -3629,7 +3745,7 @@ function performWork(request: Request): void { flushCompletedChunks(request, request.destination); } } catch (error) { - logRecoverableError(request, error); + logRecoverableError(request, error, null); fatalError(request, error); } finally { ReactSharedInternals.H = prevDispatcher; @@ -3780,7 +3896,7 @@ export function startFlowing(request: Request, destination: Destination): void { try { flushCompletedChunks(request, destination); } catch (error) { - logRecoverableError(request, error); + logRecoverableError(request, error, null); fatalError(request, error); } } @@ -3807,7 +3923,7 @@ export function abort(request: Request, reason: mixed): void { (reason: any).$$typeof === REACT_POSTPONE_TYPE ) { const postponeInstance: Postpone = (reason: any); - logPostpone(request, postponeInstance.message); + logPostpone(request, postponeInstance.message, null); emitPostponeChunk(request, errorId, postponeInstance); } else { const error = @@ -3820,7 +3936,7 @@ export function abort(request: Request, reason: mixed): void { typeof reason.then === 'function' ? new Error('The render was aborted by the server with a promise.') : reason; - const digest = logRecoverableError(request, error); + const digest = logRecoverableError(request, error, null); emitErrorChunk(request, errorId, digest, error); } abortableTasks.forEach(task => abortTask(task, request, errorId)); @@ -3858,7 +3974,7 @@ export function abort(request: Request, reason: mixed): void { flushCompletedChunks(request, request.destination); } } catch (error) { - logRecoverableError(request, error); + logRecoverableError(request, error, null); fatalError(request, error); } } From 1b0132c05acabae5aebd32c2cadddfb16bda70bc Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Sat, 6 Jul 2024 08:52:20 +0200 Subject: [PATCH 14/85] Consider dispatch from `useActionState` stable (#29665) --- .../ESLintRuleExhaustiveDeps-test.js | 51 ++++++++++++++++++- .../src/ExhaustiveDeps.js | 8 ++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 235d60349b..c9ba00f213 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -607,6 +607,8 @@ const tests = { const [state4, dispatch2] = React.useReducer(); const [state5, maybeSetState] = useFunnyState(); const [state6, maybeDispatch] = useFunnyReducer(); + const [state9, dispatch5] = useActionState(); + const [state10, dispatch6] = React.useActionState(); const [isPending1] = useTransition(); const [isPending2, startTransition2] = useTransition(); const [isPending3] = React.useTransition(); @@ -624,6 +626,8 @@ const tests = { setState2(); dispatch1(); dispatch2(); + dispatch5(); + dispatch6(); startTransition1(); startTransition2(); startTransition3(); @@ -646,7 +650,7 @@ const tests = { maybeDispatch(); }, [ // Dynamic - state1, state2, state3, state4, state5, state6, + state1, state2, state3, state4, state5, state6, state9, state10, maybeRef1, maybeRef2, isPending2, isPending4, @@ -1494,6 +1498,51 @@ const tests = { }, ], }, + { + // Affected code should use React.useActionState instead + code: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [state7, state8]); + } + `, + errors: [ + { + message: + "React Hook useEffect has missing dependencies: 'dispatch3' and 'dispatch4'. " + + 'Either include them or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [dispatch3, dispatch4, state7, state8]', + output: normalizeIndent` + function ComponentUsingFormState(props) { + const [state7, dispatch3] = useFormState(); + const [state8, dispatch4] = ReactDOM.useFormState(); + useEffect(() => { + dispatch3(); + dispatch4(); + + // dynamic + console.log(state7); + console.log(state8); + + }, [dispatch3, dispatch4, state7, state8]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent(props) { diff --git a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js index f012428961..48ccc1e6bb 100644 --- a/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js +++ b/packages/eslint-plugin-react-hooks/src/ExhaustiveDeps.js @@ -179,6 +179,8 @@ export default { // ^^^ true for this reference // const [state, dispatch] = useReducer() / React.useReducer() // ^^^ true for this reference + // const [state, dispatch] = useActionState() / React.useActionState() + // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // const onStuff = useEffectEvent(() => {}) @@ -260,7 +262,11 @@ export default { } // useEffectEvent() return value is always unstable. return true; - } else if (name === 'useState' || name === 'useReducer') { + } else if ( + name === 'useState' || + name === 'useReducer' || + name === 'useActionState' + ) { // Only consider second value in initializing tuple stable. if ( id.type === 'ArrayPattern' && From df783f9ea1b6f95e05f830602da1de5ffb325d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 8 Jul 2024 11:54:14 -0400 Subject: [PATCH 15/85] Add unknown location information to component stacks (#30290) This is the same change as in #30289 but for the main runtime - e.g. parent stacks in errorInfo.componentStack, appended stacks to console.error coming from React itself and when we add virtual frames to owner stacks. Since we don't add location information these frames look weird to some stack parsers - such as the native one. This is an existing issue when you want to use some off-the-shelf parsers to parse production component stacks for example. While we won't add Error objects to logs ourselves necessarily, some third party could want to do the same thing we do in DevTools and so we should provide the same capability to just take this trace and print it using an Error object. --- .../__tests__/ReactDOMSingletonComponents-test.js | 2 +- packages/react-server/src/ReactFizzServer.js | 2 +- packages/shared/ReactComponentStackFrame.js | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index b1d0a9cb23..ff6e6968ca 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -475,7 +475,7 @@ describe('ReactDOM HostSingleton', () => { expect(hydrationErrors).toEqual([ [ "Hydration failed because the server rendered HTML didn't match the client.", - 'at div', + 'at div ()', ], ]); expect(persistentElements).toEqual([ diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index e1e180f2f7..cf44e055ca 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -924,7 +924,7 @@ function pushServerComponentStack( let name = componentInfo.name; const env = componentInfo.env; if (env) { - name += ' (' + env + ')'; + name += ' [' + env + ']'; } task.componentStack = { tag: 3, diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index 34c9487a30..25f98bd2f4 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -24,6 +24,7 @@ import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev'; import ReactSharedInternals from 'shared/ReactSharedInternals'; let prefix; +let suffix; export function describeBuiltInComponentFrame(name: string): string { if (enableComponentStackLocations) { if (prefix === undefined) { @@ -33,17 +34,26 @@ export function describeBuiltInComponentFrame(name: string): string { } catch (x) { const match = x.stack.trim().match(/\n( *(at )?)/); prefix = (match && match[1]) || ''; + suffix = + x.stack.indexOf('\n at') > -1 + ? // V8 + ' ()' + : // JSC/Spidermonkey + x.stack.indexOf('@') > -1 + ? '@unknown:0:0' + : // Other + ''; } } // We use the prefix to ensure our stacks line up with native stack frames. - return '\n' + prefix + name; + return '\n' + prefix + name + suffix; } else { return describeComponentFrame(name); } } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); + return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : '')); } let reentry = false; From b0f51f7e5ea0ced8ee6c7f8234b531e29c3985ba Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 8 Jul 2024 13:48:24 -0400 Subject: [PATCH 16/85] Upgrade flow to 0.233.0 (#30116) See [Flow changelog](https://github.com/facebook/flow/blob/main/Changelog.md) for changes in this version. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/30116). * #30118 * #30117 * __->__ #30116 --- package.json | 8 ++-- scripts/flow/react-native-host-hooks.js | 2 +- yarn.lock | 49 +++++++++++++------------ 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 663538b506..72c8dfdf7b 100644 --- a/package.json +++ b/package.json @@ -63,14 +63,14 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.232.0", - "flow-remove-types": "^2.232.0", + "flow-bin": "^0.233.0", + "flow-remove-types": "^2.233.0", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", "gzip-size": "^5.1.1", - "hermes-eslint": "^0.20.1", - "hermes-parser": "^0.20.1", + "hermes-eslint": "^0.22.0", + "hermes-parser": "^0.22.0", "jest": "^29.4.2", "jest-cli": "^29.4.2", "jest-diff": "^29.4.2", diff --git a/scripts/flow/react-native-host-hooks.js b/scripts/flow/react-native-host-hooks.js index 4012f78c47..64f77e6dbd 100644 --- a/scripts/flow/react-native-host-hooks.js +++ b/scripts/flow/react-native-host-hooks.js @@ -97,7 +97,7 @@ declare module 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface' setChildren: (containerTag: number, reactTags: Array) => void, updateView: (reactTag: number, viewName: string, props: ?Object) => void, __takeSnapshot: ( - view?: 'window' | Element | number, + view?: 'window' | Element | number, options?: { width?: number, height?: number, diff --git a/yarn.lock b/yarn.lock index b4a5640f3c..3d545a8d65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7202,6 +7202,7 @@ eslint-plugin-no-unsanitized@3.1.2: "eslint-plugin-react-internal@link:./scripts/eslint-rules": version "0.0.0" + uid "" eslint-plugin-react@^6.7.1: version "6.10.3" @@ -8320,17 +8321,17 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -flow-bin@^0.232.0: - version "0.232.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.232.0.tgz#80587406cbb3a74577151ad27c6058b2a468c215" - integrity sha512-7uOycTN+Ys2nYRJRig5S2yN41ZokW7bC4K1GC4nCDa/3FAZLP5/mQbee6UjxFBP9MC4yUYi17bdFTFzCH8bHeg== +flow-bin@^0.233.0: + version "0.233.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.233.0.tgz#e31951c81d3ec590e1cbfd96e540f6dd2459554c" + integrity sha512-BInTgW8v6xdWzVcItgKKUYCacheMw78Xrrn0Ziii5lN+vf/RKmvVX9mFHuOSN1zawZuq7GpqmT6oIS/oQuOAQg== -flow-remove-types@^2.232.0: - version "2.232.0" - resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.232.0.tgz#a4333fee2524b57220791a130955f48e0b107f1a" - integrity sha512-t7B6axPRmSGeWU1m2j+LOBocw9ORXcomoykfMoZedd8snGum3QQpzro3eQpDoutnqKRtJliAvX6PJtFCNEjpMQ== +flow-remove-types@^2.233.0: + version "2.238.2" + resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.238.2.tgz#85c9d26e83ba395f0206a23bce438223bc035609" + integrity sha512-WJXRomjPiZ34nG14y7AceoPxg1L00FxjPSA3TDBTG2OPt8QFNtiYEmO4/3WG58n3C4wjxyVuoE6KjxQIvCDyjw== dependencies: - hermes-parser "0.20.1" + hermes-parser "0.22.0" pirates "^3.0.2" vlq "^0.2.1" @@ -9116,26 +9117,26 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -hermes-eslint@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.20.1.tgz#4a731b47a6d169bbd4514aaa74bd812fd90f3554" - integrity sha512-EhdvFV6RkPIJvbqN8oqFZO1oF4NlPWMjhMjCWkUJX1YL1MZMfkF7nSdx6RKTq6xK17yo+Bgv88L21xuH9GtRpw== +hermes-eslint@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/hermes-eslint/-/hermes-eslint-0.22.0.tgz#b4b9a58a546f9b2f33536a977bcea3f026057f67" + integrity sha512-WnD0xPY1Clvd4F68g2esS89C0NGeu/pn3sdqGXXdnlgr3jZtG5lugscRATS+0+mXOtZ6PTxSClVr2JL4BNor2Q== dependencies: esrecurse "^4.3.0" - hermes-estree "0.20.1" - hermes-parser "0.20.1" + hermes-estree "0.22.0" + hermes-parser "0.22.0" -hermes-estree@0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.20.1.tgz#0b9a544cf883a779a8e1444b915fa365bef7f72d" - integrity sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg== +hermes-estree@0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.22.0.tgz#38559502b119f728901d2cfe2ef422f277802a1d" + integrity sha512-FLBt5X9OfA8BERUdc6aZS36Xz3rRuB0Y/mfocSADWEJfomc1xfene33GdyAmtTkKTBXTN/EgAy+rjTKkkZJHlw== -hermes-parser@0.20.1, hermes-parser@^0.20.1: - version "0.20.1" - resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.20.1.tgz#ad10597b99f718b91e283f81cbe636c50c3cff92" - integrity sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA== +hermes-parser@0.22.0, hermes-parser@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.22.0.tgz#fc8e0e6c7bfa8db85b04c9f9544a102c4fcb4040" + integrity sha512-gn5RfZiEXCsIWsFGsKiykekktUoh0PdFWYocXsUdZIyWSckT6UIyPcyyUIPSR3kpnELWeK3n3ztAse7Mat6PSA== dependencies: - hermes-estree "0.20.1" + hermes-estree "0.22.0" homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3" From 094041495bc1247ff5d39906abb571e6996436be Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 8 Jul 2024 14:00:00 -0400 Subject: [PATCH 17/85] Upgrade flow to 0.234.0 (#30117) See [Flow changelog](https://github.com/facebook/flow/blob/main/Changelog.md) for changes in this version. --- package.json | 4 ++-- packages/react-reconciler/src/ReactFiberThenable.js | 2 +- packages/react-server/src/ReactFizzThenable.js | 2 +- packages/react-server/src/ReactFlightThenable.js | 2 +- packages/react/src/ReactChildren.js | 2 +- packages/react/src/ReactLazy.js | 10 ++++++++-- yarn.lock | 10 +++++----- 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 72c8dfdf7b..d5dfde958c 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.233.0", - "flow-remove-types": "^2.233.0", + "flow-bin": "^0.234.0", + "flow-remove-types": "^2.234.0", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", diff --git a/packages/react-reconciler/src/ReactFiberThenable.js b/packages/react-reconciler/src/ReactFiberThenable.js index 8c302e99a3..2f0ac5637a 100644 --- a/packages/react-reconciler/src/ReactFiberThenable.js +++ b/packages/react-reconciler/src/ReactFiberThenable.js @@ -214,7 +214,7 @@ export function trackUsedThenable( } // Check one more time in case the thenable resolved synchronously. - switch (thenable.status) { + switch ((thenable: Thenable).status) { case 'fulfilled': { const fulfilledThenable: FulfilledThenable = (thenable: any); return fulfilledThenable.value; diff --git a/packages/react-server/src/ReactFizzThenable.js b/packages/react-server/src/ReactFizzThenable.js index 1494b4188e..60117a6f52 100644 --- a/packages/react-server/src/ReactFizzThenable.js +++ b/packages/react-server/src/ReactFizzThenable.js @@ -107,7 +107,7 @@ export function trackUsedThenable( } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { + switch ((thenable: Thenable).status) { case 'fulfilled': { const fulfilledThenable: FulfilledThenable = (thenable: any); return fulfilledThenable.value; diff --git a/packages/react-server/src/ReactFlightThenable.js b/packages/react-server/src/ReactFlightThenable.js index cfda818f19..d4b1f27395 100644 --- a/packages/react-server/src/ReactFlightThenable.js +++ b/packages/react-server/src/ReactFlightThenable.js @@ -107,7 +107,7 @@ export function trackUsedThenable( } // Check one more time in case the thenable resolved synchronously - switch (thenable.status) { + switch ((thenable: Thenable).status) { case 'fulfilled': { const fulfilledThenable: FulfilledThenable = (thenable: any); return fulfilledThenable.value; diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 7296a452c0..0e4c699f47 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -127,7 +127,7 @@ function resolveThenable(thenable: Thenable): T { } // Check one more time in case the thenable resolved synchronously. - switch (thenable.status) { + switch ((thenable: Thenable).status) { case 'fulfilled': { const fulfilledThenable: FulfilledThenable = (thenable: any); return fulfilledThenable.value; diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index e1b222ad8b..d1d1088aae 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -61,7 +61,10 @@ function lazyInitializer(payload: Payload): T { // end up fixing it if the resolution was a concurrency bug. thenable.then( moduleObject => { - if (payload._status === Pending || payload._status === Uninitialized) { + if ( + (payload: Payload)._status === Pending || + payload._status === Uninitialized + ) { // Transition to the next state. const resolved: ResolvedPayload = (payload: any); resolved._status = Resolved; @@ -69,7 +72,10 @@ function lazyInitializer(payload: Payload): T { } }, error => { - if (payload._status === Pending || payload._status === Uninitialized) { + if ( + (payload: Payload)._status === Pending || + payload._status === Uninitialized + ) { // Transition to the next state. const rejected: RejectedPayload = (payload: any); rejected._status = Rejected; diff --git a/yarn.lock b/yarn.lock index 3d545a8d65..54fd49b8ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8321,12 +8321,12 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -flow-bin@^0.233.0: - version "0.233.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.233.0.tgz#e31951c81d3ec590e1cbfd96e540f6dd2459554c" - integrity sha512-BInTgW8v6xdWzVcItgKKUYCacheMw78Xrrn0Ziii5lN+vf/RKmvVX9mFHuOSN1zawZuq7GpqmT6oIS/oQuOAQg== +flow-bin@^0.234.0: + version "0.234.0" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.234.0.tgz#17dfc5aac1d928b6d7194f93bd0bf742d735c77d" + integrity sha512-uLmvfFRW6yEcz2wSJ2H6192RwknBpzAHBezDcXzmxJASxB6QzjKadhPxZvsJ74uJ+9Th1hDNuRB4mGrVUeneyA== -flow-remove-types@^2.233.0: +flow-remove-types@^2.234.0: version "2.238.2" resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.238.2.tgz#85c9d26e83ba395f0206a23bce438223bc035609" integrity sha512-WJXRomjPiZ34nG14y7AceoPxg1L00FxjPSA3TDBTG2OPt8QFNtiYEmO4/3WG58n3C4wjxyVuoE6KjxQIvCDyjw== From 21129d34a5b12ece1901cfa89d139acaef76de57 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 8 Jul 2024 14:11:11 -0400 Subject: [PATCH 18/85] Upgrade flow to 0.235.0 (#30118) See [Flow changelog](https://github.com/facebook/flow/blob/main/Changelog.md) for changes in this version. --- package.json | 4 ++-- .../src/ReactClientConsoleConfigBrowser.js | 4 ++-- .../react-client/src/ReactClientConsoleConfigPlain.js | 4 ++-- .../react-client/src/ReactClientConsoleConfigServer.js | 4 ++-- packages/react-debug-tools/src/ReactDebugHooks.js | 1 + packages/react-devtools-shared/src/backend/console.js | 2 ++ packages/react-devtools-shared/src/backend/renderer.js | 1 + packages/react-devtools-shared/src/hook.js | 2 ++ .../src/client/DOMAccessibilityRoles.js | 1 + .../src/client/DOMPropertyOperations.js | 1 + packages/react-dom-bindings/src/events/getListener.js | 1 + .../src/server/ReactDOMFlightServerHostDispatcher.js | 1 + .../src/server/ReactFizzConfigDOM.js | 1 + .../src/legacy-events/EventPluginRegistry.js | 1 + packages/react/src/ReactChildren.js | 1 + packages/shared/enqueueTask.js | 1 + yarn.lock | 10 +++++----- 17 files changed, 27 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index d5dfde958c..aafb48ecb9 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "eslint-plugin-react-internal": "link:./scripts/eslint-rules", "fbjs-scripts": "^3.0.1", "filesize": "^6.0.1", - "flow-bin": "^0.234.0", - "flow-remove-types": "^2.234.0", + "flow-bin": "^0.235.0", + "flow-remove-types": "^2.235.0", "glob": "^7.1.6", "glob-stream": "^6.1.0", "google-closure-compiler": "^20230206.0.0", diff --git a/packages/react-client/src/ReactClientConsoleConfigBrowser.js b/packages/react-client/src/ReactClientConsoleConfigBrowser.js index da87324b6d..a8a375debd 100644 --- a/packages/react-client/src/ReactClientConsoleConfigBrowser.js +++ b/packages/react-client/src/ReactClientConsoleConfigBrowser.js @@ -70,7 +70,7 @@ export function printToConsole( } else if (methodName === 'warn') { warn.apply(console, newArgs); } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } } diff --git a/packages/react-client/src/ReactClientConsoleConfigPlain.js b/packages/react-client/src/ReactClientConsoleConfigPlain.js index a4e7c3c6d7..64b61d6ed6 100644 --- a/packages/react-client/src/ReactClientConsoleConfigPlain.js +++ b/packages/react-client/src/ReactClientConsoleConfigPlain.js @@ -51,7 +51,7 @@ export function printToConsole( } else if (methodName === 'warn') { warn.apply(console, newArgs); } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } } diff --git a/packages/react-client/src/ReactClientConsoleConfigServer.js b/packages/react-client/src/ReactClientConsoleConfigServer.js index f6ecad92f3..0a62707cf6 100644 --- a/packages/react-client/src/ReactClientConsoleConfigServer.js +++ b/packages/react-client/src/ReactClientConsoleConfigServer.js @@ -71,7 +71,7 @@ export function printToConsole( } else if (methodName === 'warn') { warn.apply(console, newArgs); } else { - // eslint-disable-next-line react-internal/no-production-logging - console[methodName].apply(console, newArgs); + // $FlowFixMe[invalid-computed-prop] + console[methodName].apply(console, newArgs); // eslint-disable-line react-internal/no-production-logging } } diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 09ba351235..d7ffc6626c 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -774,6 +774,7 @@ const Dispatcher: DispatcherType = { const DispatcherProxyHandler = { get(target: DispatcherType, prop: string) { if (target.hasOwnProperty(prop)) { + // $FlowFixMe[invalid-computed-prop] return target[prop]; } const error = new Error('Missing method in Dispatcher: ' + prop); diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index e1b98b9190..3aedfa6e39 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -97,6 +97,7 @@ const injectedRenderers: Map< let targetConsole: Object = console; let targetConsoleMethods: {[string]: $FlowFixMe} = {}; for (const method in console) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } @@ -110,6 +111,7 @@ export function dangerous_setTargetConsoleForTesting( targetConsoleMethods = ({}: {[string]: $FlowFixMe}); for (const method in targetConsole) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } } diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 59af004fb3..62bf7fe4a7 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -3391,6 +3391,7 @@ export function attach( // Temporarily disable all console logging before re-running the hook. for (const method in console) { try { + // $FlowFixMe[invalid-computed-prop] originalConsoleMethods[method] = console[method]; // $FlowFixMe[prop-missing] console[method] = () => {}; diff --git a/packages/react-devtools-shared/src/hook.js b/packages/react-devtools-shared/src/hook.js index 0dd96dc471..c16409e3d9 100644 --- a/packages/react-devtools-shared/src/hook.js +++ b/packages/react-devtools-shared/src/hook.js @@ -32,6 +32,7 @@ export function installHook(target: any): DevToolsHook | null { let targetConsole: Object = console; let targetConsoleMethods: {[string]: $FlowFixMe} = {}; for (const method in console) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } @@ -42,6 +43,7 @@ export function installHook(target: any): DevToolsHook | null { targetConsoleMethods = ({}: {[string]: $FlowFixMe}); for (const method in targetConsole) { + // $FlowFixMe[invalid-computed-prop] targetConsoleMethods[method] = console[method]; } } diff --git a/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js b/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js index 0a6bbb4c5d..4c1091f245 100644 --- a/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js +++ b/packages/react-dom-bindings/src/client/DOMAccessibilityRoles.js @@ -60,6 +60,7 @@ const tagToRoleMappings = { }; function getImplicitRole(element: Element): string | null { + // $FlowFixMe[invalid-computed-prop] const mappedByTag = tagToRoleMappings[element.tagName]; if (mappedByTag !== undefined) { return mappedByTag; diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index 1d5211e451..da928058bb 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -196,6 +196,7 @@ export function setValueForPropertyOnCustomComponent( const eventName = name.slice(2, useCapture ? name.length - 7 : undefined); const prevProps = getFiberCurrentPropsFromNode(node); + // $FlowFixMe[invalid-computed-prop] const prevValue = prevProps != null ? prevProps[name] : null; if (typeof prevValue === 'function') { node.removeEventListener(eventName, prevValue, useCapture); diff --git a/packages/react-dom-bindings/src/events/getListener.js b/packages/react-dom-bindings/src/events/getListener.js index 68ab23e219..d6c7bab6d0 100644 --- a/packages/react-dom-bindings/src/events/getListener.js +++ b/packages/react-dom-bindings/src/events/getListener.js @@ -62,6 +62,7 @@ export default function getListener( // Work in progress. return null; } + // $FlowFixMe[invalid-computed-prop] const listener = props[registrationName]; if (shouldPreventMouseEvent(registrationName, inst.type, props)) { return null; diff --git a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js index 3303c07cfb..c2a9227737 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js +++ b/packages/react-dom-bindings/src/server/ReactDOMFlightServerHostDispatcher.js @@ -238,6 +238,7 @@ function trimOptions< let hasProperties = false; const trimmed: T = ({}: any); for (const key in options) { + // $FlowFixMe[invalid-computed-prop] if (options[key] != null) { hasProperties = true; (trimmed: any)[key] = options[key]; diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 594051dc35..54504deb6e 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -6073,6 +6073,7 @@ function getPreloadAsHeader( let value = `<${escapedHref}>; rel=preload; as="${escapedAs}"`; for (const paramName in params) { if (hasOwnProperty.call(params, paramName)) { + // $FlowFixMe[invalid-computed-prop] const paramValue = params[paramName]; if (typeof paramValue === 'string') { value += `; ${paramName.toLowerCase()}="${escapeStringForLinkHeaderQuotedParamValueContext( diff --git a/packages/react-native-renderer/src/legacy-events/EventPluginRegistry.js b/packages/react-native-renderer/src/legacy-events/EventPluginRegistry.js index 7d96a1eb1b..48cfd5759b 100644 --- a/packages/react-native-renderer/src/legacy-events/EventPluginRegistry.js +++ b/packages/react-native-renderer/src/legacy-events/EventPluginRegistry.js @@ -107,6 +107,7 @@ function publishEventForPlugin( if (phasedRegistrationNames) { for (const phaseName in phasedRegistrationNames) { if (phasedRegistrationNames.hasOwnProperty(phaseName)) { + // $FlowFixMe[invalid-computed-prop] const phasedRegistrationName = phasedRegistrationNames[phaseName]; publishRegistrationName( phasedRegistrationName, diff --git a/packages/react/src/ReactChildren.js b/packages/react/src/ReactChildren.js index 0e4c699f47..8fcc83dea4 100644 --- a/packages/react/src/ReactChildren.js +++ b/packages/react/src/ReactChildren.js @@ -42,6 +42,7 @@ function escape(key: string): string { ':': '=2', }; const escapedString = key.replace(escapeRegex, function (match) { + // $FlowFixMe[invalid-computed-prop] return escaperLookup[match]; }); diff --git a/packages/shared/enqueueTask.js b/packages/shared/enqueueTask.js index f2d18e7584..f7e1c746e1 100644 --- a/packages/shared/enqueueTask.js +++ b/packages/shared/enqueueTask.js @@ -16,6 +16,7 @@ export default function enqueueTask(task: () => void): void { // read require off the module object to get around the bundlers. // we don't want them to detect a require and bundle a Node polyfill. const requireString = ('require' + Math.random()).slice(0, 7); + // $FlowFixMe[invalid-computed-prop] const nodeRequire = module && module[requireString]; // assuming we're in node, let's try to get node's // version of setImmediate, bypassing fake timers if any. diff --git a/yarn.lock b/yarn.lock index 54fd49b8ff..ea0d17ca3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8321,12 +8321,12 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== -flow-bin@^0.234.0: - version "0.234.0" - resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.234.0.tgz#17dfc5aac1d928b6d7194f93bd0bf742d735c77d" - integrity sha512-uLmvfFRW6yEcz2wSJ2H6192RwknBpzAHBezDcXzmxJASxB6QzjKadhPxZvsJ74uJ+9Th1hDNuRB4mGrVUeneyA== +flow-bin@^0.235.0: + version "0.235.1" + resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.235.1.tgz#7dfca9c480bb7cb83fa3caca58386e9beca09bc3" + integrity sha512-SuXw5NQDIdSBMg/NgvS5mzdI6dPEYWubnucnYno9wWLd6xoK1nkH6t2Dn2GsML9bIoVqp3E/ni1jo18A4G4FrQ== -flow-remove-types@^2.234.0: +flow-remove-types@^2.235.0: version "2.238.2" resolved "https://registry.yarnpkg.com/flow-remove-types/-/flow-remove-types-2.238.2.tgz#85c9d26e83ba395f0206a23bce438223bc035609" integrity sha512-WJXRomjPiZ34nG14y7AceoPxg1L00FxjPSA3TDBTG2OPt8QFNtiYEmO4/3WG58n3C4wjxyVuoE6KjxQIvCDyjw== From 274c980c535bb34e17f5d97cc22ef4dd296ab413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 8 Jul 2024 16:45:24 -0400 Subject: [PATCH 19/85] Warn for useFormState on initial render (#30292) This was missed in the mount dev dispatcher. It was only in the rerender dispatcher which means that it was only logged during the rerender. Since DevTools can hide logs during rerenders, this hid the warning in StrictMode. --- fixtures/flight/src/Counter.js | 3 +-- .../ReactHooksInspectionIntegration-test.js | 8 +++----- .../react-dom/src/__tests__/ReactDOMFizzForm-test.js | 12 ++++++------ packages/react-reconciler/src/ReactFiberHooks.js | 1 + .../src/__tests__/ReactFlightDOMForm-test.js | 12 ++++++------ 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/fixtures/flight/src/Counter.js b/fixtures/flight/src/Counter.js index f89840de69..30170bed3f 100644 --- a/fixtures/flight/src/Counter.js +++ b/fixtures/flight/src/Counter.js @@ -1,12 +1,11 @@ 'use client'; import * as React from 'react'; -import {useFormState} from 'react-dom'; import Container from './Container.js'; export function Counter({incrementAction}) { - const [count, incrementFormAction] = useFormState(incrementAction, 0); + const [count, incrementFormAction] = React.useActionState(incrementAction, 0); return (
diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 0c54464b72..47d47926cf 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -11,7 +11,6 @@ 'use strict'; let React; -let ReactDOM; let ReactTestRenderer; let ReactDebugTools; let act; @@ -34,7 +33,6 @@ describe('ReactHooksInspectionIntegration', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - ReactDOM = require('react-dom'); act = require('internal-test-utils').act; ReactDebugTools = require('react-debug-tools'); useMemoCache = require('react/compiler-runtime').c; @@ -2658,9 +2656,9 @@ describe('ReactHooksInspectionIntegration', () => { }); // @gate enableAsyncActions - it('should support useFormState hook', async () => { + it('should support useActionState hook', async () => { function Foo() { - const [value] = ReactDOM.useFormState(function increment(n) { + const [value] = React.useActionState(function increment(n) { return n; }, 0); React.useMemo(() => 'memo', []); @@ -2689,7 +2687,7 @@ describe('ReactHooksInspectionIntegration', () => { }, "id": 0, "isStateEditable": false, - "name": "FormState", + "name": "ActionState", "subHooks": [], "value": 0, }, diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index b83abb5693..0ecfe3eb82 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -40,12 +40,12 @@ describe('ReactDOMFizzForm', () => { act = require('internal-test-utils').act; container = document.createElement('div'); document.body.appendChild(container); - if (__VARIANT__) { - // Remove after API is deleted. - useActionState = require('react-dom').useFormState; - } else { - useActionState = require('react').useActionState; - } + // TODO: Test the old api but it warns so needs warnings to be asserted. + // if (__VARIANT__) { + // Remove after API is deleted. + // useActionState = require('react-dom').useFormState; + // } + useActionState = require('react').useActionState; }); afterEach(() => { diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index f19f40a175..5e9d908545 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -3994,6 +3994,7 @@ if (__DEV__) { ): [Awaited, (P) => void, boolean] { currentHookNameInDev = 'useFormState'; mountHookTypesDev(); + warnOnUseFormStateInDev(); return mountActionState(action, initialState, permalink); }; (HooksDispatcherOnMountInDEV: Dispatcher).useActionState = diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js index e675e63837..b2d13c7580 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js @@ -73,12 +73,12 @@ describe('ReactFlightDOMForm', () => { ReactDOMClient = require('react-dom/client'); act = React.act; - if (__VARIANT__) { - // Remove after API is deleted. - useActionState = require('react-dom').useFormState; - } else { - useActionState = require('react').useActionState; - } + // TODO: Test the old api but it warns so needs warnings to be asserted. + // if (__VARIANT__) { + // Remove after API is deleted. + // useActionState = require('react-dom').useFormState; + // } + useActionState = require('react').useActionState; container = document.createElement('div'); document.body.appendChild(container); }); From 491a4eacce69fec4144725beaac7141da269e8cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 8 Jul 2024 18:42:58 -0400 Subject: [PATCH 20/85] [DevTools] Print component stacks as error objects to get source mapping (#30289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: Screenshot 2024-07-04 at 3 20 34 PM After: Screenshot 2024-07-05 at 6 08 28 PM Firefox: Screenshot 2024-07-05 at 6 09 50 PM The first log doesn't get a stack because it's logged before DevTools boots up and connects which is unfortunate. The second log already has a stack printed by React (this is on stable) it gets replaced by our object now. The third and following logs don't have a stack and get one appended. I only turn the stack into an error object if it matches what we would emit from DevTools anyway. Otherwise we assume it's not React. Since I had to change the format slightly to make this work, I first normalize the stack slightly before doing a comparison since it won't be 1:1. --- .eslintrc.js | 1 + .../react-devtools-core/webpack.backend.js | 2 + .../react-devtools-inline/webpack.config.js | 4 + .../src/__tests__/console-test.js | 4 +- .../src/__tests__/utils.js | 3 + .../backend/DevToolsComponentStackFrame.js | 11 ++- .../backend/DevToolsFiberComponentStack.js | 4 + .../src/backend/console.js | 83 ++++++++++++++----- .../react-devtools-shared/src/constants.js | 2 +- scripts/flow/react-devtools.js | 2 + scripts/jest/devtools/setupEnv.js | 2 + 11 files changed, 92 insertions(+), 26 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1d45d68055..f39437a7d1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -490,6 +490,7 @@ module.exports = { 'packages/react-devtools-extensions/**/*.js', 'packages/react-devtools-shared/src/hook.js', 'packages/react-devtools-shared/src/backend/console.js', + 'packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js', ], globals: { __IS_CHROME__: 'readonly', diff --git a/packages/react-devtools-core/webpack.backend.js b/packages/react-devtools-core/webpack.backend.js index 24e3ced0d7..7efd5b0b5b 100644 --- a/packages/react-devtools-core/webpack.backend.js +++ b/packages/react-devtools-core/webpack.backend.js @@ -69,6 +69,8 @@ module.exports = { __PROFILE__: false, __TEST__: NODE_ENV === 'test', __IS_FIREFOX__: false, + __IS_CHROME__: false, + __IS_EDGE__: 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-inline/webpack.config.js b/packages/react-devtools-inline/webpack.config.js index 2ab8db739a..7b153bbc13 100644 --- a/packages/react-devtools-inline/webpack.config.js +++ b/packages/react-devtools-inline/webpack.config.js @@ -73,6 +73,10 @@ module.exports = { __EXTENSION__: false, __PROFILE__: false, __TEST__: NODE_ENV === 'test', + // TODO: Should this be feature tested somehow? + __IS_CHROME__: false, + __IS_FIREFOX__: false, + __IS_EDGE__: 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/__tests__/console-test.js b/packages/react-devtools-shared/src/__tests__/console-test.js index c79850901a..75e7dc1c87 100644 --- a/packages/react-devtools-shared/src/__tests__/console-test.js +++ b/packages/react-devtools-shared/src/__tests__/console-test.js @@ -1000,7 +1000,7 @@ describe('console', () => { ); expect(mockWarn.mock.calls[1]).toHaveLength(3); expect(mockWarn.mock.calls[1][0]).toEqual( - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m', + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', ); expect(mockWarn.mock.calls[1][1]).toMatch('warn'); expect(normalizeCodeLocInfo(mockWarn.mock.calls[1][2]).trim()).toEqual( @@ -1014,7 +1014,7 @@ describe('console', () => { ); expect(mockError.mock.calls[1]).toHaveLength(3); expect(mockError.mock.calls[1][0]).toEqual( - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m', + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m', ); expect(mockError.mock.calls[1][1]).toEqual('error'); expect(normalizeCodeLocInfo(mockError.mock.calls[1][2]).trim()).toEqual( diff --git a/packages/react-devtools-shared/src/__tests__/utils.js b/packages/react-devtools-shared/src/__tests__/utils.js index c0c472e681..4a42cf2703 100644 --- a/packages/react-devtools-shared/src/__tests__/utils.js +++ b/packages/react-devtools-shared/src/__tests__/utils.js @@ -463,6 +463,9 @@ export function overrideFeatureFlags(overrideFlags) { } export function normalizeCodeLocInfo(str) { + if (typeof str === 'object' && str !== null) { + str = str.stack; + } if (typeof str !== 'string') { return str; } diff --git a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js index e20fb85e3c..e42073cc02 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js +++ b/packages/react-devtools-shared/src/backend/DevToolsComponentStackFrame.js @@ -29,12 +29,19 @@ export function describeBuiltInComponentFrame(name: string): string { prefix = (match && match[1]) || ''; } } + let suffix = ''; + if (__IS_CHROME__ || __IS_EDGE__) { + suffix = ' ()'; + } else if (__IS_FIREFOX__) { + suffix = '@unknown:0:0'; + } // We use the prefix to ensure our stacks line up with native stack frames. - return '\n' + prefix + name; + // We use a suffix to ensure it gets parsed natively. + return '\n' + prefix + name + suffix; } export function describeDebugInfoFrame(name: string, env: ?string): string { - return describeBuiltInComponentFrame(name + (env ? ' (' + env + ')' : '')); + return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : '')); } let reentry = false; diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js index a4311797de..73887c16d8 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -28,6 +28,8 @@ export function describeFiber( currentDispatcherRef: CurrentDispatcherRef, ): string { const { + HostHoistable, + HostSingleton, HostComponent, LazyComponent, SuspenseComponent, @@ -40,6 +42,8 @@ export function describeFiber( } = workTagMap; switch (workInProgress.tag) { + case HostHoistable: + case HostSingleton: case HostComponent: return describeBuiltInComponentFrame(workInProgress.type); case LazyComponent: diff --git a/packages/react-devtools-shared/src/backend/console.js b/packages/react-devtools-shared/src/backend/console.js index 3aedfa6e39..227298da2b 100644 --- a/packages/react-devtools-shared/src/backend/console.js +++ b/packages/react-devtools-shared/src/backend/console.js @@ -63,6 +63,15 @@ function isStrictModeOverride(args: Array): boolean { } } +// We add a suffix to some frames that older versions of React didn't do. +// To compare if it's equivalent we strip out the suffix to see if they're +// still equivalent. Similarly, we sometimes use [] and sometimes () so we +// strip them to for the comparison. +const frameDiffs = / \(\\)$|\@unknown\:0\:0$|\(|\)|\[|\]/gm; +function areStackTracesEqual(a: string, b: string): boolean { + return a.replace(frameDiffs, '') === b.replace(frameDiffs, ''); +} + function restorePotentiallyModifiedArgs(args: Array): Array { // If the arguments don't have any styles applied, then just copy if (!isStrictModeOverride(args)) { @@ -204,17 +213,11 @@ export function patch({ // $FlowFixMe[missing-local-annot] const overrideMethod = (...args) => { - let shouldAppendWarningStack = false; - if (method !== 'log') { - if (consoleSettingsRef.appendComponentStack) { - const lastArg = args.length > 0 ? args[args.length - 1] : null; - const alreadyHasComponentStack = - typeof lastArg === 'string' && isStringComponentStack(lastArg); - - // If we are ever called with a string that already has a component stack, - // e.g. a React error/warning, don't append a second stack. - shouldAppendWarningStack = !alreadyHasComponentStack; - } + let alreadyHasComponentStack = false; + if (method !== 'log' && consoleSettingsRef.appendComponentStack) { + const lastArg = args.length > 0 ? args[args.length - 1] : null; + alreadyHasComponentStack = + typeof lastArg === 'string' && isStringComponentStack(lastArg); // The last argument should be a component stack. } const shouldShowInlineWarningsAndErrors = @@ -244,7 +247,7 @@ export function patch({ } if ( - shouldAppendWarningStack && + consoleSettingsRef.appendComponentStack && !supportsNativeConsoleTasks(current) ) { const componentStack = getStackByFiberInDevAndProd( @@ -253,17 +256,55 @@ export function patch({ (currentDispatcherRef: any), ); if (componentStack !== '') { - if (isStrictModeOverride(args)) { - if (__IS_FIREFOX__) { - args[0] = `${args[0]} %s`; - args.push(componentStack); - } else { - args[0] = - ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK; - args.push(componentStack); + // Create a fake Error so that when we print it we get native source maps. Every + // browser will print the .stack property of the error and then parse it back for source + // mapping. Rather than print the internal slot. So it doesn't matter that the internal + // slot doesn't line up. + const fakeError = new Error(''); + // In Chromium, only the stack property is printed but in Firefox the : + // gets printed so to make the colon make sense, we name it so we print Component Stack: + // and similarly Safari leave an expandable slot. + fakeError.name = 'Component Stack'; // This gets printed + // In Chromium, the stack property needs to start with ^[\w.]*Error\b to trigger stack + // formatting. Otherwise it is left alone. So we prefix it. Otherwise we just override it + // to our own stack. + fakeError.stack = + __IS_CHROME__ || __IS_EDGE__ + ? 'Error Component Stack:' + componentStack + : componentStack; + if (alreadyHasComponentStack) { + // Only modify the component stack if it matches what we would've added anyway. + // Otherwise we assume it was a non-React stack. + if (isStrictModeOverride(args)) { + // We do nothing to Strict Mode overrides that already has a stack + // because we have already lost some context for how to format it + // since we've already merged the stack into the log at this point. + } else if ( + areStackTracesEqual( + args[args.length - 1], + componentStack, + ) + ) { + const firstArg = args[0]; + if ( + args.length > 1 && + typeof firstArg === 'string' && + firstArg.endsWith('%s') + ) { + args[0] = firstArg.slice(0, firstArg.length - 2); // Strip the %s param + } + args[args.length - 1] = fakeError; } } else { - args.push(componentStack); + args.push(fakeError); + if (isStrictModeOverride(args)) { + if (__IS_FIREFOX__) { + args[0] = `${args[0]} %o`; + } else { + args[0] = + ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK; + } + } } } } diff --git a/packages/react-devtools-shared/src/constants.js b/packages/react-devtools-shared/src/constants.js index 303ae50288..52d39f2d90 100644 --- a/packages/react-devtools-shared/src/constants.js +++ b/packages/react-devtools-shared/src/constants.js @@ -62,4 +62,4 @@ export const PROFILER_EXPORT_VERSION = 5; export const FIREFOX_CONSOLE_DIMMING_COLOR = 'color: rgba(124, 124, 124, 0.75)'; export const ANSI_STYLE_DIMMING_TEMPLATE = '\x1b[2;38;2;124;124;124m%s\x1b[0m'; export const ANSI_STYLE_DIMMING_TEMPLATE_WITH_COMPONENT_STACK = - '\x1b[2;38;2;124;124;124m%s %s\x1b[0m'; + '\x1b[2;38;2;124;124;124m%s %o\x1b[0m'; diff --git a/scripts/flow/react-devtools.js b/scripts/flow/react-devtools.js index 67355481e9..2b2d6b38be 100644 --- a/scripts/flow/react-devtools.js +++ b/scripts/flow/react-devtools.js @@ -13,3 +13,5 @@ declare const __EXTENSION__: boolean; declare const __TEST__: boolean; declare const __IS_FIREFOX__: boolean; +declare const __IS_CHROME__: boolean; +declare const __IS_EDGE__: boolean; diff --git a/scripts/jest/devtools/setupEnv.js b/scripts/jest/devtools/setupEnv.js index 019cf40e43..1021fbb587 100644 --- a/scripts/jest/devtools/setupEnv.js +++ b/scripts/jest/devtools/setupEnv.js @@ -12,6 +12,8 @@ if (!global.hasOwnProperty('localStorage')) { global.__DEV__ = process.env.NODE_ENV !== 'production'; global.__TEST__ = true; global.__IS_FIREFOX__ = false; +global.__IS_CHROME__ = false; +global.__IS_EDGE__ = false; const ReactVersionTestingAgainst = process.env.REACT_VERSION || ReactVersion; From c3cdbec0a78d39b5ff7329384cb41c4573a38212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 8 Jul 2024 22:51:59 -0400 Subject: [PATCH 21/85] [Flight] Add context for non null prototype error (#30293) We already added this for other thrown errors, not just console.errors. There's a production form of this. We just missed adding this context. Mainly the best context is the line number though which comes from owner stacks. --- packages/react-client/src/ReactFlightReplyClient.js | 3 ++- packages/react-client/src/__tests__/ReactFlight-test.js | 5 ++++- packages/react-server/src/ReactFlightServer.js | 3 ++- scripts/error-codes/codes.json | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/react-client/src/ReactFlightReplyClient.js b/packages/react-client/src/ReactFlightReplyClient.js index ff80f4b310..c4033a999d 100644 --- a/packages/react-client/src/ReactFlightReplyClient.js +++ b/packages/react-client/src/ReactFlightReplyClient.js @@ -692,7 +692,8 @@ export function processReply( if (temporaryReferences === undefined) { throw new Error( 'Only plain objects, and a few built-ins, can be passed to Server Actions. ' + - 'Classes or null prototypes are not supported.', + 'Classes or null prototypes are not supported.' + + (__DEV__ ? describeObjectForErrorMessage(parent, key) : ''), ); } // We will have written this object to the temporary reference set above diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 11969871a9..77bc717874 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1482,7 +1482,10 @@ describe('ReactFlight', () => { expect(errors).toEqual([ 'Only plain objects, and a few built-ins, can be passed to Client Components ' + - 'from Server Components. Classes or null prototypes are not supported.', + 'from Server Components. Classes or null prototypes are not supported.' + + (__DEV__ + ? '\n' + ' \n' + ' ^^^^' + : '\n' + ' {value: {}}\n' + ' ^^'), ]); }); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 30a8ece0ad..dd279943b6 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2615,7 +2615,8 @@ function renderModelDestructive( ) { throw new Error( 'Only plain objects, and a few built-ins, can be passed to Client Components ' + - 'from Server Components. Classes or null prototypes are not supported.', + 'from Server Components. Classes or null prototypes are not supported.' + + describeObjectForErrorMessage(parent, parentPropertyName), ); } if (__DEV__) { diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 088bd5b33b..ff87311764 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -483,8 +483,8 @@ "495": "Cannot taint a %s because the value is too general and not unique enough to block globally.", "496": "Only objects or functions can be passed to taintObjectReference. Try taintUniqueValue instead.", "497": "Only objects or functions can be passed to taintObjectReference.", - "498": "Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.", - "499": "Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.", + "498": "Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported.%s", + "499": "Only plain objects, and a few built-ins, can be passed to Server Actions. Classes or null prototypes are not supported.%s", "500": "React expected a headers state to exist when emitEarlyPreloads was called but did not find it. This suggests emitEarlyPreloads was called more than once per request. This is a bug in React.", "501": "The render was aborted with postpone when the shell is incomplete. Reason: %s", "502": "Cannot read a Client Context from a Server Component.", From ba95cf4b8f39acfd7c0ccf2795a19430d35ea6b3 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 9 Jul 2024 13:29:50 -0400 Subject: [PATCH 22/85] Remove propTypes on instance warning (#30296) `propTypes` are no longer supported at all in React 19, remove this outdated warning. --- packages/react-reconciler/src/ReactFiberClassComponent.js | 7 ------- packages/react-server/src/ReactFizzClassComponent.js | 7 ------- .../react/src/__tests__/ReactCoffeeScriptClass-test.coffee | 2 -- packages/react/src/__tests__/ReactES6Class-test.js | 2 -- packages/react/src/__tests__/ReactTypeScriptClass-test.ts | 2 -- 5 files changed, 20 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 9a17f375fe..b1055c0161 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -376,13 +376,6 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { name, ); } - if (instance.propTypes) { - console.error( - 'propTypes was defined as an instance property on %s. Use a static ' + - 'property to define propTypes instead.', - name, - ); - } if (instance.contextType) { console.error( 'contextType was defined as an instance property on %s. Use a static ' + diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js index 4a10d3db56..61e48d1afd 100644 --- a/packages/react-server/src/ReactFizzClassComponent.js +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -353,13 +353,6 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { name, ); } - if (instance.propTypes) { - console.error( - 'propTypes was defined as an instance property on %s. Use a static ' + - 'property to define propTypes instead.', - name, - ); - } if (instance.contextType) { console.error( 'contextType was defined as an instance property on %s. Use a static ' + diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index f0822d628f..4a4d07a78e 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -411,7 +411,6 @@ describe 'ReactCoffeeScriptClass', -> constructor: -> @contextTypes = {} @contextType = {} - @propTypes = {} getInitialState: -> getInitialStateWasCalled = true @@ -431,7 +430,6 @@ describe 'ReactCoffeeScriptClass', -> ).toErrorDev([ 'getInitialState was defined on Foo, a plain JavaScript class.', 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'propTypes was defined as an instance property on Foo.', 'contextTypes was defined as an instance property on Foo.', 'contextType was defined as an instance property on Foo.', ]) diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index 05be9c6f38..769bf5b9a5 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -459,7 +459,6 @@ describe('ReactES6Class', () => { super(); this.contextTypes = {}; this.contextType = {}; - this.propTypes = {}; } getInitialState() { getInitialStateWasCalled = true; @@ -477,7 +476,6 @@ describe('ReactES6Class', () => { expect(() => runTest(, 'SPAN', 'foo')).toErrorDev([ 'getInitialState was defined on Foo, a plain JavaScript class.', 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'propTypes was defined as an instance property on Foo.', 'contextType was defined as an instance property on Foo.', 'contextTypes was defined as an instance property on Foo.', ]); diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index ec1e39fac4..139a5c01e8 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -244,7 +244,6 @@ let getDefaultPropsWasCalled = false; class ClassicProperties extends React.Component { contextTypes = {}; contextType = {}; - propTypes = {}; getDefaultProps() { getDefaultPropsWasCalled = true; return {}; @@ -612,7 +611,6 @@ describe('ReactTypeScriptClass', function() { 'a plain JavaScript class.', 'getDefaultProps was defined on ClassicProperties, ' + 'a plain JavaScript class.', - 'propTypes was defined as an instance property on ClassicProperties.', 'contextTypes was defined as an instance property on ClassicProperties.', 'contextType was defined as an instance property on ClassicProperties.', ]); From 8aafbcf115e67df899ddbb180ef025c7c260a3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 9 Jul 2024 14:22:50 -0400 Subject: [PATCH 23/85] [Flight] Fully support serializing Map/Set in console logs (#30295) Currently we serialize Map/Set through our regular flow and not the console serialization. The console one is more forgiving than the regular one. --- .../src/__tests__/ReactFlight-test.js | 9 ++++++- .../react-server/src/ReactFlightServer.js | 26 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 77bc717874..af56b8c1a2 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -2628,7 +2628,7 @@ describe('ReactFlight', () => { return 'hello'; } function ServerComponent() { - console.log('hi', {prop: 123, fn: foo}); + console.log('hi', {prop: 123, fn: foo, map: new Map([['foo', foo]])}); throw new Error('err'); } @@ -2670,6 +2670,13 @@ describe('ReactFlight', () => { expect(typeof loggedFn).toBe('function'); expect(loggedFn).not.toBe(foo); expect(loggedFn.toString()).toBe(foo.toString()); + + const loggedMap = mockConsoleLog.mock.calls[0][1].map; + expect(loggedMap instanceof Map).toBe(true); + const loggedFn2 = loggedMap.get('foo'); + expect(typeof loggedFn2).toBe('function'); + expect(loggedFn2).not.toBe(foo); + expect(loggedFn2.toString()).toBe(foo.toString()); }); it('uses the server component debug info as the element owner in DEV', async () => { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index dd279943b6..491757f32c 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -2021,6 +2021,28 @@ function serializeSet(request: Request, set: Set): string { return '$W' + id.toString(16); } +function serializeConsoleMap( + request: Request, + counter: {objectCount: number}, + map: Map, +): string { + // Like serializeMap but for renderConsoleValue. + const entries = Array.from(map); + const id = outlineConsoleValue(request, counter, entries); + return '$Q' + id.toString(16); +} + +function serializeConsoleSet( + request: Request, + counter: {objectCount: number}, + set: Set, +): string { + // Like serializeMap but for renderConsoleValue. + const entries = Array.from(set); + const id = outlineConsoleValue(request, counter, entries); + return '$W' + id.toString(16); +} + function serializeIterator( request: Request, iterator: Iterator, @@ -3220,10 +3242,10 @@ function renderConsoleValue( } if (value instanceof Map) { - return serializeMap(request, value); + return serializeConsoleMap(request, counter, value); } if (value instanceof Set) { - return serializeSet(request, value); + return serializeConsoleSet(request, counter, value); } // TODO: FormData is not available in old Node. Remove the typeof later. if (typeof FormData === 'function' && value instanceof FormData) { From b73dcdc04ffa2dd9f2197d796388657d64ad53be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 9 Jul 2024 15:44:01 -0400 Subject: [PATCH 24/85] [Fizz] Refactor Component Stack Nodes (#30298) Component stacks have a similar problem to the problem with keyPath where we had to move it down and set it late right before recursing. Currently we work around that by popping exactly one off when something suspends. That doesn't work with the new server stacks being added which are more than one. It also meant that we kept having add a single frame that could be popped when there shouldn't need to be one. Unlike keyPath component stacks has this weird property that once something throws we might need the stack that was attempted for errors or the previous stack if we're going to retry and just recreate it. I've tried a few different approaches and I didn't like either but this is the one that seems least problematic. I first split out renderNodeDestructive into a retryNode helper. During retries only retryNode is called. When we first discover a node, we pass through renderNodeDestructive. Instead of add a component stack frame deep inside renderNodeDestructive after we've already refined a node, we now add it before in renderNodeDestructive. That way it's only added once before being attempted. This is similar to how Fiber works where in ChildFiber we match the node once to create the instance and then later do we attempt to actually render it and it's only the second part that's ever retried. This unfortunately means that we now have to refine the node down to element/lazy/thenables twice. To avoid refining the type too I move that to be done lazily. --- .../backend/DevToolsFiberComponentStack.js | 1 + .../src/__tests__/ReactDOMFizzServer-test.js | 57 ++- .../__tests__/ReactDOMFizzServerNode-test.js | 2 +- .../src/__tests__/ReactHTMLServer-test.js | 4 +- .../src/ReactFiberComponentStack.js | 1 + .../src/__tests__/ReactFlightDOMEdge-test.js | 70 +++ .../src/ReactFizzComponentStack.js | 199 ++++---- packages/react-server/src/ReactFizzServer.js | 471 +++++------------- .../babel/transform-prevent-infinite-loops.js | 2 +- 9 files changed, 346 insertions(+), 461 deletions(-) diff --git a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js index 73887c16d8..1e64b3e15f 100644 --- a/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js +++ b/packages/react-devtools-shared/src/backend/DevToolsFiberComponentStack.js @@ -47,6 +47,7 @@ export function describeFiber( case HostComponent: return describeBuiltInComponentFrame(workInProgress.type); case LazyComponent: + // TODO: When we support Thenables as component types we should rename this. return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: return describeBuiltInComponentFrame('Suspense'); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 62e0a17af0..d534df76a5 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -700,17 +700,39 @@ describe('ReactDOMFizzServer', () => { it('should client render a boundary if a lazy component rejects', async () => { let rejectComponent; + const promise = new Promise((resolve, reject) => { + rejectComponent = reject; + }); const LazyComponent = React.lazy(() => { - return new Promise((resolve, reject) => { - rejectComponent = reject; - }); + return promise; + }); + + const LazyLazy = React.lazy(async () => { + return { + default: LazyComponent, + }; + }); + + function Wrapper({children}) { + return children; + } + const LazyWrapper = React.lazy(() => { + return { + then(callback) { + callback({ + default: Wrapper, + }); + }, + }; }); function App({isClient}) { return (
}> - {isClient ? : } + + {isClient ? : } +
); @@ -744,6 +766,7 @@ describe('ReactDOMFizzServer', () => { }); pipe(writable); }); + expect(loggedErrors).toEqual([]); expect(bootstrapped).toBe(true); @@ -772,7 +795,7 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering errored:\n\n' + theError.message, expectedDigest, - componentStack(['Lazy', 'Suspense', 'div', 'App']), + componentStack(['Lazy', 'Wrapper', 'Suspense', 'div', 'App']), ], ], [ @@ -852,13 +875,9 @@ describe('ReactDOMFizzServer', () => { } await act(() => { - const {pipe} = renderToPipeableStream( - , - - { - onError, - }, - ); + const {pipe} = renderToPipeableStream(, { + onError, + }); pipe(writable); }); expect(loggedErrors).toEqual([]); @@ -896,7 +915,7 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering errored:\n\n' + theError.message, expectedDigest, - componentStack(['Lazy', 'Suspense', 'div', 'App']), + componentStack(['Suspense', 'div', 'App']), ], ], [ @@ -1395,13 +1414,13 @@ describe('ReactDOMFizzServer', () => { 'The render was aborted by the server without a reason.', expectedDigest, // We get the stack of the task when it was aborted which is why we see `h1` - componentStack(['h1', 'Suspense', 'div', 'App']), + componentStack(['AsyncText', 'h1', 'Suspense', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'The render was aborted by the server without a reason.', expectedDigest, - componentStack(['Suspense', 'main', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'main', 'div', 'App']), ], ], [ @@ -3523,13 +3542,13 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'foobar', 'a digest', - componentStack(['Suspense', 'p', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'foobar', 'a digest', - componentStack(['Suspense', 'span', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']), ], ], [ @@ -3606,13 +3625,13 @@ describe('ReactDOMFizzServer', () => { 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'uh oh', 'a digest', - componentStack(['Suspense', 'p', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'p', 'div', 'App']), ], [ 'Switched to client rendering because the server rendering aborted due to:\n\n' + 'uh oh', 'a digest', - componentStack(['Suspense', 'span', 'div', 'App']), + componentStack(['AsyncText', 'Suspense', 'span', 'div', 'App']), ], ], [ diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js index eb41a627b7..1e93c2420b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js @@ -585,7 +585,7 @@ describe('ReactDOMFizzServerNode', () => { let isComplete = false; let rendered = false; const promise = new Promise(r => (resolve = r)); - function Wait() { + function Wait({prop}) { if (!hasLoaded) { throw promise; } diff --git a/packages/react-html/src/__tests__/ReactHTMLServer-test.js b/packages/react-html/src/__tests__/ReactHTMLServer-test.js index 01c5873b56..98678083d5 100644 --- a/packages/react-html/src/__tests__/ReactHTMLServer-test.js +++ b/packages/react-html/src/__tests__/ReactHTMLServer-test.js @@ -250,9 +250,7 @@ if (!__EXPERIMENTAL__) { '\n in Bar (at **)' + '\n in Foo (at **)' + '\n in div (at **)' - : '\n in Lazy (at **)' + - '\n in div (at **)' + - '\n in div (at **)', + : '\n in div (at **)' + '\n in div (at **)', ); expect(normalizeCodeLocInfo(caughtErrors[0].ownerStack)).toBe( __DEV__ && gate(flags => flags.enableOwnerStacks) diff --git a/packages/react-reconciler/src/ReactFiberComponentStack.js b/packages/react-reconciler/src/ReactFiberComponentStack.js index 18f65530ea..a06411acad 100644 --- a/packages/react-reconciler/src/ReactFiberComponentStack.js +++ b/packages/react-reconciler/src/ReactFiberComponentStack.js @@ -39,6 +39,7 @@ function describeFiber(fiber: Fiber): string { case HostComponent: return describeBuiltInComponentFrame(fiber.type); case LazyComponent: + // TODO: When we support Thenables as component types we should rename this. return describeBuiltInComponentFrame('Lazy'); case SuspenseComponent: return describeBuiltInComponentFrame('Suspense'); diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js index 4997184078..9d4a123ac3 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js @@ -930,4 +930,74 @@ describe('ReactFlightDOMEdge', () => { '\n in Bar (at **)' + '\n in Foo (at **)', ); }); + + it('supports server components in ssr component stacks', async () => { + let reject; + const promise = new Promise((_, r) => (reject = r)); + async function Erroring() { + await promise; + return 'should not render'; + } + + const model = { + root: ReactServer.createElement(Erroring), + }; + + const stream = ReactServerDOMServer.renderToReadableStream( + model, + webpackMap, + { + onError() {}, + }, + ); + + const rootModel = await ReactServerDOMClient.createFromReadableStream( + stream, + { + ssrManifest: { + moduleMap: null, + moduleLoading: null, + }, + }, + ); + + const errors = []; + const result = ReactDOMServer.renderToReadableStream( +
{rootModel.root}
, + { + onError(error, {componentStack}) { + errors.push({ + error, + componentStack: normalizeCodeLocInfo(componentStack), + }); + }, + }, + ); + + const theError = new Error('my error'); + reject(theError); + + const expectedMessage = __DEV__ + ? 'my error' + : 'An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error.'; + + try { + await result; + } catch (x) { + expect(x).toEqual( + expect.objectContaining({ + message: expectedMessage, + }), + ); + } + + expect(errors).toEqual([ + { + error: expect.objectContaining({ + message: expectedMessage, + }), + componentStack: (__DEV__ ? '\n in Erroring' : '') + '\n in div', + }, + ]); + }); }); diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index 6e3a31b284..b280983359 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -8,52 +8,96 @@ */ import type {ReactComponentInfo} from 'shared/ReactTypes'; +import type {LazyComponent} from 'react/src/ReactLazy'; import { describeBuiltInComponentFrame, describeFunctionComponentFrame, describeClassComponentFrame, + describeDebugInfoFrame, } from 'shared/ReactComponentStackFrame'; +import { + REACT_FORWARD_REF_TYPE, + REACT_MEMO_TYPE, + REACT_LAZY_TYPE, + REACT_SUSPENSE_LIST_TYPE, + REACT_SUSPENSE_TYPE, +} from 'shared/ReactSymbols'; + import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; import {formatOwnerStack} from './ReactFizzOwnerStack'; -// DEV-only reverse linked list representing the current component stack -type BuiltInComponentStackNode = { - tag: 0, +export type ComponentStackNode = { parent: null | ComponentStackNode, - type: string, + type: + | symbol + | string + | Function + | LazyComponent + | ReactComponentInfo, owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only stack?: null | string | Error, // DEV only }; -type FunctionComponentStackNode = { - tag: 1, - parent: null | ComponentStackNode, - type: Function, - owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack?: null | string | Error, // DEV only -}; -type ClassComponentStackNode = { - tag: 2, - parent: null | ComponentStackNode, - type: Function, - owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack?: null | string | Error, // DEV only -}; -type ServerComponentStackNode = { - // DEV only - tag: 3, - parent: null | ComponentStackNode, - type: string, // name + env - owner?: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack?: null | string | Error, // DEV only -}; -export type ComponentStackNode = - | BuiltInComponentStackNode - | FunctionComponentStackNode - | ClassComponentStackNode - | ServerComponentStackNode; + +function shouldConstruct(Component: any) { + return Component.prototype && Component.prototype.isReactComponent; +} + +function describeComponentStackByType( + type: + | symbol + | string + | Function + | LazyComponent + | ReactComponentInfo, +): string { + if (typeof type === 'string') { + return describeBuiltInComponentFrame(type); + } + if (typeof type === 'function') { + if (shouldConstruct(type)) { + return describeClassComponentFrame(type); + } else { + return describeFunctionComponentFrame(type); + } + } + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case REACT_FORWARD_REF_TYPE: { + return describeFunctionComponentFrame((type: any).render); + } + case REACT_MEMO_TYPE: { + return describeFunctionComponentFrame((type: any).type); + } + case REACT_LAZY_TYPE: { + const lazyComponent: LazyComponent = (type: any); + const payload = lazyComponent._payload; + const init = lazyComponent._init; + try { + type = init(payload); + } catch (x) { + // TODO: When we support Thenables as component types we should rename this. + return describeBuiltInComponentFrame('Lazy'); + } + return describeComponentStackByType(type); + } + } + if (typeof type.name === 'string') { + return describeDebugInfoFrame(type.name, type.env); + } + } + switch (type) { + case REACT_SUSPENSE_LIST_TYPE: { + return describeBuiltInComponentFrame('SuspenseList'); + } + case REACT_SUSPENSE_TYPE: { + return describeBuiltInComponentFrame('Suspense'); + } + } + return ''; +} export function getStackByComponentStackNode( componentStack: ComponentStackNode, @@ -62,22 +106,7 @@ export function getStackByComponentStackNode( let info = ''; let node: ComponentStackNode = componentStack; do { - switch (node.tag) { - case 0: - info += describeBuiltInComponentFrame(node.type); - break; - case 1: - info += describeFunctionComponentFrame(node.type); - break; - case 2: - info += describeClassComponentFrame(node.type); - break; - case 3: - if (__DEV__) { - info += describeBuiltInComponentFrame(node.type); - break; - } - } + info += describeComponentStackByType(node.type); // $FlowFixMe[incompatible-type] we bail out when we get a null node = node.parent; } while (node); @@ -110,59 +139,41 @@ export function getOwnerStackByComponentStackNodeInDev( // add one extra frame just to describe the "current" built-in component by name. // Similarly, if there is no owner at all, then there's no stack frame so we add the name // of the root component to the stack to know which component is currently executing. - switch (componentStack.tag) { - case 0: - info += describeBuiltInComponentFrame(componentStack.type); - break; - case 1: - case 2: - if (!componentStack.owner) { - // Only if we have no other data about the callsite do we add - // the component name as the single stack frame. - info += describeFunctionComponentFrameWithoutLineNumber( - componentStack.type, - ); - } - break; - case 3: - if (!componentStack.owner) { - info += describeBuiltInComponentFrame(componentStack.type); - } - break; + if (typeof componentStack.type === 'string') { + info += describeBuiltInComponentFrame(componentStack.type); + } else if (typeof componentStack.type === 'function') { + if (!componentStack.owner) { + // Only if we have no other data about the callsite do we add + // the component name as the single stack frame. + info += describeFunctionComponentFrameWithoutLineNumber( + componentStack.type, + ); + } + } else { + if (!componentStack.owner) { + info += describeComponentStackByType(componentStack.type); + } } let owner: void | null | ComponentStackNode | ReactComponentInfo = componentStack; while (owner) { - if (typeof owner.tag === 'number') { - const node: ComponentStackNode = (owner: any); - owner = node.owner; - let debugStack = node.stack; - // If we don't actually print the stack if there is no owner of this JSX element. - // In a real app it's typically not useful since the root app is always controlled - // by the framework. These also tend to have noisy stacks because they're not rooted - // in a React render but in some imperative bootstrapping code. It could be useful - // if the element was created in module scope. E.g. hoisted. We could add a a single - // stack frame for context for example but it doesn't say much if that's a wrapper. - if (owner && debugStack) { - if (typeof debugStack !== 'string') { - // Stash the formatted stack so that we can avoid redoing the filtering. - node.stack = debugStack = formatOwnerStack(debugStack); - } - if (debugStack !== '') { - info += '\n' + debugStack; - } - } - } else if (typeof owner.stack === 'string') { - // Server Component - const ownerStack: string = owner.stack; - owner = owner.owner; - if (owner && ownerStack !== '') { - info += '\n' + ownerStack; - } - } else { - break; + let debugStack: void | null | string | Error = owner.stack; + if (typeof debugStack !== 'string' && debugStack != null) { + // Stash the formatted stack so that we can avoid redoing the filtering. + // $FlowFixMe[cannot-write]: This has been refined to a ComponentStackNode. + owner.stack = debugStack = formatOwnerStack(debugStack); + } + owner = owner.owner; + // If we don't actually print the stack if there is no owner of this JSX element. + // In a real app it's typically not useful since the root app is always controlled + // by the framework. These also tend to have noisy stacks because they're not rooted + // in a React render but in some imperative bootstrapping code. It could be useful + // if the element was created in module scope. E.g. hoisted. We could add a a single + // stack frame for context for example but it doesn't say much if that's a wrapper. + if (owner && debugStack) { + info += '\n' + debugStack; } } return info; diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index cf44e055ca..afdc9efbb2 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -472,6 +472,7 @@ function RequestInstance( emptyContextObject, null, ); + pushComponentStack(rootTask); pingedTasks.push(rootTask); } @@ -615,6 +616,7 @@ export function resumeRequest( emptyContextObject, null, ); + pushComponentStack(rootTask); pingedTasks.push(rootTask); return request; } @@ -642,6 +644,7 @@ export function resumeRequest( emptyContextObject, null, ); + pushComponentStack(rootTask); pingedTasks.push(rootTask); return request; } @@ -837,69 +840,6 @@ function getStackFromNode(stackNode: ComponentStackNode): string { return getStackByComponentStackNode(stackNode); } -function createBuiltInComponentStack( - task: Task, - type: string, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only -): ComponentStackNode { - if (__DEV__) { - return { - tag: 0, - parent: task.componentStack, - type, - owner, - stack, - }; - } - return { - tag: 0, - parent: task.componentStack, - type, - }; -} -function createFunctionComponentStack( - task: Task, - type: Function, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only -): ComponentStackNode { - if (__DEV__) { - return { - tag: 1, - parent: task.componentStack, - type, - owner, - stack, - }; - } - return { - tag: 1, - parent: task.componentStack, - type, - }; -} -function createClassComponentStack( - task: Task, - type: Function, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only -): ComponentStackNode { - if (__DEV__) { - return { - tag: 2, - parent: task.componentStack, - type, - owner, - stack, - }; - } - return { - tag: 2, - parent: task.componentStack, - type, - }; -} function pushServerComponentStack( task: Task, debugInfo: void | null | ReactDebugInfo, @@ -921,15 +861,9 @@ function pushServerComponentStack( if (enableOwnerStacks && componentInfo.stack === undefined) { continue; } - let name = componentInfo.name; - const env = componentInfo.env; - if (env) { - name += ' [' + env + ']'; - } task.componentStack = { - tag: 3, parent: task.componentStack, - type: name, + type: componentInfo, owner: componentInfo.owner, stack: componentInfo.stack, }; @@ -940,19 +874,70 @@ function pushServerComponentStack( } } +function pushComponentStack(task: Task): void { + const node = task.node; + // Create the Component Stack frame for the element we're about to try. + // It's unfortunate that we need to do this refinement twice. Once for + // the stack frame and then once again while actually + if (typeof node === 'object' && node !== null) { + switch ((node: any).$$typeof) { + case REACT_ELEMENT_TYPE: { + const element: any = node; + const type = element.type; + const owner = __DEV__ ? element._owner : null; + const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null; + if (__DEV__) { + pushServerComponentStack(task, element._debugInfo); + if (enableOwnerStacks) { + task.debugTask = element._debugTask; + } + } + task.componentStack = createComponentStackFromType( + task.componentStack, + type, + owner, + stack, + ); + break; + } + case REACT_LAZY_TYPE: { + if (__DEV__) { + const lazyNode: LazyComponentType = (node: any); + pushServerComponentStack(task, lazyNode._debugInfo); + } + break; + } + default: { + if (__DEV__) { + const maybeUsable: Object = node; + if (typeof maybeUsable.then === 'function') { + const thenable: Thenable = (maybeUsable: any); + pushServerComponentStack(task, thenable._debugInfo); + } + } + } + } + } +} + function createComponentStackFromType( - task: Task, - type: Function | string, + parent: null | ComponentStackNode, + type: Function | string | symbol, owner: null | ReactComponentInfo | ComponentStackNode, // DEV only stack: null | Error, // DEV only ): ComponentStackNode { - if (typeof type === 'string') { - return createBuiltInComponentStack(task, type, owner, stack); + if (__DEV__) { + return { + parent, + type, + owner, + stack, + }; } - if (shouldConstruct(type)) { - return createClassComponentStack(task, type, owner, stack); - } - return createFunctionComponentStack(task, type, owner, stack); + return { + parent, + type, + }; } type ThrownInfo = { @@ -1088,8 +1073,6 @@ function renderSuspenseBoundary( someTask: Task, keyPath: KeyNode, props: Object, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { if (someTask.replay !== null) { // If we're replaying through this pass, it means we're replaying through @@ -1108,12 +1091,6 @@ function renderSuspenseBoundary( // $FlowFixMe: Refined. const task: RenderTask = someTask; - const previousComponentStack = task.componentStack; - // If we end up creating the fallback task we need it to have the correct stack which is - // the stack for the boundary itself. We stash it here so we can use it if needed later - const suspenseComponentStack = (task.componentStack = - createBuiltInComponentStack(task, 'Suspense', owner, stack)); - const prevKeyPath = task.keyPath; const parentBoundary = task.blockedBoundary; const parentHoistableState = task.hoistableState; @@ -1189,9 +1166,6 @@ function renderSuspenseBoundary( // Therefore we won't need the fallback. We early return so that we don't have to create // the fallback. newBoundary.status = COMPLETED; - - // We are returning early so we need to restore the - task.componentStack = previousComponentStack; return; } } catch (error: mixed) { @@ -1234,7 +1208,6 @@ function renderSuspenseBoundary( task.hoistableState = parentHoistableState; task.blockedSegment = parentSegment; task.keyPath = prevKeyPath; - task.componentStack = previousComponentStack; } const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]]; @@ -1274,13 +1247,12 @@ function renderSuspenseBoundary( task.formatContext, task.context, task.treeContext, - // This stack should be the Suspense boundary stack because while the fallback is actually a child segment - // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself - suspenseComponentStack, + task.componentStack, true, !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, ); + pushComponentStack(suspendedFallbackTask); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. request.pingedTasks.push(suspendedFallbackTask); @@ -1296,15 +1268,7 @@ function replaySuspenseBoundary( childSlots: ResumeSlots, fallbackNodes: Array, fallbackSlots: ResumeSlots, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { - const previousComponentStack = task.componentStack; - // If we end up creating the fallback task we need it to have the correct stack which is - // the stack for the boundary itself. We stash it here so we can use it if needed later - const suspenseComponentStack = (task.componentStack = - createBuiltInComponentStack(task, 'Suspense', owner, stack)); - const prevKeyPath = task.keyPath; const previousReplaySet: ReplaySet = task.replay; @@ -1400,7 +1364,6 @@ function replaySuspenseBoundary( task.hoistableState = parentHoistableState; task.replay = previousReplaySet; task.keyPath = prevKeyPath; - task.componentStack = previousComponentStack; } const fallbackKeyPath = [keyPath[0], 'Suspense Fallback', keyPath[2]]; @@ -1425,13 +1388,12 @@ function replaySuspenseBoundary( task.formatContext, task.context, task.treeContext, - // This stack should be the Suspense boundary stack because while the fallback is actually a child segment - // of the parent boundary from a component standpoint the fallback is a child of the Suspense boundary itself - suspenseComponentStack, + task.componentStack, true, !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, ); + pushComponentStack(suspendedFallbackTask); // TODO: This should be queued at a separate lower priority queue so that we only work // on preparing fallbacks if we don't have any more main content to task on. request.pingedTasks.push(suspendedFallbackTask); @@ -1442,17 +1404,7 @@ function renderBackupSuspenseBoundary( task: Task, keyPath: KeyNode, props: Object, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ) { - const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack( - task, - 'Suspense', - owner, - stack, - ); - const content = props.children; const segment = task.blockedSegment; const prevKeyPath = task.keyPath; @@ -1467,7 +1419,6 @@ function renderBackupSuspenseBoundary( pushEndCompletedSuspenseBoundary(segment.chunks); } task.keyPath = prevKeyPath; - task.componentStack = previousComponentStack; } function renderHostElement( @@ -1476,11 +1427,7 @@ function renderHostElement( keyPath: KeyNode, type: string, props: Object, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { - const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack(task, type, owner, stack); const segment = task.blockedSegment; if (segment === null) { // Replay @@ -1534,7 +1481,6 @@ function renderHostElement( ); segment.lastPushedText = false; } - task.componentStack = previousComponentStack; } function shouldConstruct(Component: any) { @@ -1670,17 +1616,8 @@ function renderClassComponent( keyPath: KeyNode, Component: any, props: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { const resolvedProps = resolveClassComponentProps(Component, props); - const previousComponentStack = task.componentStack; - task.componentStack = createClassComponentStack( - task, - Component, - owner, - stack, - ); const maskedContext = !disableLegacyContext ? getMaskedContext(Component, task.legacyContext) : undefined; @@ -1698,7 +1635,6 @@ function renderClassComponent( Component, resolvedProps, ); - task.componentStack = previousComponentStack; } const didWarnAboutBadClass: {[string]: boolean} = {}; @@ -1715,21 +1651,11 @@ function renderFunctionComponent( keyPath: KeyNode, Component: any, props: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { let legacyContext; if (!disableLegacyContext) { legacyContext = getMaskedContext(Component, task.legacyContext); } - const previousComponentStack = task.componentStack; - task.componentStack = createFunctionComponentStack( - task, - Component, - owner, - stack, - ); - if (__DEV__) { if ( Component.prototype && @@ -1782,7 +1708,6 @@ function renderFunctionComponent( actionStateCount, actionStateMatchingIndex, ); - task.componentStack = previousComponentStack; } function finishFunctionComponent( @@ -1931,17 +1856,7 @@ function renderForwardRef( type: any, props: Object, ref: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { - const previousComponentStack = task.componentStack; - task.componentStack = createFunctionComponentStack( - task, - type.render, - owner, - stack, - ); - let propsWithoutRef; if (enableRefAsProp && 'ref' in props) { // `ref` is just a prop now, but `forwardRef` expects it to not appear in @@ -1980,7 +1895,6 @@ function renderForwardRef( actionStateCount, actionStateMatchingIndex, ); - task.componentStack = previousComponentStack; } function renderMemo( @@ -1990,24 +1904,13 @@ function renderMemo( type: any, props: Object, ref: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { const innerType = type.type; const resolvedProps = resolveDefaultPropsOnNonClassComponent( innerType, props, ); - renderElement( - request, - task, - keyPath, - innerType, - resolvedProps, - ref, - owner, - stack, - ); + renderElement(request, task, keyPath, innerType, resolvedProps, ref); } function renderContextConsumer( @@ -2074,12 +1977,7 @@ function renderLazyComponent( lazyComponent: LazyComponentType, props: Object, ref: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { - const previousComponentStack = task.componentStack; - // TODO: Do we really need this stack frame? We don't on the client. - task.componentStack = createBuiltInComponentStack(task, 'Lazy', owner, stack); let Component; if (__DEV__) { Component = callLazyInitInDEV(lazyComponent); @@ -2092,17 +1990,7 @@ function renderLazyComponent( Component, props, ); - renderElement( - request, - task, - keyPath, - Component, - resolvedProps, - ref, - owner, - stack, - ); - task.componentStack = previousComponentStack; + renderElement(request, task, keyPath, Component, resolvedProps, ref); } function renderOffscreen( @@ -2132,28 +2020,18 @@ function renderElement( type: any, props: Object, ref: any, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { if (typeof type === 'function') { if (shouldConstruct(type)) { - renderClassComponent(request, task, keyPath, type, props, owner, stack); + renderClassComponent(request, task, keyPath, type, props); return; } else { - renderFunctionComponent( - request, - task, - keyPath, - type, - props, - owner, - stack, - ); + renderFunctionComponent(request, task, keyPath, type, props); return; } } if (typeof type === 'string') { - renderHostElement(request, task, keyPath, type, props, owner, stack); + renderHostElement(request, task, keyPath, type, props); return; } @@ -2183,19 +2061,11 @@ function renderElement( return; } case REACT_SUSPENSE_LIST_TYPE: { - const preiousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack( - task, - 'SuspenseList', - owner, - stack, - ); // TODO: SuspenseList should control the boundaries. const prevKeyPath = task.keyPath; task.keyPath = keyPath; renderNodeDestructive(request, task, props.children, -1); task.keyPath = prevKeyPath; - task.componentStack = preiousComponentStack; return; } case REACT_SCOPE_TYPE: { @@ -2213,16 +2083,9 @@ function renderElement( enableSuspenseAvoidThisFallbackFizz && props.unstable_avoidThisFallback === true ) { - renderBackupSuspenseBoundary( - request, - task, - keyPath, - props, - owner, - stack, - ); + renderBackupSuspenseBoundary(request, task, keyPath, props); } else { - renderSuspenseBoundary(request, task, keyPath, props, owner, stack); + renderSuspenseBoundary(request, task, keyPath, props); } return; } @@ -2231,20 +2094,11 @@ function renderElement( if (typeof type === 'object' && type !== null) { switch (type.$$typeof) { case REACT_FORWARD_REF_TYPE: { - renderForwardRef( - request, - task, - keyPath, - type, - props, - ref, - owner, - stack, - ); + renderForwardRef(request, task, keyPath, type, props, ref); return; } case REACT_MEMO_TYPE: { - renderMemo(request, task, keyPath, type, props, ref, owner, stack); + renderMemo(request, task, keyPath, type, props, ref); return; } case REACT_PROVIDER_TYPE: { @@ -2281,16 +2135,7 @@ function renderElement( // Fall through } case REACT_LAZY_TYPE: { - renderLazyComponent( - request, - task, - keyPath, - type, - props, - ref, - owner, - stack, - ); + renderLazyComponent(request, task, keyPath, type, props, ref); return; } } @@ -2370,8 +2215,6 @@ function replayElement( props: Object, ref: any, replay: ReplaySet, - owner: null | ReactComponentInfo | ComponentStackNode, // DEV only - stack: null | Error, // DEV only ): void { // We're replaying. Find the path to follow. const replayNodes = replay.nodes; @@ -2399,7 +2242,7 @@ function replayElement( const currentNode = task.node; task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1}; try { - renderElement(request, task, keyPath, type, props, ref, owner, stack); + renderElement(request, task, keyPath, type, props, ref); if ( task.replay.pendingTasks === 1 && task.replay.nodes.length > 0 @@ -2466,8 +2309,6 @@ function replayElement( node[3], node[4] === null ? [] : node[4][2], node[4] === null ? null : node[4][3], - owner, - stack, ); } // We finished rendering this node, so now we can consume this @@ -2496,7 +2337,7 @@ function validateIterable( const isGeneratorComponent = childIndex === -1 && // Only the root child is valid task.componentStack !== null && - task.componentStack.tag === 1 && // FunctionComponent + typeof task.componentStack.type === 'function' && // FunctionComponent // $FlowFixMe[method-unbinding] Object.prototype.toString.call(task.componentStack.type) === '[object GeneratorFunction]' && @@ -2543,7 +2384,7 @@ function validateAsyncIterable( const isGeneratorComponent = childIndex === -1 && // Only the root child is valid task.componentStack !== null && - task.componentStack.tag === 1 && // FunctionComponent + typeof task.componentStack.type === 'function' && // FunctionComponent // $FlowFixMe[method-unbinding] Object.prototype.toString.call(task.componentStack.type) === '[object AsyncGeneratorFunction]' && @@ -2604,6 +2445,24 @@ function renderNodeDestructive( task.node = node; task.childIndex = childIndex; + const previousComponentStack = task.componentStack; + const previousDebugTask = + __DEV__ && enableOwnerStacks ? task.debugTask : null; + + pushComponentStack(task); + + retryNode(request, task); + + task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } +} + +function retryNode(request: Request, task: Task): void { + const node = task.node; + const childIndex = task.childIndex; + if (node === null) { return; } @@ -2628,21 +2487,8 @@ function renderNodeDestructive( ref = element.ref; } - const owner = __DEV__ ? element._owner : null; - const stack = __DEV__ && enableOwnerStacks ? element._debugStack : null; - - let previousDebugTask: null | ConsoleTask = null; - const previousComponentStack = task.componentStack; - let debugTask: null | ConsoleTask; - if (__DEV__) { - if (enableOwnerStacks) { - previousDebugTask = task.debugTask; - } - pushServerComponentStack(task, element._debugInfo); - if (enableOwnerStacks) { - task.debugTask = debugTask = element._debugTask; - } - } + const debugTask: null | ConsoleTask = + __DEV__ && enableOwnerStacks ? task.debugTask : null; const name = getComponentNameFromType(type); const keyOrIndex = @@ -2663,8 +2509,6 @@ function renderNodeDestructive( props, ref, task.replay, - owner, - stack, ), ); } else { @@ -2679,8 +2523,6 @@ function renderNodeDestructive( props, ref, task.replay, - owner, - stack, ); } // No matches found for this node. We assume it's already emitted in the @@ -2697,27 +2539,10 @@ function renderNodeDestructive( type, props, ref, - owner, - stack, ), ); } else { - renderElement( - request, - task, - keyPath, - type, - props, - ref, - owner, - stack, - ); - } - } - if (__DEV__) { - task.componentStack = previousComponentStack; - if (enableOwnerStacks) { - task.debugTask = previousDebugTask; + renderElement(request, task, keyPath, type, props, ref); } } return; @@ -2729,23 +2554,6 @@ function renderNodeDestructive( ); case REACT_LAZY_TYPE: { const lazyNode: LazyComponentType = (node: any); - const previousComponentStack = task.componentStack; - let previousDebugTask = null; - if (__DEV__) { - if (enableOwnerStacks) { - previousDebugTask = task.debugTask; - } - pushServerComponentStack(task, lazyNode._debugInfo); - } - if (!__DEV__ || task.componentStack === previousComponentStack) { - // TODO: Do we really need this stack frame? We don't on the client. - task.componentStack = createBuiltInComponentStack( - task, - 'Lazy', - null, - null, - ); - } let resolvedNode; if (__DEV__) { resolvedNode = callLazyInitInDEV(lazyNode); @@ -2754,14 +2562,6 @@ function renderNodeDestructive( const init = lazyNode._init; resolvedNode = init(payload); } - - // We restore the stack before rendering the resolved node because once the Lazy - // has resolved any future errors - task.componentStack = previousComponentStack; - if (__DEV__ && enableOwnerStacks) { - task.debugTask = previousDebugTask; - } - // Now we render the resolved node renderNodeDestructive(request, task, resolvedNode, childIndex); return; @@ -2813,15 +2613,6 @@ function renderNodeDestructive( // for new iterators, but we currently warn for rendering these // so needs some refactoring to deal with the warning. - // We need to push a component stack because if this suspends, we'll pop a stack. - const previousComponentStack = task.componentStack; - task.componentStack = createBuiltInComponentStack( - task, - 'AsyncIterable', - null, - null, - ); - // Restore the thenable state before resuming. const prevThenableState = task.thenableState; task.thenableState = null; @@ -2859,7 +2650,6 @@ function renderNodeDestructive( step = unwrapThenable(iterator.next()); } } - task.componentStack = previousComponentStack; renderChildrenArray(request, task, children, childIndex); return; } @@ -2879,19 +2669,12 @@ function renderNodeDestructive( // Clear any previous thenable state that was created by the unwrapping. task.thenableState = null; const thenable: Thenable = (maybeUsable: any); - const previousComponentStack = task.componentStack; - if (__DEV__) { - pushServerComponentStack(task, thenable._debugInfo); - } const result = renderNodeDestructive( request, task, unwrapThenable(thenable), childIndex, ); - if (__DEV__) { - task.componentStack = previousComponentStack; - } return result; } @@ -3069,8 +2852,8 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void { const parentOwner = parentStackFrame.owner; let currentComponentErrorInfo = ''; - if (parentOwner && typeof parentOwner.tag === 'number') { - const name = getComponentNameFromType((parentOwner: any).type); + if (parentOwner && typeof parentOwner.type !== 'undefined') { + const name = getComponentNameFromType(parentOwner.type); if (name) { currentComponentErrorInfo = '\n\nCheck the render method of `' + name + '`.'; @@ -3088,8 +2871,8 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void { let childOwnerAppendix = ''; if (childOwner != null && parentOwner !== childOwner) { let ownerName = null; - if (typeof childOwner.tag === 'number') { - ownerName = getComponentNameFromType((childOwner: any).type); + if (typeof childOwner.type !== 'undefined') { + ownerName = getComponentNameFromType(childOwner.type); } else if (typeof childOwner.name === 'string') { ownerName = childOwner.name; } @@ -3100,8 +2883,9 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void { } // We create a fake component stack for the child to log the stack trace from. + const previousComponentStack = task.componentStack; const stackFrame = createComponentStackFromType( - task, + task.componentStack, (child: any).type, (child: any)._owner, enableOwnerStacks ? (child: any)._debugStack : null, @@ -3113,7 +2897,7 @@ function warnForMissingKey(request: Request, task: Task, child: mixed): void { currentComponentErrorInfo, childOwnerAppendix, ); - task.componentStack = stackFrame.parent; + task.componentStack = previousComponentStack; } } @@ -3448,9 +3232,7 @@ function spawnNewSuspendedReplayTask( task.formatContext, task.context, task.treeContext, - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - task.componentStack !== null ? task.componentStack.parent : null, + task.componentStack, task.isFallback, !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, @@ -3495,9 +3277,7 @@ function spawnNewSuspendedRenderTask( task.formatContext, task.context, task.treeContext, - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - task.componentStack !== null ? task.componentStack.parent : null, + task.componentStack, task.isFallback, !disableLegacyContext ? task.legacyContext : emptyContextObject, __DEV__ && enableOwnerStacks ? task.debugTask : null, @@ -3525,6 +3305,8 @@ function renderNode( const previousKeyPath = task.keyPath; const previousTreeContext = task.treeContext; const previousComponentStack = task.componentStack; + const previousDebugTask = + __DEV__ && enableOwnerStacks ? task.debugTask : null; let x; // Store how much we've pushed at this point so we can reset it in case something // suspended partially through writing something. @@ -3569,6 +3351,9 @@ function renderNode( task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } // Restore all active ReactContexts to what they were before. switchContext(previousContext); return; @@ -3623,6 +3408,9 @@ function renderNode( task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } // Restore all active ReactContexts to what they were before. switchContext(previousContext); return; @@ -3659,6 +3447,9 @@ function renderNode( task.keyPath = previousKeyPath; task.treeContext = previousTreeContext; task.componentStack = previousComponentStack; + if (__DEV__ && enableOwnerStacks) { + task.debugTask = previousDebugTask; + } // Restore all active ReactContexts to what they were before. switchContext(previousContext); return; @@ -4196,7 +3987,7 @@ function retryRenderTask( // We call the destructive form that mutates this task. That way if something // suspends again, we can reuse the same task instead of spawning a new one. - renderNodeDestructive(request, task, task.node, task.childIndex); + retryNode(request, task); pushSegmentFinale( segment.chunks, request.renderState, @@ -4231,11 +4022,6 @@ function retryRenderTask( const ping = task.ping; x.then(ping, ping); task.thenableState = getThenableStateAfterSuspending(); - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - if (task.componentStack !== null) { - task.componentStack = task.componentStack.parent; - } return; } else if ( enablePostpone && @@ -4299,8 +4085,12 @@ function retryReplayTask(request: Request, task: ReplayTask): void { try { // We call the destructive form that mutates this task. That way if something // suspends again, we can reuse the same task instead of spawning a new one. - - renderNodeDestructive(request, task, task.node, task.childIndex); + if (typeof task.replay.slots === 'number') { + const resumeSegmentID = task.replay.slots; + resumeNode(request, task, resumeSegmentID, task.node, task.childIndex); + } else { + retryNode(request, task); + } if (task.replay.pendingTasks === 1 && task.replay.nodes.length > 0) { throw new Error( @@ -4332,11 +4122,6 @@ function retryReplayTask(request: Request, task: ReplayTask): void { const ping = task.ping; x.then(ping, ping); task.thenableState = getThenableStateAfterSuspending(); - // We pop one task off the stack because the node that suspended will be tried again, - // which will add it back onto the stack. - if (task.componentStack !== null) { - task.componentStack = task.componentStack.parent; - } return; } } diff --git a/scripts/babel/transform-prevent-infinite-loops.js b/scripts/babel/transform-prevent-infinite-loops.js index aa88377cc0..635526c14e 100644 --- a/scripts/babel/transform-prevent-infinite-loops.js +++ b/scripts/babel/transform-prevent-infinite-loops.js @@ -13,7 +13,7 @@ // This should be reasonable for all loops in the source. // Note that if the numbers are too large, the tests will take too long to fail // for this to be useful (each individual test case might hit an infinite loop). -const MAX_SOURCE_ITERATIONS = 5000; +const MAX_SOURCE_ITERATIONS = 6000; // Code in tests themselves is permitted to run longer. // For example, in the fuzz tester. const MAX_TEST_ITERATIONS = 5000; From 39e69dc665ef6f6dd1f9fe2f63348afb09694eab Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Tue, 9 Jul 2024 19:55:09 -0400 Subject: [PATCH 25/85] Dedupe legacy context warnings (#30299) Similar to other warnings about legacy APIs, only raise a warning once per component. --- ...erIntegrationLegacyContextDisabled-test.internal.js | 5 ++--- .../react-reconciler/src/ReactFiberClassComponent.js | 10 ++++++++-- packages/react-server/src/ReactFizzClassComponent.js | 10 ++++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js index 40ae0b6822..2202c8bf86 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContextDisabled-test.internal.js @@ -34,8 +34,7 @@ function initModules() { }; } -const {resetModules, itRenders, clientRenderOnBadMarkup} = - ReactDOMServerIntegrationUtils(initModules); +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules); function formatValue(val) { if (val === null) { @@ -105,7 +104,7 @@ describe('ReactDOMServerIntegrationLegacyContextDisabled', () => { , - render === clientRenderOnBadMarkup ? 4 : 3, + 3, ); expect(e.textContent).toBe('{}undefinedundefined'); expect(lifecycleContextLog).toEqual([]); diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index b1055c0161..4f7ef8530a 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -82,6 +82,8 @@ let didWarnAboutLegacyLifecyclesAndDerivedState; let didWarnAboutUndefinedDerivedState; let didWarnAboutDirectlyAssigningPropsToState; let didWarnAboutContextTypeAndContextTypes; +let didWarnAboutContextTypes; +let didWarnAboutChildContextTypes; let didWarnAboutInvalidateContextType; let didWarnOnInvalidCallback; @@ -93,6 +95,8 @@ if (__DEV__) { didWarnAboutDirectlyAssigningPropsToState = new Set(); didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutContextTypeAndContextTypes = new Set(); + didWarnAboutContextTypes = new Set(); + didWarnAboutChildContextTypes = new Set(); didWarnAboutInvalidateContextType = new Set(); didWarnOnInvalidCallback = new Set(); @@ -385,14 +389,16 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { } if (disableLegacyContext) { - if (ctor.childContextTypes) { + if (ctor.childContextTypes && !didWarnAboutChildContextTypes.has(ctor)) { + didWarnAboutChildContextTypes.add(ctor); console.error( '%s uses the legacy childContextTypes API which was removed in React 19. ' + 'Use React.createContext() instead.', name, ); } - if (ctor.contextTypes) { + if (ctor.contextTypes && !didWarnAboutContextTypes.has(ctor)) { + didWarnAboutContextTypes.add(ctor); console.error( '%s uses the legacy contextTypes API which was removed in React 19. ' + 'Use React.createContext() with static contextType instead.', diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js index 61e48d1afd..6ef87f100b 100644 --- a/packages/react-server/src/ReactFizzClassComponent.js +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -26,6 +26,8 @@ let didWarnAboutLegacyLifecyclesAndDerivedState; let didWarnAboutUndefinedDerivedState; let didWarnAboutDirectlyAssigningPropsToState; let didWarnAboutContextTypeAndContextTypes; +let didWarnAboutContextTypes; +let didWarnAboutChildContextTypes; let didWarnAboutInvalidateContextType; let didWarnOnInvalidCallback; @@ -36,6 +38,8 @@ if (__DEV__) { didWarnAboutDirectlyAssigningPropsToState = new Set(); didWarnAboutUndefinedDerivedState = new Set(); didWarnAboutContextTypeAndContextTypes = new Set(); + didWarnAboutContextTypes = new Set(); + didWarnAboutChildContextTypes = new Set(); didWarnAboutInvalidateContextType = new Set(); didWarnOnInvalidCallback = new Set(); } @@ -362,14 +366,16 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { } if (disableLegacyContext) { - if (ctor.childContextTypes) { + if (ctor.childContextTypes && !didWarnAboutChildContextTypes.has(ctor)) { + didWarnAboutChildContextTypes.add(ctor); console.error( '%s uses the legacy childContextTypes API which was removed in React 19. ' + 'Use React.createContext() instead.', name, ); } - if (ctor.contextTypes) { + if (ctor.contextTypes && !didWarnAboutContextTypes.has(ctor)) { + didWarnAboutContextTypes.add(ctor); console.error( '%s uses the legacy contextTypes API which was removed in React 19. ' + 'Use React.createContext() with static contextType instead.', From 3b2e5f27c5b72708677da27779852b9aa83ef909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 10 Jul 2024 00:15:00 -0400 Subject: [PATCH 26/85] [Fiber] Override the getCurrentStack temporarily while printing errors (#30300) Only for parent stacks. This ensures that we can use the regular mechanism for appending stack traces. E.g. you can polyfill it. This is mainly so that I can later remove the automatic appending. --- .../src/ReactFiberErrorLogger.js | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberErrorLogger.js b/packages/react-reconciler/src/ReactFiberErrorLogger.js index addb0aea43..59af033b56 100644 --- a/packages/react-reconciler/src/ReactFiberErrorLogger.js +++ b/packages/react-reconciler/src/ReactFiberErrorLogger.js @@ -95,7 +95,19 @@ export function defaultOnCaughtError( errorBoundaryName || 'Anonymous' }.`; - if (enableOwnerStacks) { + const prevGetCurrentStack = ReactSharedInternals.getCurrentStack; + if (!enableOwnerStacks) { + // The current Fiber is disconnected at this point which means that console printing + // cannot add a component stack since it terminates at the deletion node. This is not + // a problem for owner stacks which are not disconnected but for the parent component + // stacks we need to use the snapshot we've previously extracted. + const componentStack = + errorInfo.componentStack != null ? errorInfo.componentStack : ''; + ReactSharedInternals.getCurrentStack = function () { + return componentStack; + }; + } + try { if ( typeof error === 'object' && error !== null && @@ -123,21 +135,10 @@ export function defaultOnCaughtError( // We let our consoleWithStackDev wrapper add the component stack to the end. ); } - } else { - // The current Fiber is disconnected at this point which means that console printing - // cannot add a component stack since it terminates at the deletion node. This is not - // a problem for owner stacks which are not disconnected but for the parent component - // stacks we need to use the snapshot we've previously extracted. - const componentStack = - errorInfo.componentStack != null ? errorInfo.componentStack : ''; - // Don't transform to our wrapper - console['error']( - '%o\n\n%s\n\n%s\n%s', - error, - componentNameMessage, - recreateMessage, - componentStack, - ); + } finally { + if (!enableOwnerStacks) { + ReactSharedInternals.getCurrentStack = prevGetCurrentStack; + } } } else { // In production, we print the error directly. From 14fdd0e21c420deb4bb96fc1e9021b531543d15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 10 Jul 2024 00:15:15 -0400 Subject: [PATCH 27/85] [Flight] Serialize rate limited objects in console logs as marker an increase limit (#30294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This marker can then be emitted as a getter. When this object gets accessed we use a special error to let the user know what is going on. Screenshot 2024-07-08 at 10 13 46 PM When you click the `...`: Screenshot 2024-07-08 at 10 13 56 PM I also increased the object limit in console logs. It was arbitrarily set very low before. These limits are per message. So if you have a loop of many logs it can quickly add up a lot of strain on the server memory and the client. This is trying to find some tradeoff. Unfortunately we don't really do much deduping in these logs so with cyclic objects it ends up maximizing the limit and then siblings aren't logged. Ideally we should be able to lazy load them but that requires a lot of plumbing to wire up so if we can avoid it we should try to. If we want to that though one idea is to use the getter to do a sync XHR to load more data but the server needs to retain the objects in memory for an unknown amount of time. The client could maybe send a signal to retain them until a weakref clean up but even then it kind of needs a heartbeat to let the server know the client is still alive. That's a lot of complexity. There's probably more we can do to optimize deduping and other parts of the protocol to make it possible to have even higher limits. --- .../react-client/src/ReactFlightClient.js | 23 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 10 +++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index da835412f1..8243e67c51 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1231,6 +1231,29 @@ function parseModelString( } // Fallthrough } + case 'Y': { + if (__DEV__) { + // In DEV mode we encode omitted objects in logs as a getter that throws + // so that when you try to access it on the client, you know why that + // happened. + Object.defineProperty(parentObject, key, { + get: function () { + // We intentionally don't throw an error object here because it looks better + // without the stack in the console which isn't useful anyway. + // eslint-disable-next-line no-throw-literal + throw ( + 'This object has been omitted by React in the console log ' + + 'to avoid sending too much data from the server. Try logging smaller ' + + 'or more specific objects.' + ); + }, + enumerable: true, + configurable: false, + }); + return null; + } + // Fallthrough + } default: { // We assume that anything else is a reference ID. const ref = value.slice(1); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 491757f32c..a0d4b9e140 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1851,6 +1851,10 @@ function serializeSymbolReference(name: string): string { return '$S' + name; } +function serializeLimitedObject(): string { + return '$Y'; +} + function serializeNumber(number: number): string | number { if (Number.isFinite(number)) { if (number === 0 && 1 / number === -Infinity) { @@ -3181,10 +3185,10 @@ function renderConsoleValue( } } - if (counter.objectCount > 20) { + if (counter.objectCount > 500) { // We've reached our max number of objects to serialize across the wire so we serialize this - // object but no properties inside of it, as a place holder. - return Array.isArray(value) ? [] : {}; + // as a marker so that the client can error when this is accessed by the console. + return serializeLimitedObject(); } counter.objectCount++; From 8ae78f39fbd90f4a2c26c4be36030732b435192e Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Wed, 10 Jul 2024 10:33:13 -0400 Subject: [PATCH 28/85] CI: reduce job waterfall for runtime test jobs (#30303) The first job was just printing static JSON. This simplifies the job config a bit and allows the test jobs to start earlier. --- .github/workflows/runtime_test.yml | 84 ++++++++++-------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/.github/workflows/runtime_test.yml b/.github/workflows/runtime_test.yml index 118a520ad5..90ead47361 100644 --- a/.github/workflows/runtime_test.yml +++ b/.github/workflows/runtime_test.yml @@ -7,66 +7,38 @@ on: paths-ignore: - 'compiler/**' -env: - # Number of workers (one per shard) to spawn - SHARD_COUNT: 5 - jobs: - # Define the various test parameters and parallelism for this workflow - build_test_params: - name: Build test params - runs-on: ubuntu-latest - outputs: - params: ${{ steps.define-params.outputs.result }} - shard_id: ${{ steps.define-shards.outputs.result }} - steps: - - uses: actions/github-script@v7 - id: define-shards - with: - script: | - function range(from, to) { - const arr = []; - for (let n = from; n <= to; n++) { - arr.push(n); - } - return arr; - } - return range(1, process.env.SHARD_COUNT); - - uses: actions/github-script@v7 - id: define-params - with: - script: | - return [ - "-r=stable --env=development", - "-r=stable --env=production", - "-r=experimental --env=development", - "-r=experimental --env=production", - "-r=www-classic --env=development --variant=false", - "-r=www-classic --env=production --variant=false", - "-r=www-classic --env=development --variant=true", - "-r=www-classic --env=production --variant=true", - "-r=www-modern --env=development --variant=false", - "-r=www-modern --env=production --variant=false", - "-r=www-modern --env=development --variant=true", - "-r=www-modern --env=production --variant=true", - "-r=xplat --env=development --variant=false", - "-r=xplat --env=development --variant=true", - "-r=xplat --env=production --variant=false", - "-r=xplat --env=production --variant=true", - // TODO: Test more persistent configurations? - "-r=stable --env=development --persistent", - "-r=experimental --env=development --persistent" - ]; - - # Spawn a job for each shard for a given set of test params test: - name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard_id }}) + name: yarn test ${{ matrix.params }} (Shard ${{ matrix.shard }}) runs-on: ubuntu-latest - needs: build_test_params strategy: matrix: - params: ${{ fromJSON(needs.build_test_params.outputs.params) }} - shard_id: ${{ fromJSON(needs.build_test_params.outputs.shard_id) }} + params: + - "-r=stable --env=development" + - "-r=stable --env=production" + - "-r=experimental --env=development" + - "-r=experimental --env=production" + - "-r=www-classic --env=development --variant=false" + - "-r=www-classic --env=production --variant=false" + - "-r=www-classic --env=development --variant=true" + - "-r=www-classic --env=production --variant=true" + - "-r=www-modern --env=development --variant=false" + - "-r=www-modern --env=production --variant=false" + - "-r=www-modern --env=development --variant=true" + - "-r=www-modern --env=production --variant=true" + - "-r=xplat --env=development --variant=false" + - "-r=xplat --env=development --variant=true" + - "-r=xplat --env=production --variant=false" + - "-r=xplat --env=production --variant=true" + # TODO: Test more persistent configurations? + - "-r=stable --env=development --persistent" + - "-r=experimental --env=development --persistent" + shard: + - 1/5 + - 2/5 + - 3/5 + - 4/5 + - 5/5 continue-on-error: true steps: - uses: actions/checkout@v4 @@ -82,4 +54,4 @@ jobs: path: "**/node_modules" key: ${{ runner.arch }}-${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }} - run: yarn install --frozen-lockfile - - run: yarn test ${{ matrix.params }} --ci=github --shard=${{ matrix.shard_id }}/${{ env.SHARD_COUNT }} + - run: yarn test ${{ matrix.params }} --ci=github --shard=${{ matrix.shard }} From 378b305958eb7259cacfce8ad0e66eec07e07074 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Wed, 10 Jul 2024 11:53:00 -0400 Subject: [PATCH 29/85] Warn about legacy context when legacy context is not disabled (#30297) For environments that still have legacy contexts available, this adds a warning to make the remaining call sites easier to locate and encourage upgrades. --- .../src/__tests__/ReactDOMFiber-test.js | 10 ++- .../src/__tests__/ReactDOMFizzServer-test.js | 20 +++-- .../src/__tests__/ReactDOMLegacyFiber-test.js | 22 ++++- ...tDOMServerIntegrationLegacyContext-test.js | 15 +++- .../ReactErrorBoundaries-test.internal.js | 7 +- .../__tests__/ReactFunctionComponent-test.js | 12 ++- .../ReactLegacyCompositeComponent-test.js | 88 ++++++++++++++----- ...eactLegacyErrorBoundaries-test.internal.js | 3 + .../__tests__/ReactServerRendering-test.js | 16 ++-- .../ReactNativeEvents-test.internal.js | 44 +++++----- .../src/ReactFiberBeginWork.js | 43 ++++++--- .../src/ReactFiberClassComponent.js | 22 ++++- .../src/__tests__/ReactIncremental-test.js | 74 ++++++++++++++-- ...tIncrementalErrorHandling-test.internal.js | 9 +- .../src/__tests__/ReactNewContext-test.js | 14 ++- .../src/ReactFizzClassComponent.js | 24 ++++- packages/react-server/src/ReactFizzServer.js | 39 +++++--- .../ReactCoffeeScriptClass-test.coffee | 16 +++- .../__tests__/ReactContextValidator-test.js | 68 ++++++++------ .../react/src/__tests__/ReactES6Class-test.js | 11 +++ .../src/__tests__/ReactStrictMode-test.js | 37 ++++++-- .../__tests__/ReactTypeScriptClass-test.ts | 12 ++- .../createReactClassIntegration-test.js | 7 +- 23 files changed, 464 insertions(+), 149 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index a1633fa1e6..7c701219ec 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -13,10 +13,12 @@ let React; let ReactDOM; let PropTypes; let ReactDOMClient; -let root; let Scheduler; + let act; +let assertConsoleErrorDev; let assertLog; +let root; describe('ReactDOMFiber', () => { let container; @@ -29,7 +31,7 @@ describe('ReactDOMFiber', () => { ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); act = require('internal-test-utils').act; - assertLog = require('internal-test-utils').assertLog; + ({assertConsoleErrorDev, assertLog} = require('internal-test-utils')); container = document.createElement('div'); document.body.appendChild(container); @@ -732,6 +734,10 @@ describe('ReactDOMFiber', () => { await act(async () => { root.render(); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('
bar
'); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index d534df76a5..0320272b9c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -27,6 +27,8 @@ let ReactDOMFizzServer; let ReactDOMFizzStatic; let Suspense; let SuspenseList; + +let assertConsoleErrorDev; let useSyncExternalStore; let useSyncExternalStoreWithSelector; let use; @@ -116,12 +118,14 @@ describe('ReactDOMFizzServer', () => { useActionState = React.useActionState; } - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; - waitFor = InternalTestUtils.waitFor; - waitForPaint = InternalTestUtils.waitForPaint; - assertLog = InternalTestUtils.assertLog; - clientAct = InternalTestUtils.act; + ({ + assertConsoleErrorDev, + assertLog, + act: clientAct, + waitFor, + waitForAll, + waitForPaint, + } = require('internal-test-utils')); if (gate(flags => flags.source)) { // The `with-selector` module composes the main `use-sync-external-store` @@ -1950,6 +1954,10 @@ describe('ReactDOMFizzServer', () => { ); pipe(writable); }); + assertConsoleErrorDev([ + 'TestProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'TestConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(getVisibleChildren(container)).toEqual(
Loading: A diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js index b81dea9f18..c799e3b904 100644 --- a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js @@ -786,7 +786,12 @@ describe('ReactDOMLegacyFiber', () => { } } - ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(container.innerHTML).toBe(''); expect(portalContainer.innerHTML).toBe('
bar
'); }); @@ -829,7 +834,13 @@ describe('ReactDOMLegacyFiber', () => { } } - const instance = ReactDOM.render(, container); + let instance; + expect(() => { + instance = ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(portalContainer.innerHTML).toBe('
initial-initial
'); expect(container.innerHTML).toBe(''); instance.setState({bar: 'changed'}); @@ -871,7 +882,12 @@ describe('ReactDOMLegacyFiber', () => { } } - ReactDOM.render(, container); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(portalContainer.innerHTML).toBe('
initial-initial
'); expect(container.innerHTML).toBe(''); ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js index c28bf2d8e4..130bd2310e 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationLegacyContext-test.js @@ -80,6 +80,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe('purple'); }); @@ -94,6 +95,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe('purple'); }); @@ -110,6 +112,7 @@ describe('ReactDOMServerIntegration', () => { , + 1, ); expect(e.textContent).toBe(''); }); @@ -124,6 +127,7 @@ describe('ReactDOMServerIntegration', () => { , + 1, ); expect(e.textContent).toBe(''); }); @@ -141,6 +145,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe(''); }); @@ -158,6 +163,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe(''); }); @@ -174,6 +180,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe('purple'); }); @@ -190,6 +197,7 @@ describe('ReactDOMServerIntegration', () => { , + 2, ); expect(e.textContent).toBe('red'); }); @@ -228,7 +236,7 @@ describe('ReactDOMServerIntegration', () => { text2: PropTypes.string, }; - const e = await render(); + const e = await render(, 3); expect(e.querySelector('#first').textContent).toBe('purple'); expect(e.querySelector('#second').textContent).toBe('red'); }); @@ -254,7 +262,7 @@ describe('ReactDOMServerIntegration', () => { }; Child.contextTypes = {text: PropTypes.string}; - const e = await render(); + const e = await render(, 2); expect(e.textContent).toBe('foo'); }, ); @@ -278,7 +286,8 @@ describe('ReactDOMServerIntegration', () => { } const e = await render( , - render === clientRenderOnBadMarkup ? 2 : 1, + // Some warning is not de-duped and logged again on the client retry render. + render === clientRenderOnBadMarkup ? 3 : 2, ); expect(e.textContent).toBe('nope'); }, diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js index f45069e915..2d7696eee5 100644 --- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js @@ -36,6 +36,7 @@ describe('ReactErrorBoundaries', () => { let RetryErrorBoundary; let Normal; let assertLog; + let assertConsoleErrorDev; beforeEach(() => { jest.useFakeTimers(); @@ -47,8 +48,7 @@ describe('ReactErrorBoundaries', () => { act = require('internal-test-utils').act; Scheduler = require('scheduler'); - const InternalTestUtils = require('internal-test-utils'); - assertLog = InternalTestUtils.assertLog; + ({assertLog, assertConsoleErrorDev} = require('internal-test-utils')); BrokenConstructor = class extends React.Component { constructor(props) { @@ -895,6 +895,9 @@ describe('ReactErrorBoundaries', () => { , ); }); + assertConsoleErrorDev([ + 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); diff --git a/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js index d3b8926ecd..d2b1aaa434 100644 --- a/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactFunctionComponent-test.js @@ -13,6 +13,7 @@ let PropTypes; let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; function FunctionComponent(props) { return
{props.name}
; @@ -24,7 +25,7 @@ describe('ReactFunctionComponent', () => { PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('should render stateless component', async () => { @@ -109,6 +110,11 @@ describe('ReactFunctionComponent', () => { root.render(); }); + assertConsoleErrorDev([ + 'GrandParent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); + expect(el.textContent).toBe('test'); await act(() => { @@ -472,6 +478,10 @@ describe('ReactFunctionComponent', () => { await act(() => { root.render(); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + ]); expect(el.textContent).toBe('en'); }); diff --git a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js index eaf49b7527..cb8b07e72b 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js @@ -14,7 +14,9 @@ let ReactDOM; let findDOMNode; let ReactDOMClient; let PropTypes; + let act; +let assertConsoleErrorDev; describe('ReactLegacyCompositeComponent', () => { beforeEach(() => { @@ -26,7 +28,7 @@ describe('ReactLegacyCompositeComponent', () => { ReactDOM.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE .findDOMNode; PropTypes = require('prop-types'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); // @gate !disableLegacyMode @@ -119,6 +121,10 @@ describe('ReactLegacyCompositeComponent', () => { await act(() => { root.render( (component = current)} />); }); + assertConsoleErrorDev([ + 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(findDOMNode(component).innerHTML).toBe('bar'); }); @@ -183,6 +189,11 @@ describe('ReactLegacyCompositeComponent', () => { expect(parentInstance.state.flag).toBe(false); expect(childInstance.context).toEqual({foo: 'bar', flag: false}); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); + await act(() => { parentInstance.setState({flag: true}); }); @@ -242,6 +253,11 @@ describe('ReactLegacyCompositeComponent', () => { root.render( (wrapper = current)} />); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); + expect(wrapper.parentRef.current.state.flag).toEqual(true); expect(wrapper.childRef.current.context).toEqual({flag: true}); @@ -317,6 +333,13 @@ describe('ReactLegacyCompositeComponent', () => { root.render(); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Grandchild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1}); }); @@ -369,6 +392,9 @@ describe('ReactLegacyCompositeComponent', () => { await act(() => { root.render( (parentInstance = current)} />); }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); expect(childInstance).toBeNull(); @@ -376,6 +402,10 @@ describe('ReactLegacyCompositeComponent', () => { await act(() => { parentInstance.setState({flag: true}); }); + assertConsoleErrorDev([ + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); + expect(parentInstance.state.flag).toBe(true); expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); @@ -435,7 +465,12 @@ describe('ReactLegacyCompositeComponent', () => { } const div = document.createElement('div'); - ReactDOM.render(, div); + expect(() => { + ReactDOM.render(, div); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Leaf uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(div.children[0].innerHTML).toBe('noise'); div.children[0].innerHTML = 'aliens'; div.children[0].id = 'aliens'; @@ -537,20 +572,26 @@ describe('ReactLegacyCompositeComponent', () => { const div = document.createElement('div'); let parentInstance = null; - ReactDOM.render( - (parentInstance = inst)}> - - A1 - A2 - + expect(() => { + ReactDOM.render( + (parentInstance = inst)}> + + A1 + A2 + - - B1 - B2 - - , - div, - ); + + B1 + B2 + + , + div, + ); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'GrandChild uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'ChildWithContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); parentInstance.setState({ foo: 'def', @@ -733,12 +774,17 @@ describe('ReactLegacyCompositeComponent', () => { } const div = document.createElement('div'); - ReactDOM.render( - - - , - div, - ); + expect(() => { + ReactDOM.render( + + + , + div, + ); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); it('should replace state in legacy mode', async () => { diff --git a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js index f45fae7bbd..2444900ec6 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactLegacyErrorBoundaries-test.internal.js @@ -849,6 +849,9 @@ describe('ReactLegacyErrorBoundaries', () => { , container, ); + assertConsoleErrorDev([ + 'BrokenComponentWillMountWithContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); expect(container.firstChild.textContent).toBe('Caught an error: Hello.'); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index 065f7cadd7..71e6ce7224 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -371,11 +371,17 @@ describe('ReactDOMServer', () => { text: PropTypes.string, }; - const markup = ReactDOMServer.renderToStaticMarkup( - - - , - ); + let markup; + expect(() => { + markup = ReactDOMServer.renderToStaticMarkup( + + + , + ); + }).toErrorDev([ + 'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(markup).toContain('hello, world'); }); diff --git a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js index ed0521bede..46b2ad9cf1 100644 --- a/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js +++ b/packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js @@ -227,27 +227,31 @@ test('handles events on text nodes', () => { } const log = []; - ReactNative.render( - - - log.push('string touchend')} - onTouchEndCapture={() => log.push('string touchend capture')} - onTouchStart={() => log.push('string touchstart')} - onTouchStartCapture={() => log.push('string touchstart capture')}> - Text Content + expect(() => { + ReactNative.render( + + + log.push('string touchend')} + onTouchEndCapture={() => log.push('string touchend capture')} + onTouchStart={() => log.push('string touchstart')} + onTouchStartCapture={() => log.push('string touchstart capture')}> + Text Content + + log.push('number touchend')} + onTouchEndCapture={() => log.push('number touchend capture')} + onTouchStart={() => log.push('number touchstart')} + onTouchStartCapture={() => log.push('number touchstart capture')}> + {123} + - log.push('number touchend')} - onTouchEndCapture={() => log.push('number touchend capture')} - onTouchStart={() => log.push('number touchstart')} - onTouchStartCapture={() => log.push('number touchstart capture')}> - {123} - - - , - 1, - ); + , + 1, + ); + }).toErrorDev([ + 'ContextHack uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); expect(UIManager.createView).toHaveBeenCalledTimes(5); diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 5c759d9a52..d53ee83777 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -316,6 +316,7 @@ let didReceiveUpdate: boolean = false; let didWarnAboutBadClass; let didWarnAboutContextTypeOnFunctionComponent; +let didWarnAboutContextTypes; let didWarnAboutGetDerivedStateOnFunctionComponent; let didWarnAboutFunctionRefs; export let didWarnAboutReassigningProps: boolean; @@ -326,6 +327,7 @@ let didWarnAboutDefaultPropsOnFunctionComponent; if (__DEV__) { didWarnAboutBadClass = ({}: {[string]: boolean}); didWarnAboutContextTypeOnFunctionComponent = ({}: {[string]: boolean}); + didWarnAboutContextTypes = ({}: {[string]: boolean}); didWarnAboutGetDerivedStateOnFunctionComponent = ({}: {[string]: boolean}); didWarnAboutFunctionRefs = ({}: {[string]: boolean}); didWarnAboutReassigningProps = false; @@ -1130,12 +1132,27 @@ function updateFunctionComponent( // in updateFuntionComponent but only on mount validateFunctionComponentInDev(workInProgress, workInProgress.type); - if (disableLegacyContext && Component.contextTypes) { - console.error( - '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with React.useContext() instead.', - getComponentNameFromType(Component) || 'Unknown', - ); + if (Component.contextTypes) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + + if (!didWarnAboutContextTypes[componentName]) { + didWarnAboutContextTypes[componentName] = true; + if (disableLegacyContext) { + console.error( + '%s uses the legacy contextTypes API which was removed in React 19. ' + + 'Use React.createContext() with React.useContext() instead. ' + + '(https://react.dev/link/legacy-context)', + componentName, + ); + } else { + console.error( + '%s uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead. ' + + '(https://react.dev/link/legacy-context)', + componentName, + ); + } + } } } } @@ -1923,14 +1940,12 @@ function mountIncompleteClassComponent( function validateFunctionComponentInDev(workInProgress: Fiber, Component: any) { if (__DEV__) { - if (Component) { - if (Component.childContextTypes) { - console.error( - 'childContextTypes cannot be defined on a function component.\n' + - ' %s.childContextTypes = ...', - Component.displayName || Component.name || 'Component', - ); - } + if (Component && Component.childContextTypes) { + console.error( + 'childContextTypes cannot be defined on a function component.\n' + + ' %s.childContextTypes = ...', + Component.displayName || Component.name || 'Component', + ); } if (!enableRefAsProp && workInProgress.ref !== null) { let info = ''; diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 4f7ef8530a..f1419d1aad 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -393,7 +393,7 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { didWarnAboutChildContextTypes.add(ctor); console.error( '%s uses the legacy childContextTypes API which was removed in React 19. ' + - 'Use React.createContext() instead.', + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)', name, ); } @@ -401,7 +401,8 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { didWarnAboutContextTypes.add(ctor); console.error( '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with static contextType instead.', + 'Use React.createContext() with static contextType instead. ' + + '(https://react.dev/link/legacy-context)', name, ); } @@ -426,6 +427,23 @@ function checkClassInstance(workInProgress: Fiber, ctor: any, newProps: any) { name, ); } + if (ctor.childContextTypes && !didWarnAboutChildContextTypes.has(ctor)) { + didWarnAboutChildContextTypes.add(ctor); + console.error( + '%s uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)', + name, + ); + } + if (ctor.contextTypes && !didWarnAboutContextTypes.has(ctor)) { + didWarnAboutContextTypes.add(ctor); + console.error( + '%s uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. ' + + '(https://react.dev/link/legacy-context)', + name, + ); + } } if (typeof instance.componentShouldUpdate === 'function') { diff --git a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js index 2ce110bebe..399e4c01b9 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncremental-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncremental-test.js @@ -14,6 +14,8 @@ let React; let ReactNoop; let Scheduler; let PropTypes; + +let assertConsoleErrorDev; let waitForAll; let waitFor; let waitForThrow; @@ -27,11 +29,13 @@ describe('ReactIncremental', () => { Scheduler = require('scheduler'); PropTypes = require('prop-types'); - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; - waitFor = InternalTestUtils.waitFor; - waitForThrow = InternalTestUtils.waitForThrow; - assertLog = InternalTestUtils.assertLog; + ({ + assertConsoleErrorDev, + waitForAll, + waitFor, + waitForThrow, + assertLog, + } = require('internal-test-utils')); }); // Note: This is based on a similar component we use in www. We can delete @@ -1793,6 +1797,11 @@ describe('ReactIncremental', () => { 'ShowLocale {"locale":"fr"}', 'ShowBoth {"locale":"fr"}', ]); + assertConsoleErrorDev([ + 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'ShowBoth uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + ]); ReactNoop.render( @@ -1843,6 +1852,10 @@ describe('ReactIncremental', () => { 'ShowBoth {"locale":"en","route":"/about"}', 'ShowBoth {"locale":"en"}', ]); + assertConsoleErrorDev([ + 'Router uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ShowRoute uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); // @gate !disableLegacyContext @@ -1876,6 +1889,10 @@ describe('ReactIncremental', () => { 'Recurse {"n":1}', 'Recurse {"n":0}', ]); + assertConsoleErrorDev([ + 'Recurse uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Recurse uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); // @gate enableLegacyHidden && !disableLegacyContext @@ -1925,6 +1942,10 @@ describe('ReactIncremental', () => { 'ShowLocale {"locale":"fr"}', 'ShowLocale {"locale":"fr"}', ]); + assertConsoleErrorDev([ + 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ShowLocale uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); await waitForAll([ 'ShowLocale {"locale":"fr"}', @@ -2012,6 +2033,11 @@ describe('ReactIncremental', () => { 'ShowLocaleClass:read {"locale":"fr"}', 'ShowLocaleFn:read {"locale":"fr"}', ]); + assertConsoleErrorDev([ + 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + ]); statefulInst.setState({x: 1}); await waitForAll([]); @@ -2098,6 +2124,12 @@ describe('ReactIncremental', () => { 'ShowLocaleFn:read {"locale":"fr"}', ]); + assertConsoleErrorDev([ + 'Intl uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ShowLocaleClass uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'ShowLocaleFn uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + ]); + statefulInst.setState({locale: 'gr'}); await waitForAll([ // Intl is below setState() so it might have been @@ -2154,6 +2186,10 @@ describe('ReactIncremental', () => { ReactNoop.render(); await waitForAll([]); + assertConsoleErrorDev([ + 'Child uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); + // Trigger an update in the middle of the tree instance.setState({}); await waitForAll([]); @@ -2199,7 +2235,9 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - await waitForAll([]); + await expect(async () => await waitForAll([])).toErrorDev([ + 'ContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); // Trigger an update in the middle of the tree // This is necessary to reproduce the error as it currently exists. @@ -2252,6 +2290,10 @@ describe('ReactIncremental', () => { 'render', 'componentDidUpdate', ]); + + assertConsoleErrorDev([ + 'MyComponent uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); // eslint-disable-next-line jest/no-disabled-tests @@ -2384,6 +2426,10 @@ describe('ReactIncremental', () => { ); await waitForAll(['count:0']); + assertConsoleErrorDev([ + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); instance.updateCount(); await waitForAll(['count:1']); }); @@ -2440,6 +2486,11 @@ describe('ReactIncremental', () => { ); await waitForAll(['count:0']); + assertConsoleErrorDev([ + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); instance.updateCount(); await waitForAll(['count:1']); }); @@ -2505,6 +2556,11 @@ describe('ReactIncremental', () => { ); await waitForAll(['count:0']); + assertConsoleErrorDev([ + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); instance.updateCount(); await waitForAll([]); }); @@ -2580,6 +2636,11 @@ describe('ReactIncremental', () => { ); await waitForAll(['count:0, name:brian']); + assertConsoleErrorDev([ + 'TopContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'MiddleContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Child uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); topInstance.updateCount(); await waitForAll([]); middleInstance.updateName('not brian'); @@ -2685,6 +2746,7 @@ describe('ReactIncremental', () => { await expect(async () => { await waitForAll([]); }).toErrorDev([ + 'Boundary uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', 'Legacy context API has been detected within a strict-mode tree', ]); } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index 0cee8150cb..91658c562c 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1211,7 +1211,14 @@ describe('ReactIncrementalErrorHandling', () => { , ); - await waitForAll([]); + + await expect(async () => { + await waitForAll([]); + }).toErrorDev([ + 'Provider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Provider uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Connector uses the legacy contextTypes API which will be removed soon. Use React.createContext() with React.useContext() instead.', + ]); // If the context stack does not unwind, span will get 'abcde' expect(ReactNoop).toMatchRenderedOutput(); diff --git a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js index 59d88a9ffa..b8fbca8339 100644 --- a/packages/react-reconciler/src/__tests__/ReactNewContext-test.js +++ b/packages/react-reconciler/src/__tests__/ReactNewContext-test.js @@ -17,6 +17,7 @@ let gen; let waitForAll; let waitFor; let waitForThrow; +let assertConsoleErrorDev; describe('ReactNewContext', () => { beforeEach(() => { @@ -28,10 +29,12 @@ describe('ReactNewContext', () => { Scheduler = require('scheduler'); gen = require('random-seed'); - const InternalTestUtils = require('internal-test-utils'); - waitForAll = InternalTestUtils.waitForAll; - waitFor = InternalTestUtils.waitFor; - waitForThrow = InternalTestUtils.waitForThrow; + ({ + waitForAll, + waitFor, + waitForThrow, + assertConsoleErrorDev, + } = require('internal-test-utils')); }); afterEach(() => { @@ -1032,6 +1035,9 @@ describe('ReactNewContext', () => { , ); await waitForAll(['LegacyProvider', 'App', 'Child']); + assertConsoleErrorDev([ + 'LegacyProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + ]); expect(ReactNoop).toMatchRenderedOutput(); // Update App with same value (should bail out) diff --git a/packages/react-server/src/ReactFizzClassComponent.js b/packages/react-server/src/ReactFizzClassComponent.js index 6ef87f100b..08b4aaa94c 100644 --- a/packages/react-server/src/ReactFizzClassComponent.js +++ b/packages/react-server/src/ReactFizzClassComponent.js @@ -370,7 +370,7 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { didWarnAboutChildContextTypes.add(ctor); console.error( '%s uses the legacy childContextTypes API which was removed in React 19. ' + - 'Use React.createContext() instead.', + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)', name, ); } @@ -378,7 +378,8 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { didWarnAboutContextTypes.add(ctor); console.error( '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with static contextType instead.', + 'Use React.createContext() with static contextType instead. ' + + '(https://react.dev/link/legacy-context)', name, ); } @@ -386,7 +387,7 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { if (instance.contextTypes) { console.error( 'contextTypes was defined as an instance property on %s. Use a static ' + - 'property to define contextTypes instead.', + 'property to define contextTypes instead. (https://react.dev/link/legacy-context)', name, ); } @@ -403,6 +404,23 @@ function checkClassInstance(instance: any, ctor: any, newProps: any) { name, ); } + if (ctor.childContextTypes && !didWarnAboutChildContextTypes.has(ctor)) { + didWarnAboutChildContextTypes.add(ctor); + console.error( + '%s uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)', + name, + ); + } + if (ctor.contextTypes && !didWarnAboutContextTypes.has(ctor)) { + didWarnAboutContextTypes.add(ctor); + console.error( + '%s uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. ' + + '(https://react.dev/link/legacy-context)', + name, + ); + } } if (typeof instance.componentShouldUpdate === 'function') { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index afdc9efbb2..ce255e58a8 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1638,6 +1638,7 @@ function renderClassComponent( } const didWarnAboutBadClass: {[string]: boolean} = {}; +const didWarnAboutContextTypes: {[string]: boolean} = {}; const didWarnAboutContextTypeOnFunctionComponent: {[string]: boolean} = {}; const didWarnAboutGetDerivedStateOnFunctionComponent: {[string]: boolean} = {}; let didWarnAboutReassigningProps = false; @@ -1688,12 +1689,24 @@ function renderFunctionComponent( const actionStateMatchingIndex = getActionStateMatchingIndex(); if (__DEV__) { - if (disableLegacyContext && Component.contextTypes) { - console.error( - '%s uses the legacy contextTypes API which was removed in React 19. ' + - 'Use React.createContext() with React.useContext() instead.', - getComponentNameFromType(Component) || 'Unknown', - ); + if (Component.contextTypes) { + const componentName = getComponentNameFromType(Component) || 'Unknown'; + if (!didWarnAboutContextTypes[componentName]) { + didWarnAboutContextTypes[componentName] = true; + if (disableLegacyContext) { + console.error( + '%s uses the legacy contextTypes API which was removed in React 19. ' + + 'Use React.createContext() with React.useContext() instead.', + componentName, + ); + } else { + console.error( + '%s uses the legacy contextTypes API which will be removed soon. ' + + 'Use React.createContext() with React.useContext() instead.', + componentName, + ); + } + } } } if (__DEV__) { @@ -1771,14 +1784,12 @@ function finishFunctionComponent( function validateFunctionComponentInDev(Component: any): void { if (__DEV__) { - if (Component) { - if (Component.childContextTypes) { - console.error( - 'childContextTypes cannot be defined on a function component.\n' + - ' %s.childContextTypes = ...', - Component.displayName || Component.name || 'Component', - ); - } + if (Component && Component.childContextTypes) { + console.error( + 'childContextTypes cannot be defined on a function component.\n' + + ' %s.childContextTypes = ...', + Component.displayName || Component.name || 'Component', + ); } if ( diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 4a4d07a78e..22ef789f4f 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -254,7 +254,12 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Foo - test React.createElement(Outer), 'SPAN', 'foo' + expect(-> + test React.createElement(Outer), 'SPAN', 'foo' + ).toErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]) it 'renders only once when setting state in componentWillMount', -> renderCount = 0 @@ -537,7 +542,14 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Bar - test React.createElement(Foo), 'DIV', 'bar-through-context' + expect(-> + test React.createElement(Foo), 'DIV', 'bar-through-context' + ).toErrorDev( + [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ], + ) if !featureFlags.disableStringRefs it 'supports string refs', -> diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index cbe9cb2641..3f24e5bed8 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -66,11 +66,16 @@ describe('ReactContextValidator', () => { let instance; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - (instance = current)} />, - ); - }); + await expect(async () => { + await act(() => { + root.render( + (instance = current)} />, + ); + }); + }).toErrorDev([ + 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(instance.childRef.current.context).toEqual({foo: 'abc'}); }); @@ -139,9 +144,14 @@ describe('ReactContextValidator', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); + await expect(async () => { + await act(() => { + root.render(); + }); + }).toErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(constructorContext).toEqual({foo: 'abc'}); expect(renderContext).toEqual({foo: 'abc'}); @@ -187,11 +197,10 @@ describe('ReactContextValidator', () => { await act(() => { root.render(); }); - }).toErrorDev( - 'ComponentA.childContextTypes is specified but there is no ' + - 'getChildContext() method on the instance. You can either define ' + - 'getChildContext() on ComponentA or remove childContextTypes from it.', - ); + }).toErrorDev([ + 'ComponentA uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentA or remove childContextTypes from it.', + ]); // Warnings should be deduped by component type let container = document.createElement('div'); @@ -206,11 +215,10 @@ describe('ReactContextValidator', () => { await act(() => { root.render(); }); - }).toErrorDev( - 'ComponentB.childContextTypes is specified but there is no ' + - 'getChildContext() method on the instance. You can either define ' + - 'getChildContext() on ComponentB or remove childContextTypes from it.', - ); + }).toErrorDev([ + 'ComponentB uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentB or remove childContextTypes from it.', + ]); }); // TODO (bvaughn) Remove this test and the associated behavior in the future. @@ -259,9 +267,10 @@ describe('ReactContextValidator', () => { root.render(); }); }).toErrorDev([ - 'MiddleMissingContext.childContextTypes is specified but there is no ' + - 'getChildContext() method on the instance. You can either define getChildContext() ' + - 'on MiddleMissingContext or remove childContextTypes from it.', + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.', + 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', ]); expect(childContext.bar).toBeUndefined(); expect(childContext.foo).toBe('FOO'); @@ -435,10 +444,11 @@ describe('ReactContextValidator', () => { , ); }); - }).toErrorDev( - 'ComponentA declares both contextTypes and contextType static properties. ' + - 'The legacy contextTypes property will be ignored.', - ); + }).toErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead', + 'ComponentA uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'ComponentA declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', + ]); // Warnings should be deduped by component type let container = document.createElement('div'); @@ -461,10 +471,10 @@ describe('ReactContextValidator', () => { , ); }); - }).toErrorDev( - 'ComponentB declares both contextTypes and contextType static properties. ' + - 'The legacy contextTypes property will be ignored.', - ); + }).toErrorDev([ + 'ComponentB declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', + 'ComponentB uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); // @gate enableRenderableContext || !__DEV__ diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index 769bf5b9a5..3ac0b18e8e 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -13,6 +13,7 @@ let PropTypes; let React; let ReactDOM; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactES6Class', () => { let container; @@ -30,6 +31,7 @@ describe('ReactES6Class', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ({assertConsoleErrorDev} = require('internal-test-utils')); container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; @@ -287,6 +289,11 @@ describe('ReactES6Class', () => { className: PropTypes.string, }; runTest(, 'SPAN', 'foo'); + + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); } @@ -579,6 +586,10 @@ describe('ReactES6Class', () => { } Foo.childContextTypes = {bar: PropTypes.string}; runTest(, 'DIV', 'bar-through-context'); + assertConsoleErrorDev([ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); }); } diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index 6a887fc9d9..2ab9c3fa18 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -18,6 +18,7 @@ let act; let useMemo; let useState; let useReducer; +let assertConsoleErrorDev; const ReactFeatureFlags = require('shared/ReactFeatureFlags'); @@ -28,7 +29,7 @@ describe('ReactStrictMode', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); useMemo = React.useMemo; useState = React.useState; useReducer = React.useReducer; @@ -1072,11 +1073,33 @@ describe('context legacy', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( + await act(() => { + root.render(); + }); + + assertConsoleErrorDev([ + 'LegacyContextProvider uses the legacy childContextTypes API ' + + 'which will soon be removed. Use React.createContext() instead. ' + + '(https://react.dev/link/legacy-context)' + + '\n in LegacyContextProvider (at **)' + + '\n in div (at **)' + + '\n in Root (at **)', + 'LegacyContextConsumer uses the legacy contextTypes API which ' + + 'will soon be removed. Use React.createContext() with static ' + + 'contextType instead. (https://react.dev/link/legacy-context)' + + '\n in LegacyContextConsumer (at **)' + + '\n in div (at **)' + + '\n in LegacyContextProvider (at **)' + + '\n in div (at **)' + + '\n in Root (at **)', + 'FunctionalLegacyContextConsumer uses the legacy contextTypes ' + + 'API which will be removed soon. Use React.createContext() ' + + 'with React.useContext() instead. (https://react.dev/link/legacy-context)' + + '\n in FunctionalLegacyContextConsumer (at **)' + + '\n in div (at **)' + + '\n in LegacyContextProvider (at **)' + + '\n in div (at **)' + + '\n in Root (at **)', 'Legacy context API has been detected within a strict-mode tree.' + '\n\nThe old API will be supported in all 16.x releases, but applications ' + 'using it should migrate to the new version.' + @@ -1087,7 +1110,7 @@ describe('context legacy', () => { '\n in LegacyContextProvider (at **)' + '\n in div (at **)' + '\n in Root (at **)', - ); + ]); // Dedupe await act(() => { diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index 139a5c01e8..4f4564d356 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -518,7 +518,10 @@ describe('ReactTypeScriptClass', function() { if (!ReactFeatureFlags.disableLegacyContext) { it('renders based on context in the constructor', function() { - test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + expect(() => test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo')).toErrorDev([ + 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.' + ]); }); } @@ -687,8 +690,11 @@ describe('ReactTypeScriptClass', function() { }); if (!ReactFeatureFlags.disableLegacyContext) { - it('supports this.context passed via getChildContext', function() { - test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + it('supports this.context passed via getChildContext', () => { + expect(() => test(React.createElement(ProvideContext), 'DIV', 'bar-through-context')).toErrorDev([ + 'ProvideContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'ReadContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', +] ); }); } diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index 8adb37cad0..330150506c 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -10,6 +10,7 @@ 'use strict'; let act; +let assertConsoleErrorDev; let PropTypes; let React; @@ -19,7 +20,7 @@ let createReactClass; describe('create-react-class-integration', () => { beforeEach(() => { jest.resetModules(); - ({act} = require('internal-test-utils')); + ({act, assertConsoleErrorDev} = require('internal-test-utils')); PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -336,6 +337,10 @@ describe('create-react-class-integration', () => { await act(() => { root.render(); }); + assertConsoleErrorDev([ + 'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', + 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + ]); expect(container.firstChild.className).toBe('foo'); }); From 2d3f81bb6a650386832d885d7b63a7d0d517ba15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 10 Jul 2024 12:17:13 -0400 Subject: [PATCH 30/85] Format DOM Nesting Warning as Diff View + An Additional Log for Stack Trace (#30302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently we're printing parent stacks at the end of DOM nesting even with owner stacks enabled. That's because the context of parent tree is relevant for determining why two things are nested. It might not be sufficient to see the owner stack alone. I'm trying to get rid of parent stacks and rely on more of the plain owner stacks or ideally console.createTask. These are generally better anyway since the exact line for creating the JSX is available. It also lets you find a parent stack frame that is most relevant e.g. if it's hidden inside internals. For DOM nesting there's really only two stacks that are relevant. The creation of the parent and the creation of the child. Sometimes they're close enough to be the same thing. Such as for parents that can't have text children or when the ancestor is the direct parent created at the same place (same owner). Sometimes they're far apart. In this case I add a second console.error within the context of the ancestor. That way the second stack trace can be used to read the stack trace for where it was created. To preserve some parent context I now print the parent stack in a diff view format using the logic from hydration diffs. This includes some siblings and props for context. Screenshot 2024-07-10 at 12 21 38 AM Text Nodes: Screenshot 2024-07-10 at 12 37 40 AM --------- Co-authored-by: tjallingt --- .../src/client/validateDOMNesting.js | 142 +++++++-- .../src/__tests__/ReactDOMComponent-test.js | 275 +++++++++++++----- .../src/__tests__/ReactDOMForm-test.js | 12 +- .../src/__tests__/ReactDOMOption-test.js | 26 +- .../src/__tests__/validateDOMNesting-test.js | 71 +++-- .../react-reconciler/src/ReactChildFiber.js | 33 +++ .../react-reconciler/src/ReactCurrentFiber.js | 11 - .../src/ReactFiberHydrationDiffs.js | 22 +- 8 files changed, 453 insertions(+), 139 deletions(-) diff --git a/packages/react-dom-bindings/src/client/validateDOMNesting.js b/packages/react-dom-bindings/src/client/validateDOMNesting.js index 2f192cd18b..49d8b5c158 100644 --- a/packages/react-dom-bindings/src/client/validateDOMNesting.js +++ b/packages/react-dom-bindings/src/client/validateDOMNesting.js @@ -7,7 +7,54 @@ * @flow */ -import {getCurrentParentStackInDev} from 'react-reconciler/src/ReactCurrentFiber'; +import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; +import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs'; + +import {enableOwnerStacks} from 'shared/ReactFeatureFlags'; + +import { + current, + runWithFiberInDEV, +} from 'react-reconciler/src/ReactCurrentFiber'; +import { + HostComponent, + HostHoistable, + HostSingleton, + HostText, +} from 'react-reconciler/src/ReactWorkTags'; + +import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs'; + +function describeAncestors( + ancestor: Fiber, + child: Fiber, + props: null | {children: null}, +): string { + let fiber: null | Fiber = child; + let node: null | HydrationDiffNode = null; + let distanceFromLeaf = 0; + while (fiber) { + if (fiber === ancestor) { + distanceFromLeaf = 0; + } + node = { + fiber: fiber, + children: node !== null ? [node] : [], + serverProps: + fiber === child ? props : fiber === ancestor ? null : undefined, + serverTail: [], + distanceFromLeaf: distanceFromLeaf, + }; + distanceFromLeaf++; + fiber = fiber.return; + } + if (node !== null) { + // Describe the node using the hydration diff logic. + // Replace + with - to mark ancestor and child. It's kind of arbitrary. + return describeDiff(node).replaceAll(/^[+-]/gm, '>'); + } + return ''; +} type Info = {tag: string}; export type AncestorInfoDev = { @@ -440,6 +487,21 @@ function findInvalidAncestorForTag( const didWarn: {[string]: boolean} = {}; +function findAncestor(parent: null | Fiber, tagName: string): null | Fiber { + while (parent) { + switch (parent.tag) { + case HostComponent: + case HostHoistable: + case HostSingleton: + if (parent.type === tagName) { + return parent; + } + } + parent = parent.return; + } + return null; +} + function validateDOMNesting( childTag: string, ancestorInfo: AncestorInfoDev, @@ -470,6 +532,14 @@ function validateDOMNesting( } didWarn[warnKey] = true; + const child = current; + const ancestor = child ? findAncestor(child.return, ancestorTag) : null; + + const ancestorDescription = + child !== null && ancestor !== null + ? describeAncestors(ancestor, child, null) + : ''; + const tagDisplayName = '<' + childTag + '>'; if (invalidParent) { let info = ''; @@ -478,33 +548,45 @@ function validateDOMNesting( ' Add a , or to your code to match the DOM tree generated by ' + 'the browser.'; } - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, %s cannot be a child of <%s>.%s\n' + 'This will cause a hydration error.%s', tagDisplayName, ancestorTag, info, - getCurrentParentStackInDev(), + ancestorDescription, ); } else { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, %s cannot be a descendant of <%s>.\n' + 'This will cause a hydration error.%s', tagDisplayName, ancestorTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } + if (enableOwnerStacks && child) { + // For debugging purposes find the nearest ancestor that caused the issue. + // The stack trace of this ancestor can be useful to find the cause. + // If the parent is a direct parent in the same owner, we don't bother. + const parent = child.return; + if ( + ancestor !== null && + parent !== null && + (ancestor !== parent || parent._debugOwner !== child._debugOwner) + ) { + runWithFiberInDEV(ancestor, () => { + console.error( + // We repeat some context because this log might be taken out of context + // such as in React DevTools or grouped server logs. + '<%s> cannot contain a nested %s.\n' + + 'See this log for the ancestor stack trace.', + ancestorTag, + tagDisplayName, + ); + }); + } + } return false; } return true; @@ -522,31 +604,33 @@ function validateTextNesting(childText: string, parentTag: string): boolean { } didWarn[warnKey] = true; + const child = current; + const ancestor = child ? findAncestor(child, parentTag) : null; + + const ancestorDescription = + child !== null && ancestor !== null + ? describeAncestors( + ancestor, + child, + child.tag !== HostText ? {children: null} : null, + ) + : ''; + if (/\S/.test(childText)) { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, text nodes cannot be a child of <%s>.\n' + 'This will cause a hydration error.%s', parentTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } else { - // Don't transform into consoleWithStackDev here because we add a manual stack. - // We use the parent stack here instead of the owner stack because the parent - // stack has more useful context for nesting. - // TODO: Format this as a linkified "diff view" with props instead of - // a stack trace since the stack trace format is now for owner stacks. - console['error']( + console.error( 'In HTML, whitespace text nodes cannot be a child of <%s>. ' + "Make sure you don't have any extra whitespace between tags on " + 'each line of your source code.\n' + 'This will cause a hydration error.%s', parentTag, - getCurrentParentStackInDev(), + ancestorDescription, ); } return false; diff --git a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js index 6e02e17ece..d37a4ecba6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMComponent-test.js @@ -2193,13 +2193,18 @@ describe('ReactDOMComponent', () => {
, ); }); - }).toErrorDev([ - 'In HTML, cannot be a child of ' + - '
.\n' + - 'This will cause a hydration error.' + + }).toErrorDev( + 'In HTML, cannot be a child of
.\n' + + 'This will cause a hydration error.\n' + + '\n' + + '>
\n' + + '> \n' + + ' ...\n' + '\n in tr (at **)' + - '\n in div (at **)', - ]); + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in div (at **)'), + ); }); it('warns on invalid nesting at root', async () => { @@ -2215,12 +2220,13 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - 'In HTML,

cannot be a descendant ' + - 'of

.\n' + + 'In HTML,

cannot be a descendant of

.\n' + 'This will cause a hydration error.' + // There is no outer `p` here because root container is not part of the stack. '\n in p (at **)' + - '\n in span (at **)', + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in span (at **)'), ); }); @@ -2248,29 +2254,90 @@ describe('ReactDOMComponent', () => { await act(() => { root.render(); }); - }).toErrorDev([ - 'In HTML, cannot be a child of ' + - '. Add a , or to your code to match the DOM tree generated ' + - 'by the browser.\n' + - 'This will cause a hydration error.' + - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', - 'In HTML, text nodes cannot be a ' + - 'child of .\n' + - 'This will cause a hydration error.' + - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', - 'In HTML, whitespace text nodes cannot ' + - "be a child of
. Make sure you don't have any extra " + - 'whitespace between tags on each line of your source code.\n' + - 'This will cause a hydration error.' + - '\n in table (at **)' + - '\n in Foo (at **)', - ]); + }).toErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'In HTML, cannot be a child of ' + + '
. Add a , or to your code to match the DOM tree generated ' + + 'by the browser.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> \n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)', + '
cannot contain a nested .\nSee this log for the ancestor stack trace.' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + '> x\n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)', + 'In HTML, whitespace text nodes cannot ' + + "be a child of
. Make sure you don't have any extra " + + 'whitespace between tags on each line of your source code.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> {" "}\n' + + '\n in table (at **)' + + '\n in Foo (at **)', + ] + : [ + 'In HTML, cannot be a child of ' + + '
. Add a , or to your code to match the DOM tree generated ' + + 'by the browser.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> \n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, text nodes cannot be a ' + + 'child of .\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + '> x\n' + + ' ...\n' + + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)', + 'In HTML, whitespace text nodes cannot ' + + "be a child of
. Make sure you don't have any extra " + + 'whitespace between tags on each line of your source code.\n' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '>
\n' + + ' \n' + + '> {" "}\n' + + '\n in table (at **)' + + '\n in Foo (at **)', + ], + ); }); it('warns nicely for updating table rows to use text', async () => { @@ -2297,7 +2364,11 @@ describe('ReactDOMComponent', () => { 'In HTML, whitespace text nodes cannot ' + "be a child of
. Make sure you don't have any extra " + 'whitespace between tags on each line of your source code.\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + '> {" "}\n' + '\n in table (at **)' + '\n in Foo (at **)', ]); @@ -2325,12 +2396,21 @@ describe('ReactDOMComponent', () => { }).toErrorDev([ 'In HTML, text nodes cannot be a ' + 'child of .\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + '> text\n' + '\n in tr (at **)' + '\n in Row (at **)' + - '\n in tbody (at **)' + - '\n in table (at **)' + - '\n in Foo (at **)', + (gate(flags => flags.enableOwnerStacks) + ? '' + : '\n in tbody (at **)' + + '\n in table (at **)' + + '\n in Foo (at **)'), ]); }); @@ -2359,11 +2439,21 @@ describe('ReactDOMComponent', () => { root.render(); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Viz1 (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in Viz1 (at **)', + '\n in table (at **)' + '\n in Viz1 (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Viz1 (at **)', + ], ); }); @@ -2405,13 +2495,26 @@ describe('ReactDOMComponent', () => { root.render(); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)' + - '\n in Viz2 (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in Viz2 (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)' + + '\n in Viz2 (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)' + + '\n in Viz2 (at **)', + ], ); }); @@ -2446,12 +2549,23 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ], ); }); @@ -2475,10 +2589,19 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in Row (at **)' + - '\n in FancyRow (at **)' + - '\n in table (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)', + '\n in table (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in Row (at **)' + + '\n in FancyRow (at **)' + + '\n in table (at **)', + ], ); }); @@ -2506,10 +2629,19 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in tr (at **)' + - '\n in table (at **)' + - '\n in Table (at **)' + - '\n in FancyTable (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in tr (at **)', + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ] + : [ + '\n in tr (at **)' + + '\n in table (at **)' + + '\n in Table (at **)' + + '\n in FancyTable (at **)', + ], ); class Link extends React.Component { @@ -2531,11 +2663,18 @@ describe('ReactDOMComponent', () => { ); }); }).toErrorDev( - '\n in a (at **)' + - '\n in Link (at **)' + - '\n in div (at **)' + - '\n in a (at **)' + - '\n in Link (at **)', + gate(flags => flags.enableOwnerStacks) + ? [ + '\n in a (at **)' + '\n in Link (at **)', + '\n in a (at **)' + '\n in Link (at **)', + ] + : [ + '\n in a (at **)' + + '\n in Link (at **)' + + '\n in div (at **)' + + '\n in a (at **)' + + '\n in Link (at **)', + ], ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index 4c3ebecccb..7ba4bea06b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -385,12 +385,16 @@ describe('ReactDOMForm', () => { , ); }); - }).toErrorDev([ + }).toErrorDev( 'In HTML,
cannot be a descendant of .\n' + - 'This will cause a hydration error.' + + 'This will cause a hydration error.\n' + + '\n' + + '> \n' + + ' \n' + + '> \n' + '\n in form (at **)' + - '\n in form (at **)', - ]); + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in form (at **)'), + ); await submit(ref.current); diff --git a/packages/react-dom/src/__tests__/ReactDOMOption-test.js b/packages/react-dom/src/__tests__/ReactDOMOption-test.js index dab7f69b27..ce5e3c65bc 100644 --- a/packages/react-dom/src/__tests__/ReactDOMOption-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMOption-test.js @@ -53,8 +53,15 @@ describe('ReactDOMOption', () => { }).toErrorDev( 'In HTML,
cannot be a child of