Skip to content

Commit 6b1c114

Browse files
aarondillkeithamusfisker
authored
feat: better Error handling (#284)
* feat(cli): add --quiet / -q option Stops any successful output. Errors are still outputted. * refactor(cli): create output functions This commit creates stdout and stderr functions for outputting with respect to isQuiet and adds support for differing (easier to parse) messages if not connected to a TTY (eg. a pipe or process substitution) Additionally changes the 'No matching files.' message to output on stderr rather than on stdout as it was, which makes sense because this output represents erroneous usage. * fix(cli): change stderr to not respect --quiet * fix(cli): change stdout to stderr in isTerminal * fix(tests): add STD(OUT/ERR)_IS_TTY env variables these variables can be set to force output on the specified chanel as if it were connected to a terminal. macro.testCLI can now take an argument of the following form: isTerminal: { stderr: false, stdout: false }, * fix(cli): improve output of CLI when not a tty * test: added tests for --verson and --help * test: add tests for --quiet * test: include isTerminal in snapshot * test: add tests for TTY detection and integration * test: typo, stderr --> stdout * fix(cli): exit code while sort is number of failed The exit code is now the number of files which failed when sorting. On a successful run, this will be 0 and exit w/ success, but if any errors occur, this will be incremented. In check mode, this is fails + unsorted. * fix: wrap file operation in try catch Wraps fs calls in trycatch to not throw and to count failures. Also better messages and script usage * docs: document changes to tty-based output * fix(test): compatability w/ node v14 * fix: compatability with node 12 * test: add tests for improper usage * test: support for node 12&14 in error test * refactor: remove extra changes to improve diff * revert: remove all tty detection * refactor: update to meet upstream * fix: fixes error reporting with quiet * fix: bad merge * style: prettier * fix: fixes permissions on cli.js * typo * refactor: improve exit code handling, and set 2 on error * feat: added summary at end of tool run * fix: better show that output on error, is an error * refactor: cleaner logic * refactor: pass `files` to `constructor` * refactor: save `matchedFilesCount` to status * fix: remove `0 files`, use `1 file` instead of `1 files` * refactor: extract `Reporter` --------- Co-authored-by: Keith Cirkel <keithamus@users.noreply.github.com> Co-authored-by: fisker Cheung <lionkay@gmail.com>
1 parent 7be9d3a commit 6b1c114

File tree

6 files changed

+330
-47
lines changed

6 files changed

+330
-47
lines changed

cli.js

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { globbySync } from 'globby'
33
import fs from 'node:fs'
44
import sortPackageJson from './index.js'
5+
import Reporter from './reporter.js'
56

67
function showVersion() {
78
const { name, version } = JSON.parse(
@@ -26,52 +27,34 @@ If file/glob is omitted, './package.json' file will be processed.
2627
)
2728
}
2829

29-
function sortPackageJsonFiles(patterns, { isCheck, shouldBeQuiet }) {
30-
const files = globbySync(patterns)
31-
const printToStdout = shouldBeQuiet ? () => {} : console.log
32-
33-
if (files.length === 0) {
34-
console.error('No matching files.')
35-
process.exitCode = 2
36-
return
30+
function sortPackageJsonFile(file, reporter, isCheck) {
31+
const original = fs.readFileSync(file, 'utf8')
32+
const sorted = sortPackageJson(original)
33+
if (sorted === original) {
34+
return reporter.reportNotChanged(file)
3735
}
3836

39-
let notSortedFiles = 0
40-
for (const file of files) {
41-
const packageJson = fs.readFileSync(file, 'utf8')
42-
const sorted = sortPackageJson(packageJson)
43-
44-
if (sorted !== packageJson) {
45-
if (isCheck) {
46-
notSortedFiles++
47-
printToStdout(file)
48-
process.exitCode = 1
49-
} else {
50-
fs.writeFileSync(file, sorted)
51-
52-
printToStdout(`${file} is sorted!`)
53-
}
54-
}
37+
if (!isCheck) {
38+
fs.writeFileSync(file, sorted)
5539
}
5640

57-
if (isCheck) {
58-
// Print a empty line
59-
printToStdout()
41+
reporter.reportChanged(file)
42+
}
43+
44+
function sortPackageJsonFiles(patterns, options) {
45+
const files = globbySync(patterns)
46+
const reporter = new Reporter(files, options)
47+
const { isCheck } = options
6048

61-
if (notSortedFiles) {
62-
printToStdout(
63-
notSortedFiles === 1
64-
? `${notSortedFiles} of ${files.length} matched file is not sorted.`
65-
: `${notSortedFiles} of ${files.length} matched files are not sorted.`,
66-
)
67-
} else {
68-
printToStdout(
69-
files.length === 1
70-
? `${files.length} matched file is sorted.`
71-
: `${files.length} matched files are sorted.`,
72-
)
49+
for (const file of files) {
50+
try {
51+
sortPackageJsonFile(file, reporter, isCheck)
52+
} catch (error) {
53+
reporter.reportFailed(file, error)
7354
}
7455
}
56+
57+
reporter.printSummary()
7558
}
7659

7760
function run() {

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"files": [
2525
"index.js",
2626
"index.d.ts",
27-
"cli.js"
27+
"cli.js",
28+
"reporter.js"
2829
],
2930
"scripts": {
3031
"lint": "eslint .",

reporter.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const getFilesCountText = (count) => (count === 1 ? '1 file' : `${count} files`)
2+
3+
class Reporter {
4+
#hasPrinted = false
5+
#options
6+
#status
7+
#logger
8+
9+
constructor(files, options) {
10+
this.#options = options
11+
this.#status = {
12+
matchedFilesCount: files.length,
13+
failedFilesCount: 0,
14+
wellSortedFilesCount: 0,
15+
changedFilesCount: 0,
16+
}
17+
18+
this.#logger = options.shouldBeQuiet
19+
? { log() {}, error() {} }
20+
: {
21+
log: (...args) => {
22+
this.#hasPrinted = true
23+
console.log(...args)
24+
},
25+
error: (...args) => {
26+
this.#hasPrinted = true
27+
console.error(...args)
28+
},
29+
}
30+
}
31+
32+
// The file is well-sorted
33+
reportNotChanged(/* file */) {
34+
this.#status.wellSortedFilesCount++
35+
}
36+
37+
reportChanged(file) {
38+
this.#status.changedFilesCount++
39+
this.#logger.log(this.#options.isCheck ? `${file}` : `${file} is sorted!`)
40+
}
41+
42+
reportFailed(file, error) {
43+
this.#status.failedFilesCount++
44+
45+
console.error('Error on: ' + file)
46+
this.#logger.error(error.message)
47+
}
48+
49+
printSummary() {
50+
const {
51+
matchedFilesCount,
52+
failedFilesCount,
53+
changedFilesCount,
54+
wellSortedFilesCount,
55+
} = this.#status
56+
57+
if (matchedFilesCount === 0) {
58+
console.error('No matching files.')
59+
process.exitCode = 2
60+
return
61+
}
62+
63+
const { isCheck, isQuiet } = this.#options
64+
65+
if (isCheck && changedFilesCount) {
66+
process.exitCode = 1
67+
}
68+
69+
if (failedFilesCount) {
70+
process.exitCode = 2
71+
}
72+
73+
if (isQuiet) {
74+
return
75+
}
76+
77+
const { log } = this.#logger
78+
79+
// Print an empty line.
80+
if (this.#hasPrinted) {
81+
log()
82+
}
83+
84+
// Matched files
85+
log('Found %s.', getFilesCountText(matchedFilesCount))
86+
87+
// Failed files
88+
if (failedFilesCount) {
89+
log(
90+
'%s could not be %s.',
91+
getFilesCountText(failedFilesCount),
92+
isCheck ? 'checked' : 'sorted',
93+
)
94+
}
95+
96+
// Changed files
97+
if (changedFilesCount) {
98+
if (isCheck) {
99+
log(
100+
'%s %s not sorted.',
101+
getFilesCountText(changedFilesCount),
102+
changedFilesCount === 1 ? 'was' : 'were',
103+
)
104+
} else {
105+
log('%s successfully sorted.', getFilesCountText(changedFilesCount))
106+
}
107+
}
108+
109+
// Well-sorted files
110+
if (wellSortedFilesCount) {
111+
log(
112+
'%s %s already sorted.',
113+
getFilesCountText(wellSortedFilesCount),
114+
wellSortedFilesCount === 1 ? 'was' : 'were',
115+
)
116+
}
117+
}
118+
}
119+
120+
export default Reporter

tests/cli.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import fs from 'node:fs'
21
import test from 'ava'
2+
import fs from 'node:fs'
33
import { cliScript, macro } from './_helpers.js'
44

55
const badJson = {
@@ -438,3 +438,53 @@ test('run `cli --check --quiet` on duplicate patterns', macro.testCLI, {
438438
],
439439
message: 'Should not count `bad-1/package.json` more than once. Exit code 1',
440440
})
441+
442+
const badFormat = ''
443+
444+
test('run `cli --check` on 1 non-json file', macro.testCLI, {
445+
fixtures: [
446+
{
447+
file: 'notJson/package.json',
448+
content: badFormat,
449+
expect: badFormat,
450+
},
451+
],
452+
args: ['*/package.json', '--check'],
453+
message: 'Should fail to check, but not end execution.',
454+
})
455+
456+
test('run `cli --check --quiet` on 1 non-json file', macro.testCLI, {
457+
fixtures: [
458+
{
459+
file: 'notJson/package.json',
460+
content: badFormat,
461+
expect: badFormat,
462+
},
463+
],
464+
args: ['*/package.json', '--check', '--quiet'],
465+
message: 'Should output error message, but not count.',
466+
})
467+
468+
test('run `cli` on 1 non-json file', macro.testCLI, {
469+
fixtures: [
470+
{
471+
file: 'notJson/package.json',
472+
content: badFormat,
473+
expect: badFormat,
474+
},
475+
],
476+
args: ['*/package.json'],
477+
message: 'Should fail to check, but not end execution.',
478+
})
479+
480+
test('run `cli --quiet` on 1 non-json file', macro.testCLI, {
481+
fixtures: [
482+
{
483+
file: 'notJson/package.json',
484+
content: badFormat,
485+
expect: badFormat,
486+
},
487+
],
488+
args: ['*/package.json', '--quiet'],
489+
message: 'Should output error message',
490+
})

0 commit comments

Comments
 (0)