mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
module: add API for interacting with source maps
PR-URL: https://github.com/nodejs/node/pull/31132 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
2bb85b85c9
commit
521b2224c3
@ -1033,6 +1033,86 @@ import('fs').then((esmFS) => {
|
||||
});
|
||||
```
|
||||
|
||||
## Source Map V3 Support
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Helpers for for interacting with the source map cache. This cache is
|
||||
populated when source map parsing is enabled and
|
||||
[source map include directives][] are found in a modules' footer.
|
||||
|
||||
To enable source map parsing, Node.js must be run with the flag
|
||||
[`--enable-source-maps`][], or with code coverage enabled by setting
|
||||
[`NODE_V8_COVERAGE=dir`][].
|
||||
|
||||
```js
|
||||
const { findSourceMap, SourceMap } = require('module');
|
||||
```
|
||||
|
||||
### `module.findSourceMap(path[, error])`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `path` {string}
|
||||
* `error` {Error}
|
||||
* Returns: {module.SourceMap}
|
||||
|
||||
`path` is the resolved path for the file for which a corresponding source map
|
||||
should be fetched.
|
||||
|
||||
The `error` instance should be passed as the second parameter to `findSourceMap`
|
||||
in exceptional flows, e.g., when an overridden
|
||||
[`Error.prepareStackTrace(error, trace)`][] is invoked. Modules are not added to
|
||||
the module cache until they are successfully loaded, in these cases source maps
|
||||
will be associated with the `error` instance along with the `path`.
|
||||
|
||||
### Class: `module.SourceMap`
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
#### `new SourceMap(payload)`
|
||||
|
||||
* `payload` {Object}
|
||||
|
||||
Creates a new `sourceMap` instance.
|
||||
|
||||
`payload` is an object with keys matching the [Source Map V3 format][]:
|
||||
|
||||
* `file`: {string}
|
||||
* `version`: {number}
|
||||
* `sources`: {string[]}
|
||||
* `sourcesContent`: {string[]}
|
||||
* `names`: {string[]}
|
||||
* `mappings`: {string}
|
||||
* `sourceRoot`: {string}
|
||||
|
||||
#### `sourceMap.payload`
|
||||
|
||||
* Returns: {Object}
|
||||
|
||||
Getter for the payload used to construct the [`SourceMap`][] instance.
|
||||
|
||||
#### `sourceMap.findEntry(lineNumber, columnNumber)`
|
||||
|
||||
* `lineNumber` {number}
|
||||
* `columnNumber` {number}
|
||||
* Returns: {Object}
|
||||
|
||||
Given a line number and column number in the generated source file, returns
|
||||
an object representing the position in the original file. The object returned
|
||||
consists of the following keys:
|
||||
|
||||
* generatedLine: {number}
|
||||
* generatedColumn: {number}
|
||||
* originalSource: {string}
|
||||
* originalLine: {number}
|
||||
* originalColumn: {number}
|
||||
|
||||
[GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders
|
||||
[`Error`]: errors.html#errors_class_error
|
||||
[`__dirname`]: #modules_dirname
|
||||
@ -1046,3 +1126,9 @@ import('fs').then((esmFS) => {
|
||||
[module resolution]: #modules_all_together
|
||||
[module wrapper]: #modules_the_module_wrapper
|
||||
[native addons]: addons.html
|
||||
[source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
|
||||
[`--enable-source-maps`]: cli.html#cli_enable_source_maps
|
||||
[`NODE_V8_COVERAGE=dir`]: cli.html#cli_node_v8_coverage_dir
|
||||
[`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
|
||||
[`SourceMap`]: modules.html#modules_class_module_sourcemap
|
||||
[Source Map V3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
|
||||
|
@ -29,7 +29,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
|
||||
maybeOverridePrepareStackTrace(globalThis, error, trace);
|
||||
if (globalOverride !== kNoOverride) return globalOverride;
|
||||
|
||||
const { SourceMap } = require('internal/source_map/source_map');
|
||||
const errorString = ErrorToString.call(error);
|
||||
|
||||
if (trace.length === 0) {
|
||||
@ -39,16 +38,19 @@ const prepareStackTrace = (globalThis, error, trace) => {
|
||||
let str = i !== 0 ? '\n at ' : '';
|
||||
str = `${str}${t}`;
|
||||
try {
|
||||
const sourceMap = findSourceMap(t.getFileName(), error);
|
||||
if (sourceMap && sourceMap.data) {
|
||||
const sm = new SourceMap(sourceMap.data);
|
||||
const sm = findSourceMap(t.getFileName(), error);
|
||||
if (sm) {
|
||||
// Source Map V3 lines/columns use zero-based offsets whereas, in
|
||||
// stack traces, they start at 1/1.
|
||||
const [, , url, line, col] =
|
||||
sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
|
||||
if (url && line !== undefined && col !== undefined) {
|
||||
const {
|
||||
originalLine,
|
||||
originalColumn,
|
||||
originalSource
|
||||
} = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
|
||||
if (originalSource && originalLine !== undefined &&
|
||||
originalColumn !== undefined) {
|
||||
str +=
|
||||
`\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
|
||||
`\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -66,6 +66,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
Array
|
||||
} = primordials;
|
||||
|
||||
const {
|
||||
ERR_INVALID_ARG_TYPE
|
||||
} = require('internal/errors').codes;
|
||||
|
||||
let base64Map;
|
||||
|
||||
const VLQ_BASE_SHIFT = 5;
|
||||
@ -112,6 +120,7 @@ class StringCharIterator {
|
||||
* @param {SourceMapV3} payload
|
||||
*/
|
||||
class SourceMap {
|
||||
#payload;
|
||||
#reverseMappingsBySourceURL = [];
|
||||
#mappings = [];
|
||||
#sources = {};
|
||||
@ -129,17 +138,25 @@ class SourceMap {
|
||||
for (let i = 0; i < base64Digits.length; ++i)
|
||||
base64Map[base64Digits[i]] = i;
|
||||
}
|
||||
this.#parseMappingPayload(payload);
|
||||
this.#payload = cloneSourceMapV3(payload);
|
||||
this.#parseMappingPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Object} raw source map v3 payload.
|
||||
*/
|
||||
get payload() {
|
||||
return cloneSourceMapV3(this.#payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceMapV3} mappingPayload
|
||||
*/
|
||||
#parseMappingPayload = (mappingPayload) => {
|
||||
if (mappingPayload.sections)
|
||||
this.#parseSections(mappingPayload.sections);
|
||||
#parseMappingPayload = () => {
|
||||
if (this.#payload.sections)
|
||||
this.#parseSections(this.#payload.sections);
|
||||
else
|
||||
this.#parseMap(mappingPayload, 0, 0);
|
||||
this.#parseMap(this.#payload, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,24 +192,18 @@ class SourceMap {
|
||||
const entry = this.#mappings[first];
|
||||
if (!first && entry && (lineNumber < entry[0] ||
|
||||
(lineNumber === entry[0] && columnNumber < entry[1]))) {
|
||||
return null;
|
||||
return {};
|
||||
} else if (!entry) {
|
||||
return {};
|
||||
} else {
|
||||
return {
|
||||
generatedLine: entry[0],
|
||||
generatedColumn: entry[1],
|
||||
originalSource: entry[2],
|
||||
originalLine: entry[3],
|
||||
originalColumn: entry[4]
|
||||
};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sourceURL of the originating resource
|
||||
* @param {number} lineNumber in the originating resource
|
||||
* @return {Array}
|
||||
*/
|
||||
findEntryReversed(sourceURL, lineNumber) {
|
||||
const mappings = this.#reverseMappingsBySourceURL[sourceURL];
|
||||
for (; lineNumber < mappings.length; ++lineNumber) {
|
||||
const mapping = mappings[lineNumber];
|
||||
if (mapping)
|
||||
return mapping;
|
||||
}
|
||||
return this.#mappings[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -296,6 +307,23 @@ function decodeVLQ(stringCharIterator) {
|
||||
return negative ? -result : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SourceMapV3} payload
|
||||
* @return {SourceMapV3}
|
||||
*/
|
||||
function cloneSourceMapV3(payload) {
|
||||
if (typeof payload !== 'object') {
|
||||
throw new ERR_INVALID_ARG_TYPE('payload', ['Object'], payload);
|
||||
}
|
||||
payload = { ...payload };
|
||||
for (const key in payload) {
|
||||
if (payload.hasOwnProperty(key) && Array.isArray(payload[key])) {
|
||||
payload[key] = payload[key].slice(0);
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SourceMap
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ const cjsSourceMapCache = new WeakMap();
|
||||
const esmSourceMapCache = new Map();
|
||||
const { fileURLToPath, URL } = require('url');
|
||||
let Module;
|
||||
let SourceMap;
|
||||
|
||||
let experimentalSourceMaps;
|
||||
function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
|
||||
@ -222,8 +223,13 @@ function appendCJSCache(obj) {
|
||||
|
||||
// Attempt to lookup a source map, which is either attached to a file URI, or
|
||||
// keyed on an error instance.
|
||||
// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
|
||||
// requirement of error parameter.
|
||||
function findSourceMap(uri, error) {
|
||||
if (!Module) Module = require('internal/modules/cjs/loader').Module;
|
||||
if (!SourceMap) {
|
||||
SourceMap = require('internal/source_map/source_map').SourceMap;
|
||||
}
|
||||
let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
|
||||
if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
|
||||
if (sourceMap === undefined) {
|
||||
@ -235,7 +241,11 @@ function findSourceMap(uri, error) {
|
||||
sourceMap = candidateSourceMap;
|
||||
}
|
||||
}
|
||||
return sourceMap;
|
||||
if (sourceMap && sourceMap.data) {
|
||||
return new SourceMap(sourceMap.data);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -1,3 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('internal/modules/cjs/loader').Module;
|
||||
const { findSourceMap } = require('internal/source_map/source_map_cache');
|
||||
const { Module } = require('internal/modules/cjs/loader');
|
||||
const { SourceMap } = require('internal/source_map/source_map');
|
||||
|
||||
Module.findSourceMap = findSourceMap;
|
||||
Module.SourceMap = SourceMap;
|
||||
module.exports = Module;
|
||||
|
84
test/parallel/test-source-map-api.js
Normal file
84
test/parallel/test-source-map-api.js
Normal file
@ -0,0 +1,84 @@
|
||||
// Flags: --enable-source-maps
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
const { findSourceMap, SourceMap } = require('module');
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
// findSourceMap() can lookup source-maps based on URIs, in the
|
||||
// non-exceptional case.
|
||||
{
|
||||
require('../fixtures/source-map/disk-relative-path.js');
|
||||
const sourceMap = findSourceMap(
|
||||
require.resolve('../fixtures/source-map/disk-relative-path.js')
|
||||
);
|
||||
const {
|
||||
originalLine,
|
||||
originalColumn,
|
||||
originalSource
|
||||
} = sourceMap.findEntry(0, 29);
|
||||
assert.strictEqual(originalLine, 2);
|
||||
assert.strictEqual(originalColumn, 4);
|
||||
assert(originalSource.endsWith('disk.js'));
|
||||
}
|
||||
|
||||
// findSourceMap() can be used in Error.prepareStackTrace() to lookup
|
||||
// source-map attached to error.
|
||||
{
|
||||
let callSite;
|
||||
let sourceMap;
|
||||
Error.prepareStackTrace = (error, trace) => {
|
||||
const throwingRequireCallSite = trace[0];
|
||||
if (throwingRequireCallSite.getFileName().endsWith('typescript-throw.js')) {
|
||||
sourceMap = findSourceMap(throwingRequireCallSite.getFileName(), error);
|
||||
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(sourceMap);
|
||||
const {
|
||||
generatedLine,
|
||||
generatedColumn,
|
||||
originalLine,
|
||||
originalColumn,
|
||||
originalSource
|
||||
} = sourceMap.findEntry(
|
||||
callSite.getLineNumber() - 1,
|
||||
callSite.getColumnNumber() - 1
|
||||
);
|
||||
|
||||
assert.strictEqual(generatedLine, 19);
|
||||
assert.strictEqual(generatedColumn, 14);
|
||||
|
||||
assert.strictEqual(originalLine, 17);
|
||||
assert.strictEqual(originalColumn, 10);
|
||||
assert(originalSource.endsWith('typescript-throw.ts'));
|
||||
}
|
||||
|
||||
// SourceMap can be instantiated with Source Map V3 object as payload.
|
||||
{
|
||||
const payload = JSON.parse(readFileSync(
|
||||
require.resolve('../fixtures/source-map/disk.map'), 'utf8'
|
||||
));
|
||||
const sourceMap = new SourceMap(payload);
|
||||
const {
|
||||
originalLine,
|
||||
originalColumn,
|
||||
originalSource
|
||||
} = sourceMap.findEntry(0, 29);
|
||||
assert.strictEqual(originalLine, 2);
|
||||
assert.strictEqual(originalColumn, 4);
|
||||
assert(originalSource.endsWith('disk.js'));
|
||||
// The stored payload should be a clone:
|
||||
assert.strictEqual(payload.mappings, sourceMap.payload.mappings);
|
||||
assert.notStrictEqual(payload, sourceMap.payload);
|
||||
assert.strictEqual(payload.sources[0], sourceMap.payload.sources[0]);
|
||||
assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
|
||||
}
|
@ -101,6 +101,10 @@ const customTypesMap = {
|
||||
'https.Server': 'https.html#https_class_https_server',
|
||||
|
||||
'module': 'modules.html#modules_the_module_object',
|
||||
|
||||
'module.SourceMap':
|
||||
'modules.html#modules_class_module_sourcemap',
|
||||
|
||||
'require': 'modules.html#modules_require_id',
|
||||
|
||||
'Handle': 'net.html#net_server_listen_handle_backlog_callback',
|
||||
|
Loading…
Reference in New Issue
Block a user