Skip to content

Commit 4b18b0d

Browse files
committed
Add beautiful-error
1 parent 1bc7e9f commit 4b18b0d

23 files changed

+76
-740
lines changed

src/exit.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import process from 'node:process'
22

3-
import { handleInvalidOpts } from './options/invalid.js'
43
import { waitForTimeout } from './timeout.js'
54

65
// Validate `exitCode` option
@@ -10,10 +9,8 @@ export const validateExitCode = (exitCode, optName) => {
109
exitCode < MIN_EXIT_CODE ||
1110
exitCode > MAX_EXIT_CODE
1211
) {
13-
handleInvalidOpts(
14-
`must be between ${MIN_EXIT_CODE} and ${MAX_EXIT_CODE}`,
15-
exitCode,
16-
optName,
12+
throw new Error(
13+
`"${optName}" must be between ${MIN_EXIT_CODE} and ${MAX_EXIT_CODE}: ${exitCode}`,
1714
)
1815
}
1916
}

src/main.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import beautifulError from 'beautiful-error'
12
import normalizeException from 'normalize-exception'
23

34
import { exitProcess } from './exit.js'
45
import { getOpts } from './options/main.js'
5-
import { printError } from './print/main.js'
66

77
export { validateOptions } from './options/validate.js'
88

@@ -11,10 +11,25 @@ const handleCliError = (error, opts) => {
1111
const errorA = normalizeException(error)
1212
const {
1313
error: errorB,
14-
opts: { silent, stack, props, colors, icon, header, exitCode, timeout },
15-
} = getOpts(opts, errorA)
16-
printError({ error: errorB, silent, stack, props, colors, icon, header })
14+
opts: { silent, exitCode, timeout },
15+
beautifulErrorOpts,
16+
} = getOpts(errorA, opts)
17+
18+
printError(errorB, silent, beautifulErrorOpts)
1719
exitProcess(exitCode, timeout)
1820
}
1921

22+
// We pass the `error` instance to `console.error()`, so it prints not only its
23+
// `message` and `stack` but also its properties, `cause`, aggregate `errors`,
24+
// add colors, inline preview, etc. using `util.inspect()`
25+
const printError = (error, silent, beautifulErrorOpts) => {
26+
if (silent) {
27+
return
28+
}
29+
30+
const errorString = beautifulError(error, beautifulErrorOpts)
31+
// eslint-disable-next-line no-restricted-globals, no-console
32+
console.error(errorString)
33+
}
34+
2035
export default handleCliError

src/main.test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { each } from 'test-each'
44

55
import { handleError } from './helpers/main.test.js'
66

