From c064e1665c120d850b7595d00ca64ec00ef376ca Mon Sep 17 00:00:00 2001 From: sebmarkbage Date: Thu, 8 Feb 2024 16:06:43 +0000 Subject: [PATCH] [Flight] Emit debug info for a Server Component (#28272) This adds a new DEV-only row type `D` for DebugInfo. If we see this in prod, that's an error. It can contain extra debug information about the Server Components (or Promises) that were compiled away during the server render. It's DEV-only since this can contain sensitive information (similar to errors) and since it'll be a lot of data, but it's worth using the same stream for simplicity rather than a side-channel. In this first pass it's just the Server Component's name but I'll keep adding more debug info to the stream, and it won't always just be a Server Component's stack frame. Each row can get more debug rows data streaming in as it resolves and renders multiple server components in a row. The data structure is just a side-channel and it would be perfectly fine to ignore the D rows and it would behave the same as prod. With this data structure though the data is associated with the row ID / chunk, so you can't have inline meta data. This means that an inline Server Component that doesn't get an ID otherwise will need to be outlined. The way I outline Server Components is using a direct reference where it's synchronous though so on the client side it behaves the same (i.e. there's no lazy wrapper in this case). In most cases the `_debugInfo` is on the Promises that we yield and we also expose this on the `React.Lazy` wrappers. In the case where it's a synchronous render it might attach this data to Elements or Arrays (fragments) too. In a future PR I'll wire this information up with Fiber to stash it in the Fiber data structures so that DevTools can pick it up. This property and the information in it is not limited to Server Components. The name of the property that we look for probably shouldn't be `_debugInfo` since it's semi-public. Should consider the name we use for that. If it's a synchronous render that returns a string or number (text node) then we don't have anywhere to attach them to. We could add a `React.Lazy` wrapper for those but I chose to prioritize keeping the data structure untouched. Can be useful if you use Server Components to render data instead of React Nodes. DiffTrain build for [b229f540e2da91370611945f9875e00a96196df6](https://github.com/facebook/react/commit/b229f540e2da91370611945f9875e00a96196df6) --- .../facebook-www/JSXDEVRuntime-dev.classic.js | 7 ++ .../facebook-www/JSXDEVRuntime-dev.modern.js | 7 ++ compiled/facebook-www/REVISION | 2 +- compiled/facebook-www/React-dev.classic.js | 16 +++- compiled/facebook-www/React-dev.modern.js | 16 +++- compiled/facebook-www/React-prod.modern.js | 2 +- .../ReactDOMTesting-prod.modern.js | 6 +- .../ReactFlightDOMClient-dev.modern.js | 66 ++++++++++++++++- .../ReactFlightDOMClient-prod.modern.js | 4 + .../ReactFlightDOMServer-dev.modern.js | 74 ++++++++++++++++++- .../ReactFlightDOMServer-prod.modern.js | 27 +++---- .../facebook-www/ReactServer-dev.modern.js | 16 +++- .../facebook-www/ReactServer-prod.modern.js | 2 +- .../ReactTestRenderer-dev.modern.js | 2 +- 14 files changed, 218 insertions(+), 29 deletions(-) diff --git a/compiled/facebook-www/JSXDEVRuntime-dev.classic.js b/compiled/facebook-www/JSXDEVRuntime-dev.classic.js index ba099c926b..6d4e9fdeef 100644 --- a/compiled/facebook-www/JSXDEVRuntime-dev.classic.js +++ b/compiled/facebook-www/JSXDEVRuntime-dev.classic.js @@ -1054,6 +1054,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { diff --git a/compiled/facebook-www/JSXDEVRuntime-dev.modern.js b/compiled/facebook-www/JSXDEVRuntime-dev.modern.js index e5b2b52dfb..aea9509192 100644 --- a/compiled/facebook-www/JSXDEVRuntime-dev.modern.js +++ b/compiled/facebook-www/JSXDEVRuntime-dev.modern.js @@ -1054,6 +1054,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { diff --git a/compiled/facebook-www/REVISION b/compiled/facebook-www/REVISION index f5f42c5179..cca1fb214c 100644 --- a/compiled/facebook-www/REVISION +++ b/compiled/facebook-www/REVISION @@ -1 +1 @@ -37d901e2b81e12d40df7012c6f8681b8272d2555 +b229f540e2da91370611945f9875e00a96196df6 diff --git a/compiled/facebook-www/React-dev.classic.js b/compiled/facebook-www/React-dev.classic.js index bc22e32d6a..9c9019955a 100644 --- a/compiled/facebook-www/React-dev.classic.js +++ b/compiled/facebook-www/React-dev.classic.js @@ -24,7 +24,7 @@ if (__DEV__) { ) { __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); } - var ReactVersion = "18.3.0-www-classic-965940b9"; + var ReactVersion = "18.3.0-www-classic-28532002"; // ATTENTION // When adding new symbols to this file, @@ -772,6 +772,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { @@ -1808,6 +1815,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { diff --git a/compiled/facebook-www/React-dev.modern.js b/compiled/facebook-www/React-dev.modern.js index 870acd23a3..c19d216e3b 100644 --- a/compiled/facebook-www/React-dev.modern.js +++ b/compiled/facebook-www/React-dev.modern.js @@ -24,7 +24,7 @@ if (__DEV__) { ) { __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error()); } - var ReactVersion = "18.3.0-www-modern-b425cc4f"; + var ReactVersion = "18.3.0-www-modern-f7a0584c"; // ATTENTION // When adding new symbols to this file, @@ -772,6 +772,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { @@ -1808,6 +1815,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { diff --git a/compiled/facebook-www/React-prod.modern.js b/compiled/facebook-www/React-prod.modern.js index 697088b4a4..36be5deae8 100644 --- a/compiled/facebook-www/React-prod.modern.js +++ b/compiled/facebook-www/React-prod.modern.js @@ -562,4 +562,4 @@ exports.useSyncExternalStore = function ( exports.useTransition = function () { return ReactCurrentDispatcher.current.useTransition(); }; -exports.version = "18.3.0-www-modern-2c5233b3"; +exports.version = "18.3.0-www-modern-97aa8a4c"; diff --git a/compiled/facebook-www/ReactDOMTesting-prod.modern.js b/compiled/facebook-www/ReactDOMTesting-prod.modern.js index 2b050fe1e3..2ba8d6fea7 100644 --- a/compiled/facebook-www/ReactDOMTesting-prod.modern.js +++ b/compiled/facebook-www/ReactDOMTesting-prod.modern.js @@ -17066,7 +17066,7 @@ Internals.Events = [ var devToolsConfig$jscomp$inline_1784 = { findFiberByHostInstance: getClosestInstanceFromNode, bundleType: 0, - version: "18.3.0-www-modern-5ed7b4c6", + version: "18.3.0-www-modern-da44ba18", rendererPackageName: "react-dom" }; var internals$jscomp$inline_2157 = { @@ -17097,7 +17097,7 @@ var internals$jscomp$inline_2157 = { scheduleRoot: null, setRefreshHandler: null, getCurrentFiber: null, - reconcilerVersion: "18.3.0-www-modern-5ed7b4c6" + reconcilerVersion: "18.3.0-www-modern-da44ba18" }; if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) { var hook$jscomp$inline_2158 = __REACT_DEVTOOLS_GLOBAL_HOOK__; @@ -17525,4 +17525,4 @@ exports.useFormStatus = function () { return ReactCurrentDispatcher$2.current.useHostTransitionStatus(); throw Error(formatProdErrorMessage(248)); }; -exports.version = "18.3.0-www-modern-5ed7b4c6"; +exports.version = "18.3.0-www-modern-da44ba18"; diff --git a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js index 411809bed4..d8ed9df04c 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-dev.modern.js @@ -233,13 +233,18 @@ if (__DEV__) { var RESOLVED_MODEL = "resolved_model"; var RESOLVED_MODULE = "resolved_module"; var INITIALIZED = "fulfilled"; - var ERRORED = "rejected"; // $FlowFixMe[missing-this-annot] + var ERRORED = "rejected"; // Dev-only + // $FlowFixMe[missing-this-annot] function Chunk(status, value, reason, response) { this.status = status; this.value = value; this.reason = reason; this._response = response; + + { + this._debugInfo = null; + } } // We subclass Promise.prototype so that we get other methods like .catch Chunk.prototype = Object.create(Promise.prototype); // TODO: This doesn't return a new Promise chain unlike the real .then @@ -537,6 +542,13 @@ if (__DEV__) { enumerable: false, writable: true, value: true // This element has already been validated on the server. + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); } @@ -549,6 +561,13 @@ if (__DEV__) { _payload: chunk, _init: readChunk }; + + { + // Ensure we have a live array to track future debug info. + var chunkDebugInfo = chunk._debugInfo || (chunk._debugInfo = []); + lazyType._debugInfo = chunkDebugInfo; + } + return lazyType; } @@ -768,7 +787,35 @@ if (__DEV__) { switch (_chunk2.status) { case INITIALIZED: - return _chunk2.value; + var chunkValue = _chunk2.value; + + if (_chunk2._debugInfo) { + // If we have a direct reference to an object that was rendered by a synchronous + // server component, it might have some debug info about how it was rendered. + // We forward this to the underlying object. This might be a React Element or + // an Array fragment. + // If this was a string / number return value we lose the debug info. We choose + // that tradeoff to allow sync server components to return plain values and not + // use them as React Nodes necessarily. We could otherwise wrap them in a Lazy. + if ( + typeof chunkValue === "object" && + chunkValue !== null && + (Array.isArray(chunkValue) || + chunkValue.$$typeof === REACT_ELEMENT_TYPE) && + !chunkValue._debugInfo + ) { + // We should maybe use a unique symbol for arrays but this is a React owned array. + // $FlowFixMe[prop-missing]: This should be added to elements. + Object.defineProperty(chunkValue, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: _chunk2._debugInfo + }); + } + } + + return chunkValue; case PENDING: case BLOCKED: @@ -925,6 +972,12 @@ if (__DEV__) { dispatchHint(code, hintModel); } + function resolveDebugInfo(response, id, debugInfo) { + var chunk = getChunk(response, id); + var chunkDebugInfo = chunk._debugInfo || (chunk._debugInfo = []); + chunkDebugInfo.push(debugInfo); + } + function processFullRow(response, id, tag, buffer, chunk) { var stringDecoder = response._stringDecoder; var row = ""; @@ -972,6 +1025,15 @@ if (__DEV__) { return; } + case 68: /* "D" */ + { + { + var debugInfo = JSON.parse(row); + resolveDebugInfo(response, id, debugInfo); + return; + } + } + case 80: /* "P" */ // Fallthrough diff --git a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js index 854aced182..e504884acb 100644 --- a/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js +++ b/compiled/facebook-www/ReactFlightDOMClient-prod.modern.js @@ -506,6 +506,10 @@ function startReadingFromStream(response, stream) { new Chunk("fulfilled", rowTag, null, rowLength) ); break; + case 68: + throw Error( + "Failed to read a RSC payload created by a development version of React on the server while using a production version on the client. Always use matching versions on the server and the client." + ); default: (i = rowLength._chunks), (offset = i.get(rowID)) diff --git a/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js b/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js index 34fe971347..1bf27e60f3 100644 --- a/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js +++ b/compiled/facebook-www/ReactFlightDOMServer-dev.modern.js @@ -581,7 +581,10 @@ if (__DEV__) { thenableState = prevThenableState; } function getThenableStateAfterSuspending() { - var state = thenableState; + // If you use() to Suspend this should always exist but if you throw a Promise instead, + // which is not really supported anymore, it will be empty. We use the empty set as a + // marker to know if this was a replay of the same component or first attempt. + var state = thenableState || createThenableState(); thenableState = null; return state; } @@ -1314,6 +1317,23 @@ if (__DEV__) { // component suspends again, the thenable state will be restored. var prevThenableState = task.thenableState; task.thenableState = null; + + { + if (debugID === null) { + // We don't have a chunk to assign debug info. We need to outline this + // component to assign it an ID. + return outlineTask(request, task); + } else if (prevThenableState !== null); + else { + // This is a new component in the same task so we can emit more debug info. + var componentName = Component.displayName || Component.name || ""; + request.pendingChunks++; + emitDebugChunk(request, debugID, { + name: componentName + }); + } + } + prepareToUseHooksForComponent(prevThenableState); // The secondArg is always undefined in Server Components since refs error early. var secondArg = undefined; @@ -1421,6 +1441,28 @@ if (__DEV__) { // or anything else too which we also get implicitly. return element; + } // The chunk ID we're currently rendering that we can assign debug data to. + + var debugID = null; + + function outlineTask(request, task) { + var newTask = createTask( + request, + task.model, // the currently rendering element + task.keyPath, // unlike outlineModel this one carries along context + task.implicitSlot, + request.abortableTasks + ); + retryTask(request, newTask); + + if (newTask.status === COMPLETED) { + // We completed synchronously so we can refer to this by reference. This + // makes it behaves the same as prod during deserialization. + return serializeByValueID(newTask.id); + } // This didn't complete synchronously so it wouldn't have even if we didn't + // outline it, so this would reduce to a lazy reference even in prod. + + return serializeLazyID(newTask.id); } function renderElement(request, task, type, key, ref, props) { @@ -1445,7 +1487,7 @@ if (__DEV__) { if (isClientReference(type)) { // This is a reference to a Client Component. return renderClientElement(task, type, key, props); - } // This is a server-side component. + } // This is a Server Component. return renderFunctionComponent(request, task, key, type, props); } else if (typeof type === "string") { @@ -2257,6 +2299,13 @@ if (__DEV__) { request.completedRegularChunks.push(processedChunk); } + function emitDebugChunk(request, id, debugInfo) { + var json = stringify(debugInfo); + var row = serializeRowHeader("D", id) + json + "\n"; + var processedChunk = stringToChunk(row); + request.completedRegularChunks.push(processedChunk); + } + var emptyRoot = {}; function retryTask(request, task) { @@ -2265,11 +2314,18 @@ if (__DEV__) { return; } + var prevDebugID = debugID; + try { // Track the root so we know that we have to emit this object even though it // already has an ID. This is needed because we might see this object twice // in the same toJSON if it is cyclic. - modelRoot = task.model; // We call the destructive form that mutates this task. That way if something + modelRoot = task.model; + + if (true) { + // Track the ID of the current task so we can assign debug info to this id. + debugID = task.id; + } // 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. var resolvedModel = renderModelDestructive( @@ -2278,7 +2334,13 @@ if (__DEV__) { emptyRoot, "", task.model - ); // Track the root again for the resolved object. + ); + + if (true) { + // We're now past rendering this task and future renders will spawn new tasks for their + // debug info. + debugID = null; + } // Track the root again for the resolved object. modelRoot = resolvedModel; // The keyPath resets at any terminal child node. @@ -2326,6 +2388,10 @@ if (__DEV__) { task.status = ERRORED; var digest = logRecoverableError(request, x); emitErrorChunk(request, task.id, digest, x); + } finally { + { + debugID = prevDebugID; + } } } diff --git a/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js b/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js index b03f934148..fc9b664726 100644 --- a/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js +++ b/compiled/facebook-www/ReactFlightDOMServer-prod.modern.js @@ -236,7 +236,7 @@ var currentRequest$1 = null, thenableIndexCounter = 0, thenableState = null; function getThenableStateAfterSuspending() { - var state = thenableState; + var state = thenableState || []; thenableState = null; return state; } @@ -987,20 +987,21 @@ function retryTask(request, task) { request.abortableTasks.delete(task); task.status = 1; } catch (thrownValue) { - (resolvedModel = + var x = thrownValue === SuspenseException ? getSuspendedThenable() - : thrownValue), - "object" === typeof resolvedModel && - null !== resolvedModel && - "function" === typeof resolvedModel.then - ? ((request = task.ping), - resolvedModel.then(request, request), - (task.thenableState = getThenableStateAfterSuspending())) - : (request.abortableTasks.delete(task), - (task.status = 4), - (resolvedModel = logRecoverableError(request, resolvedModel)), - emitErrorChunk(request, task.id, resolvedModel)); + : thrownValue; + if ("object" === typeof x && null !== x && "function" === typeof x.then) { + var ping = task.ping; + x.then(ping, ping); + task.thenableState = getThenableStateAfterSuspending(); + } else { + request.abortableTasks.delete(task); + task.status = 4; + var digest = logRecoverableError(request, x); + emitErrorChunk(request, task.id, digest); + } + } finally { } } function performWork(request) { diff --git a/compiled/facebook-www/ReactServer-dev.modern.js b/compiled/facebook-www/ReactServer-dev.modern.js index a02e8ef30e..6c60b29956 100644 --- a/compiled/facebook-www/ReactServer-dev.modern.js +++ b/compiled/facebook-www/ReactServer-dev.modern.js @@ -564,6 +564,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { @@ -1521,6 +1528,13 @@ if (__DEV__) { enumerable: false, writable: true, value: false + }); // debugInfo contains Server Component debug information. + + Object.defineProperty(element, "_debugInfo", { + configurable: false, + enumerable: false, + writable: true, + value: null }); if (Object.freeze) { @@ -2821,7 +2835,7 @@ if (__DEV__) { console["error"](error); }; - var ReactVersion = "18.3.0-www-modern-b92e22f0"; + var ReactVersion = "18.3.0-www-modern-61a7b928"; // Patch fetch var Children = { diff --git a/compiled/facebook-www/ReactServer-prod.modern.js b/compiled/facebook-www/ReactServer-prod.modern.js index 580336a899..0415d7d8c8 100644 --- a/compiled/facebook-www/ReactServer-prod.modern.js +++ b/compiled/facebook-www/ReactServer-prod.modern.js @@ -468,4 +468,4 @@ exports.useId = function () { exports.useMemo = function (create, deps) { return ReactCurrentDispatcher.current.useMemo(create, deps); }; -exports.version = "18.3.0-www-modern-0bbe43aa"; +exports.version = "18.3.0-www-modern-8cbdba22"; diff --git a/compiled/facebook-www/ReactTestRenderer-dev.modern.js b/compiled/facebook-www/ReactTestRenderer-dev.modern.js index 7ddcde0780..0fe8e13025 100644 --- a/compiled/facebook-www/ReactTestRenderer-dev.modern.js +++ b/compiled/facebook-www/ReactTestRenderer-dev.modern.js @@ -26053,7 +26053,7 @@ if (__DEV__) { return root; } - var ReactVersion = "18.3.0-www-modern-e87be14d"; + var ReactVersion = "18.3.0-www-modern-2a20f3fe"; // Might add PROFILE later.