@vitest/coverage-v8
Advanced tools
Comparing version 0.32.4 to 1.0.0-beta.6
@@ -13,2 +13,3 @@ import { Profiler } from 'node:inspector'; | ||
excludeNodeModules?: boolean; | ||
relativePath?: boolean; | ||
}): { | ||
@@ -20,2 +21,6 @@ shouldInstrument(filePath: string): boolean; | ||
type Options = ResolvedCoverageOptions<'v8'>; | ||
type RawCoverage = Profiler.TakePreciseCoverageReturnType; | ||
type CoverageByTransformMode = Record<AfterSuiteRunMeta['transformMode'], RawCoverage[]>; | ||
type ProjectName = NonNullable<AfterSuiteRunMeta['projectName']> | typeof DEFAULT_PROJECT; | ||
declare const DEFAULT_PROJECT: unique symbol; | ||
declare class V8CoverageProvider extends BaseCoverageProvider implements CoverageProvider { | ||
@@ -26,12 +31,13 @@ name: string; | ||
testExclude: InstanceType<TestExclude>; | ||
coverages: Profiler.TakePreciseCoverageReturnType[]; | ||
coverages: Map<ProjectName, CoverageByTransformMode>; | ||
initialize(ctx: Vitest): void; | ||
resolveOptions(): Options; | ||
clean(clean?: boolean): Promise<void>; | ||
onAfterSuiteRun({ coverage }: AfterSuiteRunMeta): void; | ||
onAfterSuiteRun({ coverage, transformMode, projectName }: AfterSuiteRunMeta): void; | ||
reportCoverage({ allTestsRun }?: ReportContext): Promise<void>; | ||
private getUntestedFiles; | ||
private getSources; | ||
private mergeAndTransformCoverage; | ||
} | ||
export { V8CoverageProvider }; |
@@ -1,3 +0,3 @@ | ||
import { existsSync, promises } from 'fs'; | ||
import { fileURLToPath, pathToFileURL } from 'url'; | ||
import { existsSync, promises, writeFileSync } from 'node:fs'; | ||
import { pathToFileURL, fileURLToPath } from 'node:url'; | ||
import v8ToIstanbul from 'v8-to-istanbul'; | ||
@@ -10,5 +10,7 @@ import { mergeProcessCovs } from '@bcoe/v8-coverage'; | ||
import MagicString from 'magic-string'; | ||
import { parseModule } from 'magicast'; | ||
import remapping from '@ampproject/remapping'; | ||
import c from 'picocolors'; | ||
import { provider } from 'std-env'; | ||
import { builtinModules } from 'node:module'; | ||
import { coverageConfigDefaults, defaultExclude, defaultInclude } from 'vitest/config'; | ||
@@ -144,4 +146,30 @@ import { BaseCoverageProvider } from 'vitest/coverage'; | ||
const isWindows = process.platform === "win32"; | ||
const drive = isWindows ? process.cwd()[0] : null; | ||
drive ? drive === drive.toUpperCase() ? drive.toLowerCase() : drive.toUpperCase() : null; | ||
const queryRE = /\?.*$/s; | ||
const hashRE = /#.*$/s; | ||
function cleanUrl(url) { | ||
return url.replace(hashRE, "").replace(queryRE, ""); | ||
} | ||
/* @__PURE__ */ new Set([ | ||
...builtinModules, | ||
"assert/strict", | ||
"diagnostics_channel", | ||
"dns/promises", | ||
"fs/promises", | ||
"path/posix", | ||
"path/win32", | ||
"readline/promises", | ||
"stream/consumers", | ||
"stream/promises", | ||
"stream/web", | ||
"timers/promises", | ||
"util/types", | ||
"wasi" | ||
]); | ||
const WRAPPER_LENGTH = 185; | ||
const VITE_EXPORTS_LINE_PATTERN = /Object\.defineProperty\(__vite_ssr_exports__.*\n/g; | ||
const DEFAULT_PROJECT = Symbol.for("default-project"); | ||
class V8CoverageProvider extends BaseCoverageProvider { | ||
@@ -152,3 +180,3 @@ name = "v8"; | ||
testExclude; | ||
coverages = []; | ||
coverages = /* @__PURE__ */ new Map(); | ||
initialize(ctx) { | ||
@@ -165,6 +193,9 @@ const config = ctx.config.coverage; | ||
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 | ||
thresholds: config.thresholds && { | ||
...config.thresholds, | ||
lines: config.thresholds["100"] ? 100 : config.thresholds.lines, | ||
branches: config.thresholds["100"] ? 100 : config.thresholds.branches, | ||
functions: config.thresholds["100"] ? 100 : config.thresholds.functions, | ||
statements: config.thresholds["100"] ? 100 : config.thresholds.statements | ||
} | ||
}; | ||
@@ -176,3 +207,4 @@ this.testExclude = new _TestExclude({ | ||
excludeNodeModules: true, | ||
extension: this.options.extension | ||
extension: this.options.extension, | ||
relativePath: !this.options.allowExternal | ||
}); | ||
@@ -186,6 +218,18 @@ } | ||
await promises.rm(this.options.reportsDirectory, { recursive: true, force: true, maxRetries: 10 }); | ||
this.coverages = []; | ||
this.coverages = /* @__PURE__ */ new Map(); | ||
} | ||
onAfterSuiteRun({ coverage }) { | ||
this.coverages.push(coverage); | ||
/* | ||
* Coverage and meta information passed from Vitest runners. | ||
* Note that adding new entries here and requiring on those without | ||
* backwards compatibility is a breaking change. | ||
*/ | ||
onAfterSuiteRun({ coverage, transformMode, projectName }) { | ||
if (transformMode !== "web" && transformMode !== "ssr") | ||
throw new Error(`Invalid transform mode: ${transformMode}`); | ||
let entry = this.coverages.get(projectName || DEFAULT_PROJECT); | ||
if (!entry) { | ||
entry = { web: [], ssr: [] }; | ||
this.coverages.set(projectName || DEFAULT_PROJECT, entry); | ||
} | ||
entry[transformMode].push(coverage); | ||
} | ||
@@ -195,30 +239,22 @@ async reportCoverage({ allTestsRun } = {}) { | ||
this.ctx.logger.log(c.blue(" % ") + c.yellow("@vitest/coverage-v8 does not work on Stackblitz. Report will be empty.")); | ||
const merged = mergeProcessCovs(this.coverages); | ||
const scriptCoverages = merged.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url))); | ||
const coverageMaps = await Promise.all( | ||
Array.from(this.coverages.entries()).map(([projectName, coverages]) => [ | ||
this.mergeAndTransformCoverage(coverages.ssr, projectName, "ssr"), | ||
this.mergeAndTransformCoverage(coverages.web, projectName, "web") | ||
]).flat() | ||
); | ||
if (this.options.all && allTestsRun) { | ||
const coveredFiles = Array.from(scriptCoverages.map((r) => r.url)); | ||
const untestedFiles = await this.getUntestedFiles(coveredFiles); | ||
scriptCoverages.push(...untestedFiles); | ||
const coveredFiles = coverageMaps.map((map) => map.files()).flat(); | ||
const untestedCoverage = await this.getUntestedFiles(coveredFiles); | ||
const untestedCoverageResults = untestedCoverage.map((files) => ({ result: [files] })); | ||
coverageMaps.push(await this.mergeAndTransformCoverage(untestedCoverageResults)); | ||
} | ||
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { | ||
const sources = await this.getSources(url, functions); | ||
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0; | ||
const converter = v8ToIstanbul(url, wrapperLength, sources); | ||
await converter.load(); | ||
converter.applyCoverage(functions); | ||
return converter.toIstanbul(); | ||
})); | ||
const mergedCoverage = converted.reduce((coverage, previousCoverageMap) => { | ||
const map = libCoverage.createCoverageMap(coverage); | ||
map.merge(previousCoverageMap); | ||
return map; | ||
}, libCoverage.createCoverageMap({})); | ||
const sourceMapStore = libSourceMaps.createSourceMapStore(); | ||
const coverageMap = await sourceMapStore.transformCoverage(mergedCoverage); | ||
const coverageMap = mergeCoverageMaps(...coverageMaps); | ||
const context = libReport.createContext({ | ||
dir: this.options.reportsDirectory, | ||
coverageMap, | ||
sourceFinder: sourceMapStore.sourceFinder, | ||
watermarks: this.options.watermarks | ||
}); | ||
if (hasTerminalReporter(this.options.reporter)) | ||
this.ctx.logger.log(c.blue(" % ") + c.dim("Coverage report from ") + c.yellow(this.name)); | ||
for (const reporter of this.options.reporter) { | ||
@@ -231,33 +267,34 @@ reports.create(reporter[0], { | ||
} | ||
if (this.options.branches || this.options.functions || this.options.lines || this.options.statements) { | ||
this.checkThresholds({ | ||
if (this.options.thresholds) { | ||
const resolvedThresholds = this.resolveThresholds({ | ||
coverageMap, | ||
thresholds: { | ||
branches: this.options.branches, | ||
functions: this.options.functions, | ||
lines: this.options.lines, | ||
statements: this.options.statements | ||
}, | ||
perFile: this.options.perFile | ||
thresholds: this.options.thresholds, | ||
createCoverageMap: () => libCoverage.createCoverageMap({}) | ||
}); | ||
} | ||
if (this.options.thresholdAutoUpdate && allTestsRun) { | ||
this.updateThresholds({ | ||
coverageMap, | ||
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 | ||
this.checkThresholds({ | ||
thresholds: resolvedThresholds, | ||
perFile: this.options.thresholds.perFile | ||
}); | ||
if (this.options.thresholds.autoUpdate && allTestsRun) { | ||
if (!this.ctx.server.config.configFile) | ||
throw new Error('Missing configurationFile. The "coverage.thresholds.autoUpdate" can only be enabled when configuration file is used.'); | ||
const configFilePath = this.ctx.server.config.configFile; | ||
const configModule = parseModule(await promises.readFile(configFilePath, "utf8")); | ||
this.updateThresholds({ | ||
thresholds: resolvedThresholds, | ||
perFile: this.options.thresholds.perFile, | ||
configurationFile: { | ||
write: () => writeFileSync(configFilePath, configModule.generate().code, "utf-8"), | ||
read: () => configModule.exports.default.$type === "function-call" ? configModule.exports.default.$args[0] : configModule.exports.default | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
async getUntestedFiles(testedFiles) { | ||
const transformResults = normalizeTransformResults(this.ctx.vitenode.fetchCache); | ||
const includedFiles = await this.testExclude.glob(this.ctx.config.root); | ||
const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.href)); | ||
const uncoveredFiles = includedFiles.map((file) => pathToFileURL(resolve(this.ctx.config.root, file))).filter((file) => !testedFiles.includes(file.pathname)); | ||
return await Promise.all(uncoveredFiles.map(async (uncoveredFile) => { | ||
const { source } = await this.getSources(uncoveredFile.href); | ||
const { source } = await this.getSources(uncoveredFile.href, transformResults); | ||
return { | ||
@@ -280,12 +317,8 @@ url: uncoveredFile.href, | ||
} | ||
async getSources(url, functions = []) { | ||
var _a; | ||
async getSources(url, transformResults, functions = []) { | ||
const filePath = normalize(fileURLToPath(url)); | ||
const transformResult = this.ctx.projects.map((project) => { | ||
var _a2; | ||
return (_a2 = project.vitenode.fetchCache.get(filePath)) == null ? void 0 : _a2.result; | ||
}).filter(Boolean).shift(); | ||
const map = transformResult == null ? void 0 : transformResult.map; | ||
const code = transformResult == null ? void 0 : transformResult.code; | ||
const sourcesContent = ((_a = map == null ? void 0 : map.sourcesContent) == null ? void 0 : _a[0]) || await promises.readFile(filePath, "utf-8").catch(() => { | ||
const transformResult = transformResults.get(filePath); | ||
const map = transformResult?.map; | ||
const code = transformResult?.code; | ||
const sourcesContent = map?.sourcesContent?.[0] || await promises.readFile(filePath, "utf-8").catch(() => { | ||
const length = findLongestFunctionLength(functions); | ||
@@ -309,3 +342,28 @@ return ".".repeat(length); | ||
} | ||
async mergeAndTransformCoverage(coverages, projectName, transformMode) { | ||
const viteNode = this.ctx.projects.find((project) => project.getName() === projectName)?.vitenode || this.ctx.vitenode; | ||
const fetchCache = transformMode ? viteNode.fetchCaches[transformMode] : viteNode.fetchCache; | ||
const transformResults = normalizeTransformResults(fetchCache); | ||
const merged = mergeProcessCovs(coverages); | ||
const scriptCoverages = merged.result.filter((result) => this.testExclude.shouldInstrument(fileURLToPath(result.url))); | ||
const converted = await Promise.all(scriptCoverages.map(async ({ url, functions }) => { | ||
const sources = await this.getSources(url, transformResults, functions); | ||
const wrapperLength = sources.sourceMap ? WRAPPER_LENGTH : 0; | ||
const converter = v8ToIstanbul(url, wrapperLength, sources); | ||
await converter.load(); | ||
converter.applyCoverage(functions); | ||
return converter.toIstanbul(); | ||
})); | ||
const mergedCoverage = mergeCoverageMaps(...converted); | ||
const sourceMapStore = libSourceMaps.createSourceMapStore(); | ||
return sourceMapStore.transformCoverage(mergedCoverage); | ||
} | ||
} | ||
function mergeCoverageMaps(...coverageMaps) { | ||
return coverageMaps.reduce((coverage, previousCoverageMap) => { | ||
const map = libCoverage.createCoverageMap(coverage); | ||
map.merge(previousCoverageMap); | ||
return map; | ||
}, libCoverage.createCoverageMap({})); | ||
} | ||
function removeViteHelpersFromSourceMaps(source, map) { | ||
@@ -317,3 +375,3 @@ if (!source || !source.match(VITE_EXPORTS_LINE_PATTERN)) | ||
const mapWithoutHelpers = sourceWithoutHelpers.generateMap({ | ||
hires: true | ||
hires: "boundary" | ||
}); | ||
@@ -332,3 +390,15 @@ const combinedMap = remapping( | ||
} | ||
function normalizeTransformResults(fetchCache) { | ||
const normalized = /* @__PURE__ */ new Map(); | ||
for (const [key, value] of fetchCache.entries()) { | ||
const cleanEntry = cleanUrl(key); | ||
if (!normalized.has(cleanEntry)) | ||
normalized.set(cleanEntry, value.result); | ||
} | ||
return normalized; | ||
} | ||
function hasTerminalReporter(reporters) { | ||
return reporters.some(([reporter]) => reporter === "text" || reporter === "text-summary" || reporter === "text-lcov" || reporter === "teamcity"); | ||
} | ||
export { V8CoverageProvider }; |
{ | ||
"name": "@vitest/coverage-v8", | ||
"type": "module", | ||
"version": "0.32.4", | ||
"version": "1.0.0-beta.6", | ||
"description": "V8 coverage provider for Vitest", | ||
@@ -29,3 +29,3 @@ "author": "Anthony Fu <anthonyfu117@hotmail.com>", | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.js" | ||
"default": "./dist/index.js" | ||
}, | ||
@@ -41,3 +41,3 @@ "./*": "./*" | ||
"peerDependencies": { | ||
"vitest": ">=0.32.0 <1" | ||
"vitest": "^1.0.0-0" | ||
}, | ||
@@ -47,20 +47,21 @@ "dependencies": { | ||
"@bcoe/v8-coverage": "^0.2.3", | ||
"istanbul-lib-coverage": "^3.2.0", | ||
"istanbul-lib-report": "^3.0.0", | ||
"istanbul-lib-coverage": "^3.2.2", | ||
"istanbul-lib-report": "^3.0.1", | ||
"istanbul-lib-source-maps": "^4.0.1", | ||
"istanbul-reports": "^3.1.5", | ||
"magic-string": "^0.30.0", | ||
"istanbul-reports": "^3.1.6", | ||
"magic-string": "^0.30.5", | ||
"magicast": "^0.3.2", | ||
"picocolors": "^1.0.0", | ||
"std-env": "^3.3.3", | ||
"std-env": "^3.5.0", | ||
"test-exclude": "^6.0.0", | ||
"v8-to-istanbul": "^9.1.0" | ||
"v8-to-istanbul": "^9.2.0" | ||
}, | ||
"devDependencies": { | ||
"@types/istanbul-lib-coverage": "^2.0.4", | ||
"@types/istanbul-lib-report": "^3.0.0", | ||
"@types/istanbul-lib-source-maps": "^4.0.1", | ||
"@types/istanbul-reports": "^3.0.1", | ||
"@types/istanbul-lib-coverage": "^2.0.6", | ||
"@types/istanbul-lib-report": "^3.0.3", | ||
"@types/istanbul-lib-source-maps": "^4.0.4", | ||
"@types/istanbul-reports": "^3.0.4", | ||
"pathe": "^1.1.1", | ||
"vitest": "0.32.4", | ||
"vite-node": "0.32.4" | ||
"vitest": "1.0.0-beta.6", | ||
"vite-node": "1.0.0-beta.6" | ||
}, | ||
@@ -67,0 +68,0 @@ "scripts": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
21122
487
1
13
6
+ Addedmagicast@^0.3.2
+ Added@babel/helper-string-parser@7.24.8(transitive)
+ Added@babel/helper-validator-identifier@7.24.7(transitive)
+ Added@babel/parser@7.25.6(transitive)
+ Added@babel/types@7.25.6(transitive)
+ Added@vitest/expect@1.6.0(transitive)
+ Added@vitest/runner@1.6.0(transitive)
+ Added@vitest/snapshot@1.6.0(transitive)
+ Added@vitest/spy@1.6.0(transitive)
+ Added@vitest/utils@1.6.0(transitive)
+ Addedcross-spawn@7.0.3(transitive)
+ Addedestree-walker@3.0.3(transitive)
+ Addedexeca@8.0.1(transitive)
+ Addedget-stream@8.0.1(transitive)
+ Addedhuman-signals@5.0.0(transitive)
+ Addedis-stream@3.0.0(transitive)
+ Addedisexe@2.0.0(transitive)
+ Addedjs-tokens@9.0.0(transitive)
+ Addedlocal-pkg@0.5.0(transitive)
+ Addedmagicast@0.3.5(transitive)
+ Addedmerge-stream@2.0.0(transitive)
+ Addedmimic-fn@4.0.0(transitive)
+ Addednpm-run-path@5.3.0(transitive)
+ Addedonetime@6.0.0(transitive)
+ Addedp-limit@5.0.0(transitive)
+ Addedpath-key@3.1.14.0.0(transitive)
+ Addedshebang-command@2.0.0(transitive)
+ Addedshebang-regex@3.0.0(transitive)
+ Addedsignal-exit@4.1.0(transitive)
+ Addedstrip-final-newline@3.0.0(transitive)
+ Addedstrip-literal@2.1.0(transitive)
+ Addedtinypool@0.8.4(transitive)
+ Addedto-fast-properties@2.0.0(transitive)
+ Addedvite-node@1.6.0(transitive)
+ Addedvitest@1.6.0(transitive)
+ Addedwhich@2.0.2(transitive)
- Removed@types/chai@4.3.19(transitive)
- Removed@types/chai-subset@1.3.5(transitive)
- Removed@types/node@22.5.5(transitive)
- Removed@vitest/expect@0.34.6(transitive)
- Removed@vitest/runner@0.34.6(transitive)
- Removed@vitest/snapshot@0.34.6(transitive)
- Removed@vitest/spy@0.34.6(transitive)
- Removed@vitest/utils@0.34.6(transitive)
- Removedlocal-pkg@0.4.3(transitive)
- Removedp-limit@4.0.0(transitive)
- Removedstrip-literal@1.3.0(transitive)
- Removedtinypool@0.7.0(transitive)
- Removedundici-types@6.19.8(transitive)
- Removedvite-node@0.34.6(transitive)
- Removedvitest@0.34.6(transitive)
Updatedistanbul-lib-coverage@^3.2.2
Updatedistanbul-lib-report@^3.0.1
Updatedistanbul-reports@^3.1.6
Updatedmagic-string@^0.30.5
Updatedstd-env@^3.5.0
Updatedv8-to-istanbul@^9.2.0