From 929b89086ae70655a05866018f6447b3b6d7a4da Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 15 Dec 2019 17:53:19 -0800 Subject: [PATCH 1/2] errors: annotate frames for prepareStackTrace() When source-maps are enabled, overriding Error.prepareStackTrace() is again supported. Error.prepareStackTrace() will now be passed stack frames with the methods getOriginalFileName(), getOriginalLineNumber(), and getOriginalColumnNumber() which provide information parsed from source map. --- .../source_map/prepare_stack_trace.js | 51 ++++++++++++++++--- .../test-error-prepare-stack-trace.js | 47 +++++++++++++++++ 2 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 test/parallel/test-error-prepare-stack-trace.js diff --git a/lib/internal/source_map/prepare_stack_trace.js b/lib/internal/source_map/prepare_stack_trace.js index d6c5fce60a29e3..fb11cd67537412 100644 --- a/lib/internal/source_map/prepare_stack_trace.js +++ b/lib/internal/source_map/prepare_stack_trace.js @@ -17,15 +17,37 @@ const prepareStackTrace = (globalThis, error, trace) => { return f(error, trace); } - const { SourceMap } = require('internal/source_map/source_map'); + const decoratedTrace = decorateCallSites(error, trace); + + // `globalThis` is the global that contains the constructor which + // created `error`. + if (typeof globalThis.Error.prepareStackTrace === 'function') { + return globalThis.Error.prepareStackTrace(error, decoratedTrace); + } + const errorString = ErrorToString.call(error); - if (trace.length === 0) { + if (decoratedTrace.length === 0) { return errorString; } - const preparedTrace = trace.map((t, i) => { + const preparedTrace = decoratedTrace.map((t, i) => { let str = i !== 0 ? '\n at ' : ''; str = `${str}${t}`; + if (t.getOriginalLineNumber) { + str += `\n -> ${ + t.getOriginalFileName() + }:${ + t.getOriginalLineNumber() + }:${t.getOriginalColumnNumber()}`; + } + return str; + }); + return `${errorString}\n at ${preparedTrace.join('')}`; +}; + +function decorateCallSites(error, trace) { + const { SourceMap } = require('internal/source_map/source_map'); + return trace.map((t) => { try { const sourceMap = findSourceMap(t.getFileName(), error); if (sourceMap && sourceMap.data) { @@ -35,17 +57,30 @@ const prepareStackTrace = (globalThis, error, trace) => { const [, , url, line, col] = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); if (url && line !== undefined && col !== undefined) { - str += - `\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`; + return decorateCallSite(t, url.replace('file://', ''), line + 1, + col + 1); } } } catch (err) { debug(err.stack); } - return str; + return decorateCallSite(t, t.getFileName(), t.getLineNumber(), + t.getColumnNumber()); }); - return `${errorString}\n at ${preparedTrace.join('')}`; -}; +} + +function decorateCallSite(callSite, fileName, lineNumber, columnNumber) { + callSite.getOriginalFileName = () => { + return fileName; + }; + callSite.getOriginalLineNumber = () => { + return lineNumber; + }; + callSite.getOriginalColumnNumber = () => { + return columnNumber; + }; + return callSite; +} module.exports = { prepareStackTrace, diff --git a/test/parallel/test-error-prepare-stack-trace.js b/test/parallel/test-error-prepare-stack-trace.js new file mode 100644 index 00000000000000..37264dc6c7aa59 --- /dev/null +++ b/test/parallel/test-error-prepare-stack-trace.js @@ -0,0 +1,47 @@ +// Flags: --enable-source-maps +'use strict'; + +require('../common'); +const assert = require('assert'); + +// Error.prepareStackTrace() can be overridden with source maps enabled. +{ + let prepareCalled = false; + Error.prepareStackTrace = (_error, trace) => { + prepareCalled = true; + }; + try { + throw new Error('foo'); + } catch (err) { + err.stack; + } + assert(prepareCalled); +} + +// Error.prepareStackTrace() should expose getOriginalLineNumber(), +// getOriginalColumnNumber(), getOriginalFileName(). +{ + let callSite; + Error.prepareStackTrace = (_error, trace) => { + const throwingRequireCallSite = trace[0]; + if (throwingRequireCallSite.getFileName().endsWith('typescript-throw.js')) { + callSite = throwingRequireCallSite; + } + }; + try { + // Require a file that throws an exception, and has a source map. + require('../fixtures/source-map/typescript-throw.js'); + } catch (err) { + err.stack; // Force prepareStackTrace() to be called. + } + assert(callSite); + + assert(callSite.getFileName().endsWith('typescript-throw.js')); + assert(callSite.getOriginalFileName().endsWith('typescript-throw.ts')); + + assert.strictEqual(callSite.getLineNumber(), 20); + assert.strictEqual(callSite.getColumnNumber(), 15); + + assert.strictEqual(callSite.getOriginalLineNumber(), 18); + assert.strictEqual(callSite.getOriginalColumnNumber(), 11); +} From 0521b2191379d642c1126f0685225a74f6c26666 Mon Sep 17 00:00:00 2001 From: bcoe Date: Sun, 15 Dec 2019 18:00:21 -0800 Subject: [PATCH 2/2] chore: update docs removing caveat --- doc/api/cli.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index d098a199e055e6..95e569a89b3286 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -153,9 +153,6 @@ added: v12.12.0 Enable experimental Source Map V3 support for stack traces. -Currently, overriding `Error.prepareStackTrace` is ignored when the -`--enable-source-maps` flag is set. - ### `--experimental-conditional-exports`