summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/api/cli.md62
-rw-r--r--lib/internal/bootstrap/pre_execution.js2
-rw-r--r--lib/internal/modules/cjs/loader.js7
-rw-r--r--lib/internal/modules/esm/translators.js2
-rw-r--r--lib/internal/source_map.js152
-rw-r--r--node.gyp1
-rw-r--r--src/env.h1
-rw-r--r--src/inspector_profiler.cc60
-rw-r--r--src/inspector_profiler.h8
-rw-r--r--test/fixtures/source-map/basic.js7
-rw-r--r--test/fixtures/source-map/disk-relative-path.js2
-rw-r--r--test/fixtures/source-map/disk.js27
-rw-r--r--test/fixtures/source-map/disk.map20
-rw-r--r--test/fixtures/source-map/esm-basic.mjs4
-rw-r--r--test/fixtures/source-map/esm-dep.mjs4
-rw-r--r--test/fixtures/source-map/exit-1.js8
-rw-r--r--test/fixtures/source-map/inline-base64.js2
-rw-r--r--test/fixtures/source-map/sigint.js8
-rw-r--r--test/parallel/test-bootstrap-modules.js1
-rw-r--r--test/parallel/test-source-map.js132
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
+};
diff --git a/node.gyp b/node.gyp
index 661ed82890..dcd8904151 100644
--- a/node.gyp
+++ b/node.gyp
@@ -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',
diff --git a/src/env.h b/src/env.h
index 74d65f4fbb..03d0a79a6d 100644
--- a/src/env.h
+++ b/src/env.h
@@ -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];
+ }
+ }
+ }
+}