Skip to content

Commit ef4c975

Browse files
authoredMay 16, 2024··
fix(view): dont immediately exit on first workspace 404 (#7508)
Fixes: #5444 This PR will continue running through all workspaces for `npm view` even when a workspace encounters an `E404` error. This usually happens when you run `npm view -ws` but have private workspaces. A future iteration could log a different message if an `E404` is encountered on a private workspace, but for this PR I wanted to keep it generic since there are a number of reasons a request for a package manifest could 404.
1 parent 328f63c commit ef4c975

File tree

5 files changed

+282
-6
lines changed

5 files changed

+282
-6
lines changed
 

‎lib/commands/view.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const columns = require('cli-columns')
22
const { readFile } = require('fs/promises')
33
const jsonParse = require('json-parse-even-better-errors')
4-
const { log, output } = require('proc-log')
4+
const { log, output, META } = require('proc-log')
55
const npa = require('npm-package-arg')
66
const { resolve } = require('path')
77
const formatBytes = require('../utils/format-bytes.js')
@@ -11,6 +11,8 @@ const { inspect } = require('util')
1111
const { packument } = require('pacote')
1212
const Queryable = require('../utils/queryable.js')
1313
const BaseCommand = require('../base-cmd.js')
14+
const { getError } = require('../utils/error-message.js')
15+
const { jsonError, outputError } = require('../utils/output-error.js')
1416

1517
const readJson = file => readFile(file, 'utf8').then(jsonParse)
1618

@@ -76,10 +78,24 @@ class View extends BaseCommand {
7678
return this.exec([pkg, ...rest])
7779
}
7880

81+
const json = this.npm.config.get('json')
7982
await this.setWorkspaces()
8083

8184
for (const name of this.workspaceNames) {
82-
await this.#viewPackage(`${name}${pkg.slice(1)}`, rest, { workspace: true })
85+
try {
86+
await this.#viewPackage(`${name}${pkg.slice(1)}`, rest, { workspace: true })
87+
} catch (e) {
88+
const err = getError(e, { npm: this.npm, command: this })
89+
if (err.code !== 'E404') {
90+
throw e
91+
}
92+
if (json) {
93+
output.buffer({ [META]: true, jsonError: { [name]: jsonError(err, this.npm) } })
94+
} else {
95+
outputError(err)
96+
}
97+
process.exitCode = err.exitCode
98+
}
8399
}
84100
}
85101

