UNPKG

coveradge

Version:

Generate coverage badges during local nyc/istanbul execution

232 lines (208 loc) 6.88 kB
'use strict'; // Tried promisified async methods but were puzzlingly not working const {writeFileSync, unlinkSync} = require('fs'); const {resolve: pathResolve} = require('path'); const badgeUp = require('@rpl/badge-up').v2; const es6Templates = require('es6-template-strings'); const {loadNycConfig} = require('@istanbuljs/load-nyc-config'); /** * @param {CoveradgeOptions} cfg * @throws {TypeError} * @returns {void} */ async function coveradge (cfg) { const { format = 'svg', passColor = 'green,s{black}', failColor = 'orange,s{black}', mediumColor = 'CCCC00,s{black}', introTemplate = '', introColor = 'navy', aggregateConditions = false, output = 'coverage-badge', logging = 'off', coveragePath = './coverage/coverage-summary.json', // eslint-disable-next-line no-template-curly-in-string -- User templates textTemplate = '${condition} ${conditionPct}%', // `coverageSummary` is not available by CLI but gets // default (could allow JSON string, but probably not worth it) // eslint-disable-next-line max-len -- Long // eslint-disable-next-line n/global-require, import/no-dynamic-require -- User-based coverageSummary = require(pathResolve(process.cwd(), coveragePath)) } = cfg; if (!(new Set(['png', 'svg']).has(format))) { throw new TypeError('Bad format'); } const log = (...args) => { if (logging !== 'off') { // eslint-disable-next-line no-console -- Logging feature console.log(...args); } }; const conditions = cfg.conditions ? cfg.conditions.split(',') : null; const nycConfig = cfg.nycConfig || await loadNycConfig(); const possibleConditions = ['statements', 'branches', 'lines', 'functions']; const possibleConditionThresholds = possibleConditions.reduce( (o, condition) => { const threshold = cfg[condition + 'Threshold']; let low, medium; if (threshold) { const hyphenIdx = threshold.indexOf('-'); low = threshold.slice(0, hyphenIdx); medium = threshold.slice(hyphenIdx + 1); } const watermark = nycConfig.watermarks && nycConfig.watermarks[condition]; // Priority to CLI condition threshold, then to nyc, then default to 100 o['low_' + condition] = low ? Number.parseFloat(low) : watermark ? watermark[0] : nycConfig[condition] || 100; o['medium_' + condition] = medium ? Number.parseFloat(medium) : watermark ? watermark[1] : nycConfig[condition] || 100; return o; }, {} ); const getThresholdStatus = (condition) => { const threshold = possibleConditionThresholds['low_' + condition]; const failing = coverageSummary.total[condition].pct < threshold; if (failing) { return 'failing'; } const mediumThreshold = possibleConditionThresholds['medium_' + condition]; const medium = coverageSummary.total[condition].pct < mediumThreshold; return medium ? 'medium' : 'passing'; }; const conditionsToCheck = conditions // User only wishes to check certain conditions ? [ ...new Set(conditions.filter((condition) => { return possibleConditions.includes(condition); })) ] : possibleConditions; /** * @typedef {GenericArray} BadgeSection * @property {string} 0 string * @property {string} 1 color * @property {string} 2 [strokeColor] * @see https://github.com/yahoo/badge-up */ /** * @param {"failing"|"medium"|"passing"} status * @param {"statements"|"branches"|"lines"|"functions"} condition * @returns {BadgeSection[]} */ function getConditionBlocks (status, condition) { let color = status === 'failing' ? failColor : status === 'medium' ? mediumColor : passColor; const { pct: conditionPct, skipped: conditionSkipped, covered: conditionCovered, total: conditionTotal } = coverageSummary.total[condition]; const currentInfoObj = { status, condition: condition.charAt().toUpperCase() + condition.slice(1), conditionPct, conditionSkipped, conditionCovered, conditionTotal }; const text = es6Templates( textTemplate, { ...currentInfoObj, ...( // If user specified conditions, only allow these in template; // otherwise, allow all conditions || possibleConditions ).reduce((o, cond) => { const { pct, skipped, covered, total } = coverageSummary.total[cond]; o[cond + 'Pct'] = pct; o[cond + 'Skipped'] = skipped; o[cond + 'Covered'] = covered; o[cond + 'Total'] = total; o[cond + 'Threshold'] = possibleConditionThresholds['low_' + cond]; o[cond + 'MediumThreshold'] = possibleConditionThresholds['medium_' + cond]; return o; }, {}) } ); if (typeof color === 'string') { color = color.split(','); } return [ [text, ...color] ]; } let conditionBlocks; if (aggregateConditions) { let status = 'failing'; let condition = conditionsToCheck.find( (cond) => getThresholdStatus(cond) === 'failing' ); if (!condition) { condition = conditionsToCheck.find( (cond) => getThresholdStatus(cond) === 'medium' ); if (condition) { status = 'medium'; } else { status = 'passing'; condition = conditionsToCheck[0]; } } conditionBlocks = getConditionBlocks( status, condition ); } else { conditionBlocks = conditionsToCheck.flatMap((condition) => { return getConditionBlocks(getThresholdStatus(condition), condition); }); } const sections = [ ...(introTemplate ? [[ es6Templates(introTemplate), ...(typeof introColor === 'string' ? introColor.split(',') : introColor) ]] : [] ), ...conditionBlocks ]; log('sections', sections); const badge = await badgeUp(sections); const outputBase = output.replace(/\.(?:png|svg)$/u, ''); const svgFilePath = `${outputBase}.svg`; writeFileSync(pathResolve(process.cwd(), svgFilePath), badge + '\n'); log('Finished writing temporary SVG file...'); if (format === 'png') { // Make non-global as optional // eslint-disable-next-line max-len -- Long // eslint-disable-next-line n/global-require, n/no-unpublished-require -- Optional const {convertFile} = require('convert-svg-to-png'); const outputFile = await convertFile( pathResolve(process.cwd(), svgFilePath) ); log('Wrote file', outputFile); unlinkSync(pathResolve(process.cwd(), svgFilePath)); log('Cleaned up temporary SVG file'); } log('Done!'); } module.exports = coveradge;