@vitest/coverage-c8
Version:
C8 coverage provider for Vitest
264 lines (260 loc) • 8.74 kB
JavaScript
import { existsSync, promises } from 'fs';
import _url from 'url';
import MagicString from 'magic-string';
import remapping from '@ampproject/remapping';
import c from 'picocolors';
import { provider } from 'std-env';
import { coverageConfigDefaults } from 'vitest/config';
import { BaseCoverageProvider } from 'vitest/coverage';
import createReport from 'c8/lib/report.js';
import { checkCoverages } from 'c8/lib/commands/check-coverage.js';
function normalizeWindowsPath(input = "") {
if (!input || !input.includes("\\")) {
return input;
}
return input.replace(/\\/g, "/");
}
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
function cwd() {
if (typeof process !== "undefined") {
return process.cwd().replace(/\\/g, "/");
}
return "/";
}
const resolve = function(...arguments_) {
arguments_ = arguments_.map((argument) => normalizeWindowsPath(argument));
let resolvedPath = "";
let resolvedAbsolute = false;
for (let index = arguments_.length - 1; index >= -1 && !resolvedAbsolute; index--) {
const path = index >= 0 ? arguments_[index] : cwd();
if (!path || path.length === 0) {
continue;
}
resolvedPath = `${path}/${resolvedPath}`;
resolvedAbsolute = isAbsolute(path);
}
resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute);
if (resolvedAbsolute && !isAbsolute(resolvedPath)) {
return `/${resolvedPath}`;
}
return resolvedPath.length > 0 ? resolvedPath : ".";
};
function normalizeString(path, allowAboveRoot) {
let res = "";
let lastSegmentLength = 0;
let lastSlash = -1;
let dots = 0;
let char = null;
for (let index = 0; index <= path.length; ++index) {
if (index < path.length) {
char = path[index];
} else if (char === "/") {
break;
} else {
char = "/";
}
if (char === "/") {
if (lastSlash === index - 1 || dots === 1) ; else if (dots === 2) {
if (res.length < 2 || lastSegmentLength !== 2 || res[res.length - 1] !== "." || res[res.length - 2] !== ".") {
if (res.length > 2) {
const lastSlashIndex = res.lastIndexOf("/");
if (lastSlashIndex === -1) {
res = "";
lastSegmentLength = 0;
} else {
res = res.slice(0, lastSlashIndex);
lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
}
lastSlash = index;
dots = 0;
continue;
} else if (res.length > 0) {
res = "";
lastSegmentLength = 0;
lastSlash = index;
dots = 0;
continue;
}
}
if (allowAboveRoot) {
res += res.length > 0 ? "/.." : "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0) {
res += `/${path.slice(lastSlash + 1, index)}`;
} else {
res = path.slice(lastSlash + 1, index);
}
lastSegmentLength = index - lastSlash - 1;
}
lastSlash = index;
dots = 0;
} else if (char === "." && dots !== -1) {
++dots;
} else {
dots = -1;
}
}
return res;
}
const isAbsolute = function(p) {
return _IS_ABSOLUTE_RE.test(p);
};
const _EXTNAME_RE = /.(\.[^./]+)$/;
const extname = function(p) {
const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
return match && match[1] || "";
};
const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g;
class C8CoverageProvider extends BaseCoverageProvider {
name = "c8";
ctx;
options;
coverages = [];
initialize(ctx) {
const config = ctx.config.coverage;
this.ctx = ctx;
this.options = {
...coverageConfigDefaults,
// Provider specific defaults
excludeNodeModules: true,
allowExternal: false,
// User's options
...config,
// Resolved fields
provider: "c8",
reporter: this.resolveReporters(config.reporter || coverageConfigDefaults.reporter),
reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory),
lines: config["100"] ? 100 : config.lines,
functions: config["100"] ? 100 : config.functions,
branches: config["100"] ? 100 : config.branches,
statements: config["100"] ? 100 : config.statements
};
const banner = " DEPRECATION ";
this.ctx.logger.log(
c.bgYellow(c.black(banner)),
c.yellow("@vitest/coverage-c8 is being replaced by @vitest/coverage-v8."),
c.yellow(`
${" ".repeat(banner.length)} See`),
c.blue(c.underline("https://github.com/vitest-dev/vitest/pull/3339")),
c.yellow("for more information.")
);
}
resolveOptions() {
return this.options;
}
async clean(clean = true) {
if (clean && existsSync(this.options.reportsDirectory))
await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 });
this.coverages = [];
}
onAfterSuiteRun({ coverage }) {
this.coverages.push(coverage);
}
async reportCoverage({ allTestsRun } = {}) {
if (provider === "stackblitz")
this.ctx.logger.log(c.blue(" % ") + c.yellow("@vitest/coverage-c8 does not work on Stackblitz. Report will be empty."));
const options = {
...this.options,
all: this.options.all && allTestsRun,
reporter: this.options.reporter.map(([reporterName]) => reporterName),
reporterOptions: this.options.reporter.reduce((all, [name, options2]) => ({
...all,
[name]: {
skipFull: this.options.skipFull,
projectRoot: this.ctx.config.root,
...options2
}
}), {})
};
const report = createReport(options);
report._loadReports = () => this.coverages;
const sourceMapMeta = {};
const extensions = Array.isArray(this.options.extension) ? this.options.extension : [this.options.extension];
const fetchCache = this.ctx.projects.map(
(project) => Array.from(project.vitenode.fetchCache.entries())
).flat();
const entries = Array.from(fetchCache).filter((entry) => report._shouldInstrument(entry[0])).map(([file, { result }]) => {
if (!result.map)
return null;
const filepath = file.split("?")[0];
const url = _url.pathToFileURL(filepath).href;
const extension = extname(file) || extname(url);
return {
filepath,
url,
extension,
map: result.map,
source: result.code
};
}).filter((entry) => {
if (!entry)
return false;
if (!extensions.includes(entry.extension))
return false;
return entry.map.mappings.length > 0 && entry.map.sourcesContent && entry.map.sourcesContent.length > 0 && entry.map.sourcesContent[0] && entry.map.sourcesContent[0].length > 0;
});
await Promise.all(entries.map(async ({ url, source, map, filepath }) => {
if (url in sourceMapMeta)
return;
let code;
try {
code = (await promises.readFile(filepath)).toString();
} catch {
}
const sources = [url];
sourceMapMeta[url] = {
source,
map: {
sourcesContent: code ? [code] : void 0,
...map,
sources
}
};
}));
const offset = 185;
report._getSourceMap = (coverage) => {
const path = _url.pathToFileURL(coverage.url.split("?")[0]).href;
const data = sourceMapMeta[path];
if (!data)
return {};
return {
sourceMap: {
sourcemap: removeViteHelpersFromSourceMaps(data.source, data.map)
},
source: Array(offset).fill(".").join("") + data.source
};
};
await report.run();
await checkCoverages(options, report);
if (this.options.thresholdAutoUpdate && allTestsRun) {
this.updateThresholds({
coverageMap: await report.getCoverageMapFromAllCoverageFiles(),
thresholds: {
branches: this.options.branches,
functions: this.options.functions,
lines: this.options.lines,
statements: this.options.statements
},
perFile: this.options.perFile,
configurationFile: this.ctx.server.config.configFile
});
}
}
}
function removeViteHelpersFromSourceMaps(source, map) {
if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN))
return map;
const sourceWithoutHelpers = new MagicString(source);
sourceWithoutHelpers.replaceAll(VITE_EXPORTS_LINE_PATTERN, "\n");
const mapWithoutHelpers = sourceWithoutHelpers.generateMap({
hires: true
});
const combinedMap = remapping(
[{ ...mapWithoutHelpers, version: 3 }, map],
() => null
);
return combinedMap;
}
export { C8CoverageProvider };