[Flight] Include environment name both in the virtual URL and findSourceMapURL (#30452)
This way you can use the environment to know where to look for the source map in case you have multiple server environments. This becomes part of the public protocol since it's part of what you'll parse out of the `rsc://React/` prefixed URLs inside of `captureOwnerStack`.
Sebastian Markbåge committed
Jul 25, 2024 at 11:14 UTC
e5d22459ffd094ffd71c3e8857b62b3c6754c58d
3 files changed
+90
-29
packages/react-client/src/ReactFlightClient.js
+57
-14
@@ -232,7 +232,10 @@ Chunk.prototype.then = function <T>(
232
}
233
};
234
235
-export type FindSourceMapURLCallback = (fileName: string) => null | string;
235
+export type FindSourceMapURLCallback = (
236
+ fileName: string,
237
+ environmentName: string,
238
+) => null | string;
239
240
export type Response = {
241
_bundlerConfig: SSRModuleMap,
@@ -689,7 +692,15 @@ function createElement(
692
writable: true,
693
value: null,
694
});
695
+ let env = '';
696
if (enableOwnerStacks) {
697
+ if (owner !== null && owner.env != null) {
698
+ // Interestingly we don't actually have the environment name of where
699
+ // this JSX was created if it doesn't have an owner but if it does
700
+ // it must be the same environment as the owner. We could send it separately
701
+ // but it seems a bit unnecessary for this edge case.
702
+ env = owner.env;
703
+ }
704
let normalizedStackTrace: null | Error = null;
705
if (stack !== null) {
706
// We create a fake stack and then create an Error object inside of it.
@@ -698,7 +709,11 @@ function createElement(
709
// source mapping information.
710
// This can unfortunately happen within a user space callstack which will
711
// remain on the stack.
701
- normalizedStackTrace = createFakeJSXCallStackInDEV(response, stack);
712
+ normalizedStackTrace = createFakeJSXCallStackInDEV(
713
+ response,
714
+ stack,
715
+ env,
716
+ );
717
}
718
Object.defineProperty(element, '_debugStack', {
719
configurable: false,
@@ -713,7 +728,12 @@ function createElement(
728
console,
729
getTaskName(type),
730
);
716
- const callStack = buildFakeCallStack(response, stack, createTaskFn);
731
+ const callStack = buildFakeCallStack(
732
+ response,
733
+ stack,
734
+ env,
735
+ createTaskFn,
736
+ );
737
// This owner should ideally have already been initialized to avoid getting
738
// user stack frames on the stack.
739
const ownerTask =
@@ -1836,6 +1856,7 @@ function resolveErrorDev(
1856
const callStack = buildFakeCallStack(
1857
response,
1858
stack,
1859
+ env,
1860
// $FlowFixMe[incompatible-use]
1861
Error.bind(
1862
null,
@@ -1892,6 +1913,7 @@ function resolvePostponeDev(
1913
id: number,
1914
reason: string,
1915
stack: ReactStackTrace,
1916
+ env: string,
1917
): void {
1918
if (!__DEV__) {
1919
// These errors should never make it into a build so we don't need to encode them in codes.json
@@ -1917,6 +1939,7 @@ function resolvePostponeDev(
1939
const callStack = buildFakeCallStack(
1940
response,
1941
stack,
1942
+ env,
1943
// $FlowFixMe[incompatible-use]
1944
Error.bind(null, reason || ''),
1945
);
@@ -1961,6 +1984,7 @@ function createFakeFunction<T>(
1984
sourceMap: null | string,
1985
line: number,
1986
col: number,
1987
+ environmentName: string,
1988
): FakeFunction<T> {
1989
// This creates a fake copy of a Server Module. It represents a module that has already
1990
// executed on the server but we re-execute a blank copy for its stack frames on the client.
@@ -2013,7 +2037,13 @@ function createFakeFunction<T>(
2037
// 1) A printed stack trace string needs a unique URL to be able to source map it.
2038
// 2) If source maps are disabled or fails, you should at least be able to tell
2039
// which file it was.
2016
- code += '\n//# sourceURL=rsc://React/' + filename + '?' + fakeFunctionIdx++;
2040
+ code +=
2041
+ '\n//# sourceURL=rsc://React/' +
2042
+ encodeURIComponent(environmentName) +
2043
+ '/' +
2044
+ filename +
2045
+ '?' +
2046
+ fakeFunctionIdx++;
2047
code += '\n//# sourceMappingURL=' + sourceMap;
2048
} else if (filename) {
2049
code += '\n//# sourceURL=' + filename;
@@ -2037,19 +2067,28 @@ function createFakeFunction<T>(
2067
function buildFakeCallStack<T>(
2068
response: Response,
2069
stack: ReactStackTrace,
2070
+ environmentName: string,
2071
innerCall: () => T,
2072
): () => T {
2073
let callStack = innerCall;
2074
for (let i = 0; i < stack.length; i++) {
2075
const frame = stack[i];
2045
- const frameKey = frame.join('-');
2076
+ const frameKey = frame.join('-') + '-' + environmentName;
2077
let fn = fakeFunctionCache.get(frameKey);
2078
if (fn === undefined) {
2079
const [name, filename, line, col] = frame;
2049
- const sourceMap = response._debugFindSourceMapURL
2050
- ? response._debugFindSourceMapURL(filename)
2080
+ const findSourceMapURL = response._debugFindSourceMapURL;
2081
+ const sourceMap = findSourceMapURL
2082
+ ? findSourceMapURL(filename, environmentName)
2083
: null;
2052
- fn = createFakeFunction(name, filename, sourceMap, line, col);
2084
+ fn = createFakeFunction(
2085
+ name,
2086
+ filename,
2087
+ sourceMap,
2088
+ line,
2089
+ col,
2090
+ environmentName,
2091
+ );
2092
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
2093
// function is an input and can vary by response.
2094
fakeFunctionCache.set(frameKey, fn);
@@ -2079,7 +2118,7 @@ function initializeFakeTask(
2118
}
2119
2120
const stack = debugInfo.stack;
2082
-
2121
+ const env = componentInfo.env == null ? '' : componentInfo.env;
2122
const ownerTask =
2123
componentInfo.owner == null
2124
? null
@@ -2089,7 +2128,7 @@ function initializeFakeTask(
2128
console,
2129
getServerComponentTaskName(componentInfo),
2130
);
2092
- const callStack = buildFakeCallStack(response, stack, createTaskFn);
2131
+ const callStack = buildFakeCallStack(response, stack, env, createTaskFn);
2132
2133
let componentTask;
2134
if (ownerTask === null) {
@@ -2111,10 +2150,12 @@ const createFakeJSXCallStack = {
2150
'react-stack-bottom-frame': function (
2151
response: Response,
2152
stack: ReactStackTrace,
2153
+ environmentName: string,
2154
): Error {
2155
const callStackForError = buildFakeCallStack(
2156
response,
2157
stack,
2158
+ environmentName,
2159
fakeJSXCallSite,
2160
);
2161
return callStackForError();
@@ -2124,6 +2165,7 @@ const createFakeJSXCallStack = {
2165
const createFakeJSXCallStackInDEV: (
2166
response: Response,
2167
stack: ReactStackTrace,
2168
+ environmentName: string,
2169
) => Error = __DEV__
2170
? // We use this technique to trick minifiers to preserve the function name.
2171
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
@@ -2147,12 +2189,11 @@ function initializeFakeStack(
2189
return;
2190
}
2191
if (debugInfo.stack != null) {
2192
+ const stack = debugInfo.stack;
2193
+ const env = debugInfo.env == null ? '' : debugInfo.env;
2194
// $FlowFixMe[cannot-write]
2195
// $FlowFixMe[prop-missing]
2152
- debugInfo.debugStack = createFakeJSXCallStackInDEV(
2153
- response,
2154
- debugInfo.stack,
2155
- );
2196
+ debugInfo.debugStack = createFakeJSXCallStackInDEV(response, stack, env);
2197
}
2198
if (debugInfo.owner != null) {
2199
// Initialize any owners not yet initialized.
@@ -2221,6 +2262,7 @@ function resolveConsoleEntry(
2262
const callStack = buildFakeCallStack(
2263
response,
2264
stackTrace,
2265
+ env,
2266
printToConsole.bind(null, methodName, args, env),
2267
);
2268
if (owner != null) {
@@ -2460,6 +2502,7 @@ function processFullStringRow(
2502
id,
2503
postponeInfo.reason,
2504
postponeInfo.stack,
2505
+ postponeInfo.env,
2506
);
2507
} else {
2508
resolvePostponeProd(response, id);
packages/react-client/src/__tests__/ReactFlight-test.js
+26
-11
@@ -1241,10 +1241,10 @@ describe('ReactFlight', () => {
1241
const ClientErrorBoundary = clientReference(MyErrorBoundary);
1242
1243
function App() {
1244
- return (
1245
- <ClientErrorBoundary>
1246
- <ServerComponent />
1247
- </ClientErrorBoundary>
1244
+ return ReactServer.createElement(
1245
+ ClientErrorBoundary,
1246
+ null,
1247
+ ReactServer.createElement(ServerComponent),
1248
);
1249
}
1250
@@ -1301,13 +1301,16 @@ describe('ReactFlight', () => {
1301
],
1302
findSourceMapURLCalls: gate(flags => flags.enableOwnerStacks)
1303
? [
1304
- [__filename],
1305
- [__filename],
1304
+ [__filename, 'Server'],
1305
+ [__filename, 'Server'],
1306
// TODO: What should we request here? The outer (<anonymous>) or the inner (inspected-page.html)?
1307
- ['inspected-page.html:29:11), <anonymous>'],
1308
- ['file://~/(some)(really)(exotic-directory)/ReactFlight-test.js'],
1309
- ['file:///testing.js'],
1310
- [__filename],
1307
+ ['inspected-page.html:29:11), <anonymous>', 'Server'],
1308
+ [
1309
+ 'file://~/(some)(really)(exotic-directory)/ReactFlight-test.js',
1310
+ 'Server',
1311
+ ],
1312
+ ['file:///testing.js', 'Server'],
1313
+ [__filename, 'Server'],
1314
]
1315
: [],
1316
});
@@ -2836,6 +2839,7 @@ describe('ReactFlight', () => {
2839
); // The eval will end up normalizing these
2840
2841
let sawReactPrefix = false;
2842
+ const environments = [];
2843
await act(async () => {
2844
ReactNoop.render(
2845
<ErrorBoundary
@@ -2843,11 +2847,12 @@ describe('ReactFlight', () => {
2847
expectedEnviromentName="third-party"
2848
expectedErrorStack={expectedErrorStack}>
2849
{ReactNoopFlightClient.read(transport, {
2846
- findSourceMapURL(url) {
2850
+ findSourceMapURL(url, environmentName) {
2851
if (url.startsWith('rsc://React/')) {
2852
// We don't expect to see any React prefixed URLs here.
2853
sawReactPrefix = true;
2854
}
2855
+ environments.push(environmentName);
2856
// My not giving a source map, we should leave it intact.
2857
return null;
2858
},
@@ -2857,6 +2862,16 @@ describe('ReactFlight', () => {
2862
});
2863
2864
expect(sawReactPrefix).toBe(false);
2865
+ if (__DEV__ && gate(flags => flags.enableOwnerStacks)) {
2866
+ expect(environments.slice(0, 4)).toEqual([
2867
+ 'Server',
2868
+ 'third-party',
2869
+ 'third-party',
2870
+ 'third-party',
2871
+ ]);
2872
+ } else {
2873
+ expect(environments).toEqual([]);
2874
+ }
2875
});
2876
2877
it('can change the environment name inside a component', async () => {
packages/react-server/src/ReactFlightServer.js
+7
-4
@@ -163,10 +163,12 @@ function filterStackTrace(
163
if (url.startsWith('rsc://React/')) {
164
// This callsite is a virtual fake callsite that came from another Flight client.
165
// We need to reverse it back into the original location by stripping its prefix
166
- // and suffix.
166
+ // and suffix. We don't need the environment name because it's available on the
167
+ // parent object that will contain the stack.
168
+ const envIdx = url.indexOf('/', 12);
169
const suffixIdx = url.lastIndexOf('?');
168
- if (suffixIdx > -1) {
169
- url = callsite[1] = url.slice(12, suffixIdx);
170
+ if (envIdx > -1 && suffixIdx > -1) {
171
+ url = callsite[1] = url.slice(envIdx + 1, suffixIdx);
172
}
173
}
174
if (!filterStackFrame(url, functionName)) {
@@ -2887,6 +2889,7 @@ function emitPostponeChunk(
2889
if (__DEV__) {
2890
let reason = '';
2891
let stack: ReactStackTrace;
2892
+ const env = request.environmentName();
2893
try {
2894
// eslint-disable-next-line react-internal/safe-string-coercion
2895
reason = String(postponeInstance.message);
@@ -2894,7 +2897,7 @@ function emitPostponeChunk(
2897
} catch (x) {
2898
stack = [];
2899
}
2897
- row = serializeRowHeader('P', id) + stringify({reason, stack}) + '\n';
2900
+ row = serializeRowHeader('P', id) + stringify({reason, stack, env}) + '\n';
2901
} else {
2902
// No reason included in prod.
2903
row = serializeRowHeader('P', id) + '\n';