8000 module: add API for interacting with source maps · nodejs/node@4dced02 · GitHub
[go: up one dir, main page]

Skip to content 8000

Commit 4dced02

Browse files
bcoeMylesBorins
authored andcommitted
module: add API for interacting with source maps
PR-URL: #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>
1 parent aedbfdb commit 4dced02

File tree

8 files changed

+252
-32
lines changed
  • lib
  • test/parallel
  • tools/doc
  • 8 files changed

    +252
    -32
    lines changed

    doc/api/modules.md

    Lines changed: 86 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -1033,6 +1033,86 @@ import('fs').then((esmFS) => {
    10331033
    });
    10341034
    ```
    10351035
    1036+
    ## Source Map V3 Support
    1037+
    <!-- YAML
    1038+
    added: REPLACEME
    1039+
    -->
    1040+
    1041+
    > Stability: 1 - Experimental
    1042+
    1043+
    Helpers for for interacting with the source map cache. This cache is
    1044+
    populated when source map parsing is enabled and
    1045+
    [source map include directives][] are found in a modules' footer.
    1046+
    1047+
    To enable source map parsing, Node.js must be run with the flag
    1048+
    [`--enable-source-maps`][], or with code coverage enabled by setting
    1049+
    [`NODE_V8_COVERAGE=dir`][].
    1050+
    1051+
    ```js
    1052+
    const { findSourceMap, SourceMap } = require('module');
    1053+
    ```
    1054+
    1055+
    ### `module.findSourceMap(path[, error])`
    1056+
    <!-- YAML
    1057+
    added: REPLACEME
    1058+
    -->
    1059+
    1060+
    * `path` {string}
    1061+
    * `error` {Error}
    1062+
    * Returns: {module.SourceMap}
    1063+
    1064+
    `path` is the resolved path for the file for which a corresponding source map
    1065+
    should be fetched.
    1066+
    1067+
    The `error` instance should be passed as the second parameter to `findSourceMap`
    1068+
    in exceptional flows, e.g., when an overridden
    1069+
    [`Error.prepareStackTrace(error, trace)`][] is invoked. Modules are not added to
    1070+
    the module cache until they are successfully loaded, in these cases source maps
    1071+
    will be associated with the `error` instance along with the `path`.
    1072+
    1073+
    ### Class: `module.SourceMap`
    1074+
    <!-- YAML
    1075+
    added: REPLACEME
    1076+
    -->
    1077+
    1078+
    #### `new SourceMap(payload)`
    1079+
    1080+
    * `payload` {Object}
    1081+
    1082+
    Creates a new `sourceMap` instance.
    1083+
    1084+
    `payload` is an object with keys matching the [Source Map V3 format][]:
    1085+
    1086+
    * `file`: {string}
    1087+
    * `version`: {number}
    1088+
    * `sources`: {string[]}
    1089+
    * `sourcesContent`: {string[]}
    1090+
    * `names`: {string[]}
    1091+
    * `mappings`: {string}
    1092+
    * `sourceRoot`: {string}
    1093+
    1094+
    #### `sourceMap.payload`
    1095+
    1096+
    * Returns: {Object}
    1097+
    1098+
    Getter for the payload used to construct the [`SourceMap`][] instance.
    1099+
    1100+
    #### `sourceMap.findEntry(lineNumber, columnNumber)`
    1101+
    1102+
    * `lineNumber` {number}
    1103+
    * `columnNumber` {number}
    1104+
    * Returns: {Object}
    1105+
    1106+
    Given a line number and column number in the generated source file, returns
    1107+
    an object representing the position in the original file. The object returned
    1108+
    consists of the following keys:
    1109+
    1110+
    * generatedLine: {number}
    1111+
    * generatedColumn: {number}
    1112+
    * originalSource: {string}
    1113+
    * originalLine: {number}
    1114+
    * originalColumn: {number}
    1115+
    10361116
    [GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders
    10371117
    [`Error`]: errors.html#errors_class_error
    10381118
    [`__dirname`]: #modules_dirname
    @@ -1046,3 +1126,9 @@ import('fs').then((esmFS) => {
    10461126
    [module resolution]: #modules_all_together
    10471127
    [module wrapper]: #modules_the_module_wrapper
    10481128
    [native addons]: addons.html
    1129+
    [source map include directives]: https://sourcemaps.info/spec.html#h.lmz475t4mvbx
    1130+
    [`--enable-source-maps`]: cli.html#cli_enable_source_maps
    1131+
    [`NODE_V8_COVERAGE=dir`]: cli.html#cli_node_v8_coverage_dir
    1132+
    [`Error.prepareStackTrace(error, trace)`]: https://v8.dev/docs/stack-trace-api#customizing-stack-traces
    1133+
    [`SourceMap`]: modules.html#modules_class_module_sourcemap
    1134+
    [Source Map V3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej

    lib/internal/source_map/prepare_stack_trace.js

    Lines changed: 10 additions & 8 deletions
    Original file line numberDiff line numberDiff line change
    @@ -29,7 +29,6 @@ const prepareStackTrace = (globalThis, error, trace) => {
    2929
    maybeOverridePrepareStackTrace(globalThis, error, trace);
    3030
    if (globalOverride !== kNoOverride) return globalOverride;
    3131

    32-
    const { SourceMap } = require('internal/source_map/source_map');
    3332
    const errorString = ErrorToString.call(error);
    3433

    3534
    if (trace.length === 0) {
    @@ -39,16 +38,19 @@ const prepareStackTrace = (globalThis, error, trace) => {
    3938
    let str = i !== 0 ? '\n at ' : '';
    4039
    str = `${str}${t}`;
    4140
    try {
    42-
    const sourceMap = findSourceMap(t.getFileName(), error);
    43-
    if (sourceMap && sourceMap.data) {
    44-
    const sm = new SourceMap(sourceMap.data);
    41+
    const sm = findSourceMap(t.getFileName(), error);
    42+
    if (sm) {
    4543
    // Source Map V3 lines/columns use zero-based offsets whereas, in
    4644
    // stack traces, they start at 1/1.
    47-
    const [, , url, line, col] =
    48-
    sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
    49-
    if (url && line !== undefined && col !== undefined) {
    45+
    const {
    46+
    originalLine,
    47+
    originalColumn,
    48+
    originalSource
    49+
    } = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1);
    50+
    if (originalSource && originalLine !== undefined &&
    51+
    originalColumn !== undefined) {
    5052
    str +=
    51-
    `\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`;
    53+
    `\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`;
    5254
    }
    5355
    }
    5456
    } catch (err) {

    lib/internal/source_map/source_map.js

    Lines changed: 50 additions & 22 deletions
    Original file line numberDiff line numberDiff line change
    @@ -66,6 +66,14 @@
    6666

    6767
    'use strict';
    6868

    69+
    const {
    70+
    Array
    71+
    } = primordials;
    72+
    73+
    const {
    74+
    ERR_INVALID_ARG_TYPE
    75+
    } = require('internal/errors').codes;
    76+
    6977
    let base64Map;
    7078

    7179
    const VLQ_BASE_SHIFT = 5;
    @@ -112,6 +120,7 @@ class StringCharIterator {
    112120
    * @param {SourceMapV3} payload
    113121
    */
    114122
    class SourceMap {
    123+
    #payload;
    115124
    #reverseMappingsBySourceURL = [];
    116125
    #mappings = [];
    117126
    #sources = {};
    @@ -129,17 +138,25 @@ class SourceMap {
    129138
    for (let i = 0; i < base64Digits.length; ++i)
    130139
    base64Map[base64Digits[i]] = i;
    131140
    }
    132-
    this.#parseMappingPayload(payload);
    141+
    this.#payload = cloneSourceMapV3(payload);
    142+
    this.#parseMappingPayload();
    143+
    }
    144+
    145+
    /**
    146+
    * @return {Object} raw source map v3 payload.
    147+
    */
    148+
    get payload() {
    149+
    return cloneSourceMapV3(this.#payload);
    133150
    }
    134151

    135152
    /**
    136153
    * @param {SourceMapV3} mappingPayload
    137154
    */
    138-
    #parseMappingPayload = (mappingPayload) => {
    139-
    if (mappingPayload.sections)
    140-
    this.#parseSections(mappingPayload.sections);
    155+
    #parseMappingPayload = () => {
    156+
    if (this.#payload.sections)
    157+
    this.#parseSections(this.#payload.sections);
    141158
    else
    142-
    this.#parseMap(mappingPayload, 0, 0);
    159+
    this.#parseMap(this.#payload, 0, 0);
    143160
    }
    144161

    145162
    /**
    @@ -175,24 +192,18 @@ class SourceMap {
    175192
    const entry = this.#mappings[first];
    176193
    if (!first && entry && (lineNumber < entry[0] ||
    177194
    (lineNumber === entry[0] && columnNumber < entry[1]))) {
    178-
    return null;
    195+
    return {};
    196+
    } else if (!entry) {
    197+
    return {};
    198+
    } else {
    199+
    return {
    200+
    generatedLine: entry[0],
    201+
    generatedColumn: entry[1],
    202+
    originalSource: entry[2],
    203+
    originalLine: entry[3],
    204+
    originalColumn: entry[4]
    205+
    };
    179206
    }
    180-
    return entry;
    181-
    }
    182-
    183-
    /**
    184-
    * @param {string} sourceURL of the originating resource
    185-
    * @param {number} lineNumber in the originating resource
    186-
    * @return {Array}
    187-
    */
    188-
    findEntryReversed(sourceURL, lineNumber) {
    189-
    const mappings = this.#reverseMappingsBySourceURL[sourceURL];
    190-
    for (; lineNumber < mappings.length; ++lineNumber) {
    191-
    const mapping = mappings[lineNumber];
    192-
    if (mapping)
    193-
    return mapping;
    194-
    }
    195-
    return this.#mappings[0];
    196207
    }
    197208

    198209
    /**
    @@ -296,6 +307,23 @@ function decodeVLQ(stringCharIterator) {
    296307
    return negative ? -result : result;
    297308
    }
    298309

    310+
    /**
    311+
    * @param {SourceMapV3} payload
    312+
    * @return {SourceMapV3}
    313+
    */
    314+
    function cloneSourceMapV3(payload) {
    315+
    if (typeof payload !== 'object') {
    316+
    throw new ERR_INVALID_ARG_TYPE('payload', ['Object'], payload);
    317+
    }
    318+
    payload = { ...payload };
    319+
    for (const key in payload) {
    320+
    if (payload.hasOwnProperty(key) && Array.isArray(payload[key])) {
    321+
    payload[key] = payload[key].slice(0);
    322+
    }
    323+
    }
    324+
    return payload;
    325+
    }
    326+
    299327
    module.exports = {
    300328
    SourceMap
    301329
    };

    lib/internal/source_map/source_map_cache.js

    Lines changed: 11 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -37,6 +37,7 @@ const cjsSourceMapCache = new WeakMap();
    3737
    const esmSourceMapCache = new Map();
    3838
    const { fileURLToPath, URL } = require('url');
    3939
    let Module;
    40+
    let SourceMap;
    4041

    4142
    let experimentalSourceMaps;
    4243
    function maybeCacheSourceMap(filename, content, cjsModuleInstance) {
    @@ -222,8 +223,13 @@ function appendCJSCache(obj) {
    222223

    223224
    // Attempt to lookup a source map, which is either attached to a file URI, or
    224225
    // keyed on an error instance.
    226+
    // TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop
    227+
    // requirement of error parameter.
    225228
    function findSourceMap(uri, error) {
    226229
    if (!Module) Module = require('internal/modules/cjs/loader').Module;
    230+
    if (!SourceMap) {
    231+
    SourceMap = require('internal/source_map/source_map').SourceMap;
    232+
    }
    227233
    let sourceMap = cjsSourceMapCache.get(Module._cache[uri]);
    228234
    if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri);
    229235
    if (sourceMap === undefined) {
    @@ -235,7 +241,11 @@ function findSourceMap(uri, error) {
    235241
    sourceMap = candidateSourceMap;
    236242
    }
    237243
    }
    238-
    return sourceMap;
    244+
    if (sourceMap && sourceMap.data) {
    245+
    return new SourceMap(sourceMap.data);
    246+< CDAC div class="diff-text-inner"> } else {
    247+
    return undefined;
    248+
    }
    239249
    }
    240250

    241251
    module.exports = {

    lib/module.js

    Lines changed: 7 additions & 1 deletion
    Original file line numberDiff line numberDiff line change
    @@ -1,3 +1,9 @@
    11
    'use strict';
    22

    3-
    module.exports = require('internal/modules/cjs/loader').Module;
    3+
    const { findSourceMap } = require('internal/source_map/source_map_cache');
    4+
    const { Module } = require('internal/modules/cjs/loader');
    5+
    const { SourceMap } = require('internal/source_map/source_map');
    6+
    7+
    Module.findSourceMap = findSourceMap;
    8+
    Module.SourceMap = SourceMap;
    9+
    module.exports = Module;
    Lines changed: 84 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,84 @@
    1+
    // Flags: --enable-source-maps
    2+
    'use strict';
    3+
    4+
    require('../common');
    5+
    const assert = require('assert');
    6+
    const { findSourceMap, SourceMap } = require('module');
    7+
    const { readFileSync } = require('fs');
    8+
    9+
    // findSourceMap() can lookup source-maps based on URIs, in the
    10+
    // non-exceptional case.
    11+
    {
    12+
    require('../fixtures/source-map/disk-relative-path.js');
    13+
    const sourceMap = findSourceMap(
    14+
    require.resolve('../fixtures/source-map/disk-relative-path.js')
    15+
    );
    16+
    const {
    17+
    originalLine,
    18+
    originalColumn,
    19+
    originalSource
    20+
    } = sourceMap.findEntry(0, 29);
    21+
    assert.strictEqual(originalLine, 2);
    22+
    assert.strictEqual(originalColumn, 4);
    23+
    assert(originalSource.endsWith('disk.js'));
    24+
    }
    25+
    26+
    // findSourceMap() can be used in Error.prepareStackTrace() to lookup
    27+
    // source-map attached to error.
    28+
    {
    29+
    let callSite;
    30+
    let sourceMap;
    31+
    Error.prepareStackTrace = (error, trace) => {
    32+
    const throwingRequireCallSite = trace[0];
    33+
    if (throwingRequireCallSite.getFileName().endsWith('typescript-throw.js')) {
    34+
    sourceMap = findSourceMap(throwingRequireCallSite.getFileName(), error);
    35+
    callSite = throwingRequireCallSite;
    36+
    }
    37+
    };
    38+
    try {
    39+
    // Require a file that throws an exception, and has a source map.
    40+
    require('../fixtures/source-map/typescript-throw.js');
    41+
    } catch (err) {
    42+
    err.stack; // Force prepareStackTrace() to be called.
    43+
    }
    44+
    assert(callSite);
    45+
    assert(sourceMap);
    46+
    const {
    47+
    generatedLine,
    48+
    generatedColumn,
    49+
    originalLine,
    50+
    originalColumn,
    51+
    originalSource
    52+
    } = sourceMap.findEntry(
    53+
    callSite.getLineNumber() - 1,
    54+
    callSite.getColumnNumber() - 1
    55+
    );
    56+
    57+
    assert.strictEqual(generatedLine, 19);
    58+
    assert.strictEqual(generatedColumn, 14);
    59+
    60+
    assert.strictEqual(originalLine, 17);
    61+
    assert.strictEqual(originalColumn, 10);
    62+
    assert(originalSource.endsWith('typescript-throw.ts'));
    63+
    }
    64+
    65+
    // SourceMap can be instantiated with Source Map V3 object as payload.
    66+
    {
    67+
    const payload = JSON.parse(readFileSync(
    68+
    require.resolve('../fixtures/source-map/disk.map'), 'utf8'
    69+
    ));
    70+
    const sourceMap = new SourceMap(payload);
    71+
    const {
    72+
    originalLine,
    73+
    originalColumn,
    74+
    originalSource
    75+
    } = sourceMap.findEntry(0, 29);
    76+
    assert.strictEqual(originalLine, 2);
    77+
    assert.strictEqual(originalColumn, 4);
    78+
    assert(originalSource.endsWith('disk.js'));
    79+
    // The stored payload should be a clone:
    80+
    assert.strictEqual(payload.mappings, sourceMap.payload.mappings);
    81+
    assert.notStrictEqual(payload, sourceMap.payload);
    82+
    assert.strictEqual(payload.sources[0], sourceMap.payload.sources[0]);
    83+
    assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
    84+
    }

    tools/doc/type-parser.js

    Lines changed: 4 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -101,6 +101,10 @@ const customTypesMap = {
    101101
    'https.Server': 'https.html#https_class_https_server',
    102102

    103103
    'module': 'modules.html#modules_the_module_object',
    104+
    105+
    'module.SourceMap':
    106+
    'modules.html#modules_class_module_sourcemap',
    107+
    104108
    'require': 'modules.html#modules_require_id',
    105109

    106110
    'Handle': 'net.html#net_server_listen_handle_backlog_callback',

    0 commit comments

    Comments
     (0)
    0