GeezerVison is showing github.com — module: add API for interacting with source maps · nodejs/node@4dced02
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/modules.md +86Lines changed: 86 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1033,6 +1033,86 @@ import('fs').then((esmFS) => { | 1033 | 1033 | }); | 1034 | 1034 | ``` | 1035 | 1035 |
| | 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 | +
| 1036 | 1116 | [GLOBAL_FOLDERS]: #modules_loading_from_the_global_folders | 1037 | 1117 | [`Error`]: errors.html#errors_class_error | 1038 | 1118 | [`__dirname`]: #modules_dirname | @@ -1046,3 +1126,9 @@ import('fs').then((esmFS) => { | 1046 | 1126 | [module resolution]: #modules_all_together | 1047 | 1127 | [module wrapper]: #modules_the_module_wrapper | 1048 | 1128 | [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 |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/source_map/prepare_stack_trace.js +10-8Lines changed: 10 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -29,7 +29,6 @@ const prepareStackTrace = (globalThis, error, trace) => { | 29 | 29 | maybeOverridePrepareStackTrace(globalThis, error, trace); | 30 | 30 | if (globalOverride !== kNoOverride) return globalOverride; | 31 | 31 |
| 32 | | - const { SourceMap } = require('internal/source_map/source_map'); | 33 | 32 | const errorString = ErrorToString.call(error); | 34 | 33 |
| 35 | 34 | if (trace.length === 0) { | @@ -39,16 +38,19 @@ const prepareStackTrace = (globalThis, error, trace) => { | 39 | 38 | let str = i !== 0 ? '\n at ' : ''; | 40 | 39 | str = `${str}${t}`; | 41 | 40 | 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) { | 45 | 43 | // Source Map V3 lines/columns use zero-based offsets whereas, in | 46 | 44 | // 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) { | 50 | 52 | str += | 51 | | - `\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`; | | 53 | +`\n -> ${originalSource.replace('file://', '')}:${originalLine + 1}:${originalColumn + 1}`; | 52 | 54 | } | 53 | 55 | } | 54 | 56 | } catch (err) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/source_map/source_map.js +50-22Lines changed: 50 additions & 22 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 66 | 66 |
| 67 | 67 | 'use strict'; | 68 | 68 |
| | 69 | +const { | | 70 | + Array | | 71 | +} = primordials; | | 72 | + | | 73 | +const { | | 74 | + ERR_INVALID_ARG_TYPE | | 75 | +} = require('internal/errors').codes; | | 76 | + | 69 | 77 | let base64Map; | 70 | 78 |
| 71 | 79 | const VLQ_BASE_SHIFT = 5; | @@ -112,6 +120,7 @@ class StringCharIterator { | 112 | 120 | * @param {SourceMapV3} payload | 113 | 121 | */ | 114 | 122 | class SourceMap { | | 123 | + #payload; | 115 | 124 | #reverseMappingsBySourceURL = []; | 116 | 125 | #mappings = []; | 117 | 126 | #sources = {}; | @@ -129,17 +138,25 @@ class SourceMap { | 129 | 138 | for (let i = 0; i < base64Digits.length; ++i) | 130 | 139 | base64Map[base64Digits[i]] = i; | 131 | 140 | } | 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); | 133 | 150 | } | 134 | 151 |
| 135 | 152 | /** | 136 | 153 | * @param {SourceMapV3} mappingPayload | 137 | 154 | */ | 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); | 141 | 158 | else | 142 | | - this.#parseMap(mappingPayload, 0, 0); | | 159 | + this.#parseMap(this.#payload, 0, 0); | 143 | 160 | } | 144 | 161 |
| 145 | 162 | /** | @@ -175,24 +192,18 @@ class SourceMap { | 175 | 192 | const entry = this.#mappings[first]; | 176 | 193 | if (!first && entry && (lineNumber < entry[0] || | 177 | 194 | (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 | + }; | 179 | 206 | } | 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]; | 196 | 207 | } | 197 | 208 |
| 198 | 209 | /** | @@ -296,6 +307,23 @@ function decodeVLQ(stringCharIterator) { | 296 | 307 | return negative ? -result : result; | 297 | 308 | } | 298 | 309 |
| | 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 | + | 299 | 327 | module.exports = { | 300 | 328 | SourceMap | 301 | 329 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/source_map/source_map_cache.js +11-1Lines changed: 11 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -37,6 +37,7 @@ const cjsSourceMapCache = new WeakMap(); | 37 | 37 | const esmSourceMapCache = new Map(); | 38 | 38 | const { fileURLToPath, URL } = require('url'); | 39 | 39 | let Module; | | 40 | +let SourceMap; | 40 | 41 |
| 41 | 42 | let experimentalSourceMaps; | 42 | 43 | function maybeCacheSourceMap(filename, content, cjsModuleInstance) { | @@ -222,8 +223,13 @@ function appendCJSCache(obj) { | 222 | 223 |
| 223 | 224 | // Attempt to lookup a source map, which is either attached to a file URI, or | 224 | 225 | // keyed on an error instance. | | 226 | +// TODO(bcoe): once WeakRefs are available in Node.js, refactor to drop | | 227 | +// requirement of error parameter. | 225 | 228 | function findSourceMap(uri, error) { | 226 | 229 | if (!Module) Module = require('internal/modules/cjs/loader').Module; | | 230 | + if (!SourceMap) { | | 231 | + SourceMap = require('internal/source_map/source_map').SourceMap; | | 232 | + } | 227 | 233 | let sourceMap = cjsSourceMapCache.get(Module._cache[uri]); | 228 | 234 | if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri); | 229 | 235 | if (sourceMap === undefined) { | @@ -235,7 +241,11 @@ function findSourceMap(uri, error) { | 235 | 241 | sourceMap = candidateSourceMap; | 236 | 242 | } | 237 | 243 | } | 238 | | - return sourceMap; | | 244 | + if (sourceMap && sourceMap.data) { | | 245 | + return new SourceMap(sourceMap.data); | | 246 | + } else { | | 247 | + return undefined; | | 248 | + } | 239 | 249 | } | 240 | 250 |
| 241 | 251 | module.exports = { | |
Collapse file Copy file name to clipboard +7-1Lines changed: 7 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| 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; |
Collapse file Copy file name to clipboard +84Lines changed: 84 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 | +} |
Collapse file Copy file name to clipboard File renamed without changes. Collapse file Copy file name to clipboardExpand all lines: tools/doc/type-parser.js +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -101,6 +101,10 @@ const customTypesMap = { | 101 | 101 | 'https.Server': 'https.html#https_class_https_server', | 102 | 102 |
| 103 | 103 | 'module': 'modules.html#modules_the_module_object', | | 104 | + | | 105 | + 'module.SourceMap': | | 106 | + 'modules.html#modules_class_module_sourcemap', | | 107 | + | 104 | 108 | 'require': 'modules.html#modules_require_id', | 105 | 109 |
| 106 | 110 | 'Handle': 'net.html#net_server_listen_handle_backlog_callback', | |
|