Skip to content
This repository has been archived by the owner on Feb 1, 2022. It is now read-only.

Commit

Permalink
feat: added error context
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Jan 28, 2018
1 parent a28ee58 commit 36c2a6e
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 139 deletions.
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"ansi-styles": "^3.2.0",
"cardinal": "^1.0.0",
"chalk": "^2.3.0",
"clean-stack": "^1.3.0",
"extract-stack": "^1.0.0",
"fs-extra": "^5.0.0",
"indent-string": "^3.2.0",
"lodash": "^4.17.4",
"node-notifier": "^5.2.1",
"password-prompt": "^1.0.4",
Expand All @@ -19,14 +22,17 @@
"supports-color": "^5.1.0"
},
"devDependencies": {
"@dxcli/dev": "^2.0.13",
"@dxcli/dev": "^2.0.14",
"@dxcli/semantic-release": "^0.3.3",
"@dxcli/tslint": "^0.0.24",
"@dxcli/tslint": "^0.1.1",
"@types/clean-stack": "^1.3.0",
"@types/extract-stack": "^1.0.0",
"@types/fs-extra": "^5.0.0",
"@types/indent-string": "^3.0.0",
"@types/semver": "^5.4.0",
"chai": "^4.1.2",
"eslint": "^4.16.0",
"fancy-test": "^0.6.3",
"fancy-test": "^0.6.4",
"husky": "^0.14.3",
"mocha": "^5.0.0",
"mocha-junit-reporter": "^1.17.0",
Expand Down
7 changes: 2 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,13 @@ export class Config {
_debug = false
action: ActionBase = new Action()
errorsHandled = false
context = {}
errlog?: string

constructor() {
this.debug = process.env.DEBUG === '*'
}

get errlog(): string | undefined { return globals.errlog }
set errlog(errlog: string | undefined) {
globals.errlog = errlog
}

get debug(): boolean {
return this._debug
}
Expand Down
51 changes: 30 additions & 21 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
// tslint:disable no-console

import chalk from 'chalk'
import * as clean from 'clean-stack'
import * as extract from 'extract-stack'
import indent = require('indent-string')
import * as _ from 'lodash'
import {inspect} from 'util'

import CLI from '.'
import {config} from './config'
import deps from './deps'
import {IEventEmitter} from './events'
import styledObject from './styled/object'

export interface Message {
type: 'error'
scope: string | undefined
severity: 'fatal' | 'error' | 'warn'
error: CLIError
}

export interface Options {
exit?: number | false
context?: object
}

const arrow = process.platform === 'win32' ? ' !' : ' ▸'
Expand Down Expand Up @@ -46,6 +51,10 @@ export function getErrorMessage(err: any): string {
} else if (err.message) {
message = err.message
}
const context = err['cli-ux'] && err['cli-ux'].context
if (context && !_.isEmpty(context)) {
message += '\n' + indent(styledObject(err['cli-ux'].context), 4)
}
return message || inspect(err)
}

Expand All @@ -59,18 +68,20 @@ function displayError(err: CLIError) {
}

function render(): string {
const {severity, scope} = err['cli-ux']
if (severity === 'fatal' || config.debug) {
const {severity} = err['cli-ux']
const msg = [
_.upperFirst(severity === 'warn' ? 'warning' : severity),
': ',
getErrorMessage(err),
].join('')
if (process.env.CI || severity === 'fatal' || config.debug) {
// show stack trace
let msg = ''
if (severity !== 'error') msg += `${severity}: `
if (scope) msg += `${scope}: `
msg += err.stack || inspect(err)
return msg
let stack = err.stack || inspect(err)
stack = clean(stack, {pretty: true})
stack = extract(stack)
return [msg, stack].join('\n')
}
let bang = chalk.red(arrow)
let msg = scope ? `${scope}: ${getErrorMessage(err)}` : getErrorMessage(err)
if (severity as any === 'fatal') bang = chalk.bgRed.bold.white(' FATAL ')
let bang = severity === 'warn' ? chalk.yellow(arrow) : chalk.red(arrow)
if (severity === 'warn') bang = chalk.yellow(arrow)
return bangify(wrap(msg), bang)
}
Expand All @@ -90,11 +101,11 @@ export class CLIError extends Error {
code: string
'cli-ux': {
severity: 'fatal' | 'error' | 'warn'
scope: string | undefined
exit: number | false
context: object
}

constructor(input: any, severity: 'fatal' | 'error' | 'warn', scope: string | undefined, opts: Options) {
constructor(input: any, severity: 'fatal' | 'error' | 'warn', opts: Options) {
function getExitCode(options: Options): false | number {
let exit = options.exit === undefined ? (options as any).exitCode : options.exit
if (exit === false) return false
Expand All @@ -105,8 +116,8 @@ export class CLIError extends Error {
const err: CLIError = typeof input === 'string' ? super(input) && this : input
err['cli-ux'] = err['cli-ux'] || {
severity,
scope,
exit: severity === 'warn' ? false : getExitCode(opts),
context: {...config.context, ...opts.context},
}
return err
}
Expand All @@ -126,12 +137,12 @@ export default (e: IEventEmitter) => {

function handleUnhandleds() {
if (config.errorsHandled) return
const handleError = (scope: string) => async (err: CLIError) => {
const handleError = (_: string) => async (err: CLIError) => {
// ignore EPIPE errors
// these come from using | head and | tail
// and can be ignored
try {
const cli: typeof CLI = require('.').cli.scope(scope)
const cli: typeof CLI = require('.').cli
if (err.code === 'EPIPE') return
if (err['cli-ux'] && typeof err['cli-ux'].exit === 'number') {
displayError(err)
Expand Down Expand Up @@ -161,12 +172,10 @@ export default (e: IEventEmitter) => {
}
handleUnhandleds()

return (severity: 'fatal' | 'error' | 'warn', scope?: string) => (input: Error | string, scopeOrOpts?: string | Options, opts: Options = {}) => {
return (severity: 'fatal' | 'error' | 'warn') => (input: Error | string, opts: Options = {}) => {
if (!input) return
if (typeof scopeOrOpts === 'string') scope = scopeOrOpts
else if (typeof scopeOrOpts === 'object') opts = scopeOrOpts
const error = new CLIError(input, severity, scope, opts)
const msg: Message = {type: 'error', scope, severity, error}
const error = new CLIError(input, severity, opts)
const msg: Message = {type: 'error', severity, error}
e.emit('output', msg)
if (error['cli-ux'].exit !== false) throw error
}
Expand Down
59 changes: 27 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,38 @@ const output = Output(e)
const errors = Errors(e)
const logger = Logger.default(e)

export const scope = (_scope?: string) => {
return {
config,
scope,

trace: output('trace', _scope),
debug: output('debug', _scope),
info: output('info', _scope),
log: output('info', _scope),

warn: errors('warn', _scope),
error: errors('error', _scope),
fatal: errors('fatal', _scope),

exit(code = 1, error?: Error) { throw new ExitError(code, error) },
notify,

get prompt() { return deps.prompt.prompt },
get confirm() { return deps.prompt.confirm },
get action() { return config.action },
get styledObject() { return deps.styledObject },
get styledHeader() { return deps.styledHeader },
get styledJSON() { return deps.styledJSON },
get table() { return deps.table },

async done() {
config.action.stop()
await logger.flush()
// await flushStdout()
}
export const cli = {
config,
trace: output('trace'),
debug: output('debug'),
info: output('info'),
log: output('info'),

warn: errors('warn'),
error: errors('error'),
fatal: errors('fatal'),

exit(code = 1, error?: Error) { throw new ExitError(code, error) },
notify,

get prompt() { return deps.prompt.prompt },
get confirm() { return deps.prompt.confirm },
get action() { return config.action },
styledObject(obj: any, keys?: string[]) { cli.info(deps.styledObject(obj, keys)) },
get styledHeader() { return deps.styledHeader },
get styledJSON() { return deps.styledJSON },
get table() { return deps.table },

async done() {
config.action.stop()
await logger.flush()
// await flushStdout()
}
}

export const cli = scope()
export default cli

export {
config,
ActionBase,
CLIError,
Config,
Expand Down
2 changes: 1 addition & 1 deletion src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default (e: IEventEmitter) => {
const handleOutput = (m: Output.Message | Errors.Message) => {
if (!canWrite(m.severity)) return
const msg = m.type === 'error' ? Errors.getErrorMessage(m.error) : Output.render(m)
const output = chomp(_([timestamp(), m.severity.toUpperCase(), m.scope, msg]).compact().join(' '))
const output = chomp(_([timestamp(), m.severity.toUpperCase(), msg]).compact().join(' '))
buffer.push(deps.stripAnsi(output))
flush(50).catch(console.error)
}
Expand Down
7 changes: 3 additions & 4 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import {IEventEmitter} from './events'

export interface Message {
type: 'output'
scope: string | undefined
severity: 'trace' | 'debug' | 'info'
data: any[]
}

export function render(m: Message): string {
const msg = m.data.map(a => typeof a === 'string' ? a : inspect(a)).join(' ')
return (m.scope ? `${m.scope}: ${msg}` : msg) + '\n'
return msg + '\n'
}

function shouldLog(m: Message) {
Expand All @@ -31,8 +30,8 @@ export default (e: IEventEmitter) => {
process.stdout.write(render(m))
})

return (severity: 'trace' | 'debug' | 'info', scope?: string) => (...data: any[]) => {
const msg: Message = {type: 'output', scope, severity, data}
return (severity: 'trace' | 'debug' | 'info') => (...data: any[]) => {
const msg: Message = {type: 'output', severity, data}
e.emit('output', msg)
}
}
14 changes: 8 additions & 6 deletions src/styled/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import chalk from 'chalk'
import * as util from 'util'

export default function styledObject(obj: any, keys?: string[]) {
export default function styledObject(obj: any, keys?: string[]): string {
let output: string[] = []
let keyLengths = Object.keys(obj).map(key => key.toString().length)
let maxKeyLength = Math.max.apply(Math, keyLengths) + 2
function pp(obj: any) {
Expand All @@ -16,20 +17,21 @@ export default function styledObject(obj: any, keys?: string[]) {
return util.inspect(obj)
}
}
let logKeyValue = (key: string, value: any) => {
console.log(`${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value))
let logKeyValue = (key: string, value: any): string => {
return `${chalk.blue(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + pp(value)
}
for (let key of keys || Object.keys(obj).sort()) {
let value = obj[key]
if (Array.isArray(value)) {
if (value.length > 0) {
logKeyValue(key, value[0])
output.push(logKeyValue(key, value[0]))
for (let e of value.slice(1)) {
console.log(' '.repeat(maxKeyLength) + pp(e))
output.push(' '.repeat(maxKeyLength) + pp(e))
}
}
} else if (value !== null && value !== undefined) {
logKeyValue(key, value)
output.push(logKeyValue(key, value))
}
}
return output.join('\n')
}
23 changes: 19 additions & 4 deletions test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ import {expect, fancy} from './fancy'

describe('errors', () => {
fancy
.env({CI: null})
.stderr()
.end('warns', async output => {
.it('warns', async output => {
cli.warn('foobar')
if (process.platform === 'win32') {
expect(output.stderr).to.equal(' ! foobar\n')
expect(output.stderr).to.equal(' ! Warning: foobar\n')
} else {
expect(output.stderr).to.equal(' ▸ foobar\n')
expect(output.stderr).to.equal(' ▸ Warning: foobar\n')
}
})

fancy
.end('errors', async () => {
.env({CI: null})
.stderr()
.it('warns with context', async output => {
cli.config.context = {foo: 'bar'}
cli.warn('foobar')
if (process.platform === 'win32') {
expect(output.stderr).to.equal(' ! Warning: foobar\n ! foo: bar\n')
} else {
expect(output.stderr).to.equal(' ▸ Warning: foobar\n ▸ foo: bar\n')
}
})

fancy
.env({CI: null})
.it('errors', async () => {
expect(() => cli.error('foobar')).to.throw(/foobar/)
})
})
12 changes: 0 additions & 12 deletions test/logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,6 @@ describe('logger', () => {
expect(fs.readFileSync(cli.config.errlog!, 'utf8')).to.contain(' ERROR showerror')
})

fancy
.stdout()
.stderr()
.end('uses scope', async () => {
let _cli = cli.scope('mynewscope')
_cli.warn('showwarning')
_cli.info('hideme')
_cli.error('showerror', {exit: false})
await cli.done()
expect(fs.readFileSync(cli.config.errlog!, 'utf8')).to.contain(' WARN mynewscope showwarning')
})

fancy
.stdout()
.stderr()
Expand Down
Loading

0 comments on commit 36c2a6e

Please sign in to comment.