diff --git a/lib/commands/access.js b/lib/commands/access.js index dad0c405f713b..547fa7af01577 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -208,7 +208,7 @@ class Access extends BaseCommand { outputs[item] = lookup[val] || val } if (this.npm.config.get('json')) { - output.standard(JSON.stringify(outputs, null, 2)) + output.buffer(outputs) } else { for (const item of Object.keys(outputs).sort(localeCompare)) { if (!limiter || limiter === item) { diff --git a/lib/commands/config.js b/lib/commands/config.js index e8da195204c00..6b1447d7e8426 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -403,7 +403,7 @@ ${defData} publicConf[key] = value } - output.standard(JSON.stringify(publicConf, null, 2)) + output.buffer(publicConf) } } diff --git a/lib/commands/explain.js b/lib/commands/explain.js index 2e7d07df729a8..caaeeee1e6939 100644 --- a/lib/commands/explain.js +++ b/lib/commands/explain.js @@ -76,7 +76,7 @@ class Explain extends ArboristWorkspaceCmd { } if (this.npm.flatOptions.json) { - output.standard(JSON.stringify(expls, null, 2)) + output.buffer(expls) } else { output.standard(expls.map(expl => { return explainNode(expl, Infinity, this.npm.chalk) diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 8bb4c304b379b..8c194dac80b49 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -85,16 +85,12 @@ class Fund extends ArboristWorkspaceCmd { }) if (this.npm.config.get('json')) { - output.standard(this.printJSON(fundingInfo)) + output.buffer(fundingInfo) } else { output.standard(this.printHuman(fundingInfo)) } } - printJSON (fundingInfo) { - return JSON.stringify(fundingInfo, null, 2) - } - printHuman (fundingInfo) { const unicode = this.npm.config.get('unicode') const seenUrls = new Map() diff --git a/lib/commands/hook.js b/lib/commands/hook.js index 43da92f3f9e6c..5793b974197c8 100644 --- a/lib/commands/hook.js +++ b/lib/commands/hook.js @@ -40,7 +40,7 @@ class Hook extends BaseCommand { async add (pkg, uri, secret, opts) { const hook = await hookApi.add(pkg, uri, secret, opts) if (opts.json) { - output.standard(JSON.stringify(hook, null, 2)) + output.buffer(hook) } else if (opts.parseable) { output.standard(Object.keys(hook).join('\t')) output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) @@ -53,7 +53,7 @@ class Hook extends BaseCommand { const hooks = await hookApi.ls({ ...opts, package: pkg }) if (opts.json) { - output.standard(JSON.stringify(hooks, null, 2)) + output.buffer(hooks) } else if (opts.parseable) { output.standard(Object.keys(hooks[0]).join('\t')) hooks.forEach(hook => { @@ -80,7 +80,7 @@ class Hook extends BaseCommand { async rm (id, opts) { const hook = await hookApi.rm(id, opts) if (opts.json) { - output.standard(JSON.stringify(hook, null, 2)) + output.buffer(hook) } else if (opts.parseable) { output.standard(Object.keys(hook).join('\t')) output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) @@ -92,7 +92,7 @@ class Hook extends BaseCommand { async update (id, uri, secret, opts) { const hook = await hookApi.update(id, uri, secret, opts) if (opts.json) { - output.standard(JSON.stringify(hook, null, 2)) + output.buffer(hook) } else if (opts.parseable) { output.standard(Object.keys(hook).join('\t')) output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) diff --git a/lib/commands/org.js b/lib/commands/org.js index caebadbc545e7..613498056f556 100644 --- a/lib/commands/org.js +++ b/lib/commands/org.js @@ -100,14 +100,12 @@ class Org extends BaseCommand { org = org.replace(/^[~@]?/, '') const userCount = Object.keys(roster).length if (opts.json) { - output.standard( - JSON.stringify({ - user, - org, - userCount, - deleted: true, - }) - ) + output.buffer({ + user, + org, + userCount, + deleted: true, + }) } else if (opts.parseable) { output.standard(['user', 'org', 'userCount', 'deleted'].join('\t')) output.standard([user, org, userCount, true].join('\t')) @@ -135,7 +133,7 @@ class Org extends BaseCommand { roster = newRoster } if (opts.json) { - output.standard(JSON.stringify(roster, null, 2)) + output.buffer(roster) } else if (opts.parseable) { output.standard(['user', 'role'].join('\t')) Object.keys(roster).forEach(u => { diff --git a/lib/commands/ping.js b/lib/commands/ping.js index 0d057862baa8f..3388ba1aa378e 100644 --- a/lib/commands/ping.js +++ b/lib/commands/ping.js @@ -16,11 +16,11 @@ class Ping extends BaseCommand { const time = Date.now() - start log.notice('PONG', `${time}ms`) if (this.npm.config.get('json')) { - output.standard(JSON.stringify({ + output.buffer({ registry: cleanRegistry, time, details, - }, null, 2)) + }) } else if (Object.keys(details).length) { log.notice('PONG', JSON.stringify(details, null, 2)) } diff --git a/lib/commands/profile.js b/lib/commands/profile.js index adf534730de24..d808bb820941f 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -108,7 +108,7 @@ class Profile extends BaseCommand { } if (this.npm.config.get('json')) { - output.standard(JSON.stringify(info, null, 2)) + output.buffer(info) return } @@ -211,7 +211,7 @@ class Profile extends BaseCommand { const result = await otplease(this.npm, conf, c => set(newUser, c)) if (this.npm.config.get('json')) { - output.standard(JSON.stringify({ [prop]: result[prop] }, null, 2)) + output.buffer({ [prop]: result[prop] }) } else if (this.npm.config.get('parseable')) { output.standard(prop + '\t' + result[prop]) } else if (result[prop] != null) { @@ -378,7 +378,7 @@ class Profile extends BaseCommand { await set({ tfa: { password: password, mode: 'disable' } }, conf) if (this.npm.config.get('json')) { - output.standard(JSON.stringify({ tfa: false }, null, 2)) + output.buffer({ tfa: false }) } else if (this.npm.config.get('parseable')) { output.standard('tfa\tfalse') } else { diff --git a/lib/commands/sbom.js b/lib/commands/sbom.js index ff7377581dfa5..278c6d506b42a 100644 --- a/lib/commands/sbom.js +++ b/lib/commands/sbom.js @@ -22,10 +22,6 @@ class SBOM extends BaseCommand { 'workspaces', ] - get #parsedResponse () { - return JSON.stringify(this.#response, null, 2) - } - async exec () { const sbomFormat = this.npm.config.get('sbom-format') const packageLockOnly = this.npm.config.get('package-lock-only') @@ -35,56 +31,43 @@ class SBOM extends BaseCommand { throw this.usageError(`Must specify --sbom-format flag with one of: ${SBOM_FORMATS.join(', ')}.`) } - const Arborist = require('@npmcli/arborist') - const opts = { ...this.npm.flatOptions, path: this.npm.prefix, forceActual: true, } + const Arborist = require('@npmcli/arborist') const arb = new Arborist(opts) - let tree - if (packageLockOnly) { - try { - tree = await arb.loadVirtual(opts) - } catch (err) { - /* eslint-disable-next-line max-len */ - throw this.usageError('A package lock or shrinkwrap file is required in package-lock-only mode') - } - } else { - tree = await arb.loadActual(opts) - } + const tree = packageLockOnly ? await arb.loadVirtual(opts).catch(() => { + /* eslint-disable-next-line max-len */ + throw this.usageError('A package lock or shrinkwrap file is required in package-lock-only mode') + }) : await arb.loadActual(opts) // Collect the list of selected workspaces in the project - let wsNodes - if (this.workspaceNames && this.workspaceNames.length) { - wsNodes = arb.workspaceNodes(tree, this.workspaceNames) - } + const wsNodes = this.workspaceNames?.length + ? arb.workspaceNodes(tree, this.workspaceNames) + : null // Build the selector and query the tree for the list of nodes const selector = this.#buildSelector({ wsNodes }) log.info('sbom', `Using dependency selector: ${selector}`) const items = await tree.querySelectorAll(selector) - const errors = new Set() - for (const node of items) { - detectErrors(node).forEach(error => errors.add(error)) - } - - if (errors.size > 0) { - throw Object.assign( - new Error([...errors].join('\n')), - { code: 'ESBOMPROBLEMS' } - ) + const errors = items.flatMap(node => detectErrors(node)) + if (errors.length) { + throw Object.assign(new Error([...new Set(errors)].join('\n')), { + code: 'ESBOMPROBLEMS', + }) } // Populate the response with the list of unique nodes (sorted by location) - this.#buildResponse( - items - .sort((a, b) => localeCompare(a.location, b.location)) - ) - output.standard(this.#parsedResponse) + this.#buildResponse(items.sort((a, b) => localeCompare(a.location, b.location))) + + // TODO(BREAKING_CHANGE): all sbom output is in json mode but setting it before + // any of the errors will cause those to be thrown in json mode. + this.npm.config.set('json', true) + output.buffer(this.#response) } async execWorkspaces (args) { @@ -122,10 +105,9 @@ class SBOM extends BaseCommand { const packageType = this.npm.config.get('sbom-type') const packageLockOnly = this.npm.config.get('package-lock-only') - this.#response = - sbomFormat === 'cyclonedx' - ? cyclonedxOutput({ npm: this.npm, nodes: items, packageType, packageLockOnly }) - : spdxOutput({ npm: this.npm, nodes: items, packageType }) + this.#response = sbomFormat === 'cyclonedx' + ? cyclonedxOutput({ npm: this.npm, nodes: items, packageType, packageLockOnly }) + : spdxOutput({ npm: this.npm, nodes: items, packageType }) } } diff --git a/lib/commands/team.js b/lib/commands/team.js index c36b6ef023a26..089e917909d10 100644 --- a/lib/commands/team.js +++ b/lib/commands/team.js @@ -68,10 +68,10 @@ class Team extends BaseCommand { async create (entity, opts) { await libteam.create(entity, opts) if (opts.json) { - output.standard(JSON.stringify({ + output.buffer({ created: true, team: entity, - })) + }) } else if (opts.parseable) { output.standard(`${entity}\tcreated`) } else if (!this.npm.silent) { @@ -82,10 +82,10 @@ class Team extends BaseCommand { async destroy (entity, opts) { await libteam.destroy(entity, opts) if (opts.json) { - output.standard(JSON.stringify({ + output.buffer({ deleted: true, team: entity, - })) + }) } else if (opts.parseable) { output.standard(`${entity}\tdeleted`) } else if (!this.npm.silent) { @@ -96,11 +96,11 @@ class Team extends BaseCommand { async add (entity, user, opts) { await libteam.add(user, entity, opts) if (opts.json) { - output.standard(JSON.stringify({ + output.buffer({ added: true, team: entity, user, - })) + }) } else if (opts.parseable) { output.standard(`${user}\t${entity}\tadded`) } else if (!this.npm.silent) { @@ -111,11 +111,11 @@ class Team extends BaseCommand { async rm (entity, user, opts) { await libteam.rm(user, entity, opts) if (opts.json) { - output.standard(JSON.stringify({ + output.buffer({ removed: true, team: entity, user, - })) + }) } else if (opts.parseable) { output.standard(`${user}\t${entity}\tremoved`) } else if (!this.npm.silent) { @@ -126,7 +126,7 @@ class Team extends BaseCommand { async listUsers (entity, opts) { const users = (await libteam.lsUsers(entity, opts)).sort() if (opts.json) { - output.standard(JSON.stringify(users, null, 2)) + output.buffer(users) } else if (opts.parseable) { output.standard(users.join('\n')) } else if (!this.npm.silent) { @@ -140,7 +140,7 @@ class Team extends BaseCommand { async listTeams (entity, opts) { const teams = (await libteam.lsTeams(entity, opts)).sort() if (opts.json) { - output.standard(JSON.stringify(teams, null, 2)) + output.buffer(teams) } else if (opts.parseable) { output.standard(teams.join('\n')) } else if (!this.npm.silent) { diff --git a/lib/commands/token.js b/lib/commands/token.js index ae58891a566c2..d2e85ffe5a549 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -50,7 +50,7 @@ class Token extends BaseCommand { log.info('token', 'getting list') const tokens = await listTokens(this.npm.flatOptions) if (json) { - output.standard(JSON.stringify(tokens, null, 2)) + output.buffer(tokens) return } if (parseable) { @@ -117,7 +117,7 @@ class Token extends BaseCommand { }) ) if (json) { - output.standard(JSON.stringify(toRemove)) + output.buffer(toRemove) } else if (parseable) { output.standard(toRemove.join('\t')) } else { @@ -142,7 +142,7 @@ class Token extends BaseCommand { delete result.key delete result.updated if (json) { - output.standard(JSON.stringify(result)) + output.buffer(result) } else if (parseable) { Object.keys(result).forEach(k => output.standard(k + '\t' + result[k])) } else { diff --git a/lib/commands/version.js b/lib/commands/version.js index 549ba9b9f9c77..d6c2dd4caed75 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -126,7 +126,7 @@ class Version extends BaseCommand { } if (this.npm.config.get('json')) { - output.standard(JSON.stringify(results, null, 2)) + output.buffer(results) } else { output.standard(results) } diff --git a/lib/commands/whoami.js b/lib/commands/whoami.js index 507adb276c731..6b6e93ce7f885 100644 --- a/lib/commands/whoami.js +++ b/lib/commands/whoami.js @@ -9,9 +9,11 @@ class Whoami extends BaseCommand { async exec () { const username = await getIdentity(this.npm, { ...this.npm.flatOptions }) - output.standard( - this.npm.config.get('json') ? JSON.stringify(username) : username - ) + if (this.npm.config.get('json')) { + output.buffer(username) + } else { + output.standard(username) + } } } diff --git a/lib/npm.js b/lib/npm.js index 9d00b630857b1..5563cec21ba4d 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -260,7 +260,7 @@ class Npm { this.#logFile.off() } - finish () { + finish (err) { // Finish all our timer work, this will write the file if requested, end timers, etc this.#timers.finish({ id: this.#runId, @@ -268,6 +268,14 @@ class Npm { logfiles: this.logFiles, version: this.version, }) + + output.flush({ + [META]: true, + // json can be set during a command so we send the + // final value of it to the display layer here + json: this.loaded && this.config.get('json'), + jsonError: jsonError(err, this), + }) } exitErrorMessage () { @@ -296,16 +304,7 @@ class Npm { Object.assign(err, this.#getError(err, { pkg: localPkg })) } - // TODO: make this not need to be public - this.finish() - - output.flush({ - [META]: true, - // json can be set during a command so we send the - // final value of it to the display layer here - json: this.loaded && this.config.get('json'), - jsonError: jsonError(err, this), - }) + this.finish(err) if (err) { throw err diff --git a/lib/utils/audit-error.js b/lib/utils/audit-error.js index 10aec7592b03c..c56ec9ba86f18 100644 --- a/lib/utils/audit-error.js +++ b/lib/utils/audit-error.js @@ -22,14 +22,14 @@ const auditError = (npm, report) => { const { body: errBody } = error const body = Buffer.isBuffer(errBody) ? errBody.toString() : errBody if (npm.flatOptions.json) { - output.standard(JSON.stringify({ + output.buffer({ message: error.message, method: error.method, uri: replaceInfo(error.uri), headers: error.headers, statusCode: error.statusCode, body, - }, null, 2)) + }) } else { output.standard(body) } diff --git a/lib/utils/open-url.js b/lib/utils/open-url.js index 7d8e1cb5a2d23..03451af207e86 100644 --- a/lib/utils/open-url.js +++ b/lib/utils/open-url.js @@ -15,8 +15,11 @@ const assertValidUrl = (url) => { } const outputMsg = (json, title, url) => { - const msg = json ? JSON.stringify({ title, url }) : `${title}:\n${url}` - output.standard(msg) + if (json) { + output.buffer({ title, url }) + } else { + output.standard(`${title}:\n${url}`) + } } // attempt to open URL in web-browser, print address otherwise: diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index a858a546c4010..025479f0c8e60 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -90,7 +90,7 @@ const reifyOutput = (npm, arb) => { summary.audit = npm.command === 'audit' ? auditReport : auditReport.toJSON().metadata } - output.standard(JSON.stringify(summary, null, 2)) + output.buffer(summary) } else { packagesChangedMessage(npm, summary) packagesFundingMessage(npm, summary) diff --git a/lib/utils/verify-signatures.js b/lib/utils/verify-signatures.js index f2973316c9b76..09711581d11dd 100644 --- a/lib/utils/verify-signatures.js +++ b/lib/utils/verify-signatures.js @@ -60,10 +60,7 @@ class VerifySignatures { } if (this.npm.config.get('json')) { - output.standard(JSON.stringify({ - invalid, - missing, - }, null, 2)) + output.buffer({ invalid, missing }) return } const end = process.hrtime.bigint() diff --git a/tap-snapshots/test/lib/utils/open-url.js.test.cjs b/tap-snapshots/test/lib/utils/open-url.js.test.cjs index 92511b9284c7a..fa256ba131447 100644 --- a/tap-snapshots/test/lib/utils/open-url.js.test.cjs +++ b/tap-snapshots/test/lib/utils/open-url.js.test.cjs @@ -11,7 +11,10 @@ https://www.npmjs.com ` exports[`test/lib/utils/open-url.js TAP open url prints where to go when browser is disabled and json is enabled > printed expected message 1`] = ` -{"title":"npm home","url":"https://www.npmjs.com"} +{ + "title": "npm home", + "url": "https://www.npmjs.com" +} ` exports[`test/lib/utils/open-url.js TAP open url prints where to go when given browser does not exist > printed expected message 1`] = ` @@ -33,5 +36,8 @@ https://www.npmjs.com ` exports[`test/lib/utils/open-url.js TAP open url prompt prints json output > must match snapshot 1`] = ` -{"title":"npm home","url":"https://www.npmjs.com"} +{ + "title": "npm home", + "url": "https://www.npmjs.com" +} ` diff --git a/test/lib/utils/audit-error.js b/test/lib/utils/audit-error.js index 9d6192fbc31be..00780f9e0bcf2 100644 --- a/test/lib/utils/audit-error.js +++ b/test/lib/utils/audit-error.js @@ -19,6 +19,8 @@ const auditError = async (t, { command, error, ...config } = {}) => { res.error = err } + mock.npm.finish() + return { ...res, logs: mock.logs.warn.byTitle('audit'), diff --git a/test/lib/utils/open-url.js b/test/lib/utils/open-url.js index 452a09fac97e5..ca52e8e32196b 100644 --- a/test/lib/utils/open-url.js +++ b/test/lib/utils/open-url.js @@ -27,6 +27,8 @@ const mockOpenUrl = async (t, args, { openerResult, ...config } = {}) => { await openWithNpm(...args) } + mock.npm.finish() + return { ...mock, openUrl: openWithNpm, @@ -101,6 +103,8 @@ const mockOpenUrlPrompt = async (t, { await openUrlPrompt(...args).catch((er) => error = er) } + mock.npm.finish() + return { ...mock, openerUrl, @@ -178,7 +182,6 @@ t.test('open url prompt', async t => { t.test('does not error when opener can not find command', async t => { const { OUTPUT, error, openerUrl } = await mockOpenUrlPrompt(t, { - // openerResult: new Error('Opener failed'), openerResult: Object.assign(new Error('Opener failed'), { code: 127 }), }) diff --git a/test/lib/utils/reify-output.js b/test/lib/utils/reify-output.js index 205b7baf421f7..184f934c97013 100644 --- a/test/lib/utils/reify-output.js +++ b/test/lib/utils/reify-output.js @@ -23,6 +23,7 @@ const mockReify = async (t, reify, { command, ...config } = {}) => { }) reifyOutput(mock.npm, reify) + mock.npm.finish() return mock.joinedOutput() }