UNPKG

type-coverage

Version:

A CLI tool to check type coverage for typescript code

281 lines (279 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const minimist = require("minimist"); const chalk = require("chalk"); const fs = require("fs"); const util = require("util"); const path = require("path"); const packageJson = require("../package.json"); const type_coverage_core_1 = require("type-coverage-core"); let suppressError = false; let jsonOutput = false; const output = { succeeded: false, }; const existsAsync = util.promisify(fs.exists); const readFileAsync = util.promisify(fs.readFile); const writeFileAsync = util.promisify(fs.writeFile); function showToolVersion() { console.log(`Version: ${packageJson.version}`); } function printHelp() { console.log(`type-coverage [options] [-- file1.ts file2.ts ...] -p, --project string? tell the CLI where is the tsconfig.json --detail boolean? show detail --at-least number? fail if coverage rate < this value --debug boolean? show debug info --strict boolean? strict mode --ignore-catch boolean? ignore catch --cache boolean? enable cache --ignore-files string[]? ignore files --ignore-unread boolean? allow writes to variables with implicit any types -h,--help boolean? show help --is number? fail if coverage rate !== this value --update boolean? update "typeCoverage" in package.json to current result --update-if-higher boolean? update "typeCoverage" in package.json to current result if new type coverage is higher --ignore-nested boolean? ignore any in type arguments, eg: Promise<any> --ignore-as-assertion boolean? ignore as assertion, eg: foo as string --ignore-type-assertion boolean? ignore type assertion, eg: <string>foo --ignore-non-null-assertion boolean? ignore non-null assertion, eg: foo! --ignore-object boolean? Object type not counted as any, eg: foo: Object --ignore-empty-type boolean? empty type not counted as any, eg: foo: {} --show-relative-path boolean? show relative path in detail message --history-file string? file name where history is saved --no-detail-when-failed boolean? not show detail message when the CLI failed --report-semantic-error boolean? report typescript semantic error -- file1.ts file2.ts ... string[]? only checks these files, useful for usage with tools like lint-staged --cache-directory string? set cache directory --not-only-in-cwd boolean? include results outside current working directory --json-output boolean? output results as JSON --report-unused-ignore boolean? report unused ignore line directives `); } async function executeCommandLine() { const argv = minimist(process.argv.slice(2), { '--': true }); const showVersion = argv.v || argv.version; if (showVersion) { showToolVersion(); return; } if (argv.h || argv.help) { printHelp(); process.exit(0); } const { atLeast, debug, detail, enableCache, ignoreCatch, ignoreFiles, ignoreUnread, is, project, strict, update, updateIfHigher, ignoreNested, ignoreAsAssertion, ignoreTypeAssertion, ignoreNonNullAssertion, ignoreObject, ignoreEmptyType, showRelativePath, historyFile, noDetailWhenFailed, reportSemanticError, reportUnusedIgnore, cacheDirectory, notOnlyInCWD } = await getTarget(argv); const { correctCount, totalCount, anys } = await (0, type_coverage_core_1.lint)(project, { debug, strict, enableCache, ignoreCatch, ignoreFiles, ignoreUnreadAnys: ignoreUnread, ignoreNested, ignoreAsAssertion, ignoreTypeAssertion, ignoreNonNullAssertion, ignoreObject, ignoreEmptyType, reportSemanticError, reportUnusedIgnore, cacheDirectory, notOnlyInCWD, files: argv['--'].length > 0 ? argv['--'] : undefined, }); const percent = Math.floor(10000 * correctCount / totalCount) / 100; const atLeastFailed = typeof atLeast === 'number' && percent < atLeast; const isFailed = is && percent !== is; if (detail || (!noDetailWhenFailed && (atLeastFailed || isFailed))) { output.details = []; for (const { file, line, character, text } of anys) { const filePath = showRelativePath ? file : path.resolve(process.cwd(), file); output.details.push({ character, filePath, line, text }); } } const percentString = percent.toFixed(2); output.atLeast = atLeast; output.atLeastFailed = atLeastFailed; output.correctCount = correctCount; output.is = is; output.isFailed = isFailed; output.percent = percent; output.percentString = percentString; output.totalCount = totalCount; if (update) { await saveTarget(+percentString); } else if (updateIfHigher) { await saveTarget(+percentString, true); } if (historyFile) { await saveHistory(+percentString, historyFile); } if (atLeastFailed) { throw new Error(`The type coverage rate(${percentString}%) is lower than the target(${atLeast}%).`); } if (isFailed) { throw new Error(`The type coverage rate(${percentString}%) is not the target(${is}%).`); } } async function getTarget(argv) { let pkgCfg; const packageJsonPath = path.resolve(process.cwd(), 'package.json'); if (await existsAsync(packageJsonPath)) { const currentPackageJson = JSON.parse((await readFileAsync(packageJsonPath)).toString()); const typeCoverage = currentPackageJson.typeCoverage; if (typeCoverage) { pkgCfg = typeCoverage; } } const isCliArg = (key) => key in argv; const isPkgArg = (key) => pkgCfg ? key in pkgCfg : false; function getArgOrCfgVal(keys) { for (const key of keys) { if (isCliArg(key)) { return argv[key]; } if (pkgCfg && isPkgArg(key)) { return pkgCfg[key]; } } return undefined; } suppressError = getArgOrCfgVal(['suppressError']) || false; jsonOutput = getArgOrCfgVal(['json-output', 'jsonOutput']) || false; const atLeast = getArgOrCfgVal(['at-least', 'atLeast']); const debug = getArgOrCfgVal(['debug']); const detail = getArgOrCfgVal(['detail']); const enableCache = getArgOrCfgVal(['cache']); const ignoreCatch = getArgOrCfgVal(['ignore-catch', 'ignoreCatch']); const ignoreFiles = getArgOrCfgVal(['ignore-files', 'ignoreFiles']); const ignoreUnread = getArgOrCfgVal(['ignore-unread', 'ignoreUnread']); const is = getArgOrCfgVal(['is']); const project = getArgOrCfgVal(['p', 'project']) || '.'; const strict = getArgOrCfgVal(['strict']); const update = getArgOrCfgVal(['update']); const updateIfHigher = getArgOrCfgVal(['update-if-higher', 'updateIfHigher']); const ignoreNested = getArgOrCfgVal(['ignore-nested', 'ignoreNested']); const ignoreAsAssertion = getArgOrCfgVal(['ignore-as-assertion', 'ignoreAsAssertion']); const ignoreTypeAssertion = getArgOrCfgVal(['ignore-type-assertion', 'ignoreTypeAssertion']); const ignoreNonNullAssertion = getArgOrCfgVal(['ignore-non-null-assertion', 'ignoreNonNullAssertion']); const ignoreObject = getArgOrCfgVal(['ignore-object', 'ignoreObject']); const ignoreEmptyType = getArgOrCfgVal(['ignore-empty-type', 'ignoreEmptyType']); const showRelativePath = getArgOrCfgVal(['show-relative-path', 'showRelativePath']); const historyFile = getArgOrCfgVal(['history-file', 'historyFile']); const noDetailWhenFailed = getArgOrCfgVal(['no-detail-when-failed', 'noDetailWhenFailed']); const reportSemanticError = getArgOrCfgVal(['report-semantic-error', 'reportSemanticError']); const reportUnusedIgnore = getArgOrCfgVal(['report-unused-ignore', 'reportUnusedIgnore']); const cacheDirectory = getArgOrCfgVal(['cache-directory', 'cacheDirectory']); const notOnlyInCWD = getArgOrCfgVal(['not-only-in-cwd', 'notOnlyInCWD']); return { atLeast, debug, detail, enableCache, ignoreCatch, ignoreFiles, ignoreUnread, is, project, strict, update, updateIfHigher, ignoreNested, ignoreAsAssertion, ignoreTypeAssertion, ignoreNonNullAssertion, ignoreObject, ignoreEmptyType, showRelativePath, historyFile, noDetailWhenFailed, reportSemanticError, reportUnusedIgnore, cacheDirectory, notOnlyInCWD }; } async function saveTarget(target, ifHigher) { const packageJsonPath = path.resolve(process.cwd(), 'package.json'); if (await existsAsync(packageJsonPath)) { const currentPackageJson = JSON.parse((await readFileAsync(packageJsonPath)).toString()); if (currentPackageJson.typeCoverage) { if (currentPackageJson.typeCoverage.atLeast != null) { if (!ifHigher || target > currentPackageJson.typeCoverage.atLeast) { currentPackageJson.typeCoverage.atLeast = target; } } else if (currentPackageJson.typeCoverage.is != null) { if (!ifHigher || target > currentPackageJson.typeCoverage.is) { currentPackageJson.typeCoverage.is = target; } } await writeFileAsync(packageJsonPath, JSON.stringify(currentPackageJson, null, 2) + '\n'); } } } async function saveHistory(percentage, historyFile) { if (historyFile) { const historyFilePath = path.resolve(process.cwd(), historyFile); if (await existsAsync(historyFilePath)) { const date = new Date().toISOString(); const historyFile = JSON.parse((await readFileAsync(historyFilePath)).toString()); historyFile[date] = percentage; await writeFileAsync(historyFilePath, JSON.stringify(historyFile, null, 2) + '\n'); } else { const date = new Date().toISOString(); const historyFile = {}; historyFile[date] = percentage; await writeFileAsync(historyFilePath, JSON.stringify(historyFile, null, 2) + '\n'); } } } function printOutput(output, asJson) { if (asJson) { console.log(JSON.stringify(output, null, 2)); return; } const { details, correctCount, error, totalCount, percentString, succeeded } = output; for (const detail of details || []) { const { filePath, line, character, text } = detail; console.log(`${filePath}:${line + 1}:${character + 1}: ${text}`); } if (percentString) { const diffInfo = `${correctCount} / ${totalCount}`; if (totalCount) { console.log(succeeded ? chalk.green(`(${diffInfo}) ${percentString}%`) : chalk.red(`(${diffInfo}) ${percentString}%`)); } else { console.log(succeeded ? chalk.green(diffInfo) : chalk.red(diffInfo)); } } if (succeeded) { console.log(chalk.green('type-coverage success.')); } else { console.log(chalk.red(error)); } } executeCommandLine().then(() => { output.succeeded = true; printOutput(output, jsonOutput); }, (error) => { output.succeeded = false; if (error instanceof Error) { output.error = error.message; } else { output.error = error; } printOutput(output, jsonOutput); if (!suppressError) { process.exit(1); } });