‎lib/utils/display.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ const mergeJson = ({ [JSON_ERROR_KEY]: metaError }, buffer) => {
104104
if (obj && typeof obj === 'object') {
105105
items.push(obj)
106106
}
107-
/* istanbul ignore next - this is not used yet but will be */
108107
if (error) {
109108
errors.push(error)
110109
}
@@ -127,9 +126,8 @@ const mergeJson = ({ [JSON_ERROR_KEY]: metaError }, buffer) => {
127126
// names. So the output could already have the same key. The choice here is to overwrite
128127
// it with our error since that is (probably?) more important.
129128
// XXX(BREAKING_CHANGE): all json output should be keyed under well known keys, eg `result` and `error`
130-
/* istanbul ignore next */
131129
if (res[ERROR_KEY]) {
132-
log.warn('display', `overwriting existing ${ERROR_KEY} on json output`)
130+
log.warn('', `overwriting existing ${ERROR_KEY} on json output`)
133131
}
134132
res[ERROR_KEY] = getArrayOrObject(errors)
135133
}

‎tap-snapshots/test/lib/commands/view.js.test.cjs

+147
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,153 @@ yellow@1.0.1 'claudia'
363363
yellow@1.0.2 'claudia'
364364
`
365365

366+
exports[`test/lib/commands/view.js TAP workspaces 404 workspaces basic > must match snapshot 1`] = `
367+
368+
green@1.0.0 | ACME | deps: 2 | versions: 2
369+
green is a very important color
370+
371+
DEPRECATED!! - true
372+
373+
keywords: colors, green, crayola
374+
375+
bin: green
376+
377+
dist
378+
.tarball: http://hm.green.com/1.0.0.tgz
379+
.shasum: 123
380+
.integrity: ---
381+
.unpackedSize: 1.0 GB
382+
383+
dependencies:
384+
red: 1.0.0
385+
yellow: 1.0.0
386+
387+
maintainers:
388+
- claudia <c@yellow.com>
389+
- isaacs <i@yellow.com>
390+
391+
dist-tags:
392+
latest: 1.0.0
393+
error code E404
394+
error 404 404
395+
`
396+
397+
exports[`test/lib/commands/view.js TAP workspaces 404 workspaces json > must match snapshot 1`] = `
398+
{
399+
"green": {
400+
"_id": "green",
401+
"name": "green",
402+
"dist-tags": {
403+
"latest": "1.0.0"
404+
},
405+
"maintainers": [
406+
{
407+
"name": "claudia",
408+
"email": "c@yellow.com",
409+
"twitter": "cyellow"
410+
},
411+
{
412+
"name": "isaacs",
413+
"email": "i@yellow.com",
414+
"twitter": "iyellow"
415+
}
416+
],
417+
"keywords": [
418+
"colors",
419+
"green",
420+
"crayola"
421+
],
422+
"versions": [
423+
"1.0.0",
424+
"1.0.1"
425+
],
426+
"version": "1.0.0",
427+
"description": "green is a very important color",
428+
"bugs": {
429+
"url": "http://bugs.green.com"
430+
},
431+
"deprecated": true,
432+
"repository": {
433+
"url": "http://repository.green.com"
434+
},
435+
"license": {
436+
"type": "ACME"
437+
},
438+
"bin": {
439+
"green": "bin/green.js"
440+
},
441+
"dependencies": {
442+
"red": "1.0.0",
443+
"yellow": "1.0.0"
444+
},
445+
"dist": {
446+
"shasum": "123",
447+
"tarball": "http://hm.green.com/1.0.0.tgz",
448+
"integrity": "---",
449+
"fileCount": 1,
450+
"unpackedSize": 1000000000
451+
}
452+
},
453+
"error": {
454+
"missing-package": {
455+
"code": "E404",
456+
"summary": "404",
457+
"detail": ""
458+
}
459+
}
460+
}
461+
`
462+
463+
exports[`test/lib/commands/view.js TAP workspaces 404 workspaces json with package named error > must match snapshot 1`] = `
464+
warn overwriting existing error on json output
465+
{
466+
"error": {
467+
"missing-package": {
468+
"code": "E404",
469+
"summary": "404",
470+
"detail": ""
471+
}
472+
}
473+
}
474+
`
475+
476+
exports[`test/lib/commands/view.js TAP workspaces 404 workspaces non-404 error rejects > must match snapshot 1`] = `
477+
478+
green@1.0.0 | ACME | deps: 2 | versions: 2
479+
green is a very important color
480+
481+
DEPRECATED!! - true
482+
483+
keywords: colors, green, crayola
484+
485+
bin: green
486+
487+
dist
488+
.tarball: http://hm.green.com/1.0.0.tgz
489+
.shasum: 123
490+
.integrity: ---
491+
.unpackedSize: 1.0 GB
492+
493+
dependencies:
494+
red: 1.0.0
495+
yellow: 1.0.0
496+
497+
maintainers:
498+
- claudia <c@yellow.com>
499+
- isaacs <i@yellow.com>
500+
501+
dist-tags:
502+
latest: 1.0.0
503+
error Unknown error
504+
`
505+
506+
exports[`test/lib/commands/view.js TAP workspaces 404 workspaces non-404 error rejects with single arg > must match snapshot 1`] = `
507+
green:
508+
1.0.0
509+
unknown-error:
510+
error Unknown error
511+
`
512+
366513
exports[`test/lib/commands/view.js TAP workspaces all workspaces --json > must match snapshot 1`] = `
367514
{
368515
"green": {

‎test/fixtures/mock-logs.js

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const logsByTitle = (logs) => ({
2424
module.exports = () => {
2525
const outputs = []
2626
const outputErrors = []
27+
const fullOutput = []
2728

2829
const levelLogs = []
2930
const logs = Object.defineProperties([], {
@@ -53,6 +54,7 @@ module.exports = () => {
5354
// in the future if/when we refactor what logs look like.
5455
if (!isLog(str)) {
5556
outputErrors.push(str)
57+
fullOutput.push(str)
5658
return
5759
}
5860

@@ -70,12 +72,14 @@ module.exports = () => {
7072
const level = stripAnsi(rawLevel)
7173

7274
logs.push(str.replaceAll(prefix, `${level} `))
75+
fullOutput.push(str.replaceAll(prefix, `${level} `))
7376
levelLogs.push({ level, message: str.replaceAll(prefix, '') })
7477
},
7578
},
7679
stdout: {
7780
write: (str) => {
7881
outputs.push(trimTrailingNewline(str))
82+
fullOutput.push(trimTrailingNewline(str))
7983
},
8084
},
8185
}
@@ -88,9 +92,12 @@ module.exports = () => {
8892
clearOutput: () => {
8993
outputs.length = 0
9094
outputErrors.length = 0
95+
fullOutput.length = 0
9196
},
9297
outputErrors,
9398
joinedOutputError: () => joinAndTrimTrailingNewlines(outputs),
99+
fullOutput,
100+
joinedFullOutput: () => joinAndTrimTrailingNewlines(fullOutput),
94101
logs,
95102
clearLogs: () => {
96103
levelLogs.length = 0

‎test/lib/commands/view.js

+109-1
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,45 @@ const packument = (nv, opts) => {
270270
},
271271
},
272272
},
273+
// this is a packaged named error which will conflict with
274+
// the error key in json output
275+
error: {
276+
_id: 'error',
277+
name: 'error',
278+
'dist-tags': {
279+
latest: '1.0.0',
280+
},
281+
versions: {
282+
'1.0.0': {
283+
name: 'error',
284+
version: '1.0.0',
285+
dist: {
286+
shasum: '123',
287+
tarball: 'http://hm.error.com/1.0.0.tgz',
288+
fileCount: 1,
289+
},
290+
},
291+
},
292+
},
273293
}
294+
274295
if (nv.type === 'git') {
275296
return mocks[nv.hosted.project]
276297
}
298+
277299
if (nv.raw === './blue') {
278300
return mocks.blue
279301
}
280-
return mocks[nv.name]
302+
303+
if (mocks[nv.name]) {
304+
return mocks[nv.name]
305+
}
306+
307+
if (nv.name === 'unknown-error') {
308+
throw new Error('Unknown error')
309+
}
310+
311+
throw Object.assign(new Error('404'), { code: 'E404' })
281312
}
282313

283314
const loadMockNpm = async function (t, opts = {}) {
@@ -543,6 +574,33 @@ t.test('workspaces', async t => {
543574
},
544575
}
545576

577+
const prefixDir404 = {
578+
'test-workspace-b': {
579+
'package.json': JSON.stringify({
580+
name: 'missing-package',
581+
version: '1.2.3',
582+
}),
583+
},
584+
}
585+
586+
const prefixDirError = {
587+
'test-workspace-b': {
588+
'package.json': JSON.stringify({
589+
name: 'unknown-error',
590+
version: '1.2.3',
591+
}),
592+
},
593+
}
594+
595+
const prefixPackageNamedError = {
596+
'test-workspace-a': {
597+
'package.json': JSON.stringify({
598+
name: 'error',
599+
version: '1.2.3',
600+
}),
601+
},
602+
}
603+
546604
t.test('all workspaces', async t => {
547605
const { view, joinedOutput } = await loadMockNpm(t, {
548606
prefixDir,
@@ -624,6 +682,56 @@ t.test('workspaces', async t => {
624682
t.matchSnapshot(joinedOutput())
625683
t.matchSnapshot(logs.warn, 'should have warning of ignoring workspaces')
626684
})
685+
686+
t.test('404 workspaces', async t => {
687+
t.test('basic', async t => {
688+
const { view, joinedFullOutput } = await loadMockNpm(t, {
689+
prefixDir: { ...prefixDir, ...prefixDir404 },
690+
config: { workspaces: true, loglevel: 'error' },
691+
})
692+
await view.exec([])
693+
t.matchSnapshot(joinedFullOutput())
694+
t.equal(process.exitCode, 1)
695+
})
696+
697+
t.test('json', async t => {
698+
const { view, joinedFullOutput } = await loadMockNpm(t, {
699+
prefixDir: { ...prefixDir, ...prefixDir404 },
700+
config: { workspaces: true, json: true, loglevel: 'error' },
701+
})
702+
await view.exec([])
703+
t.matchSnapshot(joinedFullOutput())
704+
t.equal(process.exitCode, 1)
705+
})
706+
707+
t.test('json with package named error', async t => {
708+
const { view, joinedFullOutput } = await loadMockNpm(t, {
709+
prefixDir: { ...prefixDir, ...prefixDir404, ...prefixPackageNamedError },
710+
config: { workspaces: true, json: true, loglevel: 'warn' },
711+
})
712+
await view.exec([])
713+
t.matchSnapshot(joinedFullOutput())
714+
t.equal(process.exitCode, 1)
715+
})
716+
717+
t.test('non-404 error rejects', async t => {
718+
const { view, joinedFullOutput } = await loadMockNpm(t, {
719+
prefixDir: { ...prefixDir, ...prefixDirError },
720+
config: { workspaces: true, loglevel: 'error' },
721+
})
722+
await t.rejects(view.exec([]))
723+
t.matchSnapshot(joinedFullOutput())
724+
})
725+
726+
t.test('non-404 error rejects with single arg', async t => {
727+
const { view, joinedFullOutput } = await loadMockNpm(t, {
728+
prefixDir: { ...prefixDir, ...prefixDirError },
729+
config: { workspaces: true, loglevel: 'error' },
730+
})
731+
await t.rejects(view.exec(['.', 'version']))
732+
t.matchSnapshot(joinedFullOutput())
733+
})
734+
})
627735
})
628736

629737
t.test('completion', async t => {

0 commit comments

Comments
 (0)
Please sign in to comment.