diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 98460333bd2b2..ac2d3218ed825 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -12,6 +12,7 @@ import type { ReactDebugInfo, ReactComponentInfo, ReactAsyncInfo, + ReactStackTrace, } from 'shared/ReactTypes'; import type {LazyComponent} from 'react/src/ReactLazy'; @@ -624,7 +625,7 @@ function createElement( key: mixed, props: mixed, owner: null | ReactComponentInfo, // DEV-only - stack: null | string, // DEV-only + stack: null | ReactStackTrace, // DEV-only validated: number, // DEV-only ): | React$Element @@ -1738,6 +1739,27 @@ function stopStream( controller.close(row === '' ? '"$undefined"' : row); } +function formatV8Stack( + errorName: string, + errorMessage: string, + stack: null | ReactStackTrace, +): string { + let v8StyleStack = errorName + ': ' + errorMessage; + if (stack) { + for (let i = 0; i < stack.length; i++) { + const frame = stack[i]; + const [name, filename, line, col] = frame; + if (!name) { + v8StyleStack += '\n at ' + filename + ':' + line + ':' + col; + } else { + v8StyleStack += + '\n at ' + name + ' (' + filename + ':' + line + ':' + col + ')'; + } + } + } + return v8StyleStack; +} + type ErrorWithDigest = Error & {digest?: string}; function resolveErrorProd( response: Response, @@ -1773,7 +1795,7 @@ function resolveErrorDev( id: number, digest: string, message: string, - stack: string, + stack: ReactStackTrace, env: string, ): void { if (!__DEV__) { @@ -1793,7 +1815,8 @@ function resolveErrorDev( message || 'An error occurred in the Server Components render but no message was provided', ); - error.stack = stack; + // For backwards compat we use the V8 formatting when the flag is off. + error.stack = formatV8Stack(error.name, error.message, stack); } else { const callStack = buildFakeCallStack( response, @@ -1853,7 +1876,7 @@ function resolvePostponeDev( response: Response, id: number, reason: string, - stack: string, + stack: ReactStackTrace, ): void { if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json @@ -1862,11 +1885,34 @@ function resolvePostponeDev( 'resolvePostponeDev should never be called in production mode. Use resolvePostponeProd instead. This is a bug in React.', ); } - // eslint-disable-next-line react-internal/prod-error-codes - const error = new Error(reason || ''); - const postponeInstance: Postpone = (error: any); - postponeInstance.$$typeof = REACT_POSTPONE_TYPE; - postponeInstance.stack = stack; + let postponeInstance: Postpone; + if (!enableOwnerStacks) { + // Executing Error within a native stack isn't really limited to owner stacks + // but we gate it behind the same flag for now while iterating. + // eslint-disable-next-line react-internal/prod-error-codes + postponeInstance = (Error(reason || ''): any); + postponeInstance.$$typeof = REACT_POSTPONE_TYPE; + // For backwards compat we use the V8 formatting when the flag is off. + postponeInstance.stack = formatV8Stack( + postponeInstance.name, + postponeInstance.message, + stack, + ); + } else { + const callStack = buildFakeCallStack( + response, + stack, + // $FlowFixMe[incompatible-use] + Error.bind(null, reason || ''), + ); + const rootTask = response._debugRootTask; + if (rootTask != null) { + postponeInstance = rootTask.run(callStack); + } else { + postponeInstance = callStack(); + } + postponeInstance.$$typeof = REACT_POSTPONE_TYPE; + } const chunks = response._chunks; const chunk = chunks.get(id); if (!chunk) { @@ -1973,40 +2019,25 @@ function createFakeFunction( return fn; } -// This matches either of these V8 formats. -// at name (filename:0:0) -// at filename:0:0 -// at async filename:0:0 -const frameRegExp = - /^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/; - function buildFakeCallStack( response: Response, - stack: string, + stack: ReactStackTrace, innerCall: () => T, ): () => T { - const frames = stack.split('\n'); let callStack = innerCall; - for (let i = 0; i < frames.length; i++) { - const frame = frames[i]; - let fn = fakeFunctionCache.get(frame); + for (let i = 0; i < stack.length; i++) { + const frame = stack[i]; + const frameKey = frame.join('-'); + let fn = fakeFunctionCache.get(frameKey); if (fn === undefined) { - const parsed = frameRegExp.exec(frame); - if (!parsed) { - // We assume the server returns a V8 compatible stack trace. - continue; - } - const name = parsed[1] || ''; - const filename = parsed[2] || parsed[5] || ''; - const line = +(parsed[3] || parsed[6]); - const col = +(parsed[4] || parsed[7]); + const [name, filename, line, col] = frame; const sourceMap = response._debugFindSourceMapURL ? response._debugFindSourceMapURL(filename) : null; fn = createFakeFunction(name, filename, sourceMap, line, col); // TODO: This cache should technically live on the response since the _debugFindSourceMapURL // function is an input and can vary by response. - fakeFunctionCache.set(frame, fn); + fakeFunctionCache.set(frameKey, fn); } callStack = fn.bind(null, callStack); } @@ -2026,7 +2057,7 @@ function initializeFakeTask( return cachedEntry; } - if (typeof debugInfo.stack !== 'string') { + if (debugInfo.stack == null) { // If this is an error, we should've really already initialized the task. // If it's null, we can't initialize a task. return null; @@ -2064,7 +2095,7 @@ function initializeFakeTask( const createFakeJSXCallStack = { 'react-stack-bottom-frame': function ( response: Response, - stack: string, + stack: ReactStackTrace, ): Error { const callStackForError = buildFakeCallStack( response, @@ -2077,7 +2108,7 @@ const createFakeJSXCallStack = { const createFakeJSXCallStackInDEV: ( response: Response, - stack: string, + stack: ReactStackTrace, ) => Error = __DEV__ ? // We use this technique to trick minifiers to preserve the function name. (createFakeJSXCallStack['react-stack-bottom-frame'].bind( @@ -2100,7 +2131,7 @@ function initializeFakeStack( if (cachedEntry !== undefined) { return; } - if (typeof debugInfo.stack === 'string') { + if (debugInfo.stack != null) { // $FlowFixMe[cannot-write] // $FlowFixMe[prop-missing] debugInfo.debugStack = createFakeJSXCallStackInDEV( @@ -2154,8 +2185,13 @@ function resolveConsoleEntry( return; } - const payload: [string, string, null | ReactComponentInfo, string, mixed] = - parseModel(response, value); + const payload: [ + string, + ReactStackTrace, + null | ReactComponentInfo, + string, + mixed, + ] = parseModel(response, value); const methodName = payload[0]; const stackTrace = payload[1]; const owner = payload[2]; diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 363d5f71d3685..5d7e8faf49d7f 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -27,10 +27,24 @@ function normalizeCodeLocInfo(str) { ); } +function formatV8Stack(stack) { + let v8StyleStack = ''; + if (stack) { + for (let i = 0; i < stack.length; i++) { + const [name] = stack[i]; + if (v8StyleStack !== '') { + v8StyleStack += '\n'; + } + v8StyleStack += ' in ' + name + ' (at **)'; + } + } + return v8StyleStack; +} + function normalizeComponentInfo(debugInfo) { - if (typeof debugInfo.stack === 'string') { + if (Array.isArray(debugInfo.stack)) { const {debugTask, debugStack, ...copy} = debugInfo; - copy.stack = normalizeCodeLocInfo(debugInfo.stack); + copy.stack = formatV8Stack(debugInfo.stack); if (debugInfo.owner) { copy.owner = normalizeComponentInfo(debugInfo.owner); } diff --git a/packages/react-reconciler/src/ReactFiberOwnerStack.js b/packages/react-reconciler/src/ReactFiberOwnerStack.js index 208f65c841d41..fbca1c2de3808 100644 --- a/packages/react-reconciler/src/ReactFiberOwnerStack.js +++ b/packages/react-reconciler/src/ReactFiberOwnerStack.js @@ -8,7 +8,7 @@ */ // TODO: Make this configurable on the root. -const externalRegExp = /\/node\_modules\/|\(\\)/; +const externalRegExp = /\/node\_modules\/|\(\/; function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); diff --git a/packages/react-server/src/ReactFizzComponentStack.js b/packages/react-server/src/ReactFizzComponentStack.js index b0f3a0db2b76f..ab306047bfbe0 100644 --- a/packages/react-server/src/ReactFizzComponentStack.js +++ b/packages/react-server/src/ReactFizzComponentStack.js @@ -165,17 +165,17 @@ export function getOwnerStackByComponentStackNodeInDev( // TODO: Should we stash this somewhere for caching purposes? ownerStack = formatOwnerStack(owner.debugStack); owner = owner.owner; - } else if (owner.stack != null) { + } else { // Client Component const node: ComponentStackNode = (owner: any); - if (typeof owner.stack !== 'string') { - ownerStack = node.stack = formatOwnerStack(owner.stack); - } else { - ownerStack = owner.stack; + if (node.stack != null) { + if (typeof node.stack !== 'string') { + ownerStack = node.stack = formatOwnerStack(node.stack); + } else { + ownerStack = node.stack; + } } owner = owner.owner; - } else { - 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 diff --git a/packages/react-server/src/ReactFizzOwnerStack.js b/packages/react-server/src/ReactFizzOwnerStack.js index 208f65c841d41..fbca1c2de3808 100644 --- a/packages/react-server/src/ReactFizzOwnerStack.js +++ b/packages/react-server/src/ReactFizzOwnerStack.js @@ -8,7 +8,7 @@ */ // TODO: Make this configurable on the root. -const externalRegExp = /\/node\_modules\/|\(\\)/; +const externalRegExp = /\/node\_modules\/|\(\/; function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); diff --git a/packages/react-server/src/ReactFlightOwnerStack.js b/packages/react-server/src/ReactFlightOwnerStack.js index 070d187ab87b0..72951bfb4745b 100644 --- a/packages/react-server/src/ReactFlightOwnerStack.js +++ b/packages/react-server/src/ReactFlightOwnerStack.js @@ -8,7 +8,7 @@ */ // TODO: Make this configurable on the Request. -const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\\)/; +const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\/; function isNotExternal(stackFrame: string): boolean { return !externalRegExp.test(stackFrame); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 235c8cee885bd..73ed634027dd2 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -62,6 +62,8 @@ import type { ReactDebugInfo, ReactComponentInfo, ReactAsyncInfo, + ReactStackTrace, + ReactCallSite, } from 'shared/ReactTypes'; import type {ReactElement} from 'shared/ReactElementType'; import type {LazyComponent} from 'react/src/ReactLazy'; @@ -77,6 +79,7 @@ import { requestStorage, createHints, initAsyncDebugInfo, + parseStackTrace, } from './ReactFlightServerConfig'; import { @@ -131,64 +134,19 @@ import binaryToComparableString from 'shared/binaryToComparableString'; import {SuspenseException, getSuspendedThenable} from './ReactFlightThenable'; // TODO: Make this configurable on the Request. -const externalRegExp = /\/node\_modules\/| \(node\:| node\:|\(\\)/; +const externalRegExp = /\/node\_modules\/|^node\:|^$/; -function isNotExternal(stackFrame: string): boolean { - return !externalRegExp.test(stackFrame); +function isNotExternal(stackFrame: ReactCallSite): boolean { + const filename = stackFrame[1]; + return !externalRegExp.test(filename); } -function prepareStackTrace( - error: Error, - structuredStackTrace: CallSite[], -): string { - const name = error.name || 'Error'; - const message = error.message || ''; - let stack = name + ': ' + message; - for (let i = 0; i < structuredStackTrace.length; i++) { - stack += '\n at ' + structuredStackTrace[i].toString(); - } - return stack; -} - -function getStack(error: Error): string { - // We override Error.prepareStackTrace with our own version that normalizes - // the stack to V8 formatting even if the server uses other formatting. - // It also ensures that source maps are NOT applied to this since that can - // be slow we're better off doing that lazily from the client instead of - // eagerly on the server. If the stack has already been read, then we might - // not get a normalized stack and it might still have been source mapped. - // So the client still needs to be resilient to this. - const previousPrepare = Error.prepareStackTrace; - Error.prepareStackTrace = prepareStackTrace; - try { - // eslint-disable-next-line react-internal/safe-string-coercion - return String(error.stack); - } finally { - Error.prepareStackTrace = previousPrepare; - } -} - -function filterDebugStack(error: Error): string { +function filterStackTrace(error: Error, skipFrames: number): ReactStackTrace { // Since stacks can be quite large and we pass a lot of them, we filter them out eagerly // to save bandwidth even in DEV. We'll also replay these stacks on the client so by // stripping them early we avoid that overhead. Otherwise we'd normally just rely on // the DevTools or framework's ignore lists to filter them out. - let stack = getStack(error); - if (stack.startsWith('Error: react-stack-top-frame\n')) { - // V8's default formatting prefixes with the error message which we - // don't want/need. - stack = stack.slice(29); - } - let idx = stack.indexOf('react-stack-bottom-frame'); - if (idx !== -1) { - idx = stack.lastIndexOf('\n', idx); - } - if (idx !== -1) { - // Cut off everything after the bottom frame since it'll be internals. - stack = stack.slice(0, idx); - } - const frames = stack.split('\n').slice(1); - return frames.filter(isNotExternal).join('\n'); + return parseStackTrace(error, skipFrames).filter(isNotExternal); } initAsyncDebugInfo(); @@ -214,7 +172,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) { // Extract the stack. Not all console logs print the full stack but they have at // least the line it was called from. We could optimize transfer by keeping just // one stack frame but keeping it simple for now and include all frames. - const stack = filterDebugStack(new Error('react-stack-top-frame')); + const stack = filterStackTrace(new Error('react-stack-top-frame'), 1); request.pendingChunks++; // We don't currently use this id for anything but we emit it so that we can later // refer to previous logs in debug info to associate them with a component. @@ -973,7 +931,7 @@ function callWithDebugContextInDEV( if (enableOwnerStacks) { // $FlowFixMe[cannot-write] componentDebugInfo.stack = - task.debugStack === null ? null : filterDebugStack(task.debugStack); + task.debugStack === null ? null : filterStackTrace(task.debugStack, 1); // $FlowFixMe[cannot-write] componentDebugInfo.debugStack = task.debugStack; // $FlowFixMe[cannot-write] @@ -1035,7 +993,9 @@ function renderFunctionComponent( if (enableOwnerStacks) { // $FlowFixMe[cannot-write] componentDebugInfo.stack = - task.debugStack === null ? null : filterDebugStack(task.debugStack); + task.debugStack === null + ? null + : filterStackTrace(task.debugStack, 1); // $FlowFixMe[cannot-write] componentDebugInfo.debugStack = task.debugStack; // $FlowFixMe[cannot-write] @@ -1429,7 +1389,9 @@ function renderClientElement( key, props, task.debugOwner, - task.debugStack === null ? null : filterDebugStack(task.debugStack), + task.debugStack === null + ? null + : filterStackTrace(task.debugStack, 1), validated, ] : [REACT_ELEMENT_TYPE, type, key, props, task.debugOwner] @@ -2519,12 +2481,12 @@ function renderModelDestructive( // $FlowFixMe[method-unbinding] typeof value.debugTask.run === 'function') || value.debugStack instanceof Error) && + (enableOwnerStacks + ? isArray((value: any).stack) + : typeof (value: any).stack === 'undefined') && typeof value.name === 'string' && typeof value.env === 'string' && - value.owner !== undefined && - (enableOwnerStacks - ? typeof (value: any).stack === 'string' - : typeof (value: any).stack === 'undefined') + value.owner !== undefined ) { // This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we // need to omit it before serializing. @@ -2824,12 +2786,14 @@ function emitPostponeChunk( let row; if (__DEV__) { let reason = ''; - let stack = ''; + let stack: ReactStackTrace; try { // eslint-disable-next-line react-internal/safe-string-coercion reason = String(postponeInstance.message); - stack = getStack(postponeInstance); - } catch (x) {} + stack = filterStackTrace(postponeInstance, 0); + } catch (x) { + stack = []; + } row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n'; } else { // No reason included in prod. @@ -2848,13 +2812,13 @@ function emitErrorChunk( let errorInfo: any; if (__DEV__) { let message; - let stack = ''; + let stack: ReactStackTrace; let env = request.environmentName(); try { if (error instanceof Error) { // eslint-disable-next-line react-internal/safe-string-coercion message = String(error.message); - stack = getStack(error); + stack = filterStackTrace(error, 0); const errorEnv = (error: any).environmentName; if (typeof errorEnv === 'string') { // This probably came from another FlightClient as a pass through. @@ -2863,9 +2827,11 @@ function emitErrorChunk( } } else if (typeof error === 'object' && error !== null) { message = describeObjectForErrorMessage(error); + stack = []; } else { // eslint-disable-next-line react-internal/safe-string-coercion message = String(error); + stack = []; } } catch (x) { message = 'An error occurred but serializing the error message failed.'; @@ -3316,7 +3282,7 @@ function emitConsoleChunk( id: number, methodName: string, owner: null | ReactComponentInfo, - stackTrace: string, + stackTrace: ReactStackTrace, args: Array, ): void { if (!__DEV__) { diff --git a/packages/react-server/src/ReactFlightStackConfigV8.js b/packages/react-server/src/ReactFlightStackConfigV8.js new file mode 100644 index 0000000000000..4b5e5cd6afa14 --- /dev/null +++ b/packages/react-server/src/ReactFlightStackConfigV8.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {ReactStackTrace} from 'shared/ReactTypes'; + +function prepareStackTrace( + error: Error, + structuredStackTrace: CallSite[], +): string { + const name = error.name || 'Error'; + const message = error.message || ''; + let stack = name + ': ' + message; + for (let i = 0; i < structuredStackTrace.length; i++) { + stack += '\n at ' + structuredStackTrace[i].toString(); + } + return stack; +} + +function getStack(error: Error): string { + // We override Error.prepareStackTrace with our own version that normalizes + // the stack to V8 formatting even if the server uses other formatting. + // It also ensures that source maps are NOT applied to this since that can + // be slow we're better off doing that lazily from the client instead of + // eagerly on the server. If the stack has already been read, then we might + // not get a normalized stack and it might still have been source mapped. + const previousPrepare = Error.prepareStackTrace; + Error.prepareStackTrace = prepareStackTrace; + try { + // eslint-disable-next-line react-internal/safe-string-coercion + return String(error.stack); + } finally { + Error.prepareStackTrace = previousPrepare; + } +} + +// This matches either of these V8 formats. +// at name (filename:0:0) +// at filename:0:0 +// at async filename:0:0 +const frameRegExp = + /^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/; + +export function parseStackTrace( + error: Error, + skipFrames: number, +): ReactStackTrace { + let stack = getStack(error); + if (stack.startsWith('Error: react-stack-top-frame\n')) { + // V8's default formatting prefixes with the error message which we + // don't want/need. + stack = stack.slice(29); + } + let idx = stack.indexOf('react-stack-bottom-frame'); + if (idx !== -1) { + idx = stack.lastIndexOf('\n', idx); + } + if (idx !== -1) { + // Cut off everything after the bottom frame since it'll be internals. + stack = stack.slice(0, idx); + } + const frames = stack.split('\n'); + const parsedFrames: ReactStackTrace = []; + // We skip top frames here since they may or may not be parseable but we + // want to skip the same number of frames regardless. I.e. we can't do it + // in the caller. + for (let i = skipFrames; i < frames.length; i++) { + const parsed = frameRegExp.exec(frames[i]); + if (!parsed) { + continue; + } + let name = parsed[1] || ''; + if (name === '') { + name = ''; + } + let filename = parsed[2] || parsed[5] || ''; + if (filename === '') { + filename = ''; + } + const line = +(parsed[3] || parsed[6]); + const col = +(parsed[4] || parsed[7]); + parsedFrames.push([name, filename, line, col]); + } + return parsedFrames; +} diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js index 15874c64e0858..4302d6cee6400 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.custom.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.custom.js @@ -14,6 +14,8 @@ export * from '../ReactFlightServerConfigBundlerCustom'; export * from '../ReactFlightServerConfigDebugNoop'; +export * from '../ReactFlightStackConfigV8'; + export type Hints = any; export type HintCode = any; // eslint-disable-next-line no-unused-vars diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js index 3b6251d37b4b3..dc8ead927775a 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-esm.js @@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage = (null: any); export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js index 9b9440a3dae80..256eafd6c41a6 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser-turbopack.js @@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage = (null: any); export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js index 4dbfae1ce3ecf..6715ccbbafe20 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-browser.js @@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage = (null: any); export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js index bfe794c9a2c55..9ab251ed2d582 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-bun.js @@ -21,3 +21,5 @@ export const componentStorage: AsyncLocalStorage = (null: any); export * from '../ReactFlightServerConfigDebugNoop'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js index 56d00364ec247..ffca2eca98dc8 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge-turbopack.js @@ -35,4 +35,7 @@ export const createAsyncHook: HookCallbacks => AsyncHook = }; export const executionAsyncId: () => number = typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any); + export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js index 322f3a10989d4..fb6d0039f79ed 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-edge.js @@ -36,4 +36,7 @@ export const createAsyncHook: HookCallbacks => AsyncHook = }; export const executionAsyncId: () => number = typeof async_hooks === 'object' ? async_hooks.executionAsyncId : (null: any); + export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js index 15874c64e0858..4302d6cee6400 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js @@ -14,6 +14,8 @@ export * from '../ReactFlightServerConfigBundlerCustom'; export * from '../ReactFlightServerConfigDebugNoop'; +export * from '../ReactFlightStackConfigV8'; + export type Hints = any; export type HintCode = any; // eslint-disable-next-line no-unused-vars diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js index b44d6fe2457b8..a3393bc1c3034 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-esm.js @@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage = supportsComponentStorage ? new AsyncLocalStorage() : (null: any); export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks'; + export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js index eb566f3e2e43c..d3f4258625d77 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node-turbopack.js @@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage = supportsComponentStorage ? new AsyncLocalStorage() : (null: any); export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks'; + export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js index 40c58ea2dde12..48ca5af07c278 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.dom-node.js @@ -24,4 +24,7 @@ export const componentStorage: AsyncLocalStorage = supportsComponentStorage ? new AsyncLocalStorage() : (null: any); export {createHook as createAsyncHook, executionAsyncId} from 'async_hooks'; + export * from '../ReactFlightServerConfigDebugNode'; + +export * from '../ReactFlightStackConfigV8'; diff --git a/packages/react-server/src/forks/ReactFlightServerConfig.markup.js b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js index 99591bb954ea9..52a10506bd140 100644 --- a/packages/react-server/src/forks/ReactFlightServerConfig.markup.js +++ b/packages/react-server/src/forks/ReactFlightServerConfig.markup.js @@ -28,6 +28,8 @@ export const componentStorage: AsyncLocalStorage = export * from '../ReactFlightServerConfigDebugNoop'; +export * from '../ReactFlightStackConfigV8'; + export type ClientManifest = null; export opaque type ClientReference = null; // eslint-disable-line no-unused-vars export opaque type ServerReference = null; // eslint-disable-line no-unused-vars diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 94914e61f6216..c334e2b4622aa 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -178,11 +178,20 @@ export type Awaited = T extends null | void : T // argument was not an object : T; // non-thenable +export type ReactCallSite = [ + string, // function name + string, // file name TODO: model nested eval locations as nested arrays + number, // line number + number, // column number +]; + +export type ReactStackTrace = Array; + export type ReactComponentInfo = { +name?: string, +env?: string, +owner?: null | ReactComponentInfo, - +stack?: null | string, + +stack?: null | ReactStackTrace, // Stashed Data for the Specific Execution Environment. Not part of the transport protocol +debugStack?: null | Error, +debugTask?: null | ConsoleTask, @@ -191,7 +200,7 @@ export type ReactComponentInfo = { export type ReactAsyncInfo = { +started?: number, +completed?: number, - +stack?: string, + +stack?: null | ReactStackTrace, }; export type ReactDebugInfo = Array; diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index aa1f051ac4922..f49b1550afa03 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -292,13 +292,13 @@ function lazyRequireFunctionExports(moduleName) { // If this export is a function, return a wrapper function that lazily // requires the implementation from the current module cache. if (typeof originalModule[prop] === 'function') { - const wrapper = function () { - return jest.requireActual(moduleName)[prop].apply(this, arguments); - }; - // We use this to trick the filtering of Flight to exclude this frame. - Object.defineProperty(wrapper, 'name', { - value: '()', - }); + // eslint-disable-next-line no-eval + const wrapper = eval(` + (function () { + return jest.requireActual(moduleName)[prop].apply(this, arguments); + }) + // We use this to trick the filtering of Flight to exclude this frame. + //# sourceURL=`); return wrapper; } else { return originalModule[prop];