diff options
-rw-r--r-- | doc/api/cli.md | 62 | ||||
-rw-r--r-- | lib/internal/bootstrap/pre_execution.js | 2 | ||||
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 7 | ||||
-rw-r--r-- | lib/internal/modules/esm/translators.js | 2 | ||||
-rw-r--r-- | lib/internal/source_map.js | 152 | ||||
-rw-r--r-- | node.gyp | 1 | ||||
-rw-r--r-- | src/env.h | 1 | ||||
-rw-r--r-- | src/inspector_profiler.cc | 60 | ||||
-rw-r--r-- | src/inspector_profiler.h | 8 | ||||
-rw-r--r-- | test/fixtures/source-map/basic.js | 7 | ||||
-rw-r--r-- | test/fixtures/source-map/disk-relative-path.js | 2 | ||||
-rw-r--r-- | test/fixtures/source-map/disk.js | 27 | ||||
-rw-r--r-- | test/fixtures/source-map/disk.map | 20 | ||||
-rw-r--r-- | test/fixtures/source-map/esm-basic.mjs | 4 | ||||
-rw-r--r-- | test/fixtures/source-map/esm-dep.mjs | 4 | ||||
-rw-r--r-- | test/fixtures/source-map/exit-1.js | 8 | ||||
-rw-r--r-- | test/fixtures/source-map/inline-base64.js | 2 | ||||
-rw-r--r-- | test/fixtures/source-map/sigint.js | 8 | ||||
-rw-r--r-- | test/parallel/test-bootstrap-modules.js | 1 | ||||
-rw-r--r-- | test/parallel/test-source-map.js | 132 |
20 files changed, 497 insertions, 13 deletions
diff --git a/doc/api/cli.md b/doc/api/cli.md index 31e5540242..8a1ca4893a 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1110,9 +1110,19 @@ variable is strongly discouraged. ### `NODE_V8_COVERAGE=dir` -When set, Node.js will begin outputting [V8 JavaScript code coverage][] to the -directory provided as an argument. Coverage is output as an array of -[ScriptCoverage][] objects: +When set, Node.js will begin outputting [V8 JavaScript code coverage][] and +[Source Map][] data to the directory provided as an argument (coverage +information is written as JSON to files with a `coverage` prefix). + +`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it +easier to instrument applications that call the `child_process.spawn()` family +of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent +propagation. + +#### Coverage Output + +Coverage is output as an array of [ScriptCoverage][] objects on the top-level +key `result`: ```json { @@ -1126,13 +1136,46 @@ directory provided as an argument. Coverage is output as an array of } ``` -`NODE_V8_COVERAGE` will automatically propagate to subprocesses, making it -easier to instrument applications that call the `child_process.spawn()` family -of functions. `NODE_V8_COVERAGE` can be set to an empty string, to prevent -propagation. +#### Source Map Cache + +> Stability: 1 - Experimental + +If found, Source Map data is appended to the top-level key `source-map-cache` +on the JSON coverage object. + +`source-map-cache` is an object with keys representing the files source maps +were extracted from, and the values include the raw source-map URL +(in the key `url`) and the parsed Source Map V3 information (in the key `data`). -At this time coverage is only collected in the main thread and will not be -output for code executed by worker threads. +```json +{ + "result": [ + { + "scriptId": "68", + "url": "file:///absolute/path/to/source.js", + "functions": [] + } + ], + "source-map-cache": { + "file:///absolute/path/to/source.js": { + "url": "./path-to-map.json", + "data": { + "version": 3, + "sources": [ + "file:///absolute/path/to/original.js" + ], + "names": [ + "Foo", + "console", + "info" + ], + "mappings": "MAAMA,IACJC,YAAaC", + "sourceRoot": "./" + } + } + } +} +``` ### `OPENSSL_CONF=file` <!-- YAML @@ -1203,6 +1246,7 @@ greater than `4` (its current default value). For more information, see the [Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/ [REPL]: repl.html [ScriptCoverage]: https://chromedevtools.github.io/devtools-protocol/tot/Profiler#type-ScriptCoverage +[Source Map]: https://sourcemaps.info/spec.html [Subresource Integrity]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity [V8 JavaScript code coverage]: https://v8project.blogspot.com/2017/12/javascript-code-coverage.html [customizing esm specifier resolution]: esm.html#esm_customizing_esm_specifier_resolution_algorithm diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 9e8897bb00..d18d709928 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -119,7 +119,9 @@ function setupCoverageHooks(dir) { const cwd = require('internal/process/execution').tryGetCwd(); const { resolve } = require('path'); const coverageDirectory = resolve(cwd, dir); + const { sourceMapCacheToObject } = require('internal/source_map'); internalBinding('profiler').setCoverageDirectory(coverageDirectory); + internalBinding('profiler').setSourceMapCacheGetter(sourceMapCacheToObject); return coverageDirectory; } diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 51adba6086..8ef01b4499 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -31,6 +31,7 @@ const { } = primordials; const { NativeModule } = require('internal/bootstrap/loaders'); +const { maybeCacheSourceMap } = require('internal/source_map'); const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); const { deprecate } = require('internal/util'); const vm = require('vm'); @@ -845,7 +846,9 @@ Module.prototype.require = function(id) { var resolvedArgv; let hasPausedEntry = false; -function wrapSafe(filename, content) { +function wrapSafe(filename, content, cjsModuleInstance) { + maybeCacheSourceMap(filename, content, cjsModuleInstance); + if (patched) { const wrapper = Module.wrap(content); return vm.runInThisContext(wrapper, { @@ -910,7 +913,7 @@ Module.prototype._compile = function(content, filename) { manifest.assertIntegrity(moduleURL, content); } - const compiledWrapper = wrapSafe(filename, content); + const compiledWrapper = wrapSafe(filename, content, this); var inspectorWrapper = null; if (getOptionValue('--inspect-brk') && process._eval == null) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 26508e744e..352b937766 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -31,6 +31,7 @@ const { } = require('internal/errors').codes; const readFileAsync = promisify(fs.readFile); const JsonParse = JSON.parse; +const { maybeCacheSourceMap } = require('internal/source_map'); const debug = debuglog('esm'); @@ -74,6 +75,7 @@ async function importModuleDynamically(specifier, { url }) { // Strategy for loading a standard JavaScript module translators.set('module', async function moduleStrategy(url) { const source = `${await getSource(url)}`; + maybeCacheSourceMap(url, source); debug(`Translating StandardModule ${url}`); const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); const module = new ModuleWrap(source, url); diff --git a/lib/internal/source_map.js b/lib/internal/source_map.js new file mode 100644 index 0000000000..4b198ff598 --- /dev/null +++ b/lib/internal/source_map.js @@ -0,0 +1,152 @@ +'use strict'; + +// See https://sourcemaps.info/spec.html for SourceMap V3 specification. +const { Buffer } = require('buffer'); +const debug = require('internal/util/debuglog').debuglog('source_map'); +const { dirname, resolve } = require('path'); +const fs = require('fs'); +const { + normalizeReferrerURL, +} = require('internal/modules/cjs/helpers'); +const { JSON, Object } = primordials; +// For cjs, since Module._cache is exposed to users, we use a WeakMap +// keyed on module, facilitating garbage collection. +const cjsSourceMapCache = new WeakMap(); +// The esm cache is not exposed to users, so we can use a Map keyed +// on filenames. +const esmSourceMapCache = new Map(); +const { fileURLToPath, URL } = require('url'); + +function maybeCacheSourceMap(filename, content, cjsModuleInstance) { + if (!process.env.NODE_V8_COVERAGE) return; + + let basePath; + try { + filename = normalizeReferrerURL(filename); + basePath = dirname(fileURLToPath(filename)); + } catch (err) { + // This is most likely an [eval]-wrapper, which is currently not + // supported. + debug(err.stack); + return; + } + + const match = content.match(/\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/); + if (match) { + if (cjsModuleInstance) { + cjsSourceMapCache.set(cjsModuleInstance, { + url: match.groups.sourceMappingURL, + data: dataFromUrl(basePath, match.groups.sourceMappingURL) + }); + } else { + // If there is no cjsModuleInstance assume we are in a + // "modules/esm" context. + esmSourceMapCache.set(filename, { + url: match.groups.sourceMappingURL, + data: dataFromUrl(basePath, match.groups.sourceMappingURL) + }); + } + } +} + +function dataFromUrl(basePath, sourceMappingURL) { + try { + const url = new URL(sourceMappingURL); + switch (url.protocol) { + case 'data:': + return sourceMapFromDataUrl(basePath, url.pathname); + default: + debug(`unknown protocol ${url.protocol}`); + return null; + } + } catch (err) { + debug(err.stack); + // If no scheme is present, we assume we are dealing with a file path. + const sourceMapFile = resolve(basePath, sourceMappingURL); + return sourceMapFromFile(sourceMapFile); + } +} + +function sourceMapFromFile(sourceMapFile) { + try { + const content = fs.readFileSync(sourceMapFile, 'utf8'); + const data = JSON.parse(content); + return sourcesToAbsolute(dirname(sourceMapFile), data); + } catch (err) { + debug(err.stack); + return null; + } +} + +// data:[<mediatype>][;base64],<data> see: +// https://tools.ietf.org/html/rfc2397#section-2 +function sourceMapFromDataUrl(basePath, url) { + const [format, data] = url.split(','); + const splitFormat = format.split(';'); + const contentType = splitFormat[0]; + const base64 = splitFormat[splitFormat.length - 1] === 'base64'; + if (contentType === 'application/json') { + const decodedData = base64 ? + Buffer.from(data, 'base64').toString('utf8') : data; + try { + const parsedData = JSON.parse(decodedData); + return sourcesToAbsolute(basePath, parsedData); + } catch (err) { + debug(err.stack); + return null; + } + } else { + debug(`unknown content-type ${contentType}`); + return null; + } +} + +// If the sources are not absolute URLs after prepending of the "sourceRoot", +// the sources are resolved relative to the SourceMap (like resolving script +// src in a html document). +function sourcesToAbsolute(base, data) { + data.sources = data.sources.map((source) => { + source = (data.sourceRoot || '') + source; + if (!/^[\\/]/.test(source[0])) { + source = resolve(base, source); + } + if (!source.startsWith('file://')) source = `file://${source}`; + return source; + }); + // The sources array is now resolved to absolute URLs, sourceRoot should + // be updated to noop. + data.sourceRoot = ''; + return data; +} + +function sourceMapCacheToObject() { + const obj = Object.create(null); + + for (const [k, v] of esmSourceMapCache) { + obj[k] = v; + } + appendCJSCache(obj); + + if (Object.keys(obj).length === 0) { + return undefined; + } else { + return obj; + } +} + +// Since WeakMap can't be iterated over, we use Module._cache's +// keys to facilitate Source Map serialization. +function appendCJSCache(obj) { + const { Module } = require('internal/modules/cjs/loader'); + Object.keys(Module._cache).forEach((key) => { + const value = cjsSourceMapCache.get(Module._cache[key]); + if (value) { + obj[`file://${key}`] = value; + } + }); +} + +module.exports = { + sourceMapCacheToObject, + maybeCacheSourceMap +}; @@ -175,6 +175,7 @@ 'lib/internal/repl/history.js', 'lib/internal/repl/utils.js', 'lib/internal/socket_list.js', + 'lib/internal/source_map.js', 'lib/internal/test/binding.js', 'lib/internal/timers.js', 'lib/internal/tls.js', @@ -444,6 +444,7 @@ constexpr size_t kFsStatsBufferLength = V(primordials, v8::Object) \ V(promise_reject_callback, v8::Function) \ V(script_data_constructor_function, v8::Function) \ + V(source_map_cache_getter, v8::Function) \ V(tick_callback_function, v8::Function) \ V(timers_callback_function, v8::Function) \ V(tls_wrap_constructor_function, v8::Function) \ diff --git a/src/inspector_profiler.cc b/src/inspector_profiler.cc index 867f145ff5..1444a36bdf 100644 --- a/src/inspector_profiler.cc +++ b/src/inspector_profiler.cc @@ -180,6 +180,58 @@ void V8ProfilerConnection::WriteProfile(Local<String> message) { if (!GetProfile(result).ToLocal(&profile)) { return; } + + Local<String> result_s; + if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) { + fprintf(stderr, "Failed to stringify %s profile result\n", type()); + return; + } + + // Create the directory if necessary. + std::string directory = GetDirectory(); + DCHECK(!directory.empty()); + if (!EnsureDirectory(directory, type())) { + return; + } + + std::string filename = GetFilename(); + DCHECK(!filename.empty()); + std::string path = directory + kPathSeparator + filename; + + WriteResult(env_, path.c_str(), result_s); +} + +void V8CoverageConnection::WriteProfile(Local<String> message) { + Isolate* isolate = env_->isolate(); + Local<Context> context = env_->context(); + HandleScope handle_scope(isolate); + Context::Scope context_scope(context); + + // Get message.result from the response. + Local<Object> result; + if (!ParseProfile(env_, message, type()).ToLocal(&result)) { + return; + } + // Generate the profile output from the subclass. + Local<Object> profile; + if (!GetProfile(result).ToLocal(&profile)) { + return; + } + + // append source-map cache information to coverage object: + Local<Function> source_map_cache_getter = env_->source_map_cache_getter(); + Local<Value> source_map_cache_v; + if (!source_map_cache_getter->Call(env()->context(), + Undefined(isolate), 0, nullptr) + .ToLocal(&source_map_cache_v)) { + return; + } + // Avoid writing to disk if no source-map data: + if (!source_map_cache_v->IsUndefined()) { + profile->Set(context, FIXED_ONE_BYTE_STRING(isolate, "source-map-cache"), + source_map_cache_v); + } + Local<String> result_s; if (!v8::JSON::Stringify(context, profile).ToLocal(&result_s)) { fprintf(stderr, "Failed to stringify %s profile result\n", type()); @@ -385,12 +437,20 @@ static void SetCoverageDirectory(const FunctionCallbackInfo<Value>& args) { env->set_coverage_directory(*directory); } + +static void SetSourceMapCacheGetter(const FunctionCallbackInfo<Value>& args) { + CHECK(args[0]->IsFunction()); + Environment* env = Environment::GetCurrent(args); + env->set_source_map_cache_getter(args[0].As<Function>()); +} + static void Initialize(Local<Object> target, Local<Value> unused, Local<Context> context, void* priv) { Environment* env = Environment::GetCurrent(context); env->SetMethod(target, "setCoverageDirectory", SetCoverageDirectory); + env->SetMethod(target, "setSourceMapCacheGetter", SetSourceMapCacheGetter); } } // namespace profiler diff --git a/src/inspector_profiler.h b/src/inspector_profiler.h index e7d45d7de3..4a21eb3a2d 100644 --- a/src/inspector_profiler.h +++ b/src/inspector_profiler.h @@ -59,13 +59,15 @@ class V8ProfilerConnection { // which will be then written as a JSON. virtual v8::MaybeLocal<v8::Object> GetProfile( v8::Local<v8::Object> result) = 0; + virtual void WriteProfile(v8::Local<v8::String> message); private: size_t next_id() { return id_++; } - void WriteProfile(v8::Local<v8::String> message); std::unique_ptr<inspector::InspectorSession> session_; - Environment* env_ = nullptr; size_t id_ = 1; + + protected: + Environment* env_ = nullptr; }; class V8CoverageConnection : public V8ProfilerConnection { @@ -81,6 +83,8 @@ class V8CoverageConnection : public V8ProfilerConnection { std::string GetDirectory() const override; std::string GetFilename() const override; v8::MaybeLocal<v8::Object> GetProfile(v8::Local<v8::Object> result) override; + void WriteProfile(v8::Local<v8::String> message) override; + void WriteSourceMapCache(); private: std::unique_ptr<inspector::InspectorSession> session_; diff --git a/test/fixtures/source-map/basic.js b/test/fixtures/source-map/basic.js new file mode 100644 index 0000000000..a483ffb105 --- /dev/null +++ b/test/fixtures/source-map/basic.js @@ -0,0 +1,7 @@ +const a = 99; +if (true) { + const b = 101; +} else { + const c = 102; +} +//# sourceMappingURL=https://http.cat/418 diff --git a/test/fixtures/source-map/disk-relative-path.js b/test/fixtures/source-map/disk-relative-path.js new file mode 100644 index 0000000000..86d4dbc96a --- /dev/null +++ b/test/fixtures/source-map/disk-relative-path.js @@ -0,0 +1,2 @@ +class Foo{constructor(x=33){this.x=x?x:99;if(this.x){console.info("covered")}else{console.info("uncovered")}this.methodC()}methodA(){console.info("covered")}methodB(){console.info("uncovered")}methodC(){console.info("covered")}methodD(){console.info("uncovered")}}const a=new Foo(0);const b=new Foo(33);a.methodA(); +//# sourceMappingURL=./disk.map
\ No newline at end of file diff --git a/test/fixtures/source-map/disk.js b/test/fixtures/source-map/disk.js new file mode 100644 index 0000000000..e1637d5204 --- /dev/null +++ b/test/fixtures/source-map/disk.js @@ -0,0 +1,27 @@ +class Foo { + constructor (x=33) { + this.x = x ? x : 99 + if (this.x) { + console.info('covered') + } else { + console.info('uncovered') + } + this.methodC() + } + methodA () { + console.info('covered') + } + methodB () { + console.info('uncovered') + } + methodC () { + console.info('covered') + } + methodD () { + console.info('uncovered') + } +} + +const a = new Foo(0) +const b = new Foo(33) +a.methodA() diff --git a/test/fixtures/source-map/disk.map b/test/fixtures/source-map/disk.map new file mode 100644 index 0000000000..3ae8349f7e --- /dev/null +++ b/test/fixtures/source-map/disk.map @@ -0,0 +1,20 @@ +{ + "version": 3, + "sources": [ + "disk.js" + ], + "names": [ + "Foo", + "[object Object]", + "x", + "this", + "console", + "info", + "methodC", + "a", + "b", + "methodA" + ], + "mappings": "MAAMA,IACJC,YAAaC,EAAE,IACbC,KAAKD,EAAIA,EAAIA,EAAI,GACjB,GAAIC,KAAKD,EAAG,CACVE,QAAQC,KAAK,eACR,CACLD,QAAQC,KAAK,aAEfF,KAAKG,UAEPL,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,aAEfJ,UACEG,QAAQC,KAAK,WAEfJ,UACEG,QAAQC,KAAK,cAIjB,MAAME,EAAI,IAAIP,IAAI,GAClB,MAAMQ,EAAI,IAAIR,IAAI,IAClBO,EAAEE", + "sourceRoot": "./" +} diff --git a/test/fixtures/source-map/esm-basic.mjs b/test/fixtures/source-map/esm-basic.mjs new file mode 100644 index 0000000000..55747d3870 --- /dev/null +++ b/test/fixtures/source-map/esm-basic.mjs @@ -0,0 +1,4 @@ +import {foo} from './esm-dep.mjs'; +import {strictEqual} from 'assert'; +strictEqual(foo(), 'foo'); +//# sourceMappingURL=https://http.cat/405 diff --git a/test/fixtures/source-map/esm-dep.mjs b/test/fixtures/source-map/esm-dep.mjs new file mode 100644 index 0000000000..00805894af --- /dev/null +++ b/test/fixtures/source-map/esm-dep.mjs @@ -0,0 +1,4 @@ +export function foo () { + return 'foo'; +}; +//# sourceMappingURL=https://http.cat/422 diff --git a/test/fixtures/source-map/exit-1.js b/test/fixtures/source-map/exit-1.js new file mode 100644 index 0000000000..9734649a77 --- /dev/null +++ b/test/fixtures/source-map/exit-1.js @@ -0,0 +1,8 @@ +const a = 99; +if (true) { + const b = 101; +} else { + const c = 102; +} +process.exit(1); +//# sourceMappingURL=https://http.cat/404 diff --git a/test/fixtures/source-map/inline-base64.js b/test/fixtures/source-map/inline-base64.js new file mode 100644 index 0000000000..5d71df5b42 --- /dev/null +++ b/test/fixtures/source-map/inline-base64.js @@ -0,0 +1,2 @@ +var cov_263bu3eqm8=function(){var path= "./branches.js";var hash="424788076537d051b5bf0e2564aef393124eabc7";var global=new Function("return this")();var gcv="__coverage__";var coverageData={path: "./branches.js",statementMap:{"0":{start:{line:1,column:0},end:{line:7,column:1}},"1":{start:{line:2,column:2},end:{line:2,column:29}},"2":{start:{line:3,column:7},end:{line:7,column:1}},"3":{start:{line:4,column:2},end:{line:4,column:27}},"4":{start:{line:6,column:2},end:{line:6,column:29}},"5":{start:{line:10,column:2},end:{line:16,column:3}},"6":{start:{line:11,column:4},end:{line:11,column:28}},"7":{start:{line:12,column:9},end:{line:16,column:3}},"8":{start:{line:13,column:4},end:{line:13,column:31}},"9":{start:{line:15,column:4},end:{line:15,column:29}},"10":{start:{line:19,column:0},end:{line:19,column:12}},"11":{start:{line:20,column:0},end:{line:20,column:13}}},fnMap:{"0":{name:"branch",decl:{start:{line:9,column:9},end:{line:9,column:15}},loc:{start:{line:9,column:20},end:{line:17,column:1}},line:9}},branchMap:{"0":{loc:{start:{line:1,column:0},end:{line:7,column:1}},type:"if",locations:[{start:{line:1,column:0},end:{line:7,column:1}},{start:{line:1,column:0},end:{line:7,column:1}}],line:1},"1":{loc:{start:{line:3,column:7},end:{line:7,column:1}},type:"if",locations:[{start:{line:3,column:7},end:{line:7,column:1}},{start:{line:3,column:7},end:{line:7,column:1}}],line:3},"2":{loc:{start:{line:10,column:2},end:{line:16,column:3}},type:"if",locations:[{start:{line:10,column:2},end:{line:16,column:3}},{start:{line:10,column:2},end:{line:16,column:3}}],line:10},"3":{loc:{start:{line:12,column:9},end:{line:16,column:3}},type:"if",locations:[{start:{line:12,column:9},end:{line:16,column:3}},{start:{line:12,column:9},end:{line:16,column:3}}],line:12}},s:{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0},f:{"0":0},b:{"0":[0,0],"1":[0,0],"2":[0,0],"3":[0,0]},_coverageSchema:"43e27e138ebf9cfc5966b082cf9a028302ed4184",hash:"424788076537d051b5bf0e2564aef393124eabc7"};var coverage=global[gcv]||(global[gcv]={});if(coverage[path]&&coverage[path].hash===hash){return coverage[path];}return coverage[path]=coverageData;}();cov_263bu3eqm8.s[0]++;if(false){cov_263bu3eqm8.b[0][0]++;cov_263bu3eqm8.s[1]++;console.info('unreachable');}else{cov_263bu3eqm8.b[0][1]++;cov_263bu3eqm8.s[2]++;if(true){cov_263bu3eqm8.b[1][0]++;cov_263bu3eqm8.s[3]++;console.info('reachable');}else{cov_263bu3eqm8.b[1][1]++;cov_263bu3eqm8.s[4]++;console.info('unreachable');}}function branch(a){cov_263bu3eqm8.f[0]++;cov_263bu3eqm8.s[5]++;if(a){cov_263bu3eqm8.b[2][0]++;cov_263bu3eqm8.s[6]++;console.info('a = true');}else{cov_263bu3eqm8.b[2][1]++;cov_263bu3eqm8.s[7]++;if(undefined){cov_263bu3eqm8.b[3][0]++;cov_263bu3eqm8.s[8]++;console.info('unreachable');}else{cov_263bu3eqm8.b[3][1]++;cov_263bu3eqm8.s[9]++;console.info('a = false');}}}cov_263bu3eqm8.s[10]++;branch(true);cov_263bu3eqm8.s[11]++;branch(false); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4vYnJhbmNoZXMuanMiXSwibmFtZXMiOlsiY29uc29sZSIsImluZm8iLCJicmFuY2giLCJhIiwidW5kZWZpbmVkIl0sIm1hcHBpbmdzIjoic3VFQUFBLEdBQUksS0FBSixDQUFXLGdEQUNUQSxPQUFPLENBQUNDLElBQVIsQ0FBYSxhQUFiLEVBQ0QsQ0FGRCxJQUVPLG1EQUFJLElBQUosQ0FBVSxnREFDZkQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELEVBRUQsUUFBU0MsQ0FBQUEsTUFBVCxDQUFpQkMsQ0FBakIsQ0FBb0IsNkNBQ2xCLEdBQUlBLENBQUosQ0FBTyxnREFDTEgsT0FBTyxDQUFDQyxJQUFSLENBQWEsVUFBYixFQUNELENBRkQsSUFFTyxtREFBSUcsU0FBSixDQUFlLGdEQUNwQkosT0FBTyxDQUFDQyxJQUFSLENBQWEsYUFBYixFQUNELENBRk0sSUFFQSxnREFDTEQsT0FBTyxDQUFDQyxJQUFSLENBQWEsV0FBYixFQUNELEVBQ0YsQyx1QkFFREMsTUFBTSxDQUFDLElBQUQsQ0FBTixDLHVCQUNBQSxNQUFNLENBQUMsS0FBRCxDQUFOIiwic291cmNlc0NvbnRlbnQiOlsiaWYgKGZhbHNlKSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufSBlbHNlIGlmICh0cnVlKSB7XG4gIGNvbnNvbGUuaW5mbygncmVhY2hhYmxlJylcbn0gZWxzZSB7XG4gIGNvbnNvbGUuaW5mbygndW5yZWFjaGFibGUnKVxufVxuXG5mdW5jdGlvbiBicmFuY2ggKGEpIHtcbiAgaWYgKGEpIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSB0cnVlJylcbiAgfSBlbHNlIGlmICh1bmRlZmluZWQpIHtcbiAgICBjb25zb2xlLmluZm8oJ3VucmVhY2hhYmxlJylcbiAgfSBlbHNlIHtcbiAgICBjb25zb2xlLmluZm8oJ2EgPSBmYWxzZScpXG4gIH1cbn1cblxuYnJhbmNoKHRydWUpXG5icmFuY2goZmFsc2UpXG4iXX0= diff --git a/test/fixtures/source-map/sigint.js b/test/fixtures/source-map/sigint.js new file mode 100644 index 0000000000..11df66645f --- /dev/null +++ b/test/fixtures/source-map/sigint.js @@ -0,0 +1,8 @@ +const a = 99; +if (true) { + const b = 101; +} else { + const c = 102; +} +process.kill(process.pid, "SIGINT"); +//# sourceMappingURL=https://http.cat/402 diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 69aae7f9b7..36c323b977 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -55,6 +55,7 @@ const expectedModules = new Set([ 'NativeModule internal/process/task_queues', 'NativeModule internal/process/warning', 'NativeModule internal/querystring', + 'NativeModule internal/source_map', 'NativeModule internal/timers', 'NativeModule internal/url', 'NativeModule internal/util', diff --git a/test/parallel/test-source-map.js b/test/parallel/test-source-map.js new file mode 100644 index 0000000000..de728c05bf --- /dev/null +++ b/test/parallel/test-source-map.js @@ -0,0 +1,132 @@ +'use strict'; + +if (!process.features.inspector) return; + +const common = require('../common'); +const assert = require('assert'); +const { dirname } = require('path'); +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const tmpdir = require('../common/tmpdir'); +tmpdir.refresh(); + +let dirc = 0; +function nextdir() { + return process.env.NODE_V8_COVERAGE || + path.join(tmpdir.path, `source_map_${++dirc}`); +} + +// Outputs source maps when event loop is drained, with no async logic. +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + require.resolve('../fixtures/source-map/basic') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + if (output.status !== 0) { + console.log(output.stderr.toString()); + } + assert.strictEqual(output.status, 0); + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache('basic.js', coverageDirectory); + assert.strictEqual(sourceMap.url, 'https://http.cat/418'); +} + +// Outputs source maps when process.kill(process.pid, "SIGINT"); exits process. +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + require.resolve('../fixtures/source-map/sigint') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + if (!common.isWindows) { + if (output.signal !== 'SIGINT') { + console.log(output.stderr.toString()); + } + assert.strictEqual(output.signal, 'SIGINT'); + } + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache('sigint.js', coverageDirectory); + assert.strictEqual(sourceMap.url, 'https://http.cat/402'); +} + +// Outputs source maps when source-file calls process.exit(1). +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + require.resolve('../fixtures/source-map/exit-1') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache('exit-1.js', coverageDirectory); + assert.strictEqual(sourceMap.url, 'https://http.cat/404'); +} + +// Outputs source-maps for esm module. +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + '--no-warnings', + '--experimental-modules', + require.resolve('../fixtures/source-map/esm-basic.mjs') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache('esm-basic.mjs', coverageDirectory); + assert.strictEqual(sourceMap.url, 'https://http.cat/405'); +} + +// Loads source-maps with relative path from .map file on disk. +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + require.resolve('../fixtures/source-map/disk-relative-path') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + assert.strictEqual(output.status, 0); + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache( + 'disk-relative-path.js', + coverageDirectory + ); + // Source-map should have been loaded from disk and sources should have been + // rewritten, such that they're absolute paths. + assert.strictEqual( + dirname( + `file://${require.resolve('../fixtures/source-map/disk-relative-path')}`), + dirname(sourceMap.data.sources[0]) + ); +} + +// Loads source-maps from inline data URL. +{ + const coverageDirectory = nextdir(); + const output = spawnSync(process.execPath, [ + require.resolve('../fixtures/source-map/inline-base64.js') + ], { env: { ...process.env, NODE_V8_COVERAGE: coverageDirectory } }); + assert.strictEqual(output.status, 0); + assert.strictEqual(output.stderr.toString(), ''); + const sourceMap = getSourceMapFromCache( + 'inline-base64.js', + coverageDirectory + ); + // base64 JSON should have been decoded, and paths to sources should have + // been rewritten such that they're absolute: + assert.strictEqual( + dirname( + `file://${require.resolve('../fixtures/source-map/inline-base64')}`), + dirname(sourceMap.data.sources[0]) + ); +} + +function getSourceMapFromCache(fixtureFile, coverageDirectory) { + const jsonFiles = fs.readdirSync(coverageDirectory); + for (const jsonFile of jsonFiles) { + const maybeSourceMapCache = require( + path.join(coverageDirectory, jsonFile) + )['source-map-cache'] || {}; + const keys = Object.keys(maybeSourceMapCache); + for (const key of keys) { + if (key.includes(fixtureFile)) { + return maybeSourceMapCache[key]; + } + } + } +} |