Skip to content

Commit

Permalink
fix: make error message prettier
Browse files Browse the repository at this point in the history
  • Loading branch information
ShenQingchuan committed Sep 13, 2022
1 parent aa01f60 commit 1455cd3
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 65 deletions.
22 changes: 10 additions & 12 deletions src/index.ts
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
)
Expand All @@ -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!')
Expand All @@ -185,7 +183,7 @@ try {
showFileErrs({
selectedPath,
rootAbsPath,
rawErrsMap,
rawErrsMap: ctx.rawErrsMap,
})
selectedPath = rootAbsPath
}
Expand Down
14 changes: 6 additions & 8 deletions src/show-console-print.ts
Expand Up @@ -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`
)
}
118 changes: 73 additions & 45 deletions src/ts-errs-map.ts
Expand Up @@ -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(?<errCode>\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 {
Expand All @@ -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,
})
Expand All @@ -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<string[]>((prev, next) => {
if (!next) {
const infos = await Promise.all(
tscErrorStdout
.split(newLineRegExp)
.reduce<string[]>((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
}
33 changes: 33 additions & 0 deletions 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) {
Expand All @@ -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<string>((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))
})
})
}

0 comments on commit 1455cd3

Please sign in to comment.