process: initial SourceMap support via NODE_V8_COVERAGE
PR-URL: https://github.com/nodejs/node/pull/28960 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: David Carlier <devnexen@gmail.com>
This commit is contained in:
parent
e74f30894c
commit
8f06773a8c
@ -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
|
||||
|
||||
At this time coverage is only collected in the main thread and will not be
|
||||
output for code executed by worker threads.
|
||||
> 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`).
|
||||
|
||||
```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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
152
lib/internal/source_map.js
Normal file
152
lib/internal/source_map.js
Normal file
@ -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
|
||||
};
|
1
node.gyp
1
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',
|
||||
|
@ -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) \
|
||||
|
@ -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
|
||||
|
@ -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_;
|
||||
|
7
test/fixtures/source-map/basic.js
vendored
Normal file
7
test/fixtures/source-map/basic.js
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
const a = 99;
|
||||
if (true) {
|
||||
const b = 101;
|
||||
} else {
|
||||
const c = 102;
|
||||
}
|
||||
//# sourceMappingURL=https://http.cat/418
|
2
test/fixtures/source-map/disk-relative-path.js
vendored
Normal file
2
test/fixtures/source-map/disk-relative-path.js
vendored
Normal file
@ -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
|
27
test/fixtures/source-map/disk.js
vendored
Normal file
27
test/fixtures/source-map/disk.js
vendored
Normal file
@ -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()
|
20
test/fixtures/source-map/disk.map
vendored
Normal file
20
test/fixtures/source-map/disk.map
vendored
Normal file
@ -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": "./"
|
||||
}
|
4
test/fixtures/source-map/esm-basic.mjs
vendored
Normal file
4
test/fixtures/source-map/esm-basic.mjs
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import {foo} from './esm-dep.mjs';
|
||||
import {strictEqual} from 'assert';
|
||||
strictEqual(foo(), 'foo');
|
||||
//# sourceMappingURL=https://http.cat/405
|
4
test/fixtures/source-map/esm-dep.mjs
vendored
Normal file
4
test/fixtures/source-map/esm-dep.mjs
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export function foo () {
|
||||
return 'foo';
|
||||
};
|
||||
//# sourceMappingURL=https://http.cat/422
|
8
test/fixtures/source-map/exit-1.js
vendored
Normal file
8
test/fixtures/source-map/exit-1.js
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
const a = 99;
|
||||
if (true) {
|
||||
const b = 101;
|
||||
} else {
|
||||
const c = 102;
|
||||
}
|
||||
process.exit(1);
|
||||
//# sourceMappingURL=https://http.cat/404
|
2
test/fixtures/source-map/inline-base64.js
vendored
Normal file
2
test/fixtures/source-map/inline-base64.js
vendored
Normal file
@ -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=
|
8
test/fixtures/source-map/sigint.js
vendored
Normal file
8
test/fixtures/source-map/sigint.js
vendored
Normal file
@ -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
|
@ -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',
|
||||
|
132
test/parallel/test-source-map.js
Normal file
132
test/parallel/test-source-map.js
Normal file
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user