-
Notifications
You must be signed in to change notification settings - Fork 49.6k
[Flight] Eval Fake Server Component Functions to Recreate Native Stacks #29632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e4c9b4f
e5296ba
11b91f1
e68e81e
592ad3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,8 +67,11 @@ import { | |
| REACT_ELEMENT_TYPE, | ||
| REACT_POSTPONE_TYPE, | ||
| ASYNC_ITERATOR, | ||
| REACT_FRAGMENT_TYPE, | ||
| } from 'shared/ReactSymbols'; | ||
|
|
||
| import getComponentNameFromType from 'shared/getComponentNameFromType'; | ||
|
|
||
| export type {CallServerCallback, EncodeFormActionCallback}; | ||
|
|
||
| interface FlightStreamController { | ||
|
|
@@ -573,6 +576,43 @@ function nullRefGetter() { | |
| } | ||
| } | ||
|
|
||
| function getServerComponentTaskName(componentInfo: ReactComponentInfo): string { | ||
| return '<' + (componentInfo.name || '...') + '>'; | ||
| } | ||
|
|
||
| function getTaskName(type: mixed): string { | ||
| if (type === REACT_FRAGMENT_TYPE) { | ||
| return '<>'; | ||
| } | ||
| if (typeof type === 'function') { | ||
| // This is a function so it must have been a Client Reference that resolved to | ||
| // a function. We use "use client" to indicate that this is the boundary into | ||
| // the client. There should only be one for any given owner chain. | ||
| return '"use client"'; | ||
| } | ||
| if ( | ||
| typeof type === 'object' && | ||
| type !== null && | ||
| type.$$typeof === REACT_LAZY_TYPE | ||
| ) { | ||
| if (type._init === readChunk) { | ||
| // This is a lazy node created by Flight. It is probably a client reference. | ||
| // We use the "use client" string to indicate that this is the boundary into | ||
| // the client. There will only be one for any given owner chain. | ||
| return '"use client"'; | ||
| } | ||
| // We don't want to eagerly initialize the initializer in DEV mode so we can't | ||
| // call it to extract the type so we don't know the type of this component. | ||
| return '<...>'; | ||
| } | ||
| try { | ||
| const name = getComponentNameFromType(type); | ||
| return name ? '<' + name + '>' : '<...>'; | ||
| } catch (x) { | ||
| return '<...>'; | ||
| } | ||
| } | ||
|
|
||
| function createElement( | ||
| type: mixed, | ||
| key: mixed, | ||
|
|
@@ -647,11 +687,28 @@ function createElement( | |
| writable: true, | ||
| value: stack, | ||
| }); | ||
|
|
||
| let task: null | ConsoleTask = null; | ||
| if (supportsCreateTask && stack !== null) { | ||
| const createTaskFn = (console: any).createTask.bind( | ||
| console, | ||
| getTaskName(type), | ||
| ); | ||
| const callStack = buildFakeCallStack(stack, createTaskFn); | ||
| // This owner should ideally have already been initialized to avoid getting | ||
| // user stack frames on the stack. | ||
| const ownerTask = owner === null ? null : initializeFakeTask(owner); | ||
| if (ownerTask === null) { | ||
| task = callStack(); | ||
| } else { | ||
| task = ownerTask.run(callStack); | ||
| } | ||
| } | ||
| Object.defineProperty(element, '_debugTask', { | ||
| configurable: false, | ||
| enumerable: false, | ||
| writable: true, | ||
| value: null, | ||
| value: task, | ||
| }); | ||
| } | ||
| // TODO: We should be freezing the element but currently, we might write into | ||
|
|
@@ -1582,6 +1639,118 @@ function resolveHint<Code: HintCode>( | |
| dispatchHint(code, hintModel); | ||
| } | ||
|
|
||
| // eslint-disable-next-line react-internal/no-production-logging | ||
| const supportsCreateTask = | ||
| __DEV__ && enableOwnerStacks && !!(console: any).createTask; | ||
|
|
||
| const taskCache: null | WeakMap< | ||
| ReactComponentInfo | ReactAsyncInfo, | ||
| ConsoleTask, | ||
| > = supportsCreateTask ? new WeakMap() : null; | ||
|
|
||
| type FakeFunction<T> = (FakeFunction<T>) => T; | ||
| const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__ | ||
| ? new Map() | ||
| : (null: any); | ||
|
|
||
| function createFakeFunction<T>( | ||
| name: string, | ||
| filename: string, | ||
| line: number, | ||
| col: number, | ||
| ): FakeFunction<T> { | ||
| // This creates a fake copy of a Server Module. It represents a module that has already | ||
| // executed on the server but we re-execute a blank copy for its stack frames on the client. | ||
|
|
||
| const comment = | ||
| '/* This module was rendered by a Server Component. Turn on Source Maps to see the server source. */'; | ||
|
|
||
| // We generate code where the call is at the line and column of the server executed code. | ||
| // This allows us to use the original source map as the source map of this fake file to | ||
| // point to the original source. | ||
| let code; | ||
| if (line <= 1) { | ||
| code = '_=>' + ' '.repeat(col < 4 ? 0 : col - 4) + '_()\n' + comment + '\n'; | ||
| } else { | ||
| code = | ||
| comment + | ||
| '\n'.repeat(line - 2) + | ||
| '_=>\n' + | ||
| ' '.repeat(col < 1 ? 0 : col - 1) + | ||
| '_()\n'; | ||
| } | ||
|
|
||
| if (filename) { | ||
| code += '//# sourceURL=' + filename; | ||
| } | ||
|
|
||
| // eslint-disable-next-line no-eval | ||
| const fn: FakeFunction<T> = (0, eval)(code); | ||
| // $FlowFixMe[cannot-write] | ||
| Object.defineProperty(fn, 'name', {value: name || '(anonymous)'}); | ||
| // $FlowFixMe[prop-missing] | ||
| fn.displayName = name; | ||
| return fn; | ||
| } | ||
|
|
||
| const frameRegExp = | ||
| /^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|([^\)]+):(\d+):(\d+))$/; | ||
|
|
||
| function buildFakeCallStack<T>(stack: string, 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); | ||
| 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]); | ||
|
Comment on lines
+1712
to
+1714
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure that all of this will be present in the stack frame? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's not, it won't match the regexp an we'll skip the frame. This is not perfect for edge cases like built-ins. We also have to do something for Hermes for HaaS and any other VM that we choose to support. We can iterate. Bun uses JSC but it seems to use V8-style stack trace formatting too. There is also an option that use the prepareStackTrace technique on the server to get a structured stack and then send structured data in the RSC protocol instead of strings. This would also let use do arbitrary formatting on the client. I was hoping to avoid that complexity though. |
||
| fn = createFakeFunction(name, filename, line, col); | ||
| } | ||
| callStack = fn.bind(null, callStack); | ||
| } | ||
| return callStack; | ||
| } | ||
|
|
||
| function initializeFakeTask( | ||
| debugInfo: ReactComponentInfo | ReactAsyncInfo, | ||
| ): null | ConsoleTask { | ||
| if (taskCache === null || typeof debugInfo.stack !== 'string') { | ||
| return null; | ||
| } | ||
| const componentInfo: ReactComponentInfo = (debugInfo: any); // Refined | ||
| const stack: string = debugInfo.stack; | ||
| const cachedEntry = taskCache.get((componentInfo: any)); | ||
| if (cachedEntry !== undefined) { | ||
| return cachedEntry; | ||
| } | ||
|
|
||
| const ownerTask = | ||
| componentInfo.owner == null | ||
| ? null | ||
| : initializeFakeTask(componentInfo.owner); | ||
|
|
||
| // eslint-disable-next-line react-internal/no-production-logging | ||
| const createTaskFn = (console: any).createTask.bind( | ||
| console, | ||
| getServerComponentTaskName(componentInfo), | ||
| ); | ||
| const callStack = buildFakeCallStack(stack, createTaskFn); | ||
|
|
||
| if (ownerTask === null) { | ||
| return callStack(); | ||
| } else { | ||
| return ownerTask.run(callStack); | ||
| } | ||
| } | ||
|
|
||
| function resolveDebugInfo( | ||
| response: Response, | ||
| id: number, | ||
|
|
@@ -1594,6 +1763,10 @@ function resolveDebugInfo( | |
| 'resolveDebugInfo should never be called in production mode. This is a bug in React.', | ||
| ); | ||
| } | ||
| // We eagerly initialize the fake task because this resolving happens outside any | ||
| // render phase so we're not inside a user space stack at this point. If we waited | ||
| // to initialize it when we need it, we might be inside user code. | ||
| initializeFakeTask(debugInfo); | ||
| const chunk = getChunk(response, id); | ||
| const chunkDebugInfo: ReactDebugInfo = | ||
| chunk._debugInfo || (chunk._debugInfo = []); | ||
|
|
@@ -1615,12 +1788,28 @@ function resolveConsoleEntry( | |
| const payload: [string, string, null | ReactComponentInfo, string, mixed] = | ||
| parseModel(response, value); | ||
| const methodName = payload[0]; | ||
| // TODO: Restore the fake stack before logging. | ||
| // const stackTrace = payload[1]; | ||
| // const owner = payload[2]; | ||
| const stackTrace = payload[1]; | ||
| const owner = payload[2]; | ||
| const env = payload[3]; | ||
| const args = payload.slice(4); | ||
| printToConsole(methodName, args, env); | ||
| if (!enableOwnerStacks) { | ||
| // Printing with stack isn't really limited to owner stacks but | ||
| // we gate it behind the same flag for now while iterating. | ||
| printToConsole(methodName, args, env); | ||
| return; | ||
| } | ||
| const callStack = buildFakeCallStack( | ||
| stackTrace, | ||
| printToConsole.bind(null, methodName, args, env), | ||
| ); | ||
| if (owner != null) { | ||
| const task = initializeFakeTask(owner); | ||
| if (task !== null) { | ||
| task.run(callStack); | ||
| return; | ||
| } | ||
| } | ||
| callStack(); | ||
| } | ||
|
|
||
| function mergeBuffer( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking of maybe using
"use client"as the task name in this case because you're about to enter the the client and likely the client code will include the name of the client component but we may not have the code loaded yet when the element is created.