From 1455cd3a962e729e5b1564af0e3c7d53895c2f97 Mon Sep 17 00:00:00 2001 From: "tangmengyu.amber" Date: Tue, 13 Sep 2022 14:54:46 +0800 Subject: [PATCH] fix: make error message prettier --- src/index.ts | 22 ++++--- src/show-console-print.ts | 14 ++--- src/ts-errs-map.ts | 118 +++++++++++++++++++++++--------------- src/utils.ts | 33 +++++++++++ 4 files changed, 122 insertions(+), 65 deletions(-) diff --git a/src/index.ts b/src/index.ts index 0f1d4e6..2238410 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ import inquirerFileTreeSelection from 'inquirer-file-tree-selection-prompt' import ensureTscVersion from './check-tsc-version' import { showAppHeader } from './show-console-print' import { getRawErrsSumCount, getTargetDir, isFilePath } from './utils' -import { getRawErrsMap } from './ts-errs-map' +import { getRawErrsMapFromTsCompile } from './ts-errs-map' import type { Context, RawErrsMap } from './types' inquirer.registerPrompt('file-tree-selection', inquirerFileTreeSelection) @@ -106,13 +106,9 @@ try { if (isFilePath(rootAbsPath)) { throw new Error("Can't run tsc-err-dirs on single file.") } - const baseRootAndTarget = { - root: rootAbsPath, - targetAbsPath: rootAbsPath, - } // Generate a map to store errors info - const rawErrsMap = await getRawErrsMap(baseRootAndTarget) + const rawErrsMap = await getRawErrsMapFromTsCompile(rootAbsPath) if (getRawErrsSumCount(rawErrsMap) === 0) { console.log(`\nšŸŽ‰ ${chalk.bold.greenBright('Found 0 Errors.')}\n`) process.exit() @@ -149,7 +145,6 @@ try { const fileChangedMsg = `\n${chalk.blue( `[WATCH] ${chalk.white(changedPath)} has changed ...` )}` - console.log(fileChangedMsg) rejectSelectFile( new Error(fileChangedMsg) // throw out message for chokidar change event ) @@ -171,11 +166,14 @@ try { ...ctx, targetAbsPath: selectedPath, }) - } catch { + } catch (error) { // Re-generate a map to store errors info - ctx.rawErrsMap.clear() - ctx.rawErrsMap = await getRawErrsMap(baseRootAndTarget, true) - continue + if (error instanceof Error) { + console.log(error.message) + ctx.rawErrsMap.clear() + ctx.rawErrsMap = await getRawErrsMapFromTsCompile(rootAbsPath, true) + continue + } } if (!selectedPath) { throw new Error('failed to select file!') @@ -185,7 +183,7 @@ try { showFileErrs({ selectedPath, rootAbsPath, - rawErrsMap, + rawErrsMap: ctx.rawErrsMap, }) selectedPath = rootAbsPath } diff --git a/src/show-console-print.ts b/src/show-console-print.ts index a6e5626..dab17ce 100644 --- a/src/show-console-print.ts +++ b/src/show-console-print.ts @@ -16,23 +16,21 @@ export function showAppHeader() { } export function showFirstTscCompilePathInfo({ cmd, - root, - targetAbsPath, + rootAbsPath, }: { cmd: string - root: string - targetAbsPath: string + rootAbsPath: string }) { console.log( - `\n$ ${chalk.yellowBright(root)}\n ${chalk.bold.gray( - `tsc running on ${chalk.bold.blue(targetAbsPath)} ...` + `\n$ ${chalk.yellowBright(rootAbsPath)}\n ${chalk.bold.gray( + `tsc running for the first time ...` )}\n ${chalk.green('ā–·')} ${chalk.grey(` ${cmd}`)}\n` ) } -export function showTscReCompilePathInfo(targetAbsPath: string) { +export function showTscReCompilePathInfo(rootAbsPath: string) { console.log( `${chalk.bold.gray( - `Start re-run tsc on ${chalk.bold.blue(targetAbsPath)} ...` + `Start re-run tsc on ${chalk.bold.blue(rootAbsPath)} ...` )}\n` ) } diff --git a/src/ts-errs-map.ts b/src/ts-errs-map.ts index 722e822..dbf61cf 100644 --- a/src/ts-errs-map.ts +++ b/src/ts-errs-map.ts @@ -9,64 +9,91 @@ import { showFirstTscCompilePathInfo, showTscReCompilePathInfo, } from './show-console-print' -import type { RawErrsMap, RootAndTarget, TscErrorInfo } from './types' +import { getLineByIndexFromFile } from './utils' +import type { RawErrsMap, TscErrorInfo } from './types' const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const newLineRegExp = /\r?\n/ const errCodeRegExp = /error TS(?\d+)/ const tscSpinner = ora(chalk.yellow('Start tsc compiling ...')) -function makeTscErrorInfo(errInfo: string): [string, TscErrorInfo] { +async function getErrPreviewLineByIndexFromFile( + filePath: string, + line: number, + errMsg: string +) { + // line index is zero-based, so we need to minus 1 + const lineContent = await getLineByIndexFromFile(filePath, line - 1) + return `${errMsg} +${chalk.yellow(`${String(line)} ā”†`)}${lineContent}` +} + +async function makeTscErrorInfo( + errInfo: string, + rootAbsPath: string +): Promise<[string, TscErrorInfo]> { const panicMsg = 'failed to parsing error info.' const [errFilePathPos = '', ...errMsgRawArr] = errInfo.split(':') if ( !errFilePathPos || errMsgRawArr.length === 0 || errMsgRawArr.join('').length === 0 - ) + ) { throw new Error(`${panicMsg} (on first split)`) + } const errMsgRaw = errMsgRawArr.join('').trim() // get filePath, line, col const [errFilePath, errPos] = errFilePathPos .slice(0, -1) // removes the ')' .split('(') - if (!errFilePath || !errPos) + if (!errFilePath || !errPos) { throw new Error(`${panicMsg} (on \`errFilePath\` or \`errPos\`)`) + } const [errLine, errCol] = errPos.split(',') - if (!errLine || !errCol) + if (!errLine || !errCol) { throw new Error(`${panicMsg} (on \`errLine\` or \`errCol\`)`) + } // get errCode, errMsg const execArr = errCodeRegExp.exec(errMsgRaw) - if (!execArr) throw new Error(`${panicMsg} (on \`errMsgRegExp.exec\`)`) + if (!execArr) { + throw new Error(`${panicMsg} (on \`errMsgRegExp.exec\`)`) + } - const { errCode = '' } = execArr.groups ?? {} - if (!errCode) throw new Error(`${panicMsg} (on \`errCode\`)`) + const errCodeStr = execArr.groups?.errCode ?? '' + if (!errCodeStr) { + throw new Error(`${panicMsg} (on \`errCode\`)`) + } + const line = Number(errLine), + col = Number(errCol), + errCode = Number(errCodeStr) + const errMsg = await getErrPreviewLineByIndexFromFile( + path.join(rootAbsPath, errFilePath), + line, + errMsgRaw.slice(`error TS${errCode}`.length) + ) return [ errFilePath, { filePath: errFilePath, - line: Number(errLine), - col: Number(errCol), - errCode: Number(errCode), - errMsg: errMsgRaw.slice(`error TS${errCode}`.length - 1), + errCode, + line, + col, + errMsg, }, ] } export async function getTscCompileStdout( // The `cwd` dir requires an existing `tsconfig.json` file - options: RootAndTarget & { - pretty?: boolean - }, + rootAbsPath = process.cwd(), isReCompile = false ) { - const { root = process.cwd(), pretty = false, targetAbsPath = root } = options - const baseConfigPath = path.join(root, 'tsconfig.json') + const baseConfigPath = path.join(rootAbsPath, 'tsconfig.json') const baseConfigJSON = jsonc.parse(String(await readFile(baseConfigPath))) - const tmpConfigPath = path.join(root, 'tsconfig.tmp.json') + const tmpConfigPath = path.join(rootAbsPath, 'tsconfig.tmp.json') // Use a temp tsconfig try { @@ -91,17 +118,16 @@ export async function getTscCompileStdout( let tscErrorStdout = '' try { - const cmd = `tsc --noEmit --pretty ${pretty} -p ${tmpConfigPath}` + const cmd = `tsc --noEmit --pretty false -p ${tmpConfigPath}` isReCompile - ? showTscReCompilePathInfo(targetAbsPath) + ? showTscReCompilePathInfo(rootAbsPath) : showFirstTscCompilePathInfo({ cmd, - root, - targetAbsPath, + rootAbsPath, }) tscSpinner.start() const tscProcess = execaCommand(cmd, { - cwd: root, + cwd: rootAbsPath, stdout: 'pipe', reject: false, }) @@ -117,33 +143,35 @@ export async function getTscCompileStdout( } return tscErrorStdout } -export async function getRawErrsMap( - options: RootAndTarget, +export async function getRawErrsMapFromTsCompile( + rootAbsPath: string, isReCompile = false ) { - const tscErrorStdout = await getTscCompileStdout(options, isReCompile) + const tscErrorStdout = await getTscCompileStdout(rootAbsPath, isReCompile) const rawErrsMap: RawErrsMap = new Map() // Merge details line with main line (i.e. which contains file path) - tscErrorStdout - .split(newLineRegExp) - .reduce((prev, next) => { - if (!next) { + const infos = await Promise.all( + tscErrorStdout + .split(newLineRegExp) + .reduce((prev, next) => { + if (!next) { + return prev + } else if (!next.startsWith(' ')) { + prev.push(next) + } else { + prev[prev.length - 1] += `\n${next}` + } return prev - } else if (!next.startsWith(' ')) { - prev.push(next) - } else { - prev[prev.length - 1] += `\n${next}` - } - return prev - }, []) - .map((errInfoLine) => makeTscErrorInfo(errInfoLine)) - .forEach(([errFilePath, errInfo]) => { - if (!rawErrsMap.has(errFilePath)) { - rawErrsMap.set(errFilePath, [errInfo]) - } else { - rawErrsMap.get(errFilePath)?.push(errInfo) - } - }) + }, []) + .map((errInfoLine) => makeTscErrorInfo(errInfoLine, rootAbsPath)) + ) + infos.forEach(([errFilePath, errInfo]) => { + if (!rawErrsMap.has(errFilePath)) { + rawErrsMap.set(errFilePath, [errInfo]) + } else { + rawErrsMap.get(errFilePath)?.push(errInfo) + } + }) return rawErrsMap } diff --git a/src/utils.ts b/src/utils.ts index 87b9f6b..828ad5c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,6 @@ import path from 'node:path' +import readline from 'node:readline' +import fs from 'node:fs' import type { RawErrsMap } from './types' export function getRawErrsSumCount(rawErrsMap: RawErrsMap) { @@ -23,3 +25,34 @@ export function getTargetDir(dirArg: string): string { export function isFilePath(absPath: string) { return (absPath.split(path.sep).pop()?.split('.').length ?? 0) > 1 } + +function createOutOfRangeError(filePath: string, lineIndex: number) { + return new RangeError( + `Line with index ${lineIndex} does not exist in '${filePath}'. Note that line indexing is zero-based` + ) +} + +export function getLineByIndexFromFile(filePath: string, lineIndex: number) { + return new Promise((resolve, reject) => { + if (lineIndex < 0 || lineIndex % 1 !== 0) + return reject(new RangeError(`Invalid line number`)) + + let cursor = 0 + const input = fs.createReadStream(filePath) + const rl = readline.createInterface({ input }) + + rl.on('line', (line) => { + if (cursor++ === lineIndex) { + rl.close() + input.close() + resolve(line) + } + }) + + rl.on('error', reject) + + input.on('end', () => { + reject(createOutOfRangeError(filePath, lineIndex)) + }) + }) +}