Skip to content

Commit d3543e9

Browse files
committed
feat: output json formatted errors on stdout (#5716)
This also adds a new output method `outputBuffer()` which will buffer all output until it is flushed in the exit handler. This allows the exit handler to catch any errors and append them to the output when in json mode. This was necessary to not introduce a regression in the case of #2150. BREAKING CHANGE: `npm` now outputs some json errors on stdout. Previously `npm` would output all json formatted errors on stderr, making it difficult to parse as the stderr stream usually has logs already written to it. In the future, `npm` will differentiate between errors and crashes. Errors, such as `E404` and `ERESOLVE`, will be handled and will continue to be output on stdout. In the case of a crash, `npm` will log the error as usual but will not attempt to display it as json, even in `--json` mode. Moving a case from the category of an error to a crash will not be considered a breaking change. For more information see npm/rfcs#482. Closes #2740 Closes npm/statusboard#589
1 parent be642c6 commit d3543e9

File tree

5 files changed

+94
-5
lines changed

5 files changed

+94
-5
lines changed

lib/commands/ls.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class LS extends ArboristWorkspaceCmd {
177177
const [rootError] = tree.errors.filter(e =>
178178
e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json'))
179179

180-
this.npm.output(
180+
this.npm.outputBuffer(
181181
json
182182
? jsonOutput({ path, problems, result, rootError, seenItems })
183183
: parseable

lib/npm.js

+32
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class Npm extends EventEmitter {
4040
#argvClean = []
4141
#chalk = null
4242

43+
#outputBuffer = []
4344
#logFile = new LogFile()
4445
#display = new Display()
4546
#timers = new Timers({
@@ -466,6 +467,37 @@ class Npm extends EventEmitter {
466467
log.showProgress()
467468
}
468469

470+
outputBuffer (item) {
471+
this.#outputBuffer.push(item)
472+
}
473+
474+
flushOutput (jsonError) {
475+
if (!jsonError && !this.#outputBuffer.length) {
476+
return
477+
}
478+
479+
if (this.config.get('json')) {
480+
const jsonOutput = this.#outputBuffer.reduce((acc, item) => {
481+
if (typeof item === 'string') {
482+
// try to parse it as json in case its a string
483+
try {
484+
item = JSON.parse(item)
485+
} catch {
486+
return acc
487+
}
488+
}
489+
return { ...acc, ...item }
490+
}, {})
491+
this.output(JSON.stringify({ ...jsonOutput, ...jsonError }, null, 2))
492+
} else {
493+
for (const item of this.#outputBuffer) {
494+
this.output(item)
495+
}
496+
}
497+
498+
this.#outputBuffer.length = 0
499+
}
500+
469501
outputError (...msg) {
470502
log.clearProgress()
471503
// eslint-disable-next-line no-console

lib/utils/exit-handler.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ const exitHandler = err => {
136136

137137
let exitCode
138138
let noLogMessage
139+
let jsonError
139140

140141
if (err) {
141142
exitCode = 1
@@ -198,14 +199,13 @@ const exitHandler = err => {
198199
}
199200

200201
if (hasLoadedNpm && npm.config.get('json')) {
201-
const error = {
202+
jsonError = {
202203
error: {
203204
code: err.code,
204205
summary: messageText(summary),
205206
detail: messageText(detail),
206207
},
207208
}
208-
npm.outputError(JSON.stringify(error, null, 2))
209209
}
210210

211211
if (typeof err.errno === 'number') {
@@ -216,6 +216,10 @@ const exitHandler = err => {
216216
}
217217
}
218218

219+
if (hasLoadedNpm) {
220+
npm.flushOutput(jsonError)
221+
}
222+
219223
log.verbose('exit', exitCode || 0)
220224

221225
showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage

test/fixtures/mock-npm.js

+9
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,15 @@ class MockNpm {
238238
}
239239
this._mockOutputs.push(msg)
240240
}
241+
242+
// with the older fake mock npm there is no
243+
// difference between output and outputBuffer
244+
// since it just collects the output and never
245+
// calls the exit handler, so we just mock the
246+
// method the same as output.
247+
outputBuffer (...msg) {
248+
this.output(...msg)
249+
}
241250
}
242251

243252
const FakeMockNpm = (base = {}, t) => {

test/lib/utils/exit-handler.js

+46-2
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,15 @@ t.test('exit handler called - no npm with error without stack', async (t) => {
208208
})
209209

210210
t.test('console.log output using --json', async (t) => {
211-
const { exitHandler, outputErrors } = await mockExitHandler(t, {
211+
const { exitHandler, outputs } = await mockExitHandler(t, {
212212
config: { json: true },
213213
})
214214

215215
await exitHandler(err('Error: EBADTHING Something happened'))
216216

217217
t.equal(process.exitCode, 1)
218218
t.same(
219-
JSON.parse(outputErrors[0]),
219+
JSON.parse(outputs[0]),
220220
{
221221
error: {
222222
code: 'EBADTHING', // should default error code to E[A-Z]+
@@ -228,6 +228,50 @@ t.test('console.log output using --json', async (t) => {
228228
)
229229
})
230230

231+
t.test('merges output buffers errors with --json', async (t) => {
232+
const { exitHandler, outputs, npm } = await mockExitHandler(t, {
233+
config: { json: true },
234+
})
235+
236+
npm.outputBuffer({ output_data: 1 })
237+
npm.outputBuffer(JSON.stringify({ more_data: 2 }))
238+
npm.outputBuffer('not json, will be ignored')
239+
240+
await exitHandler(err('Error: EBADTHING Something happened'))
241+
242+
t.equal(process.exitCode, 1)
243+
t.same(
244+
JSON.parse(outputs[0]),
245+
{
246+
output_data: 1,
247+
more_data: 2,
248+
error: {
249+
code: 'EBADTHING', // should default error code to E[A-Z]+
250+
summary: 'Error: EBADTHING Something happened',
251+
detail: 'Error: EBADTHING Something happened',
252+
},
253+
},
254+
'should output expected json output'
255+
)
256+
})
257+
258+
t.test('output buffer without json', async (t) => {
259+
const { exitHandler, outputs, npm, logs } = await mockExitHandler(t)
260+
261+
npm.outputBuffer('output_data')
262+
npm.outputBuffer('more_data')
263+
264+
await exitHandler(err('Error: EBADTHING Something happened'))
265+
266+
t.equal(process.exitCode, 1)
267+
t.same(
268+
outputs,
269+
[['output_data'], ['more_data']],
270+
'should output expected output'
271+
)
272+
t.match(logs.error, [['code', 'EBADTHING']])
273+
})
274+
231275
t.test('throw a non-error obj', async (t) => {
232276
const { exitHandler, logs } = await mockExitHandler(t)
233277

0 commit comments

Comments
 (0)