Skip to content

Commit

Permalink
fix(runtime): use proper evaluation and stack traces based on source …
Browse files Browse the repository at this point in the history
…maps (#443)

* refactor(runtime): use newer `globalEvalWithSourceUrl` when executing code

* refactor(runtime): update outdated error stack processing

* fix(runtime): remove first newline applied by `applyPatch(, ...)`

* fix(runtime): remove extraneous systemjs `.js` extensions from unmapped filename

* fix(runtime): remove android-specific addresses from stack traces
  • Loading branch information
byCedric authored Jul 18, 2023
1 parent 0b7ebd3 commit deeeb3a
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 17 deletions.
46 changes: 33 additions & 13 deletions runtime/src/Errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,45 @@ export const report = (e: Error) => {

export const status = () => (getError() ? 'FAILURE' : 'SUCCESS');

// Prettified version of an `Error`'s stack trace. Performs the following transformations:
// 1. Replace references to the Snack client bundle code with `[snack internals]`.
// 2. Unmap references to user code to the original file and line and column.
// 3. Make column numbers one-indexed.
export const prettyStack = (e: Error) =>
(e.stack ?? '')
.replace(/https?:\/\/.+\n/g, '[snack internals]\n')
.replace(/(module:\/\/[^:]+):(\d+):(\d+)(\n|\))/g, (match, sourceURL, line, column) => {
/**
* Generate a human-readable description of the error stack trace.
* This does a few things:
* - Try to map transpiled code back to original source code with the known sourcemaps
* - Filters references to the Snack client bundle code, since that's irrelevant for the user's code
* - Filters Hermes internal bytecode references
* - Clean up file names to remove the `module://` prefix, and `.js.js` suffix
* - Clean up faulty column numbers and correct the line numbers, caused by `Files.tsx`'s `applyPatch` newlines
*/
export function prettyStack(error: Error) {
// Unmap transpiled code from known sourcemaps
const sourceUnmappedStack = error.stack?.replace(
/(module:\/\/[^:]+):(\d+):(\d+)(\n|\))/g,
(match, sourceURL, line, column) => {
const u = Modules.unmap({
sourceURL,
line: parseInt(line, 10),
column: parseInt(column, 10),
});
return u
? u.path + (u.line !== null && u.column !== null ? `:${u.line}:${u.column}\n` : '\n')
: match.replace(/module:\/+/, '').replace(/\.js\.js/, '.js');
})
.replace(/:(\d+):(\d+)\n/g, (_, line, column) => `:${line}:${parseInt(column, 10) + 1}\n`)
.replace(/module:\/+/g, '');
? // Avoid adding the `column`, `source-map@0.6.1` does not properly resolve the column. It uses the generated column number.
u.path + (u.line !== null && u.column !== null ? `:${u.line})\n` : '\n')
: match.replace(/module:\/+/, '').replace(/.([a-z]+).js/g, '.$1');
}
);

if (!sourceUnmappedStack) {
return 'No stacktrace available';
}

return sourceUnmappedStack
.split(/\r?\n/)
.filter((line) => !line.match(/https?:\/\/.+/g)) // Filter bundle-related stacks
.filter((line) => !line.match(/\(native\)/g)) // Filter (native) stacks
.filter((line) => !line.match(/InternalBytecode/g)) // Filter Hermes bytecode stacks
.filter((line) => !line.match(/\(address at/g)) // Filter Android specific address stacks
.filter(Boolean) // Filter empty lines
.join('\n');
}

// Acts as a boundary for upward error propagation in the React render tree. Displays errors with a
// friendly dialog.
Expand Down
3 changes: 2 additions & 1 deletion runtime/src/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ export const update = async ({ message }: { message: Message }) => {
s3Url: undefined,
s3Contents: undefined,
diff: newDiff,
contents: applyPatch('', newDiff),
// Remove the first newline from `applyPatch`, since this is an non-existing newline
contents: applyPatch('', newDiff).replace('\n', ''),
};
changedPaths.push(path);
}
Expand Down
9 changes: 6 additions & 3 deletions runtime/src/Modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,10 @@ export const unmap = ({
if (result) {
return {
...result,
path: sourceURL.replace(/!transpiled$/, '').replace(/^module:\/\//, ''),
path: sourceURL
.replace(/!transpiled$/, '')
.replace(/^module:\/\//, '')
.replace(/.([a-z]+).js$/, '.$1'),
};
}
}
Expand All @@ -496,7 +499,7 @@ export const unmap = ({
global.evaluate = (src, options: { filename?: string } = {}) => {
return Profiling.section(`\`Modules.evalPipeline('${options.filename}')\``, () => {
// @ts-ignore
if (global.nativeInjectHMRUpdate) {
if (global.globalEvalWithSourceUrl) {
// This function will let JavaScriptCore know about the URL of the source code so that errors
// and stack traces are annotated. Thanks, React Native devs! We do need a top-level try/catch
// to prevent a native crash if `eval`ing throws an exception though...
Expand All @@ -505,7 +508,7 @@ global.evaluate = (src, options: { filename?: string } = {}) => {
src = `(function () { try { ${src}\n } catch (e) { this.__SNACK_EVAL_EXCEPTION = e; } })();`;

// @ts-ignore
const r = global.nativeInjectHMRUpdate(src, options.filename);
const r = global.globalEvalWithSourceUrl(src, options.filename);

// @ts-ignore
if (global.__SNACK_EVAL_EXCEPTION) {
Expand Down

0 comments on commit deeeb3a

Please sign in to comment.