UNPKG

@akphi/dev-utils

Version:
168 lines (164 loc) 6.12 kB
import chalk from 'chalk'; import strip from 'strip-ansi'; import table from 'text-table'; import wrap from 'wrap-ansi'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; const CONTENT_LINE_LENGTH = 72; const PLUGIN_NAME = 'ForkTsCheckerWebpackFormatterPlugin'; class ForkTsCheckerWebpackFormatterPlugin { apply(compiler) { const tsCheckerHooks = ForkTsCheckerWebpackPlugin.getCompilerHooks(compiler); let typeCheckingStartTime; tsCheckerHooks.start.tap(PLUGIN_NAME, () => { // this hook is called when type checking is started, so we can reset the time here typeCheckingStartTime = Date.now(); }); tsCheckerHooks.error.tap(PLUGIN_NAME, (error) => console.error(error)); tsCheckerHooks.waiting.tap(PLUGIN_NAME, () => { // since a chunk of time is spent on waiting for webpack to compile, on the very first type checking // the elapsed time then will be off, and since this hook is called only on the first type checking // we set the time here so we can compute the elapsed time right after compilation to when type // checking report is complete typeCheckingStartTime = Date.now(); console.info(`${chalk.gray('i [ts]')} : Asynchronously checking type...`); }); tsCheckerHooks.issues.tap(PLUGIN_NAME, (issues, compilation) => { const errors = compilation.errors .filter((error) => error.message) .map((error) => ({ message: error.message, severity: 'error', file: 'Compilation Issues:', })); const warnings = compilation.warnings .filter((warning) => warning.message) .map((warning) => ({ message: warning.message, severity: 'warning', file: 'Compilation Issues:', })); issues = errors.concat(warnings).concat(issues); const issuesByFile = new Map(); issues.forEach((issue) => { const file = issue.file; if (!issuesByFile.has(file)) { issuesByFile.set(file, []); } issuesByFile.get(file).push({ file: issue.file, message: issue.message, code: issue.code ?? 'unknown', // NOTE: ignore end location for now line: issue.location?.start.line ?? 0, column: issue.location?.start.column ?? 0, level: issue.severity, }); }); // Sort issues by location within a file Array.from(issuesByFile.keys()).forEach((file) => issuesByFile.set( file, issuesByFile .get(file) .sort((a, b) => a.column - b.column) .sort((a, b) => a.line - b.line), ), ); // Scan and tokenize error/warning message and wrap long message const rows = []; const fileLineMap = new Map(); issuesByFile.forEach((items, filePath) => { let lineNumber = 0; const colors = { error: 'red', warning: 'yellow' }; items.forEach((item) => { const message = wrap(item.message, CONTENT_LINE_LENGTH, { hard: true, }); const lines = message.split('\n'); rows.push([ '', chalk.dim(`${item.line}:${item.column}`), chalk[colors[item.level]](item.level), chalk.blue(lines[0]), item.code, ]); lineNumber++; for (const line of lines.slice(1)) { rows.push(['', '', '', chalk.blue(line)]); lineNumber++; } }); fileLineMap.set(filePath, lineNumber); }); // Try to align messages between file by forming a table let tableFromAllLines = []; let skipFormatting = false; // if table forming failed, fall back to normal formatting (using tabs) try { tableFromAllLines = table(rows, { align: ['', 'l', 'l', 'l', 'l'], stringLength: (str) => strip(str).length, }).split('\n'); } catch (error) { console.warn(`Can't format message`, error.message); // handle error as sometimes `table()` throws on long compilation messages skipFormatting = true; } // Print result let lineCounter = 0; issuesByFile.forEach((items, filePath) => { let result = []; if (skipFormatting) { result.push('', chalk.underline(filePath)); result = result.concat( rows .slice(lineCounter, lineCounter + fileLineMap.get(filePath)) .map((row) => row.join('\t')), ); lineCounter += fileLineMap.get(filePath); } else { result.push('', chalk.underline(filePath)); result = result.concat( tableFromAllLines.slice( lineCounter, lineCounter + fileLineMap.get(filePath), ), ); lineCounter += fileLineMap.get(filePath); } console.info(result.join('\n')); }); // Summary const time = Math.round(Date.now() - typeCheckingStartTime); const warningCount = issues.filter( (issue) => issue.severity === 'warning', ).length; const errorCount = issues.filter( (issue) => issue.severity === 'error', ).length; if (!(errorCount + warningCount)) { console.info( `${chalk.gray( 'i [ts]', )} : Type checking passed successfully! [${time}ms]`, ); } else if (!errorCount) { console.info( `\n${chalk.yellowBright('!')}${chalk.gray( ' [ts]', )} : ${chalk.yellowBright( `Type checking passed with warning(s)! [${time}ms]`, )}`, ); } else { console.info( `\n${chalk.redBright('x')}${chalk.gray(' [ts]')} : ${chalk.redBright( `Type checking failed! [${time}ms]`, )}`, ); } // NOTE: since we have already reported all the issues here, we want to pass no more errors down to webpack return []; }); } } export default ForkTsCheckerWebpackFormatterPlugin;