Skip to content
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

[Flight] Add findSourceMapURL option to get a URL to load Server source maps from #29708

Merged
merged 1 commit into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fixtures/flight/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ module.exports = function (webpackEnv) {
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
: isEnvDevelopment && 'source-map',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a requirement for this feature to work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's just for client source maps. They end up off by one and not mapping to the right columns so not testing the proper experience.

// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: isEnvProduction
Expand Down
1 change: 1 addition & 0 deletions fixtures/flight/loader/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const babelOptions = {
'@babel/plugin-syntax-import-meta',
'@babel/plugin-transform-react-jsx',
],
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
};

async function babelLoad(url, context, defaultLoad) {
Expand Down
2 changes: 1 addition & 1 deletion fixtures/flight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
"dev": "concurrently \"npm run dev:region\" \"npm run dev:global\"",
"dev:global": "NODE_ENV=development BUILD_PATH=dist node --experimental-loader ./loader/global.js server/global",
"dev:region": "NODE_ENV=development BUILD_PATH=dist nodemon --watch src --watch dist -- --experimental-loader ./loader/region.js --conditions=react-server server/region",
"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",
"start": "node scripts/build.js && concurrently \"npm run start:region\" \"npm run start:global\"",
"start:global": "NODE_ENV=production node --experimental-loader ./loader/global.js server/global",
"start:region": "NODE_ENV=production node --experimental-loader ./loader/region.js --conditions=react-server server/region",
Expand Down
37 changes: 37 additions & 0 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,43 @@ app.all('/', async function (req, res, next) {

if (process.env.NODE_ENV === 'development') {
app.use(express.static('public'));

app.get('/source-maps', async function (req, res, next) {
// Proxy the request to the regional server.
const proxiedHeaders = {
'X-Forwarded-Host': req.hostname,
'X-Forwarded-For': req.ips,
'X-Forwarded-Port': 3000,
'X-Forwarded-Proto': req.protocol,
};

const promiseForData = request(
{
host: '127.0.0.1',
port: 3001,
method: req.method,
path: req.originalUrl,
headers: proxiedHeaders,
},
req
);

try {
const rscResponse = await promiseForData;
res.set('Content-type', 'application/json');
rscResponse.on('data', data => {
res.write(data);
res.flush();
});
rscResponse.on('end', data => {
res.end();
});
} catch (e) {
console.error(`Failed to proxy request: ${e.stack}`);
res.statusCode = 500;
res.end();
}
});
} else {
// In production we host the static build output.
app.use(express.static('build'));
Expand Down
66 changes: 66 additions & 0 deletions fixtures/flight/server/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ babelRegister({
],
presets: ['@babel/preset-react'],
plugins: ['@babel/transform-modules-commonjs'],
sourceMaps: process.env.NODE_ENV === 'development' ? 'inline' : false,
});

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

const nodeModule = require('node:module');

app.use(compress());

// Application
Expand Down Expand Up @@ -176,6 +179,69 @@ app.get('/todos', function (req, res) {
]);
});

if (process.env.NODE_ENV === 'development') {
const rootDir = path.resolve(__dirname, '../');

app.get('/source-maps', async function (req, res, next) {
try {
res.set('Content-type', 'application/json');
let requestedFilePath = req.query.name;

if (requestedFilePath.startsWith('file://')) {
requestedFilePath = requestedFilePath.slice(7);
}

const relativePath = path.relative(rootDir, requestedFilePath);
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
// This is outside the root directory of the app. Forbid it to be served.
res.status = 403;
res.write('{}');
res.end();
return;
}

const sourceMap = nodeModule.findSourceMap(requestedFilePath);
let map;
// There are two ways to return a source map depending on what we observe in error.stack.
// A real app will have a similar choice to make for which strategy to pick.
if (!sourceMap || Error.prepareStackTrace === undefined) {
// When --enable-source-maps is enabled, the error.stack that we use to track
// stacks will have had the source map already applied so it's pointing to the
// original source. We return a blank source map that just maps everything to
// the original source in this case.
const sourceContent = await readFile(requestedFilePath, 'utf8');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this throws when we request source map for node internal frames e.g. Error: ENOENT: no such file or directory, open 'node:internal/process/task_queues'.

Should we handle these on the server gracefully? Otherwise it might be a bit spammy. I guess the client can't handle it since it doesn't necessarily know what runtime internals are (assuming Bun uses other protocols).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally they're filtered before at the findSourceMapURL so that it can just return null instead.

This doesn't happen with just this PR though because we filter those out of the stack. It's not an issue until the next PR where it allows those to show up.

const lines = sourceContent.split('\n').length;
map = {
version: 3,
sources: [requestedFilePath],
sourcesContent: [sourceContent],
// Note: This approach to mapping each line only lets you jump to each line
// not jump to a column within a line. To do that, you need a proper source map
// generated for each parsed segment or add a segment for each column.
mappings: 'AAAA' + ';AACA'.repeat(lines - 1),
sourceRoot: '',
};
} else {
// If something has overridden prepareStackTrace it is likely not getting the
// natively applied source mapping to error.stack and so the line will point to
// the compiled output similar to how a browser works.
// E.g. ironically this can happen with the source-map-support library that is
// auto-invoked by @babel/register if external source maps are generated.
// In this case we just use the source map that the native source mapping would
// have used.
map = sourceMap.payload;
}
res.write(JSON.stringify(map));
res.end();
} catch (x) {
res.status = 500;
res.write('{}');
res.end();
console.error(x);
}
});
}

app.listen(3001, () => {
console.log('Regional Flight Server listening on port 3001...');
});
Expand Down
3 changes: 3 additions & 0 deletions fixtures/flight/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ async function hydrateApp() {
}),
{
callServer,
findSourceMapURL(fileName) {
return '/source-maps?name=' + encodeURIComponent(fileName);
},
}
);

Expand Down
37 changes: 31 additions & 6 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ Chunk.prototype.then = function <T>(
}
};

export type FindSourceMapURLCallback = (fileName: string) => null | string;

export type Response = {
_bundlerConfig: SSRModuleMap,
_moduleLoading: ModuleLoading,
Expand All @@ -255,6 +257,7 @@ export type Response = {
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
_debugRootTask?: null | ConsoleTask, // DEV-only
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
};

function readChunk<T>(chunk: SomeChunk<T>): T {
Expand Down Expand Up @@ -696,7 +699,7 @@ function createElement(
console,
getTaskName(type),
);
const callStack = buildFakeCallStack(stack, createTaskFn);
const callStack = buildFakeCallStack(response, stack, createTaskFn);
// This owner should ideally have already been initialized to avoid getting
// user stack frames on the stack.
const ownerTask =
Expand Down Expand Up @@ -1140,6 +1143,7 @@ export function createResponse(
encodeFormAction: void | EncodeFormActionCallback,
nonce: void | string,
temporaryReferences: void | TemporaryReferenceSet,
findSourceMapURL: void | FindSourceMapURLCallback,
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
Expand All @@ -1166,6 +1170,9 @@ export function createResponse(
// TODO: Make this string configurable.
response._debugRootTask = (console: any).createTask('"use server"');
}
if (__DEV__) {
response._debugFindSourceMapURL = findSourceMapURL;
}
// Don't inline this call because it causes closure to outline the call above.
response._fromJSON = createFromJSONCallback(response);
return response;
Expand Down Expand Up @@ -1673,6 +1680,7 @@ const fakeFunctionCache: Map<string, FakeFunction<any>> = __DEV__
function createFakeFunction<T>(
name: string,
filename: string,
sourceMap: null | string,
line: number,
col: number,
): FakeFunction<T> {
Expand All @@ -1697,7 +1705,9 @@ function createFakeFunction<T>(
'_()\n';
}

if (filename) {
if (sourceMap) {
code += '//# sourceMappingURL=' + sourceMap;
} else if (filename) {
code += '//# sourceURL=' + filename;
}

Expand All @@ -1720,10 +1730,18 @@ function createFakeFunction<T>(
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+)\)|([^\)]+):(\d+):(\d+))$/;
/^ {3} at (?:(.+) \(([^\)]+):(\d+):(\d+)\)|(?:async )?([^\)]+):(\d+):(\d+))$/;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add one or two examples as comments here? Doesn't need to be super exhaustive. It's just a very intimidating regex 😬

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment but I'm still ambivalent about how much we should cover here. This doesn't really cover all possible.


function buildFakeCallStack<T>(stack: string, innerCall: () => T): () => T {
function buildFakeCallStack<T>(
response: Response,
stack: string,
innerCall: () => T,
): () => T {
const frames = stack.split('\n');
let callStack = innerCall;
for (let i = 0; i < frames.length; i++) {
Expand All @@ -1739,7 +1757,13 @@ function buildFakeCallStack<T>(stack: string, innerCall: () => T): () => T {
const filename = parsed[2] || parsed[5] || '';
const line = +(parsed[3] || parsed[6]);
const col = +(parsed[4] || parsed[7]);
fn = createFakeFunction(name, filename, line, col);
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);
}
callStack = fn.bind(null, callStack);
}
Expand Down Expand Up @@ -1770,7 +1794,7 @@ function initializeFakeTask(
console,
getServerComponentTaskName(componentInfo),
);
const callStack = buildFakeCallStack(stack, createTaskFn);
const callStack = buildFakeCallStack(response, stack, createTaskFn);

if (ownerTask === null) {
const rootTask = response._debugRootTask;
Expand Down Expand Up @@ -1832,6 +1856,7 @@ function resolveConsoleEntry(
return;
}
const callStack = buildFakeCallStack(
response,
stackTrace,
printToConsole.bind(null, methodName, args, env),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

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

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
} from 'react-client/src/ReactFlightClient';

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

Expand Down Expand Up @@ -38,6 +41,7 @@ export type Options = {
moduleBaseURL?: string,
callServer?: CallServerCallback,
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
};

function createResponseFromOptions(options: void | Options) {
Expand All @@ -50,6 +54,9 @@ function createResponseFromOptions(options: void | Options) {
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

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

import type {Response} from 'react-client/src/ReactFlightClient';
import type {
Response,
FindSourceMapURLCallback,
} from 'react-client/src/ReactFlightClient';

import type {Readable} from 'stream';

Expand Down Expand Up @@ -46,6 +49,7 @@ type EncodeFormActionCallback = <A>(
export type Options = {
nonce?: string,
encodeFormAction?: EncodeFormActionCallback,
findSourceMapURL?: FindSourceMapURLCallback,
};

function createFromNodeStream<T>(
Expand All @@ -61,6 +65,9 @@ function createFromNodeStream<T>(
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
);
stream.on('data', chunk => {
processBinaryChunk(response, chunk);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

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

import type {Response as FlightResponse} from 'react-client/src/ReactFlightClient';
import type {
Response as FlightResponse,
FindSourceMapURLCallback,
} from 'react-client/src/ReactFlightClient';

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

Expand Down Expand Up @@ -37,6 +40,7 @@ type CallServerCallback = <A, T>(string, args: A) => Promise<T>;
export type Options = {
callServer?: CallServerCallback,
temporaryReferences?: TemporaryReferenceSet,
findSourceMapURL?: FindSourceMapURLCallback,
};

function createResponseFromOptions(options: void | Options) {
Expand All @@ -49,6 +53,9 @@ function createResponseFromOptions(options: void | Options) {
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
__DEV__ && options && options.findSourceMapURL
? options.findSourceMapURL
: undefined,
);
}

Expand Down
Loading