UNPKG

@vitest/coverage-c8

Version:
264 lines (260 loc) 8.74 kB
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 };