superc8 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +206 -0
- package/bin/c8.js +51 -0
- package/index.d.ts +32 -0
- package/index.js +1 -0
- package/lib/commands/check-coverage.js +83 -0
- package/lib/commands/report.js +43 -0
- package/lib/parse-args.js +229 -0
- package/lib/report.js +557 -0
- package/lib/source-map-from-file.js +109 -0
- package/package.json +75 -0
package/lib/report.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readdirSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
statSync,
|
|
5
|
+
} from 'node:fs';
|
|
6
|
+
import {
|
|
7
|
+
isAbsolute,
|
|
8
|
+
resolve,
|
|
9
|
+
extname,
|
|
10
|
+
} from 'node:path';
|
|
11
|
+
import {pathToFileURL, fileURLToPath} from 'node:url';
|
|
12
|
+
import util from 'node:util';
|
|
13
|
+
import {readFile} from 'node:fs/promises';
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
import Exclude from 'test-exclude';
|
|
16
|
+
import libCoverage from 'istanbul-lib-coverage';
|
|
17
|
+
import libReport from 'istanbul-lib-report';
|
|
18
|
+
import reports from 'istanbul-reports';
|
|
19
|
+
import v8toIstanbul from 'v8-to-istanbul';
|
|
20
|
+
import {mergeProcessCovs} from '@bcoe/v8-coverage';
|
|
21
|
+
// TODO: switch back to @c88/v8-coverage once patch is landed.
|
|
22
|
+
import getSourceMapFromFile from './source-map-from-file.js';
|
|
23
|
+
|
|
24
|
+
const {isArray} = Array;
|
|
25
|
+
const isUndefined = (a) => typeof a === 'undefined';
|
|
26
|
+
const maybeArray = (a) => isArray(a) ? a : [a];
|
|
27
|
+
const isString = (a) => typeof a === 'string';
|
|
28
|
+
const consoleError = console.error;
|
|
29
|
+
|
|
30
|
+
const debuglog = util.debuglog('c8');
|
|
31
|
+
|
|
32
|
+
class ReportInstance {
|
|
33
|
+
constructor({exclude, extension, excludeAfterRemap, include, reporter, reporterOptions, reportsDirectory, tempDirectory, watermarks, omitRelative, wrapperLength, resolve: resolvePaths, all, src, allowExternal = false, skipFull, excludeNodeModules, mergeAsync, monocartArgv}) {
|
|
34
|
+
this.reporter = reporter;
|
|
35
|
+
this.reporterOptions = reporterOptions || {};
|
|
36
|
+
this.reportsDirectory = reportsDirectory;
|
|
37
|
+
this.tempDirectory = tempDirectory;
|
|
38
|
+
this.watermarks = watermarks;
|
|
39
|
+
this.resolve = resolvePaths;
|
|
40
|
+
this.exclude = new Exclude({
|
|
41
|
+
exclude,
|
|
42
|
+
include,
|
|
43
|
+
extension,
|
|
44
|
+
relativePath: !allowExternal,
|
|
45
|
+
excludeNodeModules,
|
|
46
|
+
});
|
|
47
|
+
this.excludeAfterRemap = excludeAfterRemap;
|
|
48
|
+
this.shouldInstrumentCache = new Map();
|
|
49
|
+
this.omitRelative = omitRelative;
|
|
50
|
+
this.sourceMapCache = {};
|
|
51
|
+
this.wrapperLength = wrapperLength;
|
|
52
|
+
this.all = all;
|
|
53
|
+
this.src = this._getSrc(src);
|
|
54
|
+
this.skipFull = skipFull;
|
|
55
|
+
this.mergeAsync = mergeAsync;
|
|
56
|
+
this.monocartArgv = monocartArgv;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_getSrc(src) {
|
|
60
|
+
if (isString(src))
|
|
61
|
+
return [src];
|
|
62
|
+
|
|
63
|
+
if (Array.isArray(src))
|
|
64
|
+
return src;
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
process.cwd(),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async run() {
|
|
72
|
+
if (this.monocartArgv)
|
|
73
|
+
return this.runMonocart();
|
|
74
|
+
|
|
75
|
+
const context = libReport.createContext({
|
|
76
|
+
dir: this.reportsDirectory,
|
|
77
|
+
watermarks: this.watermarks,
|
|
78
|
+
coverageMap: await this.getCoverageMapFromAllCoverageFiles(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const _reporter of this.reporter) {
|
|
82
|
+
reports
|
|
83
|
+
.create(_reporter, {
|
|
84
|
+
skipEmpty: false,
|
|
85
|
+
skipFull: this.skipFull,
|
|
86
|
+
maxCols: process.stdout.columns || 100,
|
|
87
|
+
...this.reporterOptions[_reporter],
|
|
88
|
+
})
|
|
89
|
+
.execute(context);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async importMonocart() {
|
|
94
|
+
return import('monocart-coverage-reports');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getMonocart() {
|
|
98
|
+
let MCR;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
MCR = await this.importMonocart();
|
|
102
|
+
} catch {
|
|
103
|
+
consoleError('--experimental-monocart requires the plugin monocart-coverage-reports. Run: "npm i monocart-coverage-reports@2 --save-dev"');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return MCR;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async runMonocart() {
|
|
111
|
+
const MCR = await this.getMonocart();
|
|
112
|
+
|
|
113
|
+
if (!MCR)
|
|
114
|
+
return;
|
|
115
|
+
|
|
116
|
+
const argv = this.monocartArgv;
|
|
117
|
+
const {exclude} = this;
|
|
118
|
+
|
|
119
|
+
function getEntryFilter() {
|
|
120
|
+
return argv.entryFilter || argv.filter || function(entry) {
|
|
121
|
+
return exclude.shouldInstrument(fileURLToPath(entry.url));
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getSourceFilter() {
|
|
126
|
+
return argv.sourceFilter || argv.filter || function(sourcePath) {
|
|
127
|
+
if (argv.excludeAfterRemap)
|
|
128
|
+
return exclude.shouldInstrument(sourcePath);
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getReports() {
|
|
135
|
+
const reports = maybeArray(argv.reporter);
|
|
136
|
+
const reporterOptions = argv.reporterOptions || {};
|
|
137
|
+
|
|
138
|
+
return reports.map((reportName) => {
|
|
139
|
+
const reportOptions = {
|
|
140
|
+
...reporterOptions[reportName],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (reportName === 'text') {
|
|
144
|
+
reportOptions.skipEmpty = false;
|
|
145
|
+
reportOptions.skipFull = argv.skipFull;
|
|
146
|
+
reportOptions.maxCols = process.stdout.columns || 100;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return [reportName, reportOptions];
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --all: add empty coverage for all files
|
|
154
|
+
function getAllOptions() {
|
|
155
|
+
if (!argv.all)
|
|
156
|
+
return;
|
|
157
|
+
|
|
158
|
+
const {src} = argv;
|
|
159
|
+
const workingDirs = Array.isArray(src) ? src : isString(src) ? [src] : [
|
|
160
|
+
process.cwd(),
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
dir: workingDirs,
|
|
165
|
+
filter: (filePath) => {
|
|
166
|
+
return exclude.shouldInstrument(filePath);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function initPct(summary) {
|
|
172
|
+
for (const k of Object.keys(summary)) {
|
|
173
|
+
if (summary[k].pct === '')
|
|
174
|
+
summary[k].pct = 100;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return summary;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// adapt coverage options
|
|
181
|
+
const coverageOptions = {
|
|
182
|
+
logging: argv.logging,
|
|
183
|
+
name: argv.name,
|
|
184
|
+
|
|
185
|
+
reports: getReports(),
|
|
186
|
+
|
|
187
|
+
outputDir: argv.reportsDir,
|
|
188
|
+
baseDir: argv.baseDir,
|
|
189
|
+
|
|
190
|
+
entryFilter: getEntryFilter(),
|
|
191
|
+
sourceFilter: getSourceFilter(),
|
|
192
|
+
|
|
193
|
+
inline: argv.inline,
|
|
194
|
+
lcov: argv.lcov,
|
|
195
|
+
|
|
196
|
+
all: getAllOptions(),
|
|
197
|
+
|
|
198
|
+
clean: argv.clean,
|
|
199
|
+
// use default value for istanbul
|
|
200
|
+
defaultSummarizer: 'pkg',
|
|
201
|
+
|
|
202
|
+
onEnd: (coverageResults) => {
|
|
203
|
+
// for check coverage
|
|
204
|
+
this._allCoverageFiles = {
|
|
205
|
+
files: () => {
|
|
206
|
+
return coverageResults.files.map((it) => it.sourcePath);
|
|
207
|
+
},
|
|
208
|
+
fileCoverageFor: (file) => {
|
|
209
|
+
const fileCoverage = coverageResults.files.find((it) => it.sourcePath === file);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
toSummary: () => {
|
|
213
|
+
return initPct(fileCoverage.summary);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
getCoverageSummary: () => {
|
|
218
|
+
return initPct(coverageResults.summary);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const coverageReport = new MCR.CoverageReport(coverageOptions);
|
|
225
|
+
|
|
226
|
+
coverageReport.cleanCache();
|
|
227
|
+
// read v8 coverage data from tempDirectory
|
|
228
|
+
await coverageReport.addFromDir(argv.tempDirectory);
|
|
229
|
+
// generate report
|
|
230
|
+
await coverageReport.generate();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getCoverageMapFromAllCoverageFiles() {
|
|
234
|
+
// the merge process can be very expensive, and it's often the case that
|
|
235
|
+
// check-coverage is called immediately after a report. We memoize the
|
|
236
|
+
// result from getCoverageMapFromAllCoverageFiles() to address this
|
|
237
|
+
// use-case.
|
|
238
|
+
if (this._allCoverageFiles)
|
|
239
|
+
return this._allCoverageFiles;
|
|
240
|
+
|
|
241
|
+
const map = libCoverage.createCoverageMap();
|
|
242
|
+
let v8ProcessCov;
|
|
243
|
+
|
|
244
|
+
if (this.mergeAsync)
|
|
245
|
+
v8ProcessCov = await this._getMergedProcessCovAsync();
|
|
246
|
+
else
|
|
247
|
+
v8ProcessCov = this._getMergedProcessCov();
|
|
248
|
+
|
|
249
|
+
const resultCountPerPath = new Map();
|
|
250
|
+
|
|
251
|
+
for (const v8ScriptCov of v8ProcessCov.result) {
|
|
252
|
+
try {
|
|
253
|
+
const sources = this._getSourceMap(v8ScriptCov);
|
|
254
|
+
const path = resolve(this.resolve, v8ScriptCov.url);
|
|
255
|
+
const converter = v8toIstanbul(path, this.wrapperLength, sources, (path) => {
|
|
256
|
+
if (this.excludeAfterRemap)
|
|
257
|
+
return !this._shouldInstrument(path);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await converter.load();
|
|
261
|
+
|
|
262
|
+
if (resultCountPerPath.has(path))
|
|
263
|
+
resultCountPerPath.set(path, resultCountPerPath.get(path) + 1);
|
|
264
|
+
else
|
|
265
|
+
resultCountPerPath.set(path, 0);
|
|
266
|
+
|
|
267
|
+
converter.applyCoverage(v8ScriptCov.functions);
|
|
268
|
+
map.merge(converter.toIstanbul());
|
|
269
|
+
} catch(err) {
|
|
270
|
+
debuglog(`file: ${v8ScriptCov.url} error: ${err.stack}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this._allCoverageFiles = map;
|
|
275
|
+
return this._allCoverageFiles;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Returns source-map and fake source file, if cached during Node.js'
|
|
279
|
+
* execution. This is used to support tools like ts-node, which transpile
|
|
280
|
+
* using runtime hooks.
|
|
281
|
+
*
|
|
282
|
+
* Note: requires Node.js 13+
|
|
283
|
+
*
|
|
284
|
+
* @return {Object} sourceMap and fake source file (created from line #s).
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
_getSourceMap(v8ScriptCov) {
|
|
288
|
+
const sources = {};
|
|
289
|
+
const sourceMapAndLineLengths = this.sourceMapCache[pathToFileURL(v8ScriptCov.url).href];
|
|
290
|
+
|
|
291
|
+
if (sourceMapAndLineLengths) {
|
|
292
|
+
// See: https://github.com/nodejs/node/pull/34305
|
|
293
|
+
if (!sourceMapAndLineLengths.data)
|
|
294
|
+
return;
|
|
295
|
+
|
|
296
|
+
sources.sourceMap = {
|
|
297
|
+
sourcemap: sourceMapAndLineLengths.data,
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
if (sourceMapAndLineLengths.lineLengths) {
|
|
301
|
+
let source = '';
|
|
302
|
+
|
|
303
|
+
for (const length of sourceMapAndLineLengths.lineLengths) {
|
|
304
|
+
source += `${''.padEnd(length, '.')}\n`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
sources.source = source;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return sources;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Returns the merged V8 process coverage.
|
|
315
|
+
*
|
|
316
|
+
* The result is computed from the individual process coverages generated
|
|
317
|
+
* by Node. It represents the sum of their counts.
|
|
318
|
+
*
|
|
319
|
+
* @return {ProcessCov} Merged V8 process coverage.
|
|
320
|
+
* @private
|
|
321
|
+
*/
|
|
322
|
+
_getMergedProcessCov() {
|
|
323
|
+
const v8ProcessCovs = [];
|
|
324
|
+
const fileIndex = new Set();
|
|
325
|
+
|
|
326
|
+
// Set<string>
|
|
327
|
+
for (const v8ProcessCov of this._loadReports()) {
|
|
328
|
+
if (this._isCoverageObject(v8ProcessCov)) {
|
|
329
|
+
if (v8ProcessCov['source-map-cache'])
|
|
330
|
+
Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(v8ProcessCov['source-map-cache']));
|
|
331
|
+
|
|
332
|
+
v8ProcessCovs.push(this._normalizeProcessCov(
|
|
333
|
+
v8ProcessCov,
|
|
334
|
+
fileIndex,
|
|
335
|
+
));
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.all) {
|
|
340
|
+
const emptyReports = this._includeUncoveredFiles(fileIndex);
|
|
341
|
+
|
|
342
|
+
v8ProcessCovs.unshift({
|
|
343
|
+
result: emptyReports,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return mergeProcessCovs(v8ProcessCovs);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Returns the merged V8 process coverage.
|
|
351
|
+
*
|
|
352
|
+
* It asynchronously and incrementally reads and merges individual process coverages
|
|
353
|
+
* generated by Node. This can be used via the `--merge-async` CLI arg. It's intended
|
|
354
|
+
* to be used across a large multi-process test run.
|
|
355
|
+
*
|
|
356
|
+
* @return {ProcessCov} Merged V8 process coverage.
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
async _getMergedProcessCovAsync() {
|
|
360
|
+
const {mergeProcessCovs} = await import('@bcoe/v8-coverage');
|
|
361
|
+
const fileIndex = new Set(); // Set<string>
|
|
362
|
+
let mergedCov = null;
|
|
363
|
+
|
|
364
|
+
for (const file of readdirSync(this.tempDirectory)) {
|
|
365
|
+
try {
|
|
366
|
+
const rawFile = await readFile(resolve(this.tempDirectory, file), 'utf8');
|
|
367
|
+
|
|
368
|
+
let report = JSON.parse(rawFile);
|
|
369
|
+
|
|
370
|
+
if (this._isCoverageObject(report)) {
|
|
371
|
+
if (report['source-map-cache'])
|
|
372
|
+
Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(report['source-map-cache']));
|
|
373
|
+
|
|
374
|
+
report = this._normalizeProcessCov(report, fileIndex);
|
|
375
|
+
|
|
376
|
+
if (mergedCov)
|
|
377
|
+
mergedCov = mergeProcessCovs([mergedCov, report]);
|
|
378
|
+
else
|
|
379
|
+
mergedCov = mergeProcessCovs([report]);
|
|
380
|
+
}
|
|
381
|
+
} catch(err) {
|
|
382
|
+
debuglog(String(err.stack));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.all) {
|
|
387
|
+
const emptyReports = this._includeUncoveredFiles(fileIndex);
|
|
388
|
+
const emptyReport = {
|
|
389
|
+
result: emptyReports,
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
mergedCov = mergeProcessCovs([emptyReport, mergedCov]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return mergedCov;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Adds empty coverage reports to account for uncovered/untested code.
|
|
399
|
+
* This is only done when the `--all` flag is present.
|
|
400
|
+
*
|
|
401
|
+
* @param {Set} fileIndex list of files that have coverage
|
|
402
|
+
* @returns {Array} list of empty coverage reports
|
|
403
|
+
*/
|
|
404
|
+
_includeUncoveredFiles(fileIndex) {
|
|
405
|
+
const emptyReports = [];
|
|
406
|
+
const workingDirs = this.src;
|
|
407
|
+
const {extension} = this.exclude;
|
|
408
|
+
|
|
409
|
+
for (const workingDir of workingDirs) {
|
|
410
|
+
for (const f of this.exclude.globSync(workingDir)) {
|
|
411
|
+
const fullPath = resolve(workingDir, f);
|
|
412
|
+
|
|
413
|
+
if (!fileIndex.has(fullPath)) {
|
|
414
|
+
const ext = extname(fullPath);
|
|
415
|
+
|
|
416
|
+
if (extension.includes(ext)) {
|
|
417
|
+
const stat = statSync(fullPath);
|
|
418
|
+
const sourceMap = getSourceMapFromFile(fullPath);
|
|
419
|
+
|
|
420
|
+
if (sourceMap)
|
|
421
|
+
this.sourceMapCache[pathToFileURL(fullPath)] = {
|
|
422
|
+
data: sourceMap,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
emptyReports.push({
|
|
426
|
+
scriptId: 0,
|
|
427
|
+
url: resolve(fullPath),
|
|
428
|
+
functions: [{
|
|
429
|
+
functionName: '(empty-report)',
|
|
430
|
+
ranges: [{
|
|
431
|
+
startOffset: 0,
|
|
432
|
+
endOffset: stat.size,
|
|
433
|
+
count: 0,
|
|
434
|
+
}],
|
|
435
|
+
isBlockCoverage: true,
|
|
436
|
+
}],
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return emptyReports;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Make sure v8ProcessCov actually contains coverage information.
|
|
447
|
+
*
|
|
448
|
+
* @return {boolean} does it look like v8ProcessCov?
|
|
449
|
+
* @private
|
|
450
|
+
*/
|
|
451
|
+
_isCoverageObject(maybeV8ProcessCov) {
|
|
452
|
+
return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Returns the list of V8 process coverages generated by Node.
|
|
456
|
+
*
|
|
457
|
+
* @return {ProcessCov[]} Process coverages generated by Node.
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
_loadReports() {
|
|
461
|
+
const reports = [];
|
|
462
|
+
|
|
463
|
+
for (const file of readdirSync(this.tempDirectory)) {
|
|
464
|
+
try {
|
|
465
|
+
reports.push(JSON.parse(readFileSync(resolve(this.tempDirectory, file), 'utf8')));
|
|
466
|
+
} catch(err) {
|
|
467
|
+
debuglog(String(err.stack));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return reports;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Normalizes a process coverage.
|
|
475
|
+
*
|
|
476
|
+
* This function replaces file URLs (`url` property) by their corresponding
|
|
477
|
+
* system-dependent path and applies the current inclusion rules to filter out
|
|
478
|
+
* the excluded script coverages.
|
|
479
|
+
*
|
|
480
|
+
* The result is a copy of the input, with script coverages filtered based
|
|
481
|
+
* on their `url` and the current inclusion rules.
|
|
482
|
+
* There is no deep cloning.
|
|
483
|
+
*
|
|
484
|
+
* @param v8ProcessCov V8 process coverage to normalize.
|
|
485
|
+
* @param fileIndex a Set<string> of paths discovered in coverage
|
|
486
|
+
* @return {v8ProcessCov} Normalized V8 process coverage.
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
_normalizeProcessCov(v8ProcessCov, fileIndex) {
|
|
490
|
+
const result = [];
|
|
491
|
+
|
|
492
|
+
for (const v8ScriptCov of v8ProcessCov.result) {
|
|
493
|
+
// https://github.com/nodejs/node/pull/35498 updates Node.js'
|
|
494
|
+
// builtin module filenames:
|
|
495
|
+
if (v8ScriptCov.url.startsWith('node:'))
|
|
496
|
+
v8ScriptCov.url = `${v8ScriptCov.url.replace(/^node:/, '')}.js`;
|
|
497
|
+
|
|
498
|
+
if (v8ScriptCov.url.startsWith('file://'))
|
|
499
|
+
try {
|
|
500
|
+
v8ScriptCov.url = fileURLToPath(v8ScriptCov.url);
|
|
501
|
+
fileIndex.add(v8ScriptCov.url);
|
|
502
|
+
} catch(err) {
|
|
503
|
+
debuglog(String(err.stack));
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const shouldAddReport = !this.omitRelative
|
|
508
|
+
|| isAbsolute(v8ScriptCov.url)
|
|
509
|
+
&& this.excludeAfterRemap
|
|
510
|
+
|| this._shouldInstrument(v8ScriptCov.url);
|
|
511
|
+
|
|
512
|
+
if (shouldAddReport)
|
|
513
|
+
result.push(v8ScriptCov);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
result,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Normalizes a V8 source map cache.
|
|
522
|
+
*
|
|
523
|
+
* This function normalizes file URLs to a system-independent format.
|
|
524
|
+
*
|
|
525
|
+
* @param v8SourceMapCache V8 source map cache to normalize.
|
|
526
|
+
* @return {v8SourceMapCache} Normalized V8 source map cache.
|
|
527
|
+
* @private
|
|
528
|
+
*/
|
|
529
|
+
_normalizeSourceMapCache(v8SourceMapCache) {
|
|
530
|
+
const cache = {};
|
|
531
|
+
|
|
532
|
+
for (const fileURL of Object.keys(v8SourceMapCache)) {
|
|
533
|
+
cache[pathToFileURL(fileURLToPath(fileURL)).href] = v8SourceMapCache[fileURL];
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return cache;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* this.exclude.shouldInstrument with cache
|
|
540
|
+
*
|
|
541
|
+
* @private
|
|
542
|
+
* @return {boolean}
|
|
543
|
+
*/
|
|
544
|
+
_shouldInstrument(filename) {
|
|
545
|
+
const cacheResult = this.shouldInstrumentCache.get(filename);
|
|
546
|
+
|
|
547
|
+
if (!isUndefined(cacheResult))
|
|
548
|
+
return cacheResult;
|
|
549
|
+
|
|
550
|
+
const result = this.exclude.shouldInstrument(filename);
|
|
551
|
+
this.shouldInstrumentCache.set(filename, result);
|
|
552
|
+
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export const Report = (opts) => new ReportInstance(opts);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Node.js contributors. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to
|
|
6
|
+
* deal in the Software without restriction, including without limitation the
|
|
7
|
+
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
8
|
+
* sell copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in
|
|
12
|
+
* all copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
19
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
20
|
+
* IN THE SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
// TODO(bcoe): this logic is ported from Node.js' internal source map
|
|
23
|
+
// helpers:
|
|
24
|
+
// https://github.com/nodejs/node/blob/master/lib/internal/source_map/source_map_cache.js
|
|
25
|
+
// we should to upstream and downstream fixes.
|
|
26
|
+
import {readFileSync} from 'node:fs';
|
|
27
|
+
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
28
|
+
import util from 'node:util';
|
|
29
|
+
import {Buffer} from 'node:buffer';
|
|
30
|
+
|
|
31
|
+
const debuglog = util.debuglog('c8');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract the sourcemap url from a source file
|
|
35
|
+
* reference: https://sourcemaps.info/spec.html
|
|
36
|
+
* @param {String} file - compilation target file
|
|
37
|
+
* @returns {String} full path to source map file
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
function getSourceMapFromFile(filename) {
|
|
41
|
+
const fileBody = readFileSync(filename).toString();
|
|
42
|
+
const sourceMapLineRE = /\/[*/]#\s+sourceMappingURL=(?<sourceMappingURL>[^\s]+)/;
|
|
43
|
+
const results = fileBody.match(sourceMapLineRE);
|
|
44
|
+
|
|
45
|
+
if (results !== null) {
|
|
46
|
+
const {sourceMappingURL} = results.groups;
|
|
47
|
+
|
|
48
|
+
return dataFromUrl(pathToFileURL(filename), sourceMappingURL);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function dataFromUrl(sourceURL, sourceMappingURL) {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(sourceMappingURL);
|
|
57
|
+
|
|
58
|
+
switch(url.protocol) {
|
|
59
|
+
case 'data:':
|
|
60
|
+
return sourceMapFromDataUrl(url.pathname);
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
} catch(err) {
|
|
66
|
+
debuglog(err);
|
|
67
|
+
// If no scheme is present, we assume we are dealing with a file path.
|
|
68
|
+
const mapURL = new URL(sourceMappingURL, sourceURL).href;
|
|
69
|
+
|
|
70
|
+
return sourceMapFromFile(mapURL);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sourceMapFromFile(mapURL) {
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(fileURLToPath(mapURL), 'utf8');
|
|
77
|
+
return JSON.parse(content);
|
|
78
|
+
} catch(err) {
|
|
79
|
+
debuglog(err);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// data:[<mediatype>][;base64],<data> see:
|
|
85
|
+
// https://tools.ietf.org/html/rfc2397#section-2
|
|
86
|
+
function sourceMapFromDataUrl(url) {
|
|
87
|
+
const [format, data] = url.split(',');
|
|
88
|
+
const splitFormat = format.split(';');
|
|
89
|
+
const [contentType] = splitFormat;
|
|
90
|
+
const base64 = splitFormat.at(-1) === 'base64';
|
|
91
|
+
|
|
92
|
+
if (contentType === 'application/json') {
|
|
93
|
+
const decodedData = base64 ? Buffer
|
|
94
|
+
.from(data, 'base64')
|
|
95
|
+
.toString('utf8') : data;
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(decodedData);
|
|
99
|
+
} catch(err) {
|
|
100
|
+
debuglog(err);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
debuglog(`unexpected content-type ${contentType}`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default getSourceMapFromFile;
|