mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
195 lines
5.8 KiB
JavaScript
195 lines
5.8 KiB
JavaScript
/*
|
|
* Copyright © 2025-26 l5yth & contributors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import istanbulLibCoverage from 'istanbul-lib-coverage';
|
|
import istanbulLibReport from 'istanbul-lib-report';
|
|
import istanbulReports from 'istanbul-reports';
|
|
import v8toIstanbul from 'v8-to-istanbul';
|
|
|
|
const { createCoverageMap } = istanbulLibCoverage;
|
|
const { createContext } = istanbulLibReport;
|
|
|
|
const coverageDir = path.resolve('coverage');
|
|
const reportsDir = path.resolve('reports');
|
|
const jsonOutputName = 'javascript-coverage.json';
|
|
const lcovOutputName = 'javascript-coverage.lcov';
|
|
const projectRoot = process.cwd();
|
|
|
|
/**
|
|
* Ensure the reports directory exists so that coverage artefacts can be written.
|
|
*
|
|
* @returns {Promise<void>} A promise that resolves when the directory is available.
|
|
*/
|
|
async function ensureReportsDir() {
|
|
try {
|
|
await fs.mkdir(reportsDir, { recursive: true });
|
|
} catch (error) {
|
|
console.error('Failed to ensure reports directory', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read the coverage directory and return a deterministically ordered list of JSON files.
|
|
*
|
|
* @returns {Promise<string[]>} The absolute paths of available coverage JSON artefacts.
|
|
*/
|
|
async function listCoverageFiles() {
|
|
let entries;
|
|
try {
|
|
entries = await fs.readdir(coverageDir);
|
|
} catch (error) {
|
|
if (error.code === 'ENOENT') {
|
|
console.warn('Coverage directory not found; skipping export.');
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const coverageFiles = entries
|
|
.filter(name => name.endsWith('.json'))
|
|
.map(name => path.join(coverageDir, name))
|
|
.sort();
|
|
|
|
if (!coverageFiles.length) {
|
|
console.warn('No coverage files generated; skipping export.');
|
|
return [];
|
|
}
|
|
|
|
return coverageFiles;
|
|
}
|
|
|
|
/**
|
|
* Convert a V8 coverage URL to a project-local filesystem path.
|
|
*
|
|
* @param {string | undefined} url The coverage URL emitted by V8.
|
|
* @returns {string | null} A normalised absolute path, or null when the URL should be ignored.
|
|
*/
|
|
function normaliseFileUrl(url) {
|
|
if (!url || url.startsWith('node:')) {
|
|
return null;
|
|
}
|
|
|
|
if (!url.startsWith('file://')) {
|
|
return null;
|
|
}
|
|
|
|
let filePath;
|
|
try {
|
|
filePath = decodeURIComponent(new URL(url).pathname);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
if (!filePath.startsWith(projectRoot)) {
|
|
return null;
|
|
}
|
|
|
|
if (filePath.includes('node_modules')) {
|
|
return null;
|
|
}
|
|
|
|
return filePath;
|
|
}
|
|
|
|
/**
|
|
* Transform the raw V8 coverage reports into an Istanbul coverage map.
|
|
*
|
|
* @param {string[]} coverageFiles A list of coverage artefacts to consume.
|
|
* @returns {Promise<import('istanbul-lib-coverage').CoverageMap>} The aggregated coverage map.
|
|
*/
|
|
async function buildCoverageMap(coverageFiles) {
|
|
const coverageMap = createCoverageMap({});
|
|
|
|
for (const file of coverageFiles) {
|
|
const raw = await fs.readFile(file, 'utf8');
|
|
const parsed = JSON.parse(raw);
|
|
const entries = Array.isArray(parsed.result) ? parsed.result : [];
|
|
|
|
for (const entry of entries) {
|
|
const { url, functions } = entry;
|
|
const filePath = normaliseFileUrl(url);
|
|
if (!filePath) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const converter = v8toIstanbul(filePath, 0, {
|
|
source: await fs.readFile(filePath, 'utf8'),
|
|
});
|
|
await converter.load();
|
|
converter.applyCoverage(functions);
|
|
const fileCoverages = converter.toIstanbul();
|
|
for (const coverage of Object.values(fileCoverages)) {
|
|
if (coverage.path) {
|
|
const relativePath = path.relative(projectRoot, coverage.path);
|
|
coverage.path = relativePath || coverage.path;
|
|
}
|
|
try {
|
|
const existingCoverage = coverageMap.fileCoverageFor(coverage.path);
|
|
existingCoverage.merge(coverage);
|
|
} catch (error) {
|
|
if (error && typeof error.message === 'string' && error.message.includes('No file coverage')) {
|
|
coverageMap.addFileCoverage(coverage);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Failed to translate coverage for ${filePath}:`, error);
|
|
}
|
|
}
|
|
}
|
|
|
|
return coverageMap;
|
|
}
|
|
|
|
/**
|
|
* Persist the Istanbul coverage map as JSON and LCOV artefacts for downstream tooling.
|
|
*
|
|
* @param {import('istanbul-lib-coverage').CoverageMap} coverageMap The populated coverage map.
|
|
* @returns {Promise<void>} A promise that resolves when the outputs are written.
|
|
*/
|
|
async function writeCoverageOutputs(coverageMap) {
|
|
const jsonOutputPath = path.join(reportsDir, jsonOutputName);
|
|
const lcovOutputPath = path.join(reportsDir, lcovOutputName);
|
|
|
|
await fs.writeFile(jsonOutputPath, `${JSON.stringify(coverageMap.toJSON(), null, 2)}\n`);
|
|
|
|
const context = createContext({ dir: reportsDir, coverageMap });
|
|
istanbulReports.create('lcovonly', { file: lcovOutputName }).execute(context);
|
|
|
|
console.log(`Wrote coverage reports to ${jsonOutputPath} and ${lcovOutputPath}`);
|
|
}
|
|
|
|
await ensureReportsDir();
|
|
const coverageFiles = await listCoverageFiles();
|
|
if (!coverageFiles.length) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const coverageMap = await buildCoverageMap(coverageFiles);
|
|
if (!coverageMap.files().length) {
|
|
console.warn('No project coverage entries were recognised; skipping export.');
|
|
process.exit(0);
|
|
}
|
|
|
|
await writeCoverageOutputs(coverageMap);
|