Skip to content

Commit a72bcf4

Browse files
committed
Add findSourceMapURL option to get a URL to load Server source maps from
1 parent adbec0c commit a72bcf4

File tree

15 files changed

+204
-16
lines changed

15 files changed

+204
-16
lines changed

fixtures/flight/config/webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ module.exports = function (webpackEnv) {
199199
? shouldUseSourceMap
200200
? 'source-map'
201201
: false
202-
: isEnvDevelopment && 'cheap-module-source-map',
202+
: isEnvDevelopment && 'source-map',
203203
// These are the "entry points" to our application.
204204
// This means they will be the "root" imports that are included in JS bundle.
205205
entry: isEnvProduction

fixtures/flight/loader/region.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const babelOptions = {
1616
'@babel/plugin-syntax-import-meta',
1717
'@babel/plugin-transform-react-jsx',
1818
],
19+
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
1920
};
2021

2122
async function babelLoad(url, context, defaultLoad) {

fixtures/flight/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
7272
"dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"",
7373
"dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js server/global",
74-
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/region.js --conditions=react-server server/region",
74+
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --enable-source-maps --experimental-loader ./loader/region.js --conditions=react-server server/region",
7575
"start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"",
7676
"start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global",
7777
"start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region",

fixtures/flight/server/global.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,43 @@ app.all('/', async function (req, res, next) {
214214

215215
if (process.env.NODE_ENV === 'development') {
216216
app.use(express.static('public'));
217+
218+
app.get('/source-maps', async function (req, res, next) {
219+
// Proxy the request to the regional server.
220+
const proxiedHeaders = {
221+
'X-Forwarded-Host': req.hostname,
222+
'X-Forwarded-For': req.ips,
223+
'X-Forwarded-Port': 3000,
224+
'X-Forwarded-Proto': req.protocol,
225+
};
226+
227+
const promiseForData = request(
228+
{
229+
host: '127.0.0.1',
230+
port: 3001,
231+
method: req.method,
232+
path: req.originalUrl,
233+
headers: proxiedHeaders,
234+
},
235+
req
236+
);
237+
238+
try {
239+
const rscResponse = await promiseForData;
240+
res.set('Content-type', 'application/json');
241+
rscResponse.on('data', data => {
242+
res.write(data);
243+
res.flush();
244+
});
245+
rscResponse.on('end', data => {
246+
res.end();
247+
});
248+
} catch (e) {
249+
console.error(`Failed to proxy request: ${e.stack}`);
250+
res.statusCode = 500;
251+
res.end();
252+
}
253+
});
217254
} else {
218255
// In production we host the static build output.
219256
app.use(express.static('build'));

fixtures/flight/server/region.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ babelRegister({
2424
],
2525
presets: ['@babel/preset-react'],
2626
plugins: ['@babel/transform-modules-commonjs'],
27+
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
2728
});
2829

2930
if (typeof fetch === 'undefined') {
@@ -38,6 +39,8 @@ const app = express();
3839
const compress = require('compression');
3940
const {Readable} = require('node:stream');
4041

42+
const nodeModule = require('node:module');
43+
4144
app.use(compress());
4245

4346
// Application
@@ -176,6 +179,69 @@ app.get('/todos', function (req, res) {
176179
]);
177180
});
178181

182+
if (process.env.NODE_ENV === 'development') {
183+
const rootDir = path.resolve(__dirname, '../');
184+
185+
app.get('/source-maps', async function (req, res, next) {
186+
try {
187+
res.set('Content-type', 'application/json');
188+
let requestedFilePath = req.query.name;
189+
190+
if (requestedFilePath.startsWith('file://')) {
191+
requestedFilePath = requestedFilePath.slice(7);
192+
}
193+
194+
const relativePath = path.relative(rootDir, requestedFilePath);
195+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
196+
// This is outside the root directory of the app. Forbid it to be served.
197+
res.status = 403;
198+
res.write('{}');
199+
res.end();
200+
return;
201+
}
202+
203+
const sourceMap = nodeModule.findSourceMap(requestedFilePath);
204+
let map;
205+
// There are two ways to return a source map depending on what we observe in error.stack.
206+
// A real app will have a similar choice to make for which strategy to pick.
207+
if (!sourceMap || Error.prepareStackTrace === undefined) {
208+
// When --enable-source-maps is enabled, the error.stack that we use to track
209+
// stacks will have had the source map already applied so it's pointing to the
210+
// original source. We return a blank source map that just maps everything to
211+
// the original source in this case.
212+
const sourceContent = await readFile(requestedFilePath, 'utf8');
213+
const lines = sourceContent.split('\n').length;
214+
map = {
215+
version: 3,
216+
sources: [requestedFilePath],
217+
sourcesContent: [sourceContent],
218+
// Note: This approach to mapping each line only lets you jump to each line
219+
// not jump to a column within a line. To do that, you need a proper source map
220+
// generated for each parsed segment or add a segment for each column.
221+
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
222+
sourceRoot: '',
223+
};
224+
} else {
225+
// If something has overridden prepareStackTrace it is likely not getting the
226+
// natively applied source mapping to error.stack and so the line will point to
227+
// the compiled output similar to how a browser works.
228+
// E.g. ironically this can happen with the source-map-support library that is
229+
// auto-invoked by @babel/register if external source maps are generated.
230+
// In this case we just use the source map that the native source mapping would
231+
// have used.
232+
map = sourceMap.payload;
233+
}
234+
res.write(JSON.stringify(map));
235+
res.end();
236+
} catch (x) {
237+
res.status = 500;
238+
res.write('{}');
239+
res.end();
240+
console.error(x);
241+
}
242+
});
243+
}
244+
179245
app.listen(3001, () => {
180246
console.log('Regional Flight Server listening on port 3001...');
181247
});

fixtures/flight/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ async function hydrateApp() {
3939
}),
4040
{
4141
callServer,
42+
findSourceMapURL(fileName) {
43+
return '/source-maps?name=' + encodeURIComponent(fileName);
44+
},
4245
}
4346
);
4447

packages/react-client/src/ReactFlightClient.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ Chunk.prototype.then = function <T>(
239239
}
240240
};
241241

242+
export type FindSourceMapURLCallback = (fileName: string) => null | string;
243+
242244
export type Response = {
243245
_bundlerConfig: SSRModuleMap,
244246
_moduleLoading: ModuleLoading,
@@ -255,6 +257,7 @@ export type Response = {
255257
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
256258
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
257259
_debugRootTask?: null | ConsoleTask, // DEV-only
260+
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
258261
};
259262

260263
function readChunk<T>(chunk: SomeChunk<T>): T {
@@ -696,7 +699,7 @@ function createElement(
696699
console,
697700
getTaskName(type),
698701
);
699-
const callStack = buildFakeCallStack(stack, createTaskFn);
702+
const callStack = buildFakeCallStack(response, stack, createTaskFn);
700703
// This owner should ideally have already been initialized to avoid getting
701704
// user stack frames on the stack.
702705
const ownerTask =
@@ -1140,6 +1143,7 @@ export function createResponse(
11401143
encodeFormAction: void | EncodeFormActionCallback,
11411144
nonce: void | string,
11421145
temporaryReferences: void | TemporaryReferenceSet,
1146+
findSourceMapURL: void | FindSourceMapURLCallback,
11431147
): Response {
11441148
const chunks: Map<number, SomeChunk<any>> = new Map();
11451149
const response: Response = {
@@ -1166,6 +1170,9 @@ export function createResponse(
11661170
// TODO: Make this string configurable.
11671171
response._debugRootTask = (console: any).createTask('"use server"');
11681172
}
1173+
if (__DEV__) {
1174+
response._debugFindSourceMapURL = findSourceMapURL;
1175+
}
11691176
// Don't inline this call because it causes closure to outline the call above.
11701177
response._fromJSON = createFromJSONCallback(response);
11711178
return response;
@@ -1673,6 +1680,7 @@ const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
16731680
function createFakeFunction<T>(
16741681
name: string,
16751682
filename: string,
1683+
sourceMap: null | string,
16761684
line: number,
16771685
col: number,
16781686
): FakeFunction<T> {
@@ -1697,7 +1705,9 @@ function createFakeFunction<T>(
16971705
'_()\n';
16981706
}
16991707

1700-
if (filename) {
1708+
if (sourceMap) {
1709+
code += '//# sourceMappingURL=' + sourceMap;
1710+
} else if (filename) {
17011711
code += '//# sourceURL=' + filename;
17021712
}
17031713

@@ -1720,10 +1730,18 @@ function createFakeFunction<T>(
17201730
return fn;
17211731
}
17221732

1733+
// This matches either of these V8 formats.
1734+
// at name (filename:0:0)
1735+
// at filename:0:0
1736+
// at async filename:0:0
17231737
const frameRegExp =
1724-
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|([^\)]+):(\d+):(\d+))$/;
1738+
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
17251739

1726-
function buildFakeCallStack<T>(stack: string, innerCall: () => T): () => T {
1740+
function buildFakeCallStack<T>(
1741+
response: Response,
1742+
stack: string,
1743+
innerCall: () => T,
1744+
): () => T {
17271745
const frames = stack.split('\n');
17281746
let callStack = innerCall;
17291747
for (let i = 0; i < frames.length; i++) {
@@ -1739,7 +1757,13 @@ function buildFakeCallStack<T>(stack: string, innerCall: () => T): () => T {
17391757
const filename = parsed[2] || parsed[5] || '';
17401758
const line = +(parsed[3] || parsed[6]);
17411759
const col = +(parsed[4] || parsed[7]);
1742-
fn = createFakeFunction(name, filename, line, col);
1760+
const sourceMap = response._debugFindSourceMapURL
1761+
? response._debugFindSourceMapURL(filename)
1762+
: null;
1763+
fn = createFakeFunction(name, filename, sourceMap, line, col);
1764+
// TODO: This cache should technically live on the response since the _debugFindSourceMapURL
1765+
// function is an input and can vary by response.
1766+
fakeFunctionCache.set(frame, fn);
17431767
}
17441768
callStack = fn.bind(null, callStack);
17451769
}
@@ -1770,7 +1794,7 @@ function initializeFakeTask(
17701794
console,
17711795
getServerComponentTaskName(componentInfo),
17721796
);
1773-
const callStack = buildFakeCallStack(stack, createTaskFn);
1797+
const callStack = buildFakeCallStack(response, stack, createTaskFn);
17741798

17751799
if (ownerTask === null) {
17761800
const rootTask = response._debugRootTask;
@@ -1832,6 +1856,7 @@ function resolveConsoleEntry(
18321856
return;
18331857
}
18341858
const callStack = buildFakeCallStack(
1859+
response,
18351860
stackTrace,
18361861
printToConsole.bind(null, methodName, args, env),
18371862
);

packages/react-server-dom-esm/src/ReactFlightDOMClientBrowser.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import type {Thenable} from 'shared/ReactTypes.js';
1111

12-
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
12+
import type {
13+
Response as FlightResponse,
14+
FindSourceMapURLCallback,
15+
} from 'react-client/src/ReactFlightClient';
1316

1417
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
1518

@@ -38,6 +41,7 @@ export type Options = {
3841
moduleBaseURL?: string,
3942
callServer?: CallServerCallback,
4043
temporaryReferences?: TemporaryReferenceSet,
44+
findSourceMapURL?: FindSourceMapURLCallback,
4145
};
4246

4347
function createResponseFromOptions(options: void | Options) {
@@ -50,6 +54,9 @@ function createResponseFromOptions(options: void | Options) {
5054
options && options.temporaryReferences
5155
? options.temporaryReferences
5256
: undefined,
57+
__DEV__ && options && options.findSourceMapURL
58+
? options.findSourceMapURL
59+
: undefined,
5360
);
5461
}
5562

packages/react-server-dom-esm/src/ReactFlightDOMClientNode.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import type {Thenable, ReactCustomFormAction} from 'shared/ReactTypes.js';
1111

12-
import type {Response} from 'react-client/src/ReactFlightClient';
12+
import type {
13+
Response,
14+
FindSourceMapURLCallback,
15+
} from 'react-client/src/ReactFlightClient';
1316

1417
import type {Readable} from 'stream';
1518

@@ -46,6 +49,7 @@ type EncodeFormActionCallback = <A>(
4649
export type Options = {
4750
nonce?: string,
4851
encodeFormAction?: EncodeFormActionCallback,
52+
findSourceMapURL?: FindSourceMapURLCallback,
4953
};
5054

5155
function createFromNodeStream<T>(
@@ -61,6 +65,9 @@ function createFromNodeStream<T>(
6165
options ? options.encodeFormAction : undefined,
6266
options && typeof options.nonce === 'string' ? options.nonce : undefined,
6367
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
68+
__DEV__ && options && options.findSourceMapURL
69+
? options.findSourceMapURL
70+
: undefined,
6471
);
6572
stream.on('data', chunk => {
6673
processBinaryChunk(response, chunk);

packages/react-server-dom-turbopack/src/ReactFlightDOMClientBrowser.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
import type {Thenable} from 'shared/ReactTypes.js';
1111

12-
import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
12+
import type {
13+
Response as FlightResponse,
14+
FindSourceMapURLCallback,
15+
} from 'react-client/src/ReactFlightClient';
1316

1417
import type {ReactServerValue} from 'react-client/src/ReactFlightReplyClient';
1518

@@ -37,6 +40,7 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
3740
export type Options = {
3841
callServer?: CallServerCallback,
3942
temporaryReferences?: TemporaryReferenceSet,
43+
findSourceMapURL?: FindSourceMapURLCallback,
4044
};
4145

4246
function createResponseFromOptions(options: void | Options) {
@@ -49,6 +53,9 @@ function createResponseFromOptions(options: void | Options) {
4953
options && options.temporaryReferences
5054
? options.temporaryReferences
5155
: undefined,
56+
__DEV__ && options && options.findSourceMapURL
57+
? options.findSourceMapURL
58+
: undefined,
5259
);
5360
}
5461

0 commit comments

Comments
 (0)