From 9123de4d282bfd19ea17ad613f5a2acab0e0e162 Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Fri, 12 Apr 2024 16:13:14 -0700 Subject: [PATCH] feat: do all ouput over proc-log events --- lib/base-command.js | 4 +- lib/cli-entry.js | 10 +- lib/commands/access.js | 11 +- lib/commands/adduser.js | 4 +- lib/commands/audit.js | 58 ++-- lib/commands/cache.js | 22 +- lib/commands/completion.js | 3 +- lib/commands/config.js | 12 +- lib/commands/diff.js | 4 +- lib/commands/dist-tag.js | 10 +- lib/commands/doctor.js | 4 +- lib/commands/exec.js | 2 - lib/commands/explain.js | 5 +- lib/commands/explore.js | 4 +- lib/commands/fund.js | 7 +- lib/commands/help-search.js | 11 +- lib/commands/help.js | 3 +- lib/commands/hook.js | 39 +-- lib/commands/init.js | 5 +- lib/commands/login.js | 4 +- lib/commands/ls.js | 3 +- lib/commands/org.js | 25 +- lib/commands/outdated.js | 7 +- lib/commands/owner.js | 10 +- lib/commands/pack.js | 6 +- lib/commands/ping.js | 4 +- lib/commands/pkg.js | 5 +- lib/commands/prefix.js | 3 +- lib/commands/profile.js | 38 +-- lib/commands/publish.js | 10 +- lib/commands/query.js | 6 +- lib/commands/rebuild.js | 3 +- lib/commands/root.js | 3 +- lib/commands/run-script.js | 22 +- lib/commands/sbom.js | 4 +- lib/commands/search.js | 6 +- lib/commands/star.js | 4 +- lib/commands/stars.js | 4 +- lib/commands/team.js | 41 +-- lib/commands/token.js | 22 +- lib/commands/unpublish.js | 5 +- lib/commands/version.js | 11 +- lib/commands/view.js | 64 ++-- lib/commands/whoami.js | 3 +- lib/npm.js | 22 +- lib/utils/audit-error.js | 6 +- lib/utils/display.js | 274 ++++++++++++------ lib/utils/exit-handler.js | 21 +- lib/utils/format.js | 4 +- lib/utils/open-url-prompt.js | 5 +- lib/utils/open-url.js | 3 +- lib/utils/reify-output.js | 16 +- test/lib/commands/ci.js | 1 - test/lib/commands/pack.js | 44 +-- test/lib/commands/publish.js | 52 ++-- test/lib/npm.js | 9 - test/lib/utils/display.js | 88 ++++-- test/lib/utils/exit-handler.js | 68 +++-- workspaces/arborist/test/arborist/reify.js | 23 +- .../config/lib/definitions/definitions.js | 2 +- 60 files changed, 646 insertions(+), 523 deletions(-) diff --git a/lib/base-command.js b/lib/base-command.js index 1efda0fecba54..a5fdadb60870e 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -4,7 +4,7 @@ const { relative } = require('path') const { definitions } = require('@npmcli/config/lib/definitions') const { aliases: cmdAliases } = require('./utils/cmd-list') -const { log } = require('proc-log') +const { log, output } = require('proc-log') class BaseCommand { static workspaces = false @@ -119,7 +119,7 @@ class BaseCommand { const { config } = this.npm if (config.get('usage')) { - return this.npm.output(this.usage) + return output.standard(this.usage) } const hasWsConfig = config.get('workspaces') || config.get('workspace').length diff --git a/lib/cli-entry.js b/lib/cli-entry.js index 3c8fc04ae832c..9a5994aec19a3 100644 --- a/lib/cli-entry.js +++ b/lib/cli-entry.js @@ -18,7 +18,7 @@ module.exports = async (process, validateEngines) => { exitHandler.setNpm(npm) // only log node and npm paths in argv initially since argv can contain sensitive info. a cleaned version will be logged later - const { log } = require('proc-log') + const { log, output } = require('proc-log') log.verbose('cli', process.argv.slice(0, 2).join(' ')) log.info('using', 'npm@%s', npm.version) log.info('using', 'node@%s', process.version) @@ -41,7 +41,7 @@ module.exports = async (process, validateEngines) => { // npm -v if (npm.config.get('version', 'cli')) { - npm.output(npm.version) + output.standard(npm.version) return exitHandler() } @@ -53,7 +53,7 @@ module.exports = async (process, validateEngines) => { cmd = npm.argv.shift() if (!cmd) { - npm.output(npm.usage) + output.standard(npm.usage) process.exitCode = 1 return exitHandler() } @@ -64,8 +64,8 @@ module.exports = async (process, validateEngines) => { if (err.code === 'EUNKNOWNCOMMAND') { const didYouMean = require('./utils/did-you-mean.js') const suggestions = await didYouMean(npm.localPrefix, cmd) - npm.output(`Unknown command: "${cmd}"${suggestions}\n`) - npm.output('To see a list of supported npm commands, run:\n npm help') + output.standard(`Unknown command: "${cmd}"${suggestions}\n`) + output.standard('To see a list of supported npm commands, run:\n npm help') process.exitCode = 1 return exitHandler() } diff --git a/lib/commands/access.js b/lib/commands/access.js index 4594241b402b7..20565e274398e 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -1,5 +1,6 @@ const libnpmaccess = require('libnpmaccess') const npa = require('npm-package-arg') +const { output } = require('proc-log') const pkgJson = require('@npmcli/package-json') const localeCompare = require('@isaacs/string-locale-compare')('en') @@ -197,7 +198,7 @@ class Access extends BaseCommand { } #output (items, limiter) { - const output = {} + const outputs = {} const lookup = { __proto__: null, read: 'read-only', @@ -205,14 +206,14 @@ class Access extends BaseCommand { } for (const item in items) { const val = items[item] - output[item] = lookup[val] || val + outputs[item] = lookup[val] || val } if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify(output, null, 2)) + output.standard(JSON.stringify(outputs, null, 2)) } else { - for (const item of Object.keys(output).sort(localeCompare)) { + for (const item of Object.keys(outputs).sort(localeCompare)) { if (!limiter || limiter === item) { - this.npm.output(`${item}: ${output[item]}`) + output.standard(`${item}: ${outputs[item]}`) } } } diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 2ac4b7d3db4db..842819f2bf44b 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,4 +1,4 @@ -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') @@ -44,7 +44,7 @@ class AddUser extends BaseCommand { await this.npm.config.save('user') - this.npm.output(message) + output.standard(message) } } module.exports = AddUser diff --git a/lib/commands/audit.js b/lib/commands/audit.js index fd99459d1febd..0ec4eec44a77e 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -8,7 +8,7 @@ const tufClient = require('@sigstore/tuf') const ArboristWorkspaceCmd = require('../arborist-cmd.js') const auditError = require('../utils/audit-error.js') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const reifyFinish = require('../utils/reify-finish.js') const sortAlphabetically = (a, b) => localeCompare(a.name, b.name) @@ -65,7 +65,7 @@ class VerifySignatures { } if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ invalid, missing, }, null, 2)) @@ -77,91 +77,91 @@ class VerifySignatures { const auditedPlural = this.auditedWithKeysCount > 1 ? 's' : '' const timing = `audited ${this.auditedWithKeysCount} package${auditedPlural} in ` + `${Math.floor(Number(elapsed) / 1e9)}s` - this.npm.output(timing) - this.npm.output('') + output.standard(timing) + output.standard('') const verifiedBold = this.npm.chalk.bold('verified') if (this.verifiedSignatureCount) { if (this.verifiedSignatureCount === 1) { /* eslint-disable-next-line max-len */ - this.npm.output(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`) + output.standard(`${this.verifiedSignatureCount} package has a ${verifiedBold} registry signature`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`) + output.standard(`${this.verifiedSignatureCount} packages have ${verifiedBold} registry signatures`) } - this.npm.output('') + output.standard('') } if (this.verifiedAttestationCount) { if (this.verifiedAttestationCount === 1) { /* eslint-disable-next-line max-len */ - this.npm.output(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`) + output.standard(`${this.verifiedAttestationCount} package has a ${verifiedBold} attestation`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`) + output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`) } - this.npm.output('') + output.standard('') } if (missing.length) { const missingClr = this.npm.chalk.bold(this.npm.chalk.red('missing')) if (missing.length === 1) { /* eslint-disable-next-line max-len */ - this.npm.output(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`) + output.standard(`1 package has a ${missingClr} registry signature but the registry is providing signing keys:`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`) + output.standard(`${missing.length} packages have ${missingClr} registry signatures but the registry is providing signing keys:`) } - this.npm.output('') + output.standard('') missing.map(m => - this.npm.output(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`) + output.standard(`${this.npm.chalk.red(`${m.name}@${m.version}`)} (${m.registry})`) ) } if (invalid.length) { if (missing.length) { - this.npm.output('') + output.standard('') } const invalidClr = this.npm.chalk.bold(this.npm.chalk.red('invalid')) // We can have either invalid signatures or invalid provenance const invalidSignatures = this.invalid.filter(i => i.code === 'EINTEGRITYSIGNATURE') if (invalidSignatures.length) { if (invalidSignatures.length === 1) { - this.npm.output(`1 package has an ${invalidClr} registry signature:`) + output.standard(`1 package has an ${invalidClr} registry signature:`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`) + output.standard(`${invalidSignatures.length} packages have ${invalidClr} registry signatures:`) } - this.npm.output('') + output.standard('') invalidSignatures.map(i => - this.npm.output(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) + output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) ) - this.npm.output('') + output.standard('') } const invalidAttestations = this.invalid.filter(i => i.code === 'EATTESTATIONVERIFY') if (invalidAttestations.length) { if (invalidAttestations.length === 1) { - this.npm.output(`1 package has an ${invalidClr} attestation:`) + output.standard(`1 package has an ${invalidClr} attestation:`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`${invalidAttestations.length} packages have ${invalidClr} attestations:`) + output.standard(`${invalidAttestations.length} packages have ${invalidClr} attestations:`) } - this.npm.output('') + output.standard('') invalidAttestations.map(i => - this.npm.output(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) + output.standard(`${this.npm.chalk.red(`${i.name}@${i.version}`)} (${i.registry})`) ) - this.npm.output('') + output.standard('') } if (invalid.length === 1) { /* eslint-disable-next-line max-len */ - this.npm.output(`Someone might have tampered with this package since it was published on the registry!`) + output.standard(`Someone might have tampered with this package since it was published on the registry!`) } else { /* eslint-disable-next-line max-len */ - this.npm.output(`Someone might have tampered with these packages since they were published on the registry!`) + output.standard(`Someone might have tampered with these packages since they were published on the registry!`) } - this.npm.output('') + output.standard('') } } @@ -463,7 +463,7 @@ class Audit extends ArboristWorkspaceCmd { chalk: this.npm.chalk, }) process.exitCode = process.exitCode || result.exitCode - this.npm.output(result.report) + output.standard(result.report) } } diff --git a/lib/commands/cache.js b/lib/commands/cache.js index b6ab75a6265be..8e9b33b3d1a49 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -7,7 +7,7 @@ const BaseCommand = require('../base-command.js') const npa = require('npm-package-arg') const jsonParse = require('json-parse-even-better-errors') const localeCompare = require('@isaacs/string-locale-compare')('en') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const searchCachePackage = async (path, parsed, cacheKeys) => { /* eslint-disable-next-line max-len */ @@ -135,7 +135,7 @@ class Cache extends BaseCommand { log.warn(`Not Found: ${key}`) break } - this.npm.output(`Deleted: ${key}`) + output.standard(`Deleted: ${key}`) await cacache.rm.entry(cachePath, key) // XXX this could leave other entries without content! await cacache.rm.content(cachePath, entry.integrity) @@ -170,20 +170,20 @@ class Cache extends BaseCommand { ? `~${cache.slice(process.env.HOME.length)}` : cache const stats = await cacache.verify(cache) - this.npm.output(`Cache verified and compressed (${prefix})`) - this.npm.output(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`) + output.standard(`Cache verified and compressed (${prefix})`) + output.standard(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`) if (stats.badContentCount) { - this.npm.output(`Corrupted content removed: ${stats.badContentCount}`) + output.standard(`Corrupted content removed: ${stats.badContentCount}`) } if (stats.reclaimedCount) { /* eslint-disable-next-line max-len */ - this.npm.output(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`) + output.standard(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`) } if (stats.missingContent) { - this.npm.output(`Missing content: ${stats.missingContent}`) + output.standard(`Missing content: ${stats.missingContent}`) } - this.npm.output(`Index entries: ${stats.totalEntries}`) - this.npm.output(`Finished in ${stats.runTime.total / 1000}s`) + output.standard(`Index entries: ${stats.totalEntries}`) + output.standard(`Finished in ${stats.runTime.total / 1000}s`) } // npm cache ls [--package ...] @@ -203,10 +203,10 @@ class Cache extends BaseCommand { results.add(key) } } - [...results].sort(localeCompare).forEach(key => this.npm.output(key)) + [...results].sort(localeCompare).forEach(key => output.standard(key)) return } - cacheKeys.sort(localeCompare).forEach(key => this.npm.output(key)) + cacheKeys.sort(localeCompare).forEach(key => output.standard(key)) } } diff --git a/lib/commands/completion.js b/lib/commands/completion.js index 59113c50560bc..677d7fa2ec3fe 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -32,6 +32,7 @@ const fs = require('fs/promises') const nopt = require('nopt') const { resolve } = require('path') +const { output } = require('proc-log') const Npm = require('../npm.js') const { definitions, shorthands } = require('@npmcli/config/lib/definitions') @@ -185,7 +186,7 @@ class Completion extends BaseCommand { } if (compls.length > 0) { - this.npm.output(compls.join('\n')) + output.standard(compls.join('\n')) } } } diff --git a/lib/commands/config.js b/lib/commands/config.js index fcd21cc1f8d51..b1273120cf9ee 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -6,7 +6,7 @@ const ini = require('ini') const localeCompare = require('@isaacs/string-locale-compare')('en') const pkgJson = require('@npmcli/package-json') const { defaults, definitions } = require('@npmcli/config/lib/definitions') -const { log } = require('proc-log') +const { log, output } = require('proc-log') // These are the configs that we can nerf-dart. Not all of them currently even // *have* config definitions so we have to explicitly validate them here @@ -185,7 +185,7 @@ class Config extends BaseCommand { const pref = keys.length > 1 ? `${key}=` : '' out.push(pref + this.npm.config.get(key)) } - this.npm.output(out.join('\n')) + output.standard(out.join('\n')) } async del (keys) { @@ -282,7 +282,7 @@ ${defData} this.npm.config.repair(problems) const locations = [] - this.npm.output('The following configuration problems have been repaired:\n') + output.standard('The following configuration problems have been repaired:\n') const summary = problems.map(({ action, from, to, key, where }) => { // coverage disabled for else branch because it is intentionally omitted // istanbul ignore else @@ -295,7 +295,7 @@ ${defData} return `- \`${key}\` deleted from ${where} config` } }).join('\n') - this.npm.output(summary) + output.standard(summary) return await Promise.all(locations.map((location) => this.npm.config.save(location))) } @@ -354,7 +354,7 @@ ${defData} } } - this.npm.output(msg.join('\n').trim()) + output.standard(msg.join('\n').trim()) } async listJson () { @@ -366,7 +366,7 @@ ${defData} publicConf[key] = this.npm.config.get(key) } - this.npm.output(JSON.stringify(publicConf, null, 2)) + output.standard(JSON.stringify(publicConf, null, 2)) } } diff --git a/lib/commands/diff.js b/lib/commands/diff.js index bdd72e4dec99a..e188a38505867 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -4,7 +4,7 @@ const libnpmdiff = require('libnpmdiff') const npa = require('npm-package-arg') const pacote = require('pacote') const pickManifest = require('npm-pick-manifest') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const pkgJson = require('@npmcli/package-json') const BaseCommand = require('../base-command.js') @@ -64,7 +64,7 @@ class Diff extends BaseCommand { diffFiles: args, where: this.top, }) - return this.npm.output(res) + return output.standard(res) } async execWorkspaces (args) { diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index c6b795c57b70c..741890fc9cec7 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -1,7 +1,7 @@ const npa = require('npm-package-arg') const regFetch = require('npm-registry-fetch') const semver = require('semver') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const otplease = require('../utils/otplease.js') const pkgJson = require('@npmcli/package-json') const BaseCommand = require('../base-command.js') @@ -120,7 +120,7 @@ class DistTag extends BaseCommand { spec, } await otplease(this.npm, reqOpts, o => regFetch(url, o)) - this.npm.output(`+${t}: ${spec.name}@${version}`) + output.standard(`+${t}: ${spec.name}@${version}`) } async remove (spec, tag, opts) { @@ -146,7 +146,7 @@ class DistTag extends BaseCommand { spec, } await otplease(this.npm, reqOpts, o => regFetch(url, o)) - this.npm.output(`-${tag}: ${spec.name}@${version}`) + output.standard(`-${tag}: ${spec.name}@${version}`) } async list (spec, opts) { @@ -167,7 +167,7 @@ class DistTag extends BaseCommand { const tags = await this.fetchTags(spec, opts) const msg = Object.keys(tags).map(k => `${k}: ${tags[k]}`).sort().join('\n') - this.npm.output(msg) + output.standard(msg) return tags } catch (err) { log.error('dist-tag ls', "Couldn't get dist-tag data for", spec) @@ -180,7 +180,7 @@ class DistTag extends BaseCommand { for (const name of this.workspaceNames) { try { - this.npm.output(`${name}:`) + output.standard(`${name}:`) await this.list(npa(name), this.npm.flatOptions) } catch (err) { // set the exitCode directly, but ignore the error diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index fc1eb42efc587..e3177f7a45544 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -7,7 +7,7 @@ const pacote = require('pacote') const { resolve } = require('path') const semver = require('semver') const { promisify } = require('util') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const ping = require('../utils/ping.js') const { defaults } = require('@npmcli/config/lib/definitions') const lstat = promisify(fs.lstat) @@ -374,7 +374,7 @@ class Doctor extends BaseCommand { colWidths: [this.#checkWidth, 6], }) t.push(row) - this.npm.output(t.toString()) + output.standard(t.toString()) } actions (params) { diff --git a/lib/commands/exec.js b/lib/commands/exec.js index d532eca107c6c..f181bbb2ef6fe 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -65,7 +65,6 @@ class Exec extends BaseCommand { globalDir, chalk, } = this.npm - const output = this.npm.output.bind(this.npm) const scriptShell = this.npm.config.get('script-shell') || undefined const packages = this.npm.config.get('package') const yes = this.npm.config.get('yes') @@ -93,7 +92,6 @@ class Exec extends BaseCommand { globalPath, localBin, locationMsg, - output, packages, path, runPath, diff --git a/lib/commands/explain.js b/lib/commands/explain.js index 403274db68dfa..98ef356bfc1b3 100644 --- a/lib/commands/explain.js +++ b/lib/commands/explain.js @@ -3,6 +3,7 @@ const npa = require('npm-package-arg') const semver = require('semver') const { relative, resolve } = require('path') const validName = require('validate-npm-package-name') +const { output } = require('proc-log') const ArboristWorkspaceCmd = require('../arborist-cmd.js') class Explain extends ArboristWorkspaceCmd { @@ -75,9 +76,9 @@ class Explain extends ArboristWorkspaceCmd { } if (this.npm.flatOptions.json) { - this.npm.output(JSON.stringify(expls, null, 2)) + output.standard(JSON.stringify(expls, null, 2)) } else { - this.npm.output(expls.map(expl => { + output.standard(expls.map(expl => { return explainNode(expl, Infinity, this.npm.chalk) }).join('\n\n')) } diff --git a/lib/commands/explore.js b/lib/commands/explore.js index c24565b5c0c42..ef4743e62197d 100644 --- a/lib/commands/explore.js +++ b/lib/commands/explore.js @@ -4,7 +4,7 @@ const pkgJson = require('@npmcli/package-json') const runScript = require('@npmcli/run-script') const { join, relative } = require('path') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const completion = require('../utils/completion/installed-shallow.js') const BaseCommand = require('../base-command.js') @@ -50,7 +50,7 @@ class Explore extends BaseCommand { } if (!args.length) { - this.npm.output(`\nExploring ${path}\nType 'exit' or ^D when finished\n`) + output.standard(`\nExploring ${path}\nType 'exit' or ^D when finished\n`) } return runScript({ diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 2804d36cd5603..aa8c7b17b48b6 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -1,6 +1,7 @@ const archy = require('archy') const pacote = require('pacote') const semver = require('semver') +const { output } = require('proc-log') const npa = require('npm-package-arg') const { depth } = require('treeverse') const { readTree: getFundingInfo, normalizeFunding, isValidFunding } = require('libnpmfund') @@ -85,9 +86,9 @@ class Fund extends ArboristWorkspaceCmd { }) if (this.npm.config.get('json')) { - this.npm.output(this.printJSON(fundingInfo)) + output.standard(this.printJSON(fundingInfo)) } else { - this.npm.output(this.printHuman(fundingInfo)) + output.standard(this.printHuman(fundingInfo)) } } @@ -212,7 +213,7 @@ class Fund extends ArboristWorkspaceCmd { if (fundingSourceNumber) { ambiguousUrlMsg.unshift(`--which=${fundingSourceNumber} is not a valid index`) } - this.npm.output(ambiguousUrlMsg.join('\n')) + output.standard(ambiguousUrlMsg.join('\n')) } urlMessage (source) { diff --git a/lib/commands/help-search.js b/lib/commands/help-search.js index 273807c7469af..c3719d48f2f5a 100644 --- a/lib/commands/help-search.js +++ b/lib/commands/help-search.js @@ -1,6 +1,7 @@ const { readFile } = require('fs/promises') const path = require('path') const { glob } = require('glob') +const { output } = require('proc-log') const BaseCommand = require('../base-command.js') const globify = pattern => pattern.split('\\').join('/') @@ -24,9 +25,9 @@ class HelpSearch extends BaseCommand { const results = await this.searchFiles(args, data, files) const formatted = this.formatResults(args, results) if (!formatted.trim()) { - this.npm.output(`No matches in help for: ${args.join(' ')}\n`) + output.standard(`No matches in help for: ${args.join(' ')}\n`) } else { - this.npm.output(formatted) + output.standard(formatted) } } @@ -140,7 +141,7 @@ class HelpSearch extends BaseCommand { formatResults (args, results) { const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 - const output = results.map(res => { + const formattedOutput = results.map(res => { const out = [res.cmd] const r = Object.keys(res.hits) .map(k => `${k}:${res.hits[k]}`) @@ -183,10 +184,10 @@ class HelpSearch extends BaseCommand { const finalOut = results.length && !this.npm.config.get('long') ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' + '—'.repeat(cols - 1) + '\n' + - output + '\n' + + formattedOutput + '\n' + '—'.repeat(cols - 1) + '\n' + '(run with -l or --long to see more context)' - : output + : formattedOutput return finalOut.trim() } diff --git a/lib/commands/help.js b/lib/commands/help.js index 39c580f9a6871..1f51713a8d051 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -2,6 +2,7 @@ const spawn = require('@npmcli/promise-spawn') const path = require('path') const openUrl = require('../utils/open-url.js') const { glob } = require('glob') +const { output } = require('proc-log') const localeCompare = require('@isaacs/string-locale-compare')('en') const { deref } = require('../utils/cmd-list.js') @@ -50,7 +51,7 @@ class Help extends BaseCommand { const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*' if (!args.length) { - return this.npm.output(this.npm.usage) + return output.standard(this.npm.usage) } // npm help foo bar baz: search topics diff --git a/lib/commands/hook.js b/lib/commands/hook.js index b0f52a801f571..5e6b593cccfd6 100644 --- a/lib/commands/hook.js +++ b/lib/commands/hook.js @@ -2,6 +2,7 @@ const hookApi = require('libnpmhook') const otplease = require('../utils/otplease.js') const relativeDate = require('tiny-relative-date') const Table = require('cli-table3') +const { output } = require('proc-log') const BaseCommand = require('../base-command.js') class Hook extends BaseCommand { @@ -40,31 +41,31 @@ class Hook extends BaseCommand { async add (pkg, uri, secret, opts) { const hook = await hookApi.add(pkg, uri, secret, opts) if (opts.json) { - this.npm.output(JSON.stringify(hook, null, 2)) + output.standard(JSON.stringify(hook, null, 2)) } else if (opts.parseable) { - this.npm.output(Object.keys(hook).join('\t')) - this.npm.output(Object.keys(hook).map(k => hook[k]).join('\t')) + output.standard(Object.keys(hook).join('\t')) + output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!this.npm.silent) { - this.npm.output(`+ ${this.hookName(hook)} ${opts.unicode ? ' ➜ ' : ' -> '} ${hook.endpoint}`) + output.standard(`+ ${this.hookName(hook)} ${opts.unicode ? ' ➜ ' : ' -> '} ${hook.endpoint}`) } } async ls (pkg, opts) { const hooks = await hookApi.ls({ ...opts, package: pkg }) if (opts.json) { - this.npm.output(JSON.stringify(hooks, null, 2)) + output.standard(JSON.stringify(hooks, null, 2)) } else if (opts.parseable) { - this.npm.output(Object.keys(hooks[0]).join('\t')) + output.standard(Object.keys(hooks[0]).join('\t')) hooks.forEach(hook => { - this.npm.output(Object.keys(hook).map(k => hook[k]).join('\t')) + output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) }) } else if (!hooks.length) { - this.npm.output("You don't have any hooks configured yet.") + output.standard("You don't have any hooks configured yet.") } else if (!this.npm.silent) { if (hooks.length === 1) { - this.npm.output('You have one hook configured.') + output.standard('You have one hook configured.') } else { - this.npm.output(`You have ${hooks.length} hooks configured.`) + output.standard(`You have ${hooks.length} hooks configured.`) } const table = new Table({ head: ['id', 'target', 'endpoint'] }) @@ -86,31 +87,31 @@ class Hook extends BaseCommand { table.push([{ colSpan: 2, content: 'never triggered' }]) } }) - this.npm.output(table.toString()) + output.standard(table.toString()) } } async rm (id, opts) { const hook = await hookApi.rm(id, opts) if (opts.json) { - this.npm.output(JSON.stringify(hook, null, 2)) + output.standard(JSON.stringify(hook, null, 2)) } else if (opts.parseable) { - this.npm.output(Object.keys(hook).join('\t')) - this.npm.output(Object.keys(hook).map(k => hook[k]).join('\t')) + output.standard(Object.keys(hook).join('\t')) + output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!this.npm.silent) { - this.npm.output(`- ${this.hookName(hook)} ${opts.unicode ? ' ✘ ' : ' X '} ${hook.endpoint}`) + output.standard(`- ${this.hookName(hook)} ${opts.unicode ? ' ✘ ' : ' X '} ${hook.endpoint}`) } } async update (id, uri, secret, opts) { const hook = await hookApi.update(id, uri, secret, opts) if (opts.json) { - this.npm.output(JSON.stringify(hook, null, 2)) + output.standard(JSON.stringify(hook, null, 2)) } else if (opts.parseable) { - this.npm.output(Object.keys(hook).join('\t')) - this.npm.output(Object.keys(hook).map(k => hook[k]).join('\t')) + output.standard(Object.keys(hook).join('\t')) + output.standard(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!this.npm.silent) { - this.npm.output(`+ ${this.hookName(hook)} ${opts.unicode ? ' ➜ ' : ' -> '} ${hook.endpoint}`) + output.standard(`+ ${this.hookName(hook)} ${opts.unicode ? ' ➜ ' : ' -> '} ${hook.endpoint}`) } } diff --git a/lib/commands/init.js b/lib/commands/init.js index d3d2efd60589d..d45dbaa1fa0d7 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -6,7 +6,7 @@ const npa = require('npm-package-arg') const libexec = require('libnpmexec') const mapWorkspaces = require('@npmcli/map-workspaces') const PackageJson = require('@npmcli/package-json') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const updateWorkspaces = require('../workspaces/update-workspaces.js') const posixPath = p => p.split('\\').join('/') @@ -130,7 +130,6 @@ class Init extends BaseCommand { globalBin, chalk, } = this.npm - const output = this.npm.output.bind(this.npm) const runPath = path const scriptShell = this.npm.config.get('script-shell') || undefined const yes = this.npm.config.get('yes') @@ -154,7 +153,7 @@ class Init extends BaseCommand { const initFile = this.npm.config.get('init-module') if (!this.npm.config.get('yes') && !this.npm.config.get('force')) { - this.npm.output([ + output.standard([ 'This utility will walk you through creating a package.json file.', 'It only covers the most common items, and tries to guess sensible defaults.', '', diff --git a/lib/commands/login.js b/lib/commands/login.js index 87e598debcd31..d38aec51289cc 100644 --- a/lib/commands/login.js +++ b/lib/commands/login.js @@ -1,4 +1,4 @@ -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { redactLog: replaceInfo } = require('@npmcli/redact') const auth = require('../utils/auth.js') @@ -44,7 +44,7 @@ class Login extends BaseCommand { await this.npm.config.save('user') - this.npm.output(message) + output.standard(message) } } module.exports = Login diff --git a/lib/commands/ls.js b/lib/commands/ls.js index aef3be2828a5a..ff954dec49cc7 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -4,6 +4,7 @@ const relativePrefix = `.${sep}` const archy = require('archy') const { breadth } = require('treeverse') const npa = require('npm-package-arg') +const { output } = require('proc-log') const _depth = Symbol('depth') const _dedupe = Symbol('dedupe') @@ -176,7 +177,7 @@ class LS extends ArboristWorkspaceCmd { const [rootError] = tree.errors.filter(e => e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json')) - this.npm.outputBuffer( + output.buffer( json ? jsonOutput({ path, problems, result, rootError, seenItems }) : parseable ? parseableOutput({ seenNodes, global, long }) : humanOutput({ chalk, result, seenItems, unicode }) diff --git a/lib/commands/org.js b/lib/commands/org.js index 1f32d41ff7306..8881ded70f638 100644 --- a/lib/commands/org.js +++ b/lib/commands/org.js @@ -2,6 +2,7 @@ const liborg = require('libnpmorg') const otplease = require('../utils/otplease.js') const Table = require('cli-table3') const BaseCommand = require('../base-command.js') +const { output } = require('proc-log') class Org extends BaseCommand { static description = 'Manage orgs' @@ -68,14 +69,14 @@ class Org extends BaseCommand { const memDeets = await liborg.set(org, user, role, opts) if (opts.json) { - this.npm.output(JSON.stringify(memDeets, null, 2)) + output.standard(JSON.stringify(memDeets, null, 2)) } else if (opts.parseable) { - this.npm.output(['org', 'orgsize', 'user', 'role'].join('\t')) - this.npm.output( + output.standard(['org', 'orgsize', 'user', 'role'].join('\t')) + output.standard( [memDeets.org.name, memDeets.org.size, memDeets.user, memDeets.role].join('\t') ) } else if (!this.npm.silent) { - this.npm.output( + output.standard( `Added ${memDeets.user} as ${memDeets.role} to ${memDeets.org.name}. You now have ${ memDeets.org.size } member${memDeets.org.size === 1 ? '' : 's'} in this org.` @@ -100,7 +101,7 @@ class Org extends BaseCommand { org = org.replace(/^[~@]?/, '') const userCount = Object.keys(roster).length if (opts.json) { - this.npm.output( + output.standard( JSON.stringify({ user, org, @@ -109,10 +110,10 @@ class Org extends BaseCommand { }) ) } else if (opts.parseable) { - this.npm.output(['user', 'org', 'userCount', 'deleted'].join('\t')) - this.npm.output([user, org, userCount, true].join('\t')) + output.standard(['user', 'org', 'userCount', 'deleted'].join('\t')) + output.standard([user, org, userCount, true].join('\t')) } else if (!this.npm.silent) { - this.npm.output( + output.standard( `Successfully removed ${user} from ${org}. You now have ${userCount} member${ userCount === 1 ? '' : 's' } in this org.` @@ -135,11 +136,11 @@ class Org extends BaseCommand { roster = newRoster } if (opts.json) { - this.npm.output(JSON.stringify(roster, null, 2)) + output.standard(JSON.stringify(roster, null, 2)) } else if (opts.parseable) { - this.npm.output(['user', 'role'].join('\t')) + output.standard(['user', 'role'].join('\t')) Object.keys(roster).forEach(u => { - this.npm.output([u, roster[u]].join('\t')) + output.standard([u, roster[u]].join('\t')) }) } else if (!this.npm.silent) { const table = new Table({ head: ['user', 'role'] }) @@ -148,7 +149,7 @@ class Org extends BaseCommand { .forEach(u => { table.push([u, roster[u]]) }) - this.npm.output(table.toString()) + output.standard(table.toString()) } } } diff --git a/lib/commands/outdated.js b/lib/commands/outdated.js index 27b29b314b745..a75afe18e6d93 100644 --- a/lib/commands/outdated.js +++ b/lib/commands/outdated.js @@ -4,6 +4,7 @@ const pacote = require('pacote') const table = require('text-table') const npa = require('npm-package-arg') const pickManifest = require('npm-pick-manifest') +const { output } = require('proc-log') const localeCompare = require('@isaacs/string-locale-compare')('en') const ArboristWorkspaceCmd = require('../arborist-cmd.js') @@ -83,9 +84,9 @@ class Outdated extends ArboristWorkspaceCmd { // display results if (this.npm.config.get('json')) { - this.npm.output(this.makeJSON(outdated)) + output.standard(this.makeJSON(outdated)) } else if (this.npm.config.get('parseable')) { - this.npm.output(this.makeParseable(outdated)) + output.standard(this.makeParseable(outdated)) } else { const outList = outdated.map(x => this.makePretty(x)) const outHead = ['Package', @@ -107,7 +108,7 @@ class Outdated extends ArboristWorkspaceCmd { align: ['l', 'r', 'r', 'r', 'l'], stringLength: s => stripVTControlCharacters(s).length, } - this.npm.output(table(outTable, tableOpts)) + output.standard(table(outTable, tableOpts)) } } diff --git a/lib/commands/owner.js b/lib/commands/owner.js index ccb85fae91f55..cfd40df2151e6 100644 --- a/lib/commands/owner.js +++ b/lib/commands/owner.js @@ -1,7 +1,7 @@ const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') const pacote = require('pacote') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const otplease = require('../utils/otplease.js') const pkgJson = require('@npmcli/package-json') const BaseCommand = require('../base-command.js') @@ -115,9 +115,9 @@ class Owner extends BaseCommand { const packumentOpts = { ...this.npm.flatOptions, fullMetadata: true, preferOnline: true } const { maintainers } = await pacote.packument(spec, packumentOpts) if (!maintainers || !maintainers.length) { - this.npm.output('no admin found') + output.standard('no admin found') } else { - this.npm.output(maintainers.map(m => `${m.name} <${m.email}>`).join('\n')) + output.standard(maintainers.map(m => `${m.name} <${m.email}>`).join('\n')) } } catch (err) { log.error('owner ls', "Couldn't get owner data", redact(pkg)) @@ -216,9 +216,9 @@ class Owner extends BaseCommand { }) }) if (addOrRm === 'add') { - this.npm.output(`+ ${user} (${spec.name})`) + output.standard(`+ ${user} (${spec.name})`) } else { - this.npm.output(`- ${user} (${spec.name})`) + output.standard(`- ${user} (${spec.name})`) } return res } catch (err) { diff --git a/lib/commands/pack.js b/lib/commands/pack.js index b482c54dc7ac4..463329235d417 100644 --- a/lib/commands/pack.js +++ b/lib/commands/pack.js @@ -1,7 +1,7 @@ const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { getContents, logTar } = require('../utils/tar.js') const BaseCommand = require('../base-command.js') @@ -58,13 +58,13 @@ class Pack extends BaseCommand { } if (json) { - this.npm.output(JSON.stringify(tarballs, null, 2)) + output.standard(JSON.stringify(tarballs, null, 2)) return } for (const tar of tarballs) { logTar(tar, { unicode }) - this.npm.output(tar.filename.replace(/^@/, '').replace(/\//, '-')) + output.standard(tar.filename.replace(/^@/, '').replace(/\//, '-')) } } diff --git a/lib/commands/ping.js b/lib/commands/ping.js index a16278fc3e130..3ae4ed80a22cb 100644 --- a/lib/commands/ping.js +++ b/lib/commands/ping.js @@ -1,5 +1,5 @@ const { redact } = require('@npmcli/redact') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const pingUtil = require('../utils/ping.js') const BaseCommand = require('../base-command.js') @@ -16,7 +16,7 @@ class Ping extends BaseCommand { const time = Date.now() - start log.notice('PONG', `${time}ms`) if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ registry: cleanRegistry, time, details, diff --git a/lib/commands/pkg.js b/lib/commands/pkg.js index 49a66823cca99..26e60fec48786 100644 --- a/lib/commands/pkg.js +++ b/lib/commands/pkg.js @@ -1,3 +1,4 @@ +const { output } = require('proc-log') const PackageJson = require('@npmcli/package-json') const BaseCommand = require('../base-command.js') const Queryable = require('../utils/queryable.js') @@ -62,7 +63,7 @@ class Pkg extends BaseCommand { } // when running in workspaces names, make sure to key by workspace // name the results of each value retrieved in each ws - this.npm.output(JSON.stringify(result, null, 2)) + output.standard(JSON.stringify(result, null, 2)) } async get (args) { @@ -85,7 +86,7 @@ class Pkg extends BaseCommand { // only outputs if not running with workspaces config // execWorkspaces will handle the output otherwise if (!this.workspaces) { - this.npm.output(JSON.stringify(result, null, 2)) + output.standard(JSON.stringify(result, null, 2)) } return result diff --git a/lib/commands/prefix.js b/lib/commands/prefix.js index 264b819fc7692..6c3d6f886f12c 100644 --- a/lib/commands/prefix.js +++ b/lib/commands/prefix.js @@ -1,3 +1,4 @@ +const { output } = require('proc-log') const BaseCommand = require('../base-command.js') class Prefix extends BaseCommand { @@ -7,7 +8,7 @@ class Prefix extends BaseCommand { static usage = ['[-g]'] async exec (args) { - return this.npm.output(this.npm.prefix) + return output.standard(this.npm.prefix) } } module.exports = Prefix diff --git a/lib/commands/profile.js b/lib/commands/profile.js index 5ef0d0dbe7c57..98a8dcd050ee9 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -1,6 +1,6 @@ const inspect = require('util').inspect const { URL } = require('url') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const npmProfile = require('npm-profile') const qrcodeTerminal = require('qrcode-terminal') const Table = require('cli-table3') @@ -110,7 +110,7 @@ class Profile extends BaseCommand { } if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify(info, null, 2)) + output.standard(JSON.stringify(info, null, 2)) return } @@ -142,14 +142,14 @@ class Profile extends BaseCommand { .filter((arg) => arg.trim() !== '') .map((arg) => cleaned[arg]) .join('\t') - this.npm.output(values) + output.standard(values) } else { if (this.npm.config.get('parseable')) { for (const key of Object.keys(info)) { if (key === 'tfa') { - this.npm.output(`${key}\t${cleaned[tfa]}`) + output.standard(`${key}\t${cleaned[tfa]}`) } else { - this.npm.output(`${key}\t${info[key]}`) + output.standard(`${key}\t${info[key]}`) } } } else { @@ -158,7 +158,7 @@ class Profile extends BaseCommand { table.push({ [this.npm.chalk.bold(key)]: cleaned[key] }) } - this.npm.output(table.toString()) + output.standard(table.toString()) } } } @@ -216,13 +216,13 @@ class Profile extends BaseCommand { const result = await otplease(this.npm, conf, c => npmProfile.set(newUser, c)) if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify({ [prop]: result[prop] }, null, 2)) + output.standard(JSON.stringify({ [prop]: result[prop] }, null, 2)) } else if (this.npm.config.get('parseable')) { - this.npm.output(prop + '\t' + result[prop]) + output.standard(prop + '\t' + result[prop]) } else if (result[prop] != null) { - this.npm.output('Set', prop, 'to', result[prop]) + output.standard('Set', prop, 'to', result[prop]) } else { - this.npm.output('Set', prop) + output.standard('Set', prop) } } @@ -320,7 +320,7 @@ class Profile extends BaseCommand { const challenge = await npmProfile.set(info, conf) if (challenge.tfa === null) { - this.npm.output('Two factor authentication mode changed to: ' + mode) + output.standard('Two factor authentication mode changed to: ' + mode) return } @@ -337,7 +337,7 @@ class Profile extends BaseCommand { const secret = otpauth.searchParams.get('secret') const code = await qrcode(challenge.tfa) - this.npm.output( + output.standard( 'Scan into your authenticator app:\n' + code + '\n Or enter code:', secret ) @@ -348,17 +348,17 @@ class Profile extends BaseCommand { const result = await npmProfile.set({ tfa: [interactiveOTP] }, conf) - this.npm.output( + output.standard( '2FA successfully enabled. Below are your recovery codes, ' + 'please print these out.' ) - this.npm.output( + output.standard( 'You will need these to recover access to your account ' + 'if you lose your authentication device.' ) for (const tfaCode of result.tfa) { - this.npm.output('\t' + tfaCode) + output.standard('\t' + tfaCode) } } @@ -367,7 +367,7 @@ class Profile extends BaseCommand { const info = await npmProfile.get(conf) if (!info.tfa || info.tfa.pending) { - this.npm.output('Two factor authentication not enabled.') + output.standard('Two factor authentication not enabled.') return } @@ -383,11 +383,11 @@ class Profile extends BaseCommand { await npmProfile.set({ tfa: { password: password, mode: 'disable' } }, conf) if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify({ tfa: false }, null, 2)) + output.standard(JSON.stringify({ tfa: false }, null, 2)) } else if (this.npm.config.get('parseable')) { - this.npm.output('tfa\tfalse') + output.standard('tfa\tfalse') } else { - this.npm.output('Two factor authentication disabled.') + output.standard('Two factor authentication disabled.') } } } diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 94a36d8d41212..8758422f5e9a7 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -1,4 +1,4 @@ -const { log } = require('proc-log') +const { log, output } = require('proc-log') const semver = require('semver') const pack = require('libnpmpack') const libpub = require('libnpmpublish').publish @@ -142,9 +142,9 @@ class Publish extends BaseCommand { if (!this.suppressOutput) { if (!silent && json) { - this.npm.output(JSON.stringify(pkgContents, null, 2)) + output.standard(JSON.stringify(pkgContents, null, 2)) } else if (!silent) { - this.npm.output(`+ ${pkgContents.id}`) + output.standard(`+ ${pkgContents.id}`) } } @@ -181,14 +181,14 @@ class Publish extends BaseCommand { // This needs to be in-line w/ the rest of the output that non-JSON // publish generates if (!silent && !json) { - this.npm.output(`+ ${pkgContents.id}`) + output.standard(`+ ${pkgContents.id}`) } else { results[name] = pkgContents } } if (!silent && json) { - this.npm.output(JSON.stringify(results, null, 2)) + output.standard(JSON.stringify(results, null, 2)) } } diff --git a/lib/commands/query.js b/lib/commands/query.js index 6bee73d1ba2ad..319d3ee0cc293 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -2,7 +2,7 @@ const { resolve } = require('path') const BaseCommand = require('../base-command.js') -const { log } = require('proc-log') +const { log, output } = require('proc-log') class QuerySelectorItem { constructor (node) { @@ -83,7 +83,7 @@ class Query extends BaseCommand { this.buildResponse(items) this.checkExpected(this.#response.length) - this.npm.output(this.parsedResponse) + output.standard(this.parsedResponse) } async execWorkspaces (args) { @@ -107,7 +107,7 @@ class Query extends BaseCommand { this.buildResponse(items) } this.checkExpected(this.#response.length) - this.npm.output(this.parsedResponse) + output.standard(this.parsedResponse) } // builds a normalized inventory diff --git a/lib/commands/rebuild.js b/lib/commands/rebuild.js index 8af96f725555c..8858edd1da349 100644 --- a/lib/commands/rebuild.js +++ b/lib/commands/rebuild.js @@ -1,4 +1,5 @@ const { resolve } = require('path') +const { output } = require('proc-log') const npa = require('npm-package-arg') const semver = require('semver') @@ -56,7 +57,7 @@ class Rebuild extends ArboristWorkspaceCmd { await arb.rebuild() } - this.npm.output('rebuilt dependencies successfully') + output.standard('rebuilt dependencies successfully') } isNode (specs, node) { diff --git a/lib/commands/root.js b/lib/commands/root.js index 7749c602456b7..f1f9579d103fd 100644 --- a/lib/commands/root.js +++ b/lib/commands/root.js @@ -1,3 +1,4 @@ +const { output } = require('proc-log') const BaseCommand = require('../base-command.js') class Root extends BaseCommand { static description = 'Display npm root' @@ -5,7 +6,7 @@ class Root extends BaseCommand { static params = ['global'] async exec () { - this.npm.output(this.npm.dir) + output.standard(this.npm.dir) } } module.exports = Root diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 2a7142a881941..d86f9b7326e3b 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -1,7 +1,7 @@ const runScript = require('@npmcli/run-script') const { isServerPackage } = runScript const pkgJson = require('@npmcli/package-json') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const didYouMean = require('../utils/did-you-mean.js') const { isWindowsShell } = require('../utils/is-windows.js') @@ -135,13 +135,13 @@ class RunScript extends BaseCommand { } if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify(scripts, null, 2)) + output.standard(JSON.stringify(scripts, null, 2)) return allScripts } if (this.npm.config.get('parseable')) { for (const [script, cmd] of Object.entries(scripts)) { - this.npm.output(`${script}:${cmd}`) + output.standard(`${script}:${cmd}`) } return allScripts @@ -158,7 +158,7 @@ class RunScript extends BaseCommand { const colorize = this.npm.chalk if (cmds.length) { - this.npm.output( + output.standard( `${colorize.reset(colorize.bold('Lifecycle scripts'))} included in ${colorize.green( pkgid )}:` @@ -166,24 +166,24 @@ class RunScript extends BaseCommand { } for (const script of cmds) { - this.npm.output(prefix + script + indent + colorize.dim(scripts[script])) + output.standard(prefix + script + indent + colorize.dim(scripts[script])) } if (!cmds.length && runScripts.length) { - this.npm.output( + output.standard( `${colorize.bold('Scripts')} available in ${colorize.green(pkgid)} via \`${colorize.blue( 'npm run-script' )}\`:` ) } else if (runScripts.length) { - this.npm.output(`\navailable via \`${colorize.blue('npm run-script')}\`:`) + output.standard(`\navailable via \`${colorize.blue('npm run-script')}\`:`) } for (const script of runScripts) { - this.npm.output(prefix + script + indent + colorize.dim(scripts[script])) + output.standard(prefix + script + indent + colorize.dim(scripts[script])) } - this.npm.output('') + output.standard('') return allScripts } @@ -220,7 +220,7 @@ class RunScript extends BaseCommand { const { content: { scripts, name } } = await pkgJson.normalize(workspacePath) res[name] = { ...scripts } } - this.npm.output(JSON.stringify(res, null, 2)) + output.standard(JSON.stringify(res, null, 2)) return } @@ -228,7 +228,7 @@ class RunScript extends BaseCommand { for (const workspacePath of this.workspacePaths) { const { content: { scripts, name } } = await pkgJson.normalize(workspacePath) for (const [script, cmd] of Object.entries(scripts || {})) { - this.npm.output(`${name}:${script}:${cmd}`) + output.standard(`${name}:${script}:${cmd}`) } } return diff --git a/lib/commands/sbom.js b/lib/commands/sbom.js index aea94099ef3b9..4b20bf5ea1f76 100644 --- a/lib/commands/sbom.js +++ b/lib/commands/sbom.js @@ -2,7 +2,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en') const BaseCommand = require('../base-command.js') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { cyclonedxOutput } = require('../utils/sbom-cyclonedx.js') const { spdxOutput } = require('../utils/sbom-spdx.js') @@ -86,7 +86,7 @@ class SBOM extends BaseCommand { items .sort((a, b) => localeCompare(a.location, b.location)) ) - this.npm.output(this.#parsedResponse) + output.standard(this.#parsedResponse) } async execWorkspaces (args) { diff --git a/lib/commands/search.js b/lib/commands/search.js index d1db948b34ba9..4a69c77a256e7 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -1,7 +1,7 @@ const { Minipass } = require('minipass') const Pipeline = require('minipass-pipeline') const libSearch = require('libnpmsearch') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const formatSearchStream = require('../utils/format-search-stream.js') @@ -99,12 +99,12 @@ class Search extends BaseCommand { if (!anyOutput) { anyOutput = true } - this.npm.output(chunk.toString('utf8')) + output.standard(chunk.toString('utf8')) }) await p.promise() if (!anyOutput && !this.npm.config.get('json') && !this.npm.config.get('parseable')) { - this.npm.output('No matches found for ' + (args.map(JSON.stringify).join(' '))) + output.standard('No matches found for ' + (args.map(JSON.stringify).join(' '))) } log.silly('search', 'search completed') diff --git a/lib/commands/star.js b/lib/commands/star.js index 54ebdb535865d..39165d8c3d8dc 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -1,6 +1,6 @@ const fetch = require('npm-registry-fetch') const npa = require('npm-package-arg') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity') const BaseCommand = require('../base-command.js') @@ -62,7 +62,7 @@ class Star extends BaseCommand { body, }) - this.npm.output(show + ' ' + pkg.name) + output.standard(show + ' ' + pkg.name) log.verbose('star', data) return data } diff --git a/lib/commands/stars.js b/lib/commands/stars.js index f4a8321692f93..1d92d97d7760a 100644 --- a/lib/commands/stars.js +++ b/lib/commands/stars.js @@ -1,5 +1,5 @@ const fetch = require('npm-registry-fetch') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const getIdentity = require('../utils/get-identity.js') const BaseCommand = require('../base-command.js') @@ -25,7 +25,7 @@ class Stars extends BaseCommand { } for (const row of rows) { - this.npm.output(row.value) + output.standard(row.value) } } catch (err) { if (err.code === 'ENEEDAUTH') { diff --git a/lib/commands/team.js b/lib/commands/team.js index 3c6cf305a6e5f..4ffaa5ff86ab9 100644 --- a/lib/commands/team.js +++ b/lib/commands/team.js @@ -1,5 +1,6 @@ const columns = require('cli-columns') const libteam = require('libnpmteam') +const { output } = require('proc-log') const otplease = require('../utils/otplease.js') @@ -68,86 +69,86 @@ class Team extends BaseCommand { async create (entity, opts) { await libteam.create(entity, opts) if (opts.json) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ created: true, team: entity, })) } else if (opts.parseable) { - this.npm.output(`${entity}\tcreated`) + output.standard(`${entity}\tcreated`) } else if (!this.npm.silent) { - this.npm.output(`+@${entity}`) + output.standard(`+@${entity}`) } } async destroy (entity, opts) { await libteam.destroy(entity, opts) if (opts.json) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ deleted: true, team: entity, })) } else if (opts.parseable) { - this.npm.output(`${entity}\tdeleted`) + output.standard(`${entity}\tdeleted`) } else if (!this.npm.silent) { - this.npm.output(`-@${entity}`) + output.standard(`-@${entity}`) } } async add (entity, user, opts) { await libteam.add(user, entity, opts) if (opts.json) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ added: true, team: entity, user, })) } else if (opts.parseable) { - this.npm.output(`${user}\t${entity}\tadded`) + output.standard(`${user}\t${entity}\tadded`) } else if (!this.npm.silent) { - this.npm.output(`${user} added to @${entity}`) + output.standard(`${user} added to @${entity}`) } } async rm (entity, user, opts) { await libteam.rm(user, entity, opts) if (opts.json) { - this.npm.output(JSON.stringify({ + output.standard(JSON.stringify({ removed: true, team: entity, user, })) } else if (opts.parseable) { - this.npm.output(`${user}\t${entity}\tremoved`) + output.standard(`${user}\t${entity}\tremoved`) } else if (!this.npm.silent) { - this.npm.output(`${user} removed from @${entity}`) + output.standard(`${user} removed from @${entity}`) } } async listUsers (entity, opts) { const users = (await libteam.lsUsers(entity, opts)).sort() if (opts.json) { - this.npm.output(JSON.stringify(users, null, 2)) + output.standard(JSON.stringify(users, null, 2)) } else if (opts.parseable) { - this.npm.output(users.join('\n')) + output.standard(users.join('\n')) } else if (!this.npm.silent) { const plural = users.length === 1 ? '' : 's' const more = users.length === 0 ? '' : ':\n' - this.npm.output(`\n@${entity} has ${users.length} user${plural}${more}`) - this.npm.output(columns(users, { padding: 1 })) + output.standard(`\n@${entity} has ${users.length} user${plural}${more}`) + output.standard(columns(users, { padding: 1 })) } } async listTeams (entity, opts) { const teams = (await libteam.lsTeams(entity, opts)).sort() if (opts.json) { - this.npm.output(JSON.stringify(teams, null, 2)) + output.standard(JSON.stringify(teams, null, 2)) } else if (opts.parseable) { - this.npm.output(teams.join('\n')) + output.standard(teams.join('\n')) } else if (!this.npm.silent) { const plural = teams.length === 1 ? '' : 's' const more = teams.length === 0 ? '' : ':\n' - this.npm.output(`\n@${entity} has ${teams.length} team${plural}${more}`) - this.npm.output(columns(teams.map(t => `@${t}`), { padding: 1 })) + output.standard(`\n@${entity} has ${teams.length} team${plural}${more}`) + output.standard(columns(teams.map(t => `@${t}`), { padding: 1 })) } } } diff --git a/lib/commands/token.js b/lib/commands/token.js index d87949fd77af5..ba842f9747948 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -1,5 +1,5 @@ const Table = require('cli-table3') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const profile = require('npm-profile') const otplease = require('../utils/otplease.js') @@ -51,12 +51,12 @@ class Token extends BaseCommand { log.info('token', 'getting list') const tokens = profile.listTokens(conf) if (conf.json) { - this.npm.output(JSON.stringify(tokens, null, 2)) + output.standard(JSON.stringify(tokens, null, 2)) return } else if (conf.parseable) { - this.npm.output(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t')) + output.standard(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t')) tokens.forEach(token => { - this.npm.output( + output.standard( [ token.key, token.token, @@ -83,7 +83,7 @@ class Token extends BaseCommand { token.cidr_whitelist ? token.cidr_whitelist.join(', ') : '', ]) }) - this.npm.output(table.toString()) + output.standard(table.toString()) } async rm (args) { @@ -119,11 +119,11 @@ class Token extends BaseCommand { }) ) if (conf.json) { - this.npm.output(JSON.stringify(toRemove)) + output.standard(JSON.stringify(toRemove)) } else if (conf.parseable) { - this.npm.output(toRemove.join('\t')) + output.standard(toRemove.join('\t')) } else { - this.npm.output('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : '')) + output.standard('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : '')) } } @@ -143,15 +143,15 @@ class Token extends BaseCommand { delete result.key delete result.updated if (conf.json) { - this.npm.output(JSON.stringify(result)) + output.standard(JSON.stringify(result)) } else if (conf.parseable) { - Object.keys(result).forEach(k => this.npm.output(k + '\t' + result[k])) + Object.keys(result).forEach(k => output.standard(k + '\t' + result[k])) } else { const table = new Table() for (const k of Object.keys(result)) { table.push({ [this.npm.chalk.bold(k)]: String(result[k]) }) } - this.npm.output(table.toString()) + output.standard(table.toString()) } } diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index bf02d96712825..3e7e8c8b624a2 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -2,11 +2,10 @@ const libaccess = require('libnpmaccess') const libunpub = require('libnpmpublish').unpublish const npa = require('npm-package-arg') const pacote = require('pacote') +const { output, log } = require('proc-log') const pkgJson = require('@npmcli/package-json') - const { flatten } = require('@npmcli/config/lib/definitions') const getIdentity = require('../utils/get-identity.js') -const { log } = require('proc-log') const otplease = require('../utils/otplease.js') const LAST_REMAINING_VERSION_ERROR = 'Refusing to delete the last version of the package. ' + @@ -161,7 +160,7 @@ class Unpublish extends BaseCommand { await otplease(this.npm, opts, o => libunpub(spec, o)) } if (!silent) { - this.npm.output(`- ${spec.name}${pkgVersion}`) + output.standard(`- ${spec.name}${pkgVersion}`) } } diff --git a/lib/commands/version.js b/lib/commands/version.js index 029a6fdd3101e..9776b70240519 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -1,6 +1,7 @@ const libnpmversion = require('libnpmversion') const { resolve } = require('path') const { promisify } = require('util') +const { output } = require('proc-log') const readFile = promisify(require('fs').readFile) const updateWorkspaces = require('../workspaces/update-workspaces.js') @@ -78,7 +79,7 @@ class Version extends BaseCommand { ...this.npm.flatOptions, path: this.npm.prefix, }) - return this.npm.output(`${prefix}${version}`) + return output.standard(`${prefix}${version}`) } async changeWorkspaces (args) { @@ -86,14 +87,14 @@ class Version extends BaseCommand { await this.setWorkspaces() const updatedWorkspaces = [] for (const [name, path] of this.workspaces) { - this.npm.output(name) + output.standard(name) const version = await libnpmversion(args[0], { ...this.npm.flatOptions, 'git-tag-version': false, path, }) updatedWorkspaces.push(name) - this.npm.output(`${prefix}${version}`) + output.standard(`${prefix}${version}`) } return this.update(updatedWorkspaces) } @@ -115,9 +116,9 @@ class Version extends BaseCommand { } if (this.npm.config.get('json')) { - this.npm.output(JSON.stringify(results, null, 2)) + output.standard(JSON.stringify(results, null, 2)) } else { - this.npm.output(results) + output.standard(results) } } diff --git a/lib/commands/view.js b/lib/commands/view.js index 25a45ab016758..9fb0f8add1ca7 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -1,7 +1,7 @@ const columns = require('cli-columns') const fs = require('fs') const jsonParse = require('json-parse-even-better-errors') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const npa = require('npm-package-arg') const { resolve } = require('path') const formatBytes = require('../utils/format-bytes.js') @@ -118,7 +118,7 @@ class View extends BaseCommand { const msg = await this.jsonData(reducedData, pckmnt._id) if (msg !== '') { - this.npm.output(msg) + output.standard(msg) } } } @@ -157,10 +157,10 @@ class View extends BaseCommand { if (wholePackument) { data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]][''])) } else { - this.npm.output(`${name}:`) + output.standard(`${name}:`) const msg = await this.jsonData(reducedData, pckmnt._id) if (msg !== '') { - this.npm.output(msg) + output.standard(msg) } } } else { @@ -171,7 +171,7 @@ class View extends BaseCommand { } } if (Object.keys(results).length > 0) { - this.npm.output(JSON.stringify(results, null, 2)) + output.standard(JSON.stringify(results, null, 2)) } } @@ -374,61 +374,61 @@ class View extends BaseCommand { info.license = chalk.green(info.license) } - this.npm.output('') - this.npm.output( + output.standard('') + output.standard( chalk.underline.bold(`${info.name}@${info.version}`) + ' | ' + info.license + ' | deps: ' + (info.deps.length ? chalk.cyan(info.deps.length) : chalk.green('none')) + ' | versions: ' + info.versions ) - info.description && this.npm.output(info.description) + info.description && output.standard(info.description) if (info.repo || info.site) { - info.site && this.npm.output(chalk.cyan(info.site)) + info.site && output.standard(chalk.cyan(info.site)) } const warningSign = unicode ? ' ⚠️ ' : '!!' - info.deprecated && this.npm.output( + info.deprecated && output.standard( `\n${chalk.bold.red('DEPRECATED')}${ warningSign } - ${info.deprecated}` ) if (info.keywords.length) { - this.npm.output('') - this.npm.output(`keywords: ${chalk.yellow(info.keywords.join(', '))}`) + output.standard('') + output.standard(`keywords: ${chalk.yellow(info.keywords.join(', '))}`) } if (info.bins.length) { - this.npm.output('') - this.npm.output(`bin: ${chalk.yellow(info.bins.join(', '))}`) + output.standard('') + output.standard(`bin: ${chalk.yellow(info.bins.join(', '))}`) } - this.npm.output('') - this.npm.output('dist') - this.npm.output(`.tarball: ${info.tarball}`) - this.npm.output(`.shasum: ${info.shasum}`) - info.integrity && this.npm.output(`.integrity: ${info.integrity}`) - info.unpackedSize && this.npm.output(`.unpackedSize: ${info.unpackedSize}`) + output.standard('') + output.standard('dist') + output.standard(`.tarball: ${info.tarball}`) + output.standard(`.shasum: ${info.shasum}`) + info.integrity && output.standard(`.integrity: ${info.integrity}`) + info.unpackedSize && output.standard(`.unpackedSize: ${info.unpackedSize}`) const maxDeps = 24 if (info.deps.length) { - this.npm.output('') - this.npm.output('dependencies:') - this.npm.output(columns(info.deps.slice(0, maxDeps), { padding: 1 })) + output.standard('') + output.standard('dependencies:') + output.standard(columns(info.deps.slice(0, maxDeps), { padding: 1 })) if (info.deps.length > maxDeps) { - this.npm.output(`(...and ${info.deps.length - maxDeps} more.)`) + output.standard(`(...and ${info.deps.length - maxDeps} more.)`) } } if (info.maintainers && info.maintainers.length) { - this.npm.output('') - this.npm.output('maintainers:') - info.maintainers.forEach((u) => this.npm.output(`- ${u}`)) + output.standard('') + output.standard('maintainers:') + info.maintainers.forEach((u) => output.standard(`- ${u}`)) } - this.npm.output('') - this.npm.output('dist-tags:') - this.npm.output(columns(info.tags)) + output.standard('') + output.standard('dist-tags:') + output.standard(columns(info.tags)) if (info.publisher || info.modified) { let publishInfo = 'published' @@ -438,8 +438,8 @@ class View extends BaseCommand { if (info.publisher) { publishInfo += ` by ${info.publisher}` } - this.npm.output('') - this.npm.output(publishInfo) + output.standard('') + output.standard(publishInfo) } } } diff --git a/lib/commands/whoami.js b/lib/commands/whoami.js index 154cc870391ba..e05993abdd5bf 100644 --- a/lib/commands/whoami.js +++ b/lib/commands/whoami.js @@ -1,3 +1,4 @@ +const { output } = require('proc-log') const getIdentity = require('../utils/get-identity.js') const BaseCommand = require('../base-command.js') @@ -8,7 +9,7 @@ class Whoami extends BaseCommand { async exec (args) { const username = await getIdentity(this.npm, { ...this.npm.flatOptions }) - this.npm.output( + output.standard( this.npm.config.get('json') ? JSON.stringify(username) : username ) } diff --git a/lib/npm.js b/lib/npm.js index 55e866bd492e7..8449e2e2775a8 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -244,7 +244,13 @@ class Npm { this.time('npm:load:display', () => { this.#display.load({ loglevel: this.config.get('loglevel'), - chalk: this.logChalk, // Use logChalk since that is based on stderr + // TODO: only pass in logColor and color and create chalk instances + // in display load method. Then remove chalk getters from npm and + // producers should emit chalk-templates (or something else). + stdoutChalk: this.#chalk, + stdoutColor: this.color, + stderrChalk: this.#logChalk, + stderrColor: this.logColor, timing: this.config.get('timing'), unicode: this.config.get('unicode'), progress: this.flatOptions.progress, @@ -433,22 +439,12 @@ class Npm { return usage(this) } + // TODO: move to proc-log and remove forceLog (...args) { this.#display.forceLog(...args) } - output (...args) { - this.#display.output(...args) - } - - outputError (...args) { - this.#display.outputError(...args) - } - - outputBuffer (arg) { - this.#display.outputBuffer(arg) - } - + // TODO: move to proc-log and remove flushOutput (jsonError) { this.#display.flushOutput(jsonError) } diff --git a/lib/utils/audit-error.js b/lib/utils/audit-error.js index de3a026553dfc..10aec7592b03c 100644 --- a/lib/utils/audit-error.js +++ b/lib/utils/audit-error.js @@ -1,4 +1,4 @@ -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { redactLog: replaceInfo } = require('@npmcli/redact') // print an error or just nothing if the audit report has an error @@ -22,7 +22,7 @@ const auditError = (npm, report) => { const { body: errBody } = error const body = Buffer.isBuffer(errBody) ? errBody.toString() : errBody if (npm.flatOptions.json) { - npm.output(JSON.stringify({ + output.standard(JSON.stringify({ message: error.message, method: error.method, uri: replaceInfo(error.uri), @@ -31,7 +31,7 @@ const auditError = (npm, report) => { body, }, null, 2)) } else { - npm.output(body) + output.standard(body) } throw 'audit endpoint returned an error' diff --git a/lib/utils/display.js b/lib/utils/display.js index ba62ae5e0aee0..05cdee6f9dbe4 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -1,7 +1,7 @@ const proggy = require('proggy') -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { explain } = require('./explain-eresolve.js') -const { formatWithOptions, format } = require('./format') +const { formatWithOptions } = require('./format') const COLOR_PALETTE = ({ chalk: c }) => ({ heading: c.white.bgBlack, @@ -17,7 +17,13 @@ const COLOR_PALETTE = ({ chalk: c }) => ({ silly: c.inverse, }) -const LEVELS = log.LEVELS.reduce((acc, key) => { +const LOG_LEVELS = log.LEVELS.reduce((acc, key) => { + acc[key] = key + return acc +}, {}) + +// TODO: move flush to proc-log +const OUTPUT_LEVELS = ['flush', ...output.LEVELS].reduce((acc, key) => { acc[key] = key return acc }, {}) @@ -55,20 +61,20 @@ const LEVEL_OPTIONS = { const LEVEL_METHODS = { ...LEVEL_OPTIONS, - [LEVELS.timing]: { + [LOG_LEVELS.timing]: { show: ({ timing, index }) => !!timing && index !== 0, }, } -const safeJsonParse = (maybeJsonStr) => { - if (typeof maybeJsonStr !== 'string') { - return maybeJsonStr - } - try { - return JSON.parse(maybeJsonStr) - } catch { - return {} +const tryJsonParse = (value) => { + if (typeof value === 'string') { + try { + return JSON.parse(value) + } catch { + return {} + } } + return value } const setBlocking = (stream) => { @@ -81,17 +87,30 @@ const setBlocking = (stream) => { return stream } +const getLevel = (stringOrLevelObject) => { + if (typeof stringOrLevelObject === 'string') { + return { level: stringOrLevelObject } + } + return stringOrLevelObject +} + class Display { - // pause by default until config is loaded - #paused = true + #logState = { + buffering: true, + buffer: [], + } - // buffers to store logs when paused - #logBuffer = [] - #outputBuffer = [] + #outputState = { + buffering: true, + buffer: [], + } // colors - #chalk - #colors + #stdoutChalk + #stdoutColor + #stderrChalk + #stderrColor + #logColors // progress #progress @@ -109,138 +128,215 @@ class Display { constructor ({ stdout, stderr }) { this.#stdout = setBlocking(stdout) this.#stderr = setBlocking(stderr) + + // Handlers are set immediately so they can buffer all events process.on('log', this.#logHandler) + process.on('output', this.#outputHandler) } off () { process.off('log', this.#logHandler) - this.#logBuffer.length = 0 + this.#logState.buffer.length = 0 + + process.off('output', this.#outputHandler) + this.#outputState.buffer.length = 0 + if (this.#progress) { this.#progress.stop() } } - load ({ loglevel, chalk, timing, unicode, progress, json, heading }) { - this.#chalk = chalk - this.#colors = COLOR_PALETTE({ chalk }) + load ({ + heading, + json, + loglevel, + progress, + stderrChalk, + stderrColor, + stdoutChalk, + stdoutColor, + timing, + unicode, + }) { + this.#stdoutColor = stdoutColor + this.#stdoutChalk = stdoutChalk + + this.#stderrColor = stderrColor + this.#stderrChalk = stderrChalk + + this.#logColors = COLOR_PALETTE({ chalk: stderrChalk }) this.#levelIndex = LEVEL_OPTIONS[loglevel].index this.#timing = timing this.#json = json this.#heading = heading + // In silent mode we remove all the handlers if (this.#levelIndex <= 0) { this.off() - } else { - log.resume() - if (progress) { - this.#startProgress({ unicode }) - } + return } - } - forceLog (level, ...args) { - // This will show the log regardless of the current loglevel, except when silent - this.#logHandler({ level, force: true }, ...args) - } + // Emit resume event on the logs which will flush output + log.resume() + + // TODO: this should be a proc-log method `proc-log.output.flush`? + this.#outputHandler(OUTPUT_LEVELS.flush) - output (...args) { - // TODO: make this respect silent option - this.#stdout.write(format(...args)) + this.#startProgress({ progress, unicode }) } - outputError (...args) { - this.#stderr.write(format(...args)) + // STREAM WRITES + + // Write formatted and (non-)colorized output to streams + #stdoutWrite (options, ...args) { + this.#stdout.write(formatWithOptions({ colors: this.#stdoutColor, ...options }, ...args)) } - outputBuffer (item) { - this.#outputBuffer.push(item) + #stderrWrite (options, ...args) { + this.#stderr.write(formatWithOptions({ colors: this.#stderrColor, ...options }, ...args)) } - flushOutput (jsonError) { - if (!jsonError && !this.#outputBuffer.length) { + // HANDLERS + + // Arrow function assigned to a private class field so it can be passed + // directly as a listener and still reference "this" + #logHandler = (...args) => { + if (args[0] === LOG_LEVELS.resume) { + this.#logState.buffering = false + this.#logState.buffer.forEach((item) => this.#tryWriteLog(...item)) + this.#logState.buffer.length = 0 return } - if (this.#json) { - const output = this.#outputBuffer.reduce((a, i) => ({ ...a, ...safeJsonParse(i) }), {}) - this.output(JSON.stringify({ ...output, ...jsonError }, null, 2)) - } else { - this.#outputBuffer.forEach((item) => this.output(item)) + if (args[0] === LOG_LEVELS.pause) { + this.#logState.buffering = true + return } - this.#outputBuffer.length = 0 + if (this.#logState.buffering) { + this.#logState.buffer.push(args) + return + } + + this.#tryWriteLog(...args) } - #write (...args) { - const { level: levelName, force = false } = typeof args[0] === 'string' - ? { level: args[0] } : args[0] + // Arrow function assigned to a private class field so it can be passed + // directly as a listener and still reference "this" + #outputHandler = (...args) => { + if (args[0] === OUTPUT_LEVELS.flush) { + this.#outputState.buffering = false + if (args[1] && this.#json) { + const json = {} + for (const [, item] of this.#outputState.buffer) { + Object.assign(json, tryJsonParse(item)) + } + this.#writeOutput('standard', JSON.stringify({ ...json, ...args[1] }, null, 2)) + } else { + this.#outputState.buffer.forEach((item) => this.#writeOutput(...item)) + } + this.#outputState.buffer.length = 0 + return + } - if (levelName === LEVELS.pause) { - this.#paused = true + if (args[0] === OUTPUT_LEVELS.buffer) { + this.#outputState.buffer.push(['standard', ...args.slice(1)]) return } - if (levelName === LEVELS.resume) { - this.#paused = false - this.#logBuffer.forEach((item) => this.#write(...item)) - this.#logBuffer.length = 0 + if (this.#outputState.buffering) { + this.#outputState.buffer.push(args) return } - if (this.#paused) { - this.#logBuffer.push(args) + this.#writeOutput(...args) + } + + // OUTPUT + + #writeOutput (...args) { + const { level } = getLevel(args.shift()) + + if (level === OUTPUT_LEVELS.standard) { + this.#stdoutWrite({}, ...args) return } - const level = LEVEL_METHODS[levelName] - const show = level.show ?? (({ index }) => level.index <= index) + if (level === OUTPUT_LEVELS.error) { + this.#stderrWrite({}, ...args) + } + } - if ((force && level.index !== 0) || show({ index: this.#levelIndex, timing: this.#timing })) { - // this mutates the array so we can pass args directly to format later - const [, title] = args.splice(0, 2) - const prefix = [ - this.#colors.heading(this.#heading), - this.#colors[levelName](level.label ?? levelName), - title ? this.#colors.title(title) : null, - ] - this.#stderr.write(formatWithOptions({ prefix }, ...args)) - } else if (this.#progress) { - // TODO: make this display a single log line of filtered messages + // TODO: move this to proc-log and remove this public method + flushOutput (jsonError) { + this.#outputHandler(OUTPUT_LEVELS.flush, jsonError) + } + + // LOGS + + // TODO: make proc-log able to send signal data like `force` + // when that happens, remove this public method + forceLog (level, ...args) { + // This will show the log regardless of the current loglevel except when silent + if (this.#levelIndex !== 0) { + this.#logHandler({ level, force: true }, ...args) } } - #logHandler = (level, ...args) => { + #tryWriteLog (...args) { try { - this.#log(level, ...args) + // Also (and this is a really inexcusable kludge), we patch the + // log.warn() method so that when we see a peerDep override + // explanation from Arborist, we can replace the object with a + // highly abbreviated explanation of what's being overridden. + // TODO: this could probably be moved to arborist now that display is refactored + const [level, heading, message, expl] = args + if (level === LOG_LEVELS.warn && heading === 'ERESOLVE' && expl && typeof expl === 'object') { + this.#writeLog(level, heading, message) + this.#writeLog(level, '', explain(expl, this.#stderrChalk, 2)) + return + } + this.#writeLog(...args) } catch (ex) { try { // if it crashed once, it might again! - this.#write(LEVELS.verbose, null, `attempt to log crashed`, ...args, ex) + this.#writeLog(LOG_LEVELS.verbose, null, `attempt to log crashed`, ...args, ex) } catch (ex2) { - /* istanbul ignore next - this happens if the object has an inspect method that crashes */ + // This happens if the object has an inspect method that crashes so just console.error + // with the errors but don't do anything else that might error again. // eslint-disable-next-line no-console console.error(`attempt to log crashed`, ex, ex2) } } } - #log (...args) { - const [level, heading, message, expl] = args - if (level === LEVELS.warn && heading === 'ERESOLVE' && expl && typeof expl === 'object') { - // Also (and this is a really inexcusable kludge), we patch the - // log.warn() method so that when we see a peerDep override - // explanation from Arborist, we can replace the object with a - // highly abbreviated explanation of what's being overridden. - // TODO: this could probably be moved to arborist now that display is refactored - this.#write(level, heading, message) - this.#write(level, '', explain(expl, this.#chalk, 2)) - return + #writeLog (...args) { + const { level, force = false } = getLevel(args.shift()) + + const levelOpts = LEVEL_METHODS[level] + const show = levelOpts.show ?? (({ index }) => levelOpts.index <= index) + + if (force || show({ index: this.#levelIndex, timing: this.#timing })) { + // this mutates the array so we can pass args directly to format later + const title = args.shift() + const prefix = [ + this.#logColors.heading(this.#heading), + this.#logColors[level](levelOpts.label ?? level), + title ? this.#logColors.title(title) : null, + ] + this.#stderrWrite({ prefix }, ...args) + } else if (this.#progress) { + // TODO: make this display a single log line of filtered messages } - this.#write(...args) } - #startProgress ({ unicode }) { + // PROGRESS + + #startProgress ({ progress, unicode }) { + if (!progress) { + return + } this.#progress = proggy.createClient({ normalize: true }) // TODO: implement proggy trackers in arborist/doctor // TODO: listen to progress events here and build progress UI diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index 3f2ffaaf0c807..ce1bfaa54a56a 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -1,7 +1,6 @@ const os = require('os') const fs = require('fs') - -const { log } = require('proc-log') +const { log, output } = require('proc-log') const errorMessage = require('./error-message.js') const { redactLog: replaceInfo } = require('@npmcli/redact') @@ -32,10 +31,13 @@ process.on('exit', code => { if (!exitHandlerCalled) { process.exitCode = code || 1 log.error('', 'Exit handler never called!') - // eslint-disable-next-line no-console - console.error('') log.error('', 'This is an error with npm itself. Please report this error at:') log.error('', ' ') + + // This gets logged regardless of silent mode + // eslint-disable-next-line no-console + console.error('') + showLogFileError = true } @@ -60,11 +62,8 @@ process.on('exit', code => { const logMethod = showLogFileError ? 'error' : timing ? 'info' : null if (logMethod) { - if (!npm.silent) { - // just a line break if not in silent mode - // eslint-disable-next-line no-console - console.error('') - } + // just a line break, will be ignored in silent mode + output.error('') const message = [] @@ -108,6 +107,7 @@ const exitHandler = err => { if (!npm) { err = err || new Error('Exit prior to setting npm in exit handler') + // Don't use proc-log here since npm was never set // eslint-disable-next-line no-console console.error(err.stack || err.message) return process.exit(1) @@ -115,12 +115,14 @@ const exitHandler = err => { if (!hasLoadedNpm) { err = err || new Error('Exit prior to config file resolving.') + // Don't use proc-log here since npm was never loaded // eslint-disable-next-line no-console console.error(err.stack || err.message) } // only show the notification if it finished. if (typeof npm.updateNotification === 'string') { + // TODO: use proc-log to send `force: true` along with event npm.forceLog('notice', '', npm.updateNotification) } @@ -198,6 +200,7 @@ const exitHandler = err => { } if (hasLoadedNpm) { + // TODO: use proc-log.output.flush() once it exists npm.flushOutput(jsonError) } diff --git a/lib/utils/format.js b/lib/utils/format.js index 97f01996a2025..abfbf9e331704 100644 --- a/lib/utils/format.js +++ b/lib/utils/format.js @@ -47,6 +47,4 @@ const formatWithOptions = ({ prefix: prefixes = [], eol = '\n', ...options }, .. return lines.reduce((acc, l) => `${acc}${prefix}${prefix && l ? ' ' : ''}${l}${eol}`, '') } -const format = (...args) => formatWithOptions({}, ...args) - -module.exports = { format, formatWithOptions } +module.exports = { formatWithOptions } diff --git a/lib/utils/open-url-prompt.js b/lib/utils/open-url-prompt.js index 71a68c253c050..261cf370da6bd 100644 --- a/lib/utils/open-url-prompt.js +++ b/lib/utils/open-url-prompt.js @@ -1,4 +1,5 @@ const readline = require('readline') +const { output } = require('proc-log') const open = require('./open-url.js') function print (npm, title, url) { @@ -6,7 +7,7 @@ function print (npm, title, url) { const message = json ? JSON.stringify({ title, url }) : `${title}:\n${url}` - npm.output(message) + output.standard(message) } // Prompt to open URL in browser if possible @@ -48,7 +49,7 @@ const promptOpen = async (npm, url, title, prompt, emitter) => { rl.close() // clear the prompt line - npm.output('') + output.standard('') resolve(false) }) diff --git a/lib/utils/open-url.js b/lib/utils/open-url.js index 77bb1d03d8e16..46b7abc731fa1 100644 --- a/lib/utils/open-url.js +++ b/lib/utils/open-url.js @@ -1,4 +1,5 @@ const promiseSpawn = require('@npmcli/promise-spawn') +const { output } = require('proc-log') const { URL } = require('url') @@ -16,7 +17,7 @@ const open = async (npm, url, errMsg, isFile) => { }, null, 2) : `${errMsg}:\n ${url}\n` - npm.output(alternateMsg) + output.standard(alternateMsg) } if (browser === false) { diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index 58cf76dda7837..40f1722f246e9 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -9,7 +9,7 @@ // found 37 vulnerabilities (5 low, 7 moderate, 25 high) // run `npm audit fix` to fix them, or `npm audit` for details -const { log } = require('proc-log') +const { log, output } = require('proc-log') const { depth } = require('treeverse') const ms = require('ms') const npmAuditReport = require('npm-audit-report') @@ -99,7 +99,7 @@ const reifyOutput = (npm, arb) => { }) if (diffTable) { - npm.output('\n' + diffTable.toString()) + output.standard('\n' + diffTable.toString()) } } @@ -115,7 +115,7 @@ const reifyOutput = (npm, arb) => { summary.audit = npm.command === 'audit' ? auditReport : auditReport.toJSON().metadata } - npm.output(JSON.stringify(summary, null, 2)) + output.standard(JSON.stringify(summary, null, 2)) } else { packagesChangedMessage(npm, summary) packagesFundingMessage(npm, summary) @@ -134,7 +134,7 @@ const printAuditReport = (npm, report) => { if (!res || !res.report) { return } - npm.output(`\n${res.report}`) + output.standard(`\n${res.report}`) } const getAuditReport = (npm, report) => { @@ -206,7 +206,7 @@ const packagesChangedMessage = (npm, { added, removed, changed, audited }) => { } msg.push(` in ${ms(Date.now() - npm.started)}`) - npm.output(msg.join('')) + output.standard(msg.join('')) } const packagesFundingMessage = (npm, { funding }) => { @@ -214,11 +214,11 @@ const packagesFundingMessage = (npm, { funding }) => { return } - npm.output('') + output.standard('') const pkg = funding === 1 ? 'package' : 'packages' const is = funding === 1 ? 'is' : 'are' - npm.output(`${funding} ${pkg} ${is} looking for funding`) - npm.output(' run `npm fund` for details') + output.standard(`${funding} ${pkg} ${is} looking for funding`) + output.standard(' run `npm fund` for details') } module.exports = reifyOutput diff --git a/test/lib/commands/ci.js b/test/lib/commands/ci.js index 681ccad7d87a7..6ab9662fc8be5 100644 --- a/test/lib/commands/ci.js +++ b/test/lib/commands/ci.js @@ -164,7 +164,6 @@ t.test('lifecycle scripts', async t => { }, mocks: { '@npmcli/run-script': (opts) => { - t.ok(opts.banner) scripts.push(opts.event) }, }, diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index 93d4da22d31c2..067e84a0980e0 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -121,28 +121,17 @@ t.test('foreground-scripts defaults to true', async t => { config: { 'dry-run': true }, }) - /* eslint no-console: 0 */ - // TODO: replace this with `const results = t.intercept(console, 'log')` - const log = console.log - t.teardown(() => { - console.log = log - }) - const caughtLogs = [] - console.log = (...args) => { - caughtLogs.push(args) - } - // end TODO - await npm.exec('pack', []) const filename = 'test-fg-scripts-0.0.0.tgz' - t.same( - caughtLogs, + t.strictSame( + outputs, [ - ['\n> test-fg-scripts@0.0.0 prepack\n> echo prepack!\n'], - ['\n> test-fg-scripts@0.0.0 postpack\n> echo postpack!\n'], + '\n> test-fg-scripts@0.0.0 prepack\n> echo prepack!\n', + '\n> test-fg-scripts@0.0.0 postpack\n> echo postpack!\n', + filename, ], - 'prepack and postpack log to stdout') - t.strictSame(outputs, [filename]) + 'prepack and postpack log to stdout' + ) t.matchSnapshot(logs.notice, 'logs pack contents') t.throws(() => fs.statSync(path.resolve(npm.prefix, filename))) }) @@ -163,25 +152,10 @@ t.test('foreground-scripts can still be set to false', async t => { config: { 'dry-run': true, 'foreground-scripts': false }, }) - /* eslint no-console: 0 */ - // TODO: replace this with `const results = t.intercept(console, 'log')` - const log = console.log - t.teardown(() => { - console.log = log - }) - const caughtLogs = [] - console.log = (...args) => { - caughtLogs.push(args) - } - // end TODO - await npm.exec('pack', []) const filename = 'test-fg-scripts-0.0.0.tgz' - t.same( - caughtLogs, - [], - 'prepack and postpack do not log to stdout') - t.strictSame(outputs, [filename]) + + t.strictSame(outputs, [filename], 'prepack and postpack do not log to stdout') t.matchSnapshot(logs.notice, 'logs pack contents') t.throws(() => fs.statSync(path.resolve(npm.prefix, filename))) }) diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 751cd97d8acf6..85a66d88b8b34 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -83,6 +83,8 @@ t.test('re-loads publishConfig.registry if added during script process', async t const { joinedOutput, npm } = await loadMockNpm(t, { config: { [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + // Keep output from leaking into tap logs for readability + 'foreground-scripts': false, }, prefixDir: { 'package.json': JSON.stringify({ @@ -136,6 +138,8 @@ t.test('prioritize CLI flags over publishConfig', async t => { const { joinedOutput, npm } = await loadMockNpm(t, { config: { [`${alternateRegistry.slice(6)}/:_authToken`]: 'test-other-token', + // Keep output from leaking into tap logs for readability + 'foreground-scripts': false, }, prefixDir: { 'package.json': JSON.stringify({ @@ -220,7 +224,7 @@ t.test('dry-run', async t => { }) t.test('foreground-scripts defaults to true', async t => { - const { joinedOutput, npm, logs } = await loadMockNpm(t, { + const { outputs, npm, logs } = await loadMockNpm(t, { config: { 'dry-run': true, ...auth, @@ -238,33 +242,22 @@ t.test('foreground-scripts defaults to true', async t => { }, }) - /* eslint no-console: 0 */ - // TODO: replace this with `const results = t.intercept(console, 'log')` - const log = console.log - t.teardown(() => { - console.log = log - }) - const caughtLogs = [] - console.log = (...args) => { - caughtLogs.push(args) - } - // end TODO - await npm.exec('publish', []) - t.equal(joinedOutput(), `+ test-fg-scripts@0.0.0`) + t.matchSnapshot(logs.notice) - t.same( - caughtLogs, + t.strictSame( + outputs, [ - ['\n> test-fg-scripts@0.0.0 prepack\n> echo prepack!\n'], - ['\n> test-fg-scripts@0.0.0 postpack\n> echo postpack!\n'], + '\n> test-fg-scripts@0.0.0 prepack\n> echo prepack!\n', + '\n> test-fg-scripts@0.0.0 postpack\n> echo postpack!\n', + `+ test-fg-scripts@0.0.0`, ], 'prepack and postpack log to stdout') }) t.test('foreground-scripts can still be set to false', async t => { - const { joinedOutput, npm, logs } = await loadMockNpm(t, { + const { outputs, npm, logs } = await loadMockNpm(t, { config: { 'dry-run': true, 'foreground-scripts': false, @@ -283,25 +276,13 @@ t.test('foreground-scripts can still be set to false', async t => { }, }) - /* eslint no-console: 0 */ - // TODO: replace this with `const results = t.intercept(console, 'log')` - const log = console.log - t.teardown(() => { - console.log = log - }) - const caughtLogs = [] - console.log = (...args) => { - caughtLogs.push(args) - } - // end TODO - await npm.exec('publish', []) - t.equal(joinedOutput(), `+ test-fg-scripts@0.0.0`) + t.matchSnapshot(logs.notice) - t.same( - caughtLogs, - [], + t.strictSame( + outputs, + [`+ test-fg-scripts@0.0.0`], 'prepack and postpack do not log to stdout') }) @@ -871,6 +852,7 @@ t.test('manifest', async t => { const { npm } = await loadMockNpm(t, { config: { ...auth, + 'foreground-scripts': false, }, chdir: () => root, mocks: { diff --git a/test/lib/npm.js b/test/lib/npm.js index ad776fc65b573..59400ce8da9f1 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -476,15 +476,6 @@ t.test('timings', async t => { } }) -t.test('outputs cleaned messages', async t => { - const { outputs, outputErrors, npm } = await loadMockNpm(t) - npm.output('hello\x00world') - npm.outputError('error\x00world') - - t.match(outputs, ['hello^@world']) - t.match(outputErrors, ['error^@world']) -}) - t.test('aliases and typos', async t => { const { Npm } = await loadMockNpm(t, { init: false }) t.throws(() => Npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js index a10b0dd621e60..31ea7bbed8add 100644 --- a/test/lib/utils/display.js +++ b/test/lib/utils/display.js @@ -1,49 +1,77 @@ const t = require('tap') const tmock = require('../../fixtures/tmock') const mockLogs = require('../../fixtures/mock-logs') +const mockGlobals = require('@npmcli/mock-globals') const { inspect } = require('util') const mockDisplay = async (t, { mocks, load } = {}) => { const { Chalk } = await import('chalk') - const { log } = require('proc-log') + const { log, output } = require('proc-log') + const logs = mockLogs() + const Display = tmock(t, '{LIB}/utils/display', mocks) const display = new Display(logs.streams) - display.load({ + const displayLoad = (opts) => display.load({ loglevel: 'silly', - chalk: new Chalk({ level: 0 }), + stderrChalk: new Chalk({ level: 0 }), + stderrColor: false, heading: 'npm', - ...load, + ...opts, }) + + if (load !== false) { + displayLoad(load) + } + t.teardown(() => display.off()) return { display, + output, log, + displayLoad, ...logs.logs, } } t.test('can log cleanly', async (t) => { + const { log, logs } = await mockDisplay(t) + + log.error('', 'test\x00message') + t.match(logs.error, ['test^@message']) +}) + +t.test('can handle special eresolves', async (t) => { const explains = [] const { log, logs } = await mockDisplay(t, { mocks: { '{LIB}/utils/explain-eresolve.js': { explain: (...args) => { explains.push(args) - return 'explanation' + return 'EXPLAIN' }, }, }, }) - log.error('', 'test\x00message') - t.match(logs.error, ['test^@message']) - log.warn('ERESOLVE', 'hello', { some: 'object' }) - t.match(logs.warn, ['ERESOLVE hello']) + t.strictSame(logs.warn, ['ERESOLVE hello', 'EXPLAIN']) t.match(explains, [[{ some: 'object' }, Function, 2]]) }) +t.test('can buffer output when paused', async t => { + const { displayLoad, outputs, output } = await mockDisplay(t, { + load: false, + }) + + output.buffer('Message 1') + output.standard('Message 2') + + t.strictSame(outputs, []) + displayLoad() + t.strictSame(outputs, ['Message 1', 'Message 2']) +}) + t.test('can do progress', async (t) => { const { log, logs } = await mockDisplay(t, { load: { @@ -58,24 +86,38 @@ t.test('can do progress', async (t) => { }) t.test('handles log throwing', async (t) => { - const { log, logs } = await mockDisplay(t, { - mocks: { - '{LIB}/utils/explain-eresolve.js': { - explain: () => { - throw new Error('explain') - }, - }, - }, - }) + class ThrowInspect { + #crashes = 0; - log.warn('ERESOLVE', 'hello', { some: 'object' }) + [inspect.custom] () { + throw new Error(`Crashed ${++this.#crashes}`) + } + } + + const errors = [] + mockGlobals(t, { 'console.error': (...msg) => errors.push(msg) }) + + const { log, logs } = await mockDisplay(t) + + log.error('woah', new ThrowInspect()) + + t.strictSame(logs.error, []) + t.equal(errors.length, 1) + t.match(errors[0], [ + 'attempt to log crashed', + new Error('Crashed 1'), + new Error('Crashed 2'), + ]) +}) - t.match(logs.verbose[0], - `attempt to log crashed ERESOLVE hello { some: 'object' } Error: explain`) +t.test('incorrect levels', async t => { + const { outputs } = await mockDisplay(t) + process.emit('output', 'not a real level') + t.strictSame(outputs, [], 'output is ignored') }) t.test('Display.clean', async (t) => { - const { display, outputs, clearOutput } = await mockDisplay(t) + const { output, outputs, clearOutput } = await mockDisplay(t) class CustomObj { #inspected @@ -136,7 +178,7 @@ t.test('Display.clean', async (t) => { ] for (const [dirty, clean] of tests) { - display.output(dirty) + output.standard(dirty) t.equal(outputs[0], clean) clearOutput() } diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index 597792da73e63..8af47d9abc26c 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -3,6 +3,7 @@ const fs = require('fs') const fsMiniPass = require('fs-minipass') const { join, resolve } = require('path') const EventEmitter = require('events') +const { output } = require('proc-log') const { load: loadMockNpm } = require('../../fixtures/mock-npm') const mockGlobals = require('@npmcli/mock-globals') const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot') @@ -102,7 +103,10 @@ const mockExitHandler = async (t, { config, mocks, files, ...opts } = {}) => { return { ...rest, - errors, + errors: () => [ + ...rest.outputErrors, + ...errors, + ], npm, // Make it async to make testing ergonomics a little easier so we dont need // to t.plan() every test to make sure we get process.exit called. @@ -174,7 +178,7 @@ t.test('exit handler never called - loglevel silent', async (t) => { }) process.emit('exit', 1) t.strictSame(logs.error, []) - t.strictSame(errors, [''], 'logs one empty string to console.error') + t.strictSame(errors(), [''], 'one empty string') }) t.test('exit handler never called - loglevel notice', async (t) => { @@ -185,7 +189,7 @@ t.test('exit handler never called - loglevel notice', async (t) => { 'Exit handler never called!', /error with npm itself/, ]) - t.strictSame(errors, ['', ''], 'logs two empty strings to console.error') + t.strictSame(errors(), ['', ''], 'two empty string on output') }) t.test('exit handler never called - no npm', async (t) => { @@ -193,31 +197,34 @@ t.test('exit handler never called - no npm', async (t) => { process.emit('exit', 1) t.equal(process.exitCode, 1) t.strictSame(logs.error, []) - t.strictSame(errors, [''], 'logs one empty string to console.error') + t.strictSame(errors(), [''], 'one empty string') }) t.test('exit handler called - no npm', async (t) => { const { exitHandler, errors } = await mockExitHandler(t, { init: false }) await exitHandler() t.equal(process.exitCode, 1) - t.match(errors, [/Error: Exit prior to setting npm in exit handler/]) + t.equal(errors().length, 1) + t.match(errors(), [/Error: Exit prior to setting npm in exit handler/]) }) t.test('exit handler called - no npm with error', async (t) => { const { exitHandler, errors } = await mockExitHandler(t, { init: false }) await exitHandler(err('something happened')) t.equal(process.exitCode, 1) - t.match(errors, [/Error: something happened/]) + t.equal(errors().length, 1) + t.match(errors(), [/Error: something happened/]) }) t.test('exit handler called - no npm with error without stack', async (t) => { const { exitHandler, errors } = await mockExitHandler(t, { init: false }) await exitHandler(err('something happened', {}, true)) t.equal(process.exitCode, 1) - t.match(errors, [/something happened/]) + t.equal(errors().length, 1) + t.match(errors(), [/something happened/]) }) -t.test('console.log output using --json', async (t) => { +t.test('standard output using --json', async (t) => { const { exitHandler, outputs } = await mockExitHandler(t, { config: { json: true }, }) @@ -239,13 +246,13 @@ t.test('console.log output using --json', async (t) => { }) t.test('merges output buffers errors with --json', async (t) => { - const { exitHandler, outputs, npm } = await mockExitHandler(t, { + const { exitHandler, outputs } = await mockExitHandler(t, { config: { json: true }, }) - npm.outputBuffer({ output_data: 1 }) - npm.outputBuffer(JSON.stringify({ more_data: 2 })) - npm.outputBuffer('not json, will be ignored') + output.buffer({ output_data: 1 }) + output.buffer(JSON.stringify({ more_data: 2 })) + output.buffer('not json, will be ignored') await exitHandler(err('Error: EBADTHING Something happened')) @@ -266,10 +273,10 @@ t.test('merges output buffers errors with --json', async (t) => { }) t.test('output buffer without json', async (t) => { - const { exitHandler, outputs, npm, logs } = await mockExitHandler(t) + const { exitHandler, outputs, logs } = await mockExitHandler(t) - npm.outputBuffer('output_data') - npm.outputBuffer('more_data') + output.buffer('output_data') + output.buffer('more_data') await exitHandler(err('Error: EBADTHING Something happened')) @@ -307,8 +314,10 @@ t.test('throw a string error', async (t) => { ]) }) -t.test('update notification', async (t) => { - const { exitHandler, logs, npm } = await mockExitHandler(t) +t.test('update notification - shows even with loglevel error', async (t) => { + const { exitHandler, logs, npm } = await mockExitHandler(t, { + config: { loglevel: 'error' }, + }) npm.updateNotification = 'you should update npm!' await exitHandler() @@ -318,6 +327,17 @@ t.test('update notification', async (t) => { ]) }) +t.test('update notification - hidden with silent', async (t) => { + const { exitHandler, logs, npm } = await mockExitHandler(t, { + config: { loglevel: 'silent' }, + }) + npm.updateNotification = 'you should update npm!' + + await exitHandler() + + t.strictSame(logs.notice, []) +}) + t.test('npm.config not ready', async (t) => { const { exitHandler, logs, errors } = await mockExitHandler(t, { load: false, @@ -326,7 +346,8 @@ t.test('npm.config not ready', async (t) => { await exitHandler() t.equal(process.exitCode, 1) - t.match(errors, [ + t.equal(errors().length, 1) + t.match(errors(), [ /Error: Exit prior to config file resolving./, ], 'should exit with config error msg') t.strictSame(logs, [], 'no logs if it doesnt load') @@ -584,7 +605,7 @@ t.test('defaults to log error msg if stack is missing when unloaded', async (t) await exitHandler(err('Error with no stack', { code: 'ENOSTACK', errno: 127 }, true)) t.equal(process.exitCode, 127) - t.same(errors, ['Error with no stack'], 'should use error msg') + t.strictSame(errors(), ['Error with no stack'], 'should use error msg') t.strictSame(logs.error, []) }) @@ -608,25 +629,28 @@ t.test('do no fancy handling for shellouts', async t => { }) t.test('shellout with a numeric error code', async t => { - const { exitHandler, logs } = await mockShelloutExit(t) + const { exitHandler, logs, errors } = await mockShelloutExit(t) await exitHandler(err('', 5)) t.equal(process.exitCode, 5, 'got expected exit code') t.strictSame(logs.error, [], 'no noisy warnings') t.strictSame(logs.warn, [], 'no noisy warnings') + t.strictSame(errors(), []) }) t.test('shellout without a numeric error code (something in npm)', async t => { - const { exitHandler, logs } = await mockShelloutExit(t) + const { exitHandler, logs, errors } = await mockShelloutExit(t) await exitHandler(err('', 'banana stand')) t.equal(process.exitCode, 1, 'got expected exit code') // should log some warnings and errors, because something weird happened t.strictNotSame(logs.error, [], 'bring the noise') + t.strictSame(errors(), ['']) }) t.test('shellout with code=0 (extra weird?)', async t => { - const { exitHandler, logs } = await mockShelloutExit(t) + const { exitHandler, logs, errors } = await mockShelloutExit(t) await exitHandler(Object.assign(new Error(), { code: 0 })) t.equal(process.exitCode, 1, 'got expected exit code') t.strictNotSame(logs.error, [], 'bring the noise') + t.strictSame(errors(), ['']) }) }) diff --git a/workspaces/arborist/test/arborist/reify.js b/workspaces/arborist/test/arborist/reify.js index 46dc367cd3f08..db7df07e46bcd 100644 --- a/workspaces/arborist/test/arborist/reify.js +++ b/workspaces/arborist/test/arborist/reify.js @@ -101,6 +101,16 @@ const warningTracker = () => { } } +const outputTracker = () => { + const list = [] + const onlog = (...msg) => msg[0] === 'standard' && list.push(msg) + process.on('output', onlog) + return () => { + process.removeListener('output', onlog) + return list + } +} + const debugLogTracker = () => { const list = [] mockDebug.log = (...msg) => list.push(msg) @@ -2593,19 +2603,12 @@ t.test('runs dependencies script if tree changes', async (t) => { t.not(fs.existsSync(expectedPath), `did not run ${script}`) } - // take over console.log as run-script is going to print a banner for these because - // they're running in the foreground - const _log = console.log - t.teardown(() => { - console.log = _log - }) - const logs = [] - console.log = (msg) => logs.push(msg) + const outputs = outputTracker() + // reify again, this time adding a new dependency await reify(path, { foregroundScripts: true, add: ['once@^1.4.0'] }) - console.log = _log - t.match(logs, [/predependencies/, /dependencies/, /postdependencies/], 'logged banners') + t.match(outputs(), [/predependencies/, /dependencies/, /postdependencies/], 'logged banners') // files should exist again for (const script of ['predependencies', 'dependencies', 'postdependencies']) { diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index 03b3099c4c391..3565cdb4feb44 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -1861,7 +1861,7 @@ const definitions = { }, }), 'script-shell': new Definition('script-shell', { - default: shell, + default: null, defaultDescription: ` '/bin/sh' on POSIX systems, 'cmd.exe' on Windows `,