7+
const testError = new TypeError('test')
8+
79
each(
810
[
911
{ error: undefined, expectedMessage: 'Error: undefined' },
@@ -26,3 +28,8 @@ test.serial('validateOpts() throws on invalid options', (t) => {
2628
test.serial('validateOpts() does not throw on valid options', (t) => {
2729
t.notThrows(validateOptions.bind(undefined, { silent: true }))
2830
})
31+
32+
test.serial('Does not log if "silent" is true', (t) => {
33+
const { consoleArg } = handleError(testError, { silent: true })
34+
t.is(consoleArg, undefined)
35+
})

src/options/classes.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
import isPlainObj from 'is-plain-obj'
22

33
import { removeUndefined } from './default.js'
4-
import { handleInvalidOpts } from './invalid.js'
54

6-
// Validate `classes` option
7-
export const validateClasses = (classes, optName, validateAllOpts) => {
8-
if (!isPlainObj(classes)) {
9-
handleInvalidOpts('must be a plain object', classes, optName)
5+
// `options.classes.{ErrorName}.*` is like `options.*` but only applies if
6+
// `error.name` matches.
7+
export const applyClassesOpts = ({ name }, opts = {}) => {
8+
if (!isPlainObj(opts)) {
9+
throw new Error(`options must be a plain object: ${opts}`)
1010
}
1111

12-
if (optName.length > 1) {
13-
handleInvalidOpts('must not be defined', classes, optName)
14-
}
12+
const { classes = {}, ...optsA } = opts
13+
validateObject(classes, 'classes')
14+
15+
const classesOpts = classes[name] || classes.default || {}
16+
validateObject(classesOpts, `classes.${name}`)
1517

16-
Object.entries(classes).forEach(([className, classOpts]) => {
17-
validateAllOpts(classOpts, [...optName, className])
18-
})
18+
return { ...optsA, ...removeUndefined(classesOpts) }
1919
}
2020

21-
// `options.classes.{ErrorName}.*` is like `options.*` but only applies if
22-
// `error.name` matches.
23-
export const applyClassesOpts = ({ name }, { classes = {}, ...opts } = {}) => {
24-
const classesOpts = classes[name] || classes.default || {}
25-
return { ...opts, ...removeUndefined(classesOpts) }
21+
const validateObject = (value, optName) => {
22+
if (!isPlainObj(value)) {
23+
throw new Error(`"${optName}" must be a plain object: ${value}`)
24+
}
2625
}

src/options/default.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ export const applyDefaultOpts = (opts) => ({
1212
// Default values of options
1313
export const DEFAULT_OPTS = {
1414
silent: false,
15-
stack: true,
16-
props: true,
17-
icon: 'cross',
18-
header: 'red bold',
1915
exitCode: DEFAULT_EXIT_CODE,
2016
timeout: DEFAULT_TIMEOUT,
2117
}

src/options/invalid.js

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/options/main.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,25 @@ import normalizeException from 'normalize-exception'
22

33
import { INVALID_OPTS_EXIT_CODE } from '../exit.js'
44

5-
import { applyClassesOpts } from './classes.js'
65
import { applyDefaultOpts, DEFAULT_OPTS } from './default.js'
7-
import { validateOptions } from './validate.js'
6+
import { normalizeOptions } from './validate.js'
87

98
// Normalize and validate options
10-
export const getOpts = (opts, error) => {
9+
export const getOpts = (error, opts) => {
1110
try {
12-
return safeGetOpts(opts, error)
11+
return safeGetOpts(error, opts)
1312
} catch (error_) {
1413
// eslint-disable-next-line fp/no-mutation
1514
error_.message = `handle-cli-error invalid usage: ${error_.message}`
1615
const errorA = normalizeException(error_)
17-
return { error: errorA, opts: INVALID_OPTS }
16+
return { error: errorA, opts: INVALID_OPTS, beautifulErrorOpts: {} }
1817
}
1918
}
2019

21-
const safeGetOpts = (opts, error) => {
22-
validateOptions(opts)
23-
const optsA = applyClassesOpts(error, opts)
20+
const safeGetOpts = (error, opts = {}) => {
21+
const { opts: optsA, beautifulErrorOpts } = normalizeOptions(error.name, opts)
2422
const optsB = applyDefaultOpts(optsA)
25-
return { error, opts: optsB }
23+
return { error, opts: optsB, beautifulErrorOpts }
2624
}
2725

2826
// Options used when invalid input is passed

src/options/validate.js

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,53 @@
1+
import { validateOptions as validateBeautifulOptions } from 'beautiful-error'
12
import isPlainObj from 'is-plain-obj'
23

34
import { validateExitCode } from '../exit.js'
45
import { validateTimeout } from '../timeout.js'
56

6-
import { validateClasses } from './classes.js'
7-
import { handleInvalidOpts } from './invalid.js'
7+
import { applyClassesOpts } from './classes.js'
88

99
// Validate option values.
1010
// This is exported, although not documented.
1111
export const validateOptions = (opts) => {
12-
validateAllOpts(opts, [])
13-
}
14-
15-
const validateAllOpts = (opts, optName) => {
16-
if (opts === undefined) {
17-
return
18-
}
19-
2012
if (!isPlainObj(opts)) {
21-
handleInvalidOpts('must be a plain object', opts, optName)
13+
return
2214
}
2315

24-
Object.entries(opts).forEach(([key, optValue]) => {
25-
validateOpt(optValue, [...optName, key])
16+
const { classes } = opts
17+
const names =
18+
isPlainObj(classes) && Object.keys(classes).length !== 0
19+
? Object.keys(classes)
20+
: ['default']
21+
names.forEach((name) => {
22+
normalizeOptions(name, opts)
2623
})
2724
}
2825

29-
const validateOpt = (optValue, optName) => {
30-
if (optValue === undefined || BEAUTIFUL_ERROR_OPTS.has(optName)) {
31-
return
32-
}
33-
34-
const validator = VALIDATORS[optName.at(-1)]
26+
export const normalizeOptions = (name, opts) => {
27+
const { silent, exitCode, timeout, ...beautifulErrorOpts } = applyClassesOpts(
28+
name,
29+
opts,
30+
)
31+
const optsA = { silent, exitCode, timeout }
32+
Object.entries(optsA).forEach(validateOpt)
33+
validateBeautifulOptions(beautifulErrorOpts)
34+
return { opts: optsA, beautifulErrorOpts }
35+
}
3536

36-
if (validator === undefined) {
37-
handleInvalidOpts('is an unknown option', '', optName)
37+
const validateOpt = ([optName, optValue]) => {
38+
if (optValue !== undefined) {
39+
VALIDATORS[optName](optValue, optName)
3840
}
39-
40-
validator(optValue, optName, validateAllOpts)
4141
}
4242

4343
const validateBooleanOpt = (value, optName) => {
4444
if (typeof value !== 'boolean') {
45-
handleInvalidOpts('must be a boolean', value, optName)
45+
throw new TypeError(`"${optName}" must be a boolean: ${value}`)
4646
}
4747
}
4848

4949
const VALIDATORS = {
5050
silent: validateBooleanOpt,
5151
exitCode: validateExitCode,
5252
timeout: validateTimeout,
53-
classes: validateClasses,
5453
}
55-
56-
const BEAUTIFUL_ERROR_OPTS = new Set([
57-
'stack',
58-
'props',
59-
'colors',
60-
'icon',
61-
'header',
62-
])

src/print/colors.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/print/colors.test.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)