diff --git a/lib/base-command.js b/lib/base-command.js index 0adff8e5d95ea..883a4b0ced5dd 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -2,17 +2,65 @@ const { relative } = require('path') -const ConfigDefinitions = require('./utils/config/definitions.js') +const definitions = require('./utils/config/definitions.js') const getWorkspaces = require('./workspaces/get-workspaces.js') - -const cmdAliases = require('./utils/cmd-list').aliases +const { aliases: cmdAliases } = require('./utils/cmd-list') class BaseCommand { static workspaces = false static ignoreImplicitWorkspace = true + // these are all overridden by individual commands + static name = null + static description = null + static params = null + + // this is a static so that we can read from it without instantiating a command + // which would require loading the config + static get describeUsage () { + const wrapWidth = 80 + const { description, usage = [''], name, params } = this + + const fullUsage = [ + `${description}`, + '', + 'Usage:', + ...usage.map(u => `npm ${name} ${u}`.trim()), + ] + + if (params) { + let results = '' + let line = '' + for (const param of params) { + const paramUsage = `[${definitions[param].usage}]` + if (line.length + paramUsage.length > wrapWidth) { + results = [results, line].filter(Boolean).join('\n') + line = '' + } + line = [line, paramUsage].filter(Boolean).join(' ') + } + fullUsage.push('') + fullUsage.push('Options:') + fullUsage.push([results, line].filter(Boolean).join('\n')) + } + + const aliases = Object.entries(cmdAliases).reduce((p, [k, v]) => { + return p.concat(v === name ? k : []) + }, []) + + if (aliases.length) { + const plural = aliases.length === 1 ? '' : 'es' + fullUsage.push('') + fullUsage.push(`alias${plural}: ${aliases.join(', ')}`) + } + + fullUsage.push('') + fullUsage.push(`Run "npm help ${name}" for more info`) + + return fullUsage.join('\n') + } + constructor (npm) { - this.wrapWidth = 80 this.npm = npm const { config } = this.npm @@ -39,59 +87,7 @@ class BaseCommand { } get usage () { - const usage = [ - `${this.description}`, - '', - 'Usage:', - ] - - if (!this.constructor.usage) { - usage.push(`npm ${this.name}`) - } else { - usage.push(...this.constructor.usage.map(u => `npm ${this.name} ${u}`)) - } - - if (this.params) { - usage.push('') - usage.push('Options:') - usage.push(this.wrappedParams) - } - - const aliases = Object.keys(cmdAliases).reduce((p, c) => { - if (cmdAliases[c] === this.name) { - p.push(c) - } - return p - }, []) - - if (aliases.length === 1) { - usage.push('') - usage.push(`alias: ${aliases.join(', ')}`) - } else if (aliases.length > 1) { - usage.push('') - usage.push(`aliases: ${aliases.join(', ')}`) - } - - usage.push('') - usage.push(`Run "npm help ${this.name}" for more info`) - - return usage.join('\n') - } - - get wrappedParams () { - let results = '' - let line = '' - - for (const param of this.params) { - const usage = `[${ConfigDefinitions[param].usage}]` - if (line.length && line.length + usage.length > this.wrapWidth) { - results = [results, line].filter(Boolean).join('\n') - line = '' - } - line = [line, usage].filter(Boolean).join(' ') - } - results = [results, line].filter(Boolean).join('\n') - return results + return this.constructor.describeUsage } usageError (prefix = '') { diff --git a/lib/cli-entry.js b/lib/cli-entry.js index 42788416a13f2..aad06e0690385 100644 --- a/lib/cli-entry.js +++ b/lib/cli-entry.js @@ -53,7 +53,7 @@ module.exports = async (process, validateEngines) => { cmd = npm.argv.shift() if (!cmd) { - npm.output(await npm.usage) + npm.output(npm.usage) process.exitCode = 1 return exitHandler() } @@ -63,7 +63,7 @@ module.exports = async (process, validateEngines) => { } catch (err) { if (err.code === 'EUNKNOWNCOMMAND') { const didYouMean = require('./utils/did-you-mean.js') - const suggestions = await didYouMean(npm, npm.localPrefix, cmd) + 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') process.exitCode = 1 diff --git a/lib/commands/access.js b/lib/commands/access.js index 2417f3a3f2c85..99c1264a84eda 100644 --- a/lib/commands/access.js +++ b/lib/commands/access.js @@ -45,7 +45,7 @@ class Access extends BaseCommand { 'revoke []', ] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return commands diff --git a/lib/commands/audit.js b/lib/commands/audit.js index dfdb783fc3023..8db29b0c67f33 100644 --- a/lib/commands/audit.js +++ b/lib/commands/audit.js @@ -412,7 +412,7 @@ class Audit extends ArboristWorkspaceCmd { static usage = ['[fix|signatures]'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { diff --git a/lib/commands/cache.js b/lib/commands/cache.js index 66b432dfc13a6..50bb35e3544df 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -73,7 +73,7 @@ class Cache extends BaseCommand { 'verify', ] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['add', 'clean', 'verify', 'ls'] diff --git a/lib/commands/completion.js b/lib/commands/completion.js index efbad9d61001b..38205ad8fc49e 100644 --- a/lib/commands/completion.js +++ b/lib/commands/completion.js @@ -33,8 +33,9 @@ const fs = require('fs/promises') const nopt = require('nopt') const { resolve } = require('path') +const Npm = require('../npm.js') const { definitions, shorthands } = require('../utils/config/index.js') -const { commands, aliases } = require('../utils/cmd-list.js') +const { commands, aliases, deref } = require('../utils/cmd-list.js') const configNames = Object.keys(definitions) const shorthandNames = Object.keys(shorthands) const allConfs = configNames.concat(shorthandNames) @@ -48,7 +49,7 @@ class Completion extends BaseCommand { static name = 'completion' // completion for the completion command - async completion (opts) { + static async completion (opts) { if (opts.w > 2) { return } @@ -156,10 +157,14 @@ class Completion extends BaseCommand { // at this point, if words[1] is some kind of npm command, // then complete on it. // otherwise, do nothing - const impl = await this.npm.cmd(cmd) - if (impl.completion) { - const comps = await impl.completion(opts) - return this.wrap(opts, comps) + try { + const { completion } = Npm.cmd(cmd) + if (completion) { + const comps = await completion(opts, this.npm) + return this.wrap(opts, comps) + } + } catch { + // it wasnt a valid command, so do nothing } } @@ -267,7 +272,7 @@ const cmdCompl = (opts, npm) => { return matches } - const derefs = new Set([...matches.map(c => npm.deref(c))]) + const derefs = new Set([...matches.map(c => deref(c))]) if (derefs.size === 1) { return [...derefs] } diff --git a/lib/commands/config.js b/lib/commands/config.js index 576c8b56a6f9c..b49cdd648f9d0 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -74,7 +74,7 @@ class Config extends BaseCommand { static skipConfigValidation = true - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv[1] !== 'config') { argv.unshift('config') diff --git a/lib/commands/deprecate.js b/lib/commands/deprecate.js index 844d5f60a02ab..ada2bac40f2fd 100644 --- a/lib/commands/deprecate.js +++ b/lib/commands/deprecate.js @@ -17,13 +17,13 @@ class Deprecate extends BaseCommand { static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts, npm) { if (opts.conf.argv.remain.length > 1) { return [] } - const username = await getIdentity(this.npm, this.npm.flatOptions) - const packages = await libaccess.getPackages(username, this.npm.flatOptions) + const username = await getIdentity(npm, npm.flatOptions) + const packages = await libaccess.getPackages(username, npm.flatOptions) return Object.keys(packages) .filter((name) => packages[name] === 'write' && diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index 584fbbc8ad6e0..15f9622d14906 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -19,7 +19,7 @@ class DistTag extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['add', 'rm', 'ls'] diff --git a/lib/commands/edit.js b/lib/commands/edit.js index a671a5d6bad5d..fbc7840a39876 100644 --- a/lib/commands/edit.js +++ b/lib/commands/edit.js @@ -38,8 +38,8 @@ class Edit extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/explain.js b/lib/commands/explain.js index a72514fb97805..403274db68dfa 100644 --- a/lib/commands/explain.js +++ b/lib/commands/explain.js @@ -18,9 +18,9 @@ class Explain extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts, npm) { const completion = require('../utils/completion/installed-deep.js') - return completion(this.npm, opts) + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/explore.js b/lib/commands/explore.js index 63667114d3f68..7a03ea4eabd7f 100644 --- a/lib/commands/explore.js +++ b/lib/commands/explore.js @@ -17,8 +17,8 @@ class Explore extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 1e8981967fc32..2804d36cd5603 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -36,9 +36,9 @@ class Fund extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts, npm) { const completion = require('../utils/completion/installed-deep.js') - return completion(this.npm, opts) + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/get.js b/lib/commands/get.js index 7a59bd9052d67..4bf5d2caf8264 100644 --- a/lib/commands/get.js +++ b/lib/commands/get.js @@ -1,3 +1,4 @@ +const Npm = require('../npm.js') const BaseCommand = require('../base-command.js') class Get extends BaseCommand { @@ -9,9 +10,9 @@ class Get extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { - const config = await this.npm.cmd('config') - return config.completion(opts) + static async completion (opts) { + const Config = Npm.cmd('config') + return Config.completion(opts) } async exec (args) { diff --git a/lib/commands/help.js b/lib/commands/help.js index 4b40ef37afa2d..39c580f9a6871 100644 --- a/lib/commands/help.js +++ b/lib/commands/help.js @@ -3,6 +3,7 @@ const path = require('path') const openUrl = require('../utils/open-url.js') const { glob } = require('glob') const localeCompare = require('@isaacs/string-locale-compare')('en') +const { deref } = require('../utils/cmd-list.js') const globify = pattern => pattern.split('\\').join('/') const BaseCommand = require('../base-command.js') @@ -26,11 +27,11 @@ class Help extends BaseCommand { static usage = [' []'] static params = ['viewer'] - async completion (opts) { + static async completion (opts, npm) { if (opts.conf.argv.remain.length > 2) { return [] } - const g = path.resolve(this.npm.npmRoot, 'man/man[0-9]/*.[0-9]') + const g = path.resolve(npm.npmRoot, 'man/man[0-9]/*.[0-9]') let files = await glob(globify(g)) // preserve glob@8 behavior files = files.sort((a, b) => a.localeCompare(b, 'en')) @@ -49,7 +50,7 @@ class Help extends BaseCommand { const manSearch = /^\d+$/.test(args[0]) ? `man${args.shift()}` : 'man*' if (!args.length) { - return this.npm.output(await this.npm.usage) + return this.npm.output(this.npm.usage) } // npm help foo bar baz: search topics @@ -58,7 +59,7 @@ class Help extends BaseCommand { } // `npm help package.json` - const arg = (this.npm.deref(args[0]) || args[0]).replace('.json', '-json') + const arg = (deref(args[0]) || args[0]).replace('.json', '-json') // find either section.n or npm-section.n const f = globify(path.resolve(this.npm.npmRoot, `man/${manSearch}/?(npm-)${arg}.[0-9]*`)) diff --git a/lib/commands/install.js b/lib/commands/install.js index e3b7099268c0a..2bfd20a72658f 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -38,7 +38,7 @@ class Install extends ArboristWorkspaceCmd { static usage = ['[ ...]'] - async completion (opts) { + static async completion (opts) { const { partialWord } = opts // install can complete to a folder with a package.json, or any package. // if it has a slash, then it's gotta be a folder diff --git a/lib/commands/link.js b/lib/commands/link.js index 725912c2e7aa6..a81450a247ed6 100644 --- a/lib/commands/link.js +++ b/lib/commands/link.js @@ -35,8 +35,8 @@ class Link extends ArboristWorkspaceCmd { ...super.params, ] - async completion (opts) { - const dir = this.npm.globalDir + static async completion (opts, npm) { + const dir = npm.globalDir const files = await readdir(dir) return files.filter(f => !/^[._-]/.test(f)) } diff --git a/lib/commands/ls.js b/lib/commands/ls.js index eb9114802d5e0..92300b1c404a3 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -40,9 +40,9 @@ class LS extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts, npm) { const completion = require('../utils/completion/installed-deep.js') - return completion(this.npm, opts) + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/org.js b/lib/commands/org.js index 575ff75e2a6cf..1f32d41ff7306 100644 --- a/lib/commands/org.js +++ b/lib/commands/org.js @@ -14,7 +14,7 @@ class Org extends BaseCommand { static params = ['registry', 'otp', 'json', 'parseable'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain if (argv.length === 2) { return ['set', 'rm', 'ls'] diff --git a/lib/commands/owner.js b/lib/commands/owner.js index 201f348d689a3..5b54dd41f3d60 100644 --- a/lib/commands/owner.js +++ b/lib/commands/owner.js @@ -34,7 +34,7 @@ class Owner extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts, npm) { const argv = opts.conf.argv.remain if (argv.length > 3) { return [] @@ -50,17 +50,17 @@ class Owner extends BaseCommand { // reaches registry in order to autocomplete rm if (argv[2] === 'rm') { - if (this.npm.global) { + if (npm.global) { return [] } - const { name } = await readJson(this.npm.prefix) + const { name } = await readJson(npm.prefix) if (!name) { return [] } const spec = npa(name) const data = await pacote.packument(spec, { - ...this.npm.flatOptions, + ...npm.flatOptions, fullMetadata: true, }) if (data && data.maintainers && data.maintainers.length) { diff --git a/lib/commands/profile.js b/lib/commands/profile.js index 4fba1209e0335..a7d4ac2f29fbe 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -53,7 +53,7 @@ class Profile extends BaseCommand { 'otp', ] - async completion (opts) { + static async completion (opts) { var argv = opts.conf.argv.remain if (!argv[2]) { diff --git a/lib/commands/rebuild.js b/lib/commands/rebuild.js index 527447e427917..8af96f725555c 100644 --- a/lib/commands/rebuild.js +++ b/lib/commands/rebuild.js @@ -18,9 +18,9 @@ class Rebuild extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts, npm) { const completion = require('../utils/completion/installed-deep.js') - return completion(this.npm, opts) + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 6b568dab65249..13efdde750a82 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -35,10 +35,10 @@ class RunScript extends BaseCommand { static ignoreImplicitWorkspace = false static isShellout = true - async completion (opts) { + static async completion (opts, npm) { const argv = opts.conf.argv.remain if (argv.length === 2) { - const { content: { scripts = {} } } = await pkgJson.normalize(this.npm.localPrefix) + const { content: { scripts = {} } } = await pkgJson.normalize(npm.localPrefix) .catch(er => ({ content: {} })) if (opts.isFish) { return Object.keys(scripts).map(s => `${s}\t${scripts[s].slice(0, 30)}`) @@ -90,7 +90,7 @@ class RunScript extends BaseCommand { return } - const suggestions = await didYouMean(this.npm, path, event) + const suggestions = await didYouMean(path, event) throw new Error( `Missing script: "${event}"${suggestions}\n\nTo see a list of scripts, run:\n npm run` ) diff --git a/lib/commands/set.js b/lib/commands/set.js index a660b372e5677..f315d183845c5 100644 --- a/lib/commands/set.js +++ b/lib/commands/set.js @@ -1,3 +1,4 @@ +const Npm = require('../npm.js') const BaseCommand = require('../base-command.js') class Set extends BaseCommand { @@ -9,8 +10,9 @@ class Set extends BaseCommand { // TODO /* istanbul ignore next */ - async completion (opts) { - return this.npm.cmd('config').completion(opts) + static async completion (opts) { + const Config = Npm.cmd('config') + return Config.completion(opts) } async exec (args) { diff --git a/lib/commands/team.js b/lib/commands/team.js index 2d4fc663715e4..3c6cf305a6e5f 100644 --- a/lib/commands/team.js +++ b/lib/commands/team.js @@ -24,7 +24,7 @@ class Team extends BaseCommand { static ignoreImplicitWorkspace = false - async completion (opts) { + static async completion (opts) { const { conf: { argv: { remain: argv } } } = opts const subcommands = ['create', 'destroy', 'add', 'rm', 'ls'] diff --git a/lib/commands/token.js b/lib/commands/token.js index bc2e4f3796364..c24684b3dd614 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -14,7 +14,7 @@ class Token extends BaseCommand { static usage = ['list', 'revoke ', 'create [--read-only] [--cidr=list]'] static params = ['read-only', 'cidr', 'registry', 'otp'] - async completion (opts) { + static async completion (opts) { const argv = opts.conf.argv.remain const subcommands = ['list', 'revoke', 'create'] if (argv.length === 2) { diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js index 6792e59fbd468..07775efb9cf2f 100644 --- a/lib/commands/uninstall.js +++ b/lib/commands/uninstall.js @@ -14,8 +14,8 @@ class Uninstall extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { - return completion(this.npm, opts) + static async completion (opts, npm) { + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 0df1ce0cc74f9..66985297b9574 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -22,7 +22,7 @@ class Unpublish extends BaseCommand { static workspaces = true static ignoreImplicitWorkspace = false - async getKeysOfVersions (name, opts) { + static async getKeysOfVersions (name, opts) { const pkgUri = npa(name).escapedName const json = await npmFetch.json(`${pkgUri}?write=true`, { ...opts, @@ -31,15 +31,15 @@ class Unpublish extends BaseCommand { return Object.keys(json.versions) } - async completion (args) { + static async completion (args, npm) { const { partialWord, conf } = args if (conf.argv.remain.length >= 3) { return [] } - const opts = { ...this.npm.flatOptions } - const username = await getIdentity(this.npm, { ...opts }).catch(() => null) + const opts = { ...npm.flatOptions } + const username = await getIdentity(npm, { ...opts }).catch(() => null) if (!username) { return [] } @@ -105,7 +105,7 @@ class Unpublish extends BaseCommand { if (manifest && manifest.name === spec.name && manifest.publishConfig) { flatten(manifest.publishConfig, opts) } - const versions = await this.getKeysOfVersions(spec.name, opts) + const versions = await Unpublish.getKeysOfVersions(spec.name, opts) if (versions.length === 1 && !force) { throw this.usageError(LAST_REMAINING_VERSION_ERROR) } diff --git a/lib/commands/update.js b/lib/commands/update.js index 26be5ad681983..caa69dd317ca6 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -31,9 +31,9 @@ class Update extends ArboristWorkspaceCmd { // TODO /* istanbul ignore next */ - async completion (opts) { + static async completion (opts, npm) { const completion = require('../utils/completion/installed-deep.js') - return completion(this.npm, opts) + return completion(npm, opts) } async exec (args) { diff --git a/lib/commands/version.js b/lib/commands/version.js index a523283671791..029a6fdd3101e 100644 --- a/lib/commands/version.js +++ b/lib/commands/version.js @@ -28,7 +28,7 @@ class Version extends BaseCommand { /* eslint-disable-next-line max-len */ static usage = ['[ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]'] - async completion (opts) { + static async completion (opts) { const { conf: { argv: { remain }, diff --git a/lib/commands/view.js b/lib/commands/view.js index bbe7dcdd18bbf..f118184124db9 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -29,7 +29,7 @@ class View extends BaseCommand { static ignoreImplicitWorkspace = false static usage = ['[] [[.subfield]...]'] - async completion (opts) { + static async completion (opts, npm) { if (opts.conf.argv.remain.length <= 2) { // There used to be registry completion here, but it stopped // making sense somewhere around 50,000 packages on the registry @@ -37,13 +37,13 @@ class View extends BaseCommand { } // have the package, get the fields const config = { - ...this.npm.flatOptions, + ...npm.flatOptions, fullMetadata: true, preferOnline: true, } const spec = npa(opts.conf.argv.remain[2]) const pckmnt = await packument(spec, config) - const defaultTag = this.npm.config.get('tag') + const defaultTag = npm.config.get('tag') const dv = pckmnt.versions[pckmnt['dist-tags'][defaultTag]] pckmnt.versions = Object.keys(pckmnt.versions).sort(semver.compareLoose) diff --git a/lib/npm.js b/lib/npm.js index 472b4d9b6d709..afeaf0e79ebf2 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -3,7 +3,6 @@ const Config = require('@npmcli/config') const chalk = require('chalk') const which = require('which') const fs = require('fs/promises') -const abbrev = require('abbrev') // Patch the global fs module here at the app level require('graceful-fs').gracefulify(require('fs')) @@ -17,13 +16,23 @@ const log = require('./utils/log-shim') const replaceInfo = require('./utils/replace-info.js') const updateNotifier = require('./utils/update-notifier.js') const pkg = require('../package.json') -const { commands, aliases } = require('./utils/cmd-list.js') +const { deref } = require('./utils/cmd-list.js') class Npm { static get version () { return pkg.version } + static cmd (c) { + const command = deref(c) + if (!command) { + throw Object.assign(new Error(`Unknown command ${c}`), { + code: 'EUNKNOWNCOMMAND', + }) + } + return require(`./commands/${command}.js`) + } + updateNotification = null loadErr = null argv = [] @@ -79,53 +88,9 @@ class Npm { return this.constructor.version } - deref (c) { - if (!c) { - return - } - - // Translate camelCase to snake-case (i.e. installTest to install-test) - if (c.match(/[A-Z]/)) { - c = c.replace(/([A-Z])/g, m => '-' + m.toLowerCase()) - } - - // if they asked for something exactly we are done - if (commands.includes(c)) { - return c - } - - // if they asked for a direct alias - if (aliases[c]) { - return aliases[c] - } - - const abbrevs = abbrev(commands.concat(Object.keys(aliases))) - - // first deref the abbrev, if there is one - // then resolve any aliases - // so `npm install-cl` will resolve to `install-clean` then to `ci` - let a = abbrevs[c] - while (aliases[a]) { - a = aliases[a] - } - return a - } - - // Get an instantiated npm command - // npm.command is already taken as the currently running command, a refactor - // would be needed to change this - async cmd (cmd) { - await this.load() - - const cmdId = this.deref(cmd) - if (!cmdId) { - throw Object.assign(new Error(`Unknown command ${cmd}`), { - code: 'EUNKNOWNCOMMAND', - }) - } - - const Impl = require(`./commands/${cmdId}.js`) - const command = new Impl(this) + setCmd (cmd) { + const Command = Npm.cmd(cmd) + const command = new Command(this) // since 'test', 'start', 'stop', etc. commands re-enter this function // to call the run-script command, we need to only set it one time. @@ -138,8 +103,14 @@ class Npm { } // Call an npm command + // TODO: tests are currently the only time the second + // parameter of args is used. When called via `lib/cli.js` the config is + // loaded and this.argv is set to the remaining command line args. We should + // consider testing the CLI the same way it is used and not allow args to be + // passed in directly. async exec (cmd, args = this.argv) { - const command = await this.cmd(cmd) + const command = this.setCmd(cmd) + const timeEnd = this.time(`command:${cmd}`) // this is async but we dont await it, since its ok if it doesnt diff --git a/lib/utils/cmd-list.js b/lib/utils/cmd-list.js index e5479139033d5..9bd252bc3facc 100644 --- a/lib/utils/cmd-list.js +++ b/lib/utils/cmd-list.js @@ -1,3 +1,5 @@ +const abbrev = require('abbrev') + // These correspond to filenames in lib/commands // Please keep this list sorted alphabetically const commands = [ @@ -136,7 +138,40 @@ const aliases = { 'add-user': 'adduser', } +const deref = (c) => { + if (!c) { + return + } + + // Translate camelCase to snake-case (i.e. installTest to install-test) + if (c.match(/[A-Z]/)) { + c = c.replace(/([A-Z])/g, m => '-' + m.toLowerCase()) + } + + // if they asked for something exactly we are done + if (commands.includes(c)) { + return c + } + + // if they asked for a direct alias + if (aliases[c]) { + return aliases[c] + } + + const abbrevs = abbrev(commands.concat(Object.keys(aliases))) + + // first deref the abbrev, if there is one + // then resolve any aliases + // so `npm install-cl` will resolve to `install-clean` then to `ci` + let a = abbrevs[c] + while (aliases[a]) { + a = aliases[a] + } + return a +} + module.exports = { aliases, commands, + deref, } diff --git a/lib/utils/did-you-mean.js b/lib/utils/did-you-mean.js index e6c1ba3f72e5b..ff3c812b46c3c 100644 --- a/lib/utils/did-you-mean.js +++ b/lib/utils/did-you-mean.js @@ -1,13 +1,13 @@ +const Npm = require('../npm') const { distance } = require('fastest-levenshtein') const pkgJson = require('@npmcli/package-json') const { commands } = require('./cmd-list.js') -const didYouMean = async (npm, path, scmd) => { - // const cmd = await npm.cmd(str) +const didYouMean = async (path, scmd) => { const close = commands.filter(cmd => distance(scmd, cmd) < scmd.length * 0.4 && scmd !== cmd) let best = [] for (const str of close) { - const cmd = await npm.cmd(str) + const cmd = Npm.cmd(str) best.push(` npm ${str} # ${cmd.description}`) } // We would already be suggesting this in `npm x` so omit them here diff --git a/lib/utils/npm-usage.js b/lib/utils/npm-usage.js index b04ad33f9dd79..1bd790ca601bc 100644 --- a/lib/utils/npm-usage.js +++ b/lib/utils/npm-usage.js @@ -8,9 +8,9 @@ const INDENT = 4 const indent = (repeat = INDENT) => ' '.repeat(repeat) const indentNewline = (repeat) => `\n${indent(repeat)}` -module.exports = async (npm) => { +module.exports = (npm) => { const browser = npm.config.get('viewer') === 'browser' ? ' (in a browser)' : '' - const allCommands = npm.config.get('long') ? await cmdUsages(npm) : cmdNames() + const allCommands = npm.config.get('long') ? cmdUsages(npm.constructor) : cmdNames() return `npm @@ -57,13 +57,12 @@ const cmdNames = () => { return indentNewline() + out.join(indentNewline()).slice(2) } -const cmdUsages = async (npm) => { +const cmdUsages = (Npm) => { // return a string of : let maxLen = 0 const set = [] for (const c of commands) { - const { usage } = await npm.cmd(c) - set.push([c, usage.split('\n')]) + set.push([c, Npm.cmd(c).describeUsage.split('\n')]) maxLen = Math.max(maxLen, c.length) } diff --git a/mock-registry/lib/index.js b/mock-registry/lib/index.js index 345da72990d06..924af05d5b6c4 100644 --- a/mock-registry/lib/index.js +++ b/mock-registry/lib/index.js @@ -30,6 +30,9 @@ class MockRegistry { static tnock (t, host, opts, { debug = false, strict = false } = {}) { const noMatch = (req) => { + if (debug) { + console.error('NO MATCH', t.name, req.options ? req.options : req.path) + } if (strict) { // There are network requests that get caught regardless of error code. // Turning on strict mode requires that those requests get explicitly @@ -38,9 +41,6 @@ class MockRegistry { // tests. We should work towards making this the default for all tests. t.fail(`Unmatched request: ${JSON.stringify(req, null, 2)}`) } - if (debug) { - console.error('NO MATCH', t.name, req.options ? req.options : req.path) - } } Nock.emitter.on('no match', noMatch) diff --git a/tap-snapshots/test/lib/commands/diff.js.test.cjs b/tap-snapshots/test/lib/commands/diff.js.test.cjs index 533b4f196e661..e87086d7d9b8f 100644 --- a/tap-snapshots/test/lib/commands/diff.js.test.cjs +++ b/tap-snapshots/test/lib/commands/diff.js.test.cjs @@ -33,7 +33,7 @@ index v0.1.0..v1.0.0 100644 +++ b/package.json @@ -1,4 +1,4 @@ { - "name": "foo", + "name": "@npmcli/foo", - "version": "0.1.0" + "version": "1.0.0" } diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 29d6b57fcb65a..e46fd065b9eb6 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -165,6 +165,10 @@ Array [ ] ` +exports[`test/lib/docs.js TAP command list > deref 1`] = ` +Function deref(c) +` + exports[`test/lib/docs.js TAP config > all definitions 1`] = ` #### \`_auth\` diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 650123cbac366..b91ee8a3933a3 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -113,6 +113,7 @@ const setupMockNpm = async (t, { // preload a command command = null, // string name of the command exec = null, // optionally exec the command before returning + setCmd = false, // test dirs prefixDir = {}, homeDir = {}, @@ -251,16 +252,25 @@ const setupMockNpm = async (t, { const mockCommand = {} if (command) { - const cmd = await npm.cmd(command) - const usage = await cmd.usage - mockCommand.cmd = cmd + const Cmd = mockNpm.Npm.cmd(command) + if (setCmd) { + // XXX(hack): This is a hack to allow fake-ish tests to set the currently + // running npm command without running exec. Generally, we should rely on + // actually exec-ing the command to asserting the state of the world + // through what is printed/on disk/etc. This is a stop-gap to allow tests + // that are time intensive to convert to continue setting the npm command + // this way. TODO: remove setCmd from all tests and remove the setCmd + // method from `lib/npm.js` + npm.setCmd(command) + } + mockCommand.cmd = new Cmd(npm) mockCommand[command] = { - usage, + usage: Cmd.describeUsage, exec: (args) => npm.exec(command, args), - completion: (args) => cmd.completion(args), + completion: (args) => Cmd.completion(args, npm), } if (exec) { - await mockCommand[command].exec(exec) + await mockCommand[command].exec(exec === true ? [] : exec) // assign string output to the command now that we have it // for easier testing mockCommand[command].output = mockNpm.joinedOutput() diff --git a/test/fixtures/sandbox.js b/test/fixtures/sandbox.js index 460609628c8ab..01a5e562fd9b2 100644 --- a/test/fixtures/sandbox.js +++ b/test/fixtures/sandbox.js @@ -328,8 +328,8 @@ class Sandbox extends EventEmitter { this[_npm].output = (...args) => this[_output].push(args) await this[_npm].load() - const impl = await this[_npm].cmd(command) - return impl.completion({ + const Cmd = Npm.cmd(command) + return Cmd.completion({ partialWord: partial, conf: { argv: { diff --git a/test/lib/commands/access.js b/test/lib/commands/access.js index d1839aaaef219..7aec33701297c 100644 --- a/test/lib/commands/access.js +++ b/test/lib/commands/access.js @@ -7,8 +7,7 @@ const token = 'test-auth-token' const auth = { '//registry.npmjs.org/:_authToken': 'test-auth-token' } t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const access = await npm.cmd('access') + const { access } = await loadMockNpm(t, { command: 'access' }) const testComp = (argv, expect) => { const res = access.completion({ conf: { argv: { remain: argv } } }) t.resolves(res, expect, argv.join(' ')) diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index e12af12f46388..410e8c4987ca6 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -37,8 +37,7 @@ const mockAddUser = async (t, { stdin: stdinLines, registry: registryUrl, ...opt } t.test('usage', async t => { - const { npm } = await loadMockNpm(t) - const adduser = await npm.cmd('adduser') + const { adduser } = await loadMockNpm(t, { command: 'adduser' }) t.match(adduser.usage, 'adduser', 'usage has command name in it') }) diff --git a/test/lib/commands/audit.js b/test/lib/commands/audit.js index 911484fe479ce..4014e73387351 100644 --- a/test/lib/commands/audit.js +++ b/test/lib/commands/audit.js @@ -211,8 +211,7 @@ t.test('audit fix - bulk endpoint', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const audit = await npm.cmd('audit') + const { audit } = await loadMockNpm(t, { command: 'audit' }) t.test('fix', async t => { await t.resolveMatch( audit.completion({ conf: { argv: { remain: ['npm', 'audit'] } } }), diff --git a/test/lib/commands/bugs.js b/test/lib/commands/bugs.js index bf45b9eee81ab..953c8e6345a2a 100644 --- a/test/lib/commands/bugs.js +++ b/test/lib/commands/bugs.js @@ -43,8 +43,7 @@ const pacote = { } t.test('usage', async (t) => { - const { npm } = await loadMockNpm(t) - const bugs = await npm.cmd('bugs') + const { bugs } = await loadMockNpm(t, { command: 'bugs' }) t.match(bugs.usage, 'bugs', 'usage has command name in it') }) diff --git a/test/lib/commands/cache.js b/test/lib/commands/cache.js index c3adef42d424c..15ee4dc80aa1a 100644 --- a/test/lib/commands/cache.js +++ b/test/lib/commands/cache.js @@ -302,8 +302,7 @@ t.test('cache verify w/ extra output', async t => { }) t.test('cache completion', async t => { - const { npm } = await loadMockNpm(t) - const cache = await npm.cmd('cache') + const { cache } = await loadMockNpm(t, { command: 'cache' }) const { completion } = cache const testComp = (argv, expect) => { diff --git a/test/lib/commands/completion.js b/test/lib/commands/completion.js index 93ea6210a89a5..904d9410fdd6c 100644 --- a/test/lib/commands/completion.js +++ b/test/lib/commands/completion.js @@ -10,6 +10,7 @@ const completionScript = fs const loadMockCompletion = async (t, o = {}) => { const { globals = {}, windows, ...options } = o const res = await loadMockNpm(t, { + command: 'completion', ...options, globals: (dirs) => ({ 'process.platform': windows ? 'win32' : 'posix', @@ -18,10 +19,8 @@ const loadMockCompletion = async (t, o = {}) => { ...(typeof globals === 'function' ? globals(dirs) : globals), }), }) - const completion = await res.npm.cmd('completion') return { resetGlobals: res.mockedGlobals.reset, - completion, ...res, } } @@ -88,7 +87,7 @@ t.test('completion', async t => { }, }) - await completion.exec({}) + await completion.exec() t.equal(data, completionScript, 'wrote the completion script') }) @@ -113,7 +112,7 @@ t.test('completion', async t => { }, }) - await completion.exec({}) + await completion.exec() t.equal(data, completionScript, 'wrote the completion script') }) @@ -192,7 +191,7 @@ t.test('completion', async t => { t.test('windows without bash', async t => { const { outputs, completion } = await loadMockCompletion(t, { windows: true }) await t.rejects( - completion.exec({}), + completion.exec(), { code: 'ENOTSUP', message: /completion supported only in MINGW/ }, 'returns the correct error' ) diff --git a/test/lib/commands/deprecate.js b/test/lib/commands/deprecate.js index 48513c7303a01..4ae146fd3aee0 100644 --- a/test/lib/commands/deprecate.js +++ b/test/lib/commands/deprecate.js @@ -12,13 +12,13 @@ const versions = ['1.0.0', '1.0.1', '1.0.1-pre'] const packages = { foo: 'write', bar: 'write', baz: 'write', buzz: 'read' } t.test('completion', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, deprecate } = await loadMockNpm(t, { + command: 'deprecate', config: { ...auth, }, }) - const deprecate = await npm.cmd('deprecate') const testComp = async (argv, expect) => { const res = await deprecate.completion({ conf: { argv: { remain: argv } } }) diff --git a/test/lib/commands/diff.js b/test/lib/commands/diff.js index d9ff9e5dad0e6..36a9e4bc17d9f 100644 --- a/test/lib/commands/diff.js +++ b/test/lib/commands/diff.js @@ -25,7 +25,9 @@ const mockDiff = async (t, { ...opts } = {}) => { const tarballFixtures = Object.entries(tarballs).reduce((acc, [spec, fixture]) => { - const [name, version] = spec.split('@') + const lastAt = spec.lastIndexOf('@') + const name = spec.slice(0, lastAt) + const version = spec.slice(lastAt + 1) acc[name] = acc[name] || {} acc[name][version] = fixture if (!acc[name][version]['package.json']) { @@ -39,6 +41,7 @@ const mockDiff = async (t, { const { prefixDir, globalPrefixDir, otherDirs, config, ...rest } = opts const { npm, ...res } = await loadMockNpm(t, { + command: 'diff', prefixDir: jsonifyTestdir(prefixDir), otherDirs: jsonifyTestdir({ tarballs: tarballFixtures, ...otherDirs }), globalPrefixDir: jsonifyTestdir(globalPrefixDir), @@ -75,7 +78,7 @@ const mockDiff = async (t, { } if (exec) { - await npm.exec('diff', exec) + await res.diff.exec(exec) res.output = res.joinedOutput() } @@ -98,13 +101,13 @@ const assertFoo = async (t, arg) => { const { output } = await mockDiff(t, { diff, prefixDir: { - 'package.json': { name: 'foo', version: '1.0.0' }, + 'package.json': { name: '@npmcli/foo', version: '1.0.0' }, 'index.js': 'const version = "1.0.0"', 'a.js': 'const a = "a@1.0.0"', 'b.js': 'const b = "b@1.0.0"', }, tarballs: { - 'foo@0.1.0': { + '@npmcli/foo@0.1.0': { 'index.js': 'const version = "0.1.0"', 'a.js': 'const a = "a@0.1.0"', 'b.js': 'const b = "b@0.1.0"', @@ -162,17 +165,17 @@ t.test('no args', async t => { t.test('single arg', async t => { t.test('spec using cwd package name', async t => { - await assertFoo(t, 'foo@0.1.0') + await assertFoo(t, '@npmcli/foo@0.1.0') }) t.test('unknown spec, no package.json', async t => { await rejectDiff(t, /Needs multiple arguments to compare or run from a project dir./, { - diff: ['foo@1.0.0'], + diff: ['@npmcli/foo@1.0.0'], }) }) t.test('spec using semver range', async t => { - await assertFoo(t, 'foo@~0.1.0') + await assertFoo(t, '@npmcli/foo@~0.1.0') }) t.test('version', async t => { @@ -429,17 +432,17 @@ t.test('single arg', async t => { t.test('use project name in project dir', async t => { const { output } = await mockDiff(t, { - diff: 'foo', + diff: '@npmcli/foo', prefixDir: { - 'package.json': { name: 'foo', version: '1.0.0' }, + 'package.json': { name: '@npmcli/foo', version: '1.0.0' }, }, tarballs: { - 'foo@2.2.2': {}, + '@npmcli/foo@2.2.2': {}, }, exec: [], }) - t.match(output, 'foo') + t.match(output, '@npmcli/foo') t.match(output, /-\s*"version": "2\.2\.2"/) t.match(output, /\+\s*"version": "1\.0\.0"/) }) @@ -448,17 +451,17 @@ t.test('single arg', async t => { const { output } = await mockDiff(t, { diff: '../other/other-pkg', prefixDir: { - 'package.json': { name: 'foo', version: '1.0.0' }, + 'package.json': { name: '@npmcli/foo', version: '1.0.0' }, }, otherDirs: { 'other-pkg': { - 'package.json': { name: 'foo', version: '2.0.0' }, + 'package.json': { name: '@npmcli/foo', version: '2.0.0' }, }, }, exec: [], }) - t.match(output, 'foo') + t.match(output, '@npmcli/foo') t.match(output, /-\s*"version": "2\.0\.0"/) t.match(output, /\+\s*"version": "1\.0\.0"/) }) diff --git a/test/lib/commands/dist-tag.js b/test/lib/commands/dist-tag.js index 4cc241f74582d..1c63ce497d3fb 100644 --- a/test/lib/commands/dist-tag.js +++ b/test/lib/commands/dist-tag.js @@ -77,22 +77,15 @@ const mockDist = async (t, { ...npmOpts } = {}) => { const mock = await mockNpm(t, { ...npmOpts, + command: 'dist-tag', mocks: { 'npm-registry-fetch': Object.assign(nrf, realFetch, { json: getTag }), }, }) - const usage = await mock.npm.cmd('dist-tag').then(c => c.usage) - return { ...mock, - distTag: { - exec: (args) => mock.npm.exec('dist-tag', args), - usage, - completion: (remain) => mock.npm.cmd('dist-tag').then(c => c.completion({ - conf: { argv: { remain } }, - })), - }, + distTag: mock['dist-tag'], fetchOpts: () => fetchOpts, result: () => mock.joinedOutput(), logs: () => { @@ -365,10 +358,10 @@ t.test('remove missing pkg name', async t => { t.test('completion', async t => { const { distTag } = await mockDist(t) - const match = distTag.completion(['npm', 'dist-tag']) + const match = distTag.completion({ conf: { argv: { remain: ['npm', 'dist-tag'] } } }) t.resolveMatch(match, ['add', 'rm', 'ls'], 'should list npm dist-tag commands for completion') - const noMatch = distTag.completion(['npm', 'dist-tag', 'foobar']) + const noMatch = distTag.completion({ conf: { argv: { remain: ['npm', 'dist-tag', 'foobar'] } } }) t.resolveMatch(noMatch, []) }) diff --git a/test/lib/commands/explain.js b/test/lib/commands/explain.js index 79c917a1cd452..f4d898797bcbe 100644 --- a/test/lib/commands/explain.js +++ b/test/lib/commands/explain.js @@ -4,6 +4,7 @@ const mockNpm = require('../../fixtures/mock-npm.js') const mockExplain = async (t, opts) => { const mock = await mockNpm(t, { + command: 'explain', mocks: { // keep the snapshots pared down a bit, since this has its own tests. '{LIB}/utils/explain-dep.js': { @@ -16,15 +17,7 @@ const mockExplain = async (t, opts) => { ...opts, }) - const usage = await mock.npm.cmd('explain').then(c => c.usage) - - return { - ...mock, - explain: { - usage, - exec: (args) => mock.npm.exec('explain', args), - }, - } + return mock } t.test('no args throws usage', async t => { diff --git a/test/lib/commands/help.js b/test/lib/commands/help.js index e38f1bbce24d4..3fda9fb6e07fd 100644 --- a/test/lib/commands/help.js +++ b/test/lib/commands/help.js @@ -61,18 +61,13 @@ const mockHelp = async (t, { mocks: { '@npmcli/promise-spawn': mockSpawn }, otherDirs: { ...manPages.fixtures }, config, + command: 'help', + exec: execArgs, ...opts, }) - const help = await npm.cmd('help') - const exec = execArgs - ? await npm.exec('help', execArgs) - : (...a) => npm.exec('help', a) - return { npm, - help, - exec, manPages: manPages.pages, getArgs: () => args, ...rest, @@ -80,8 +75,8 @@ const mockHelp = async (t, { } t.test('npm help', async t => { - const { exec, joinedOutput } = await mockHelp(t) - await exec() + const { help, joinedOutput } = await mockHelp(t) + await help.exec() t.match(joinedOutput(), 'npm ', 'showed npm usage') }) @@ -216,17 +211,17 @@ t.test('npm help - works in the presence of strange man pages', async t => { }) t.test('rejects with code', async t => { - const { exec } = await mockHelp(t, { + const { help } = await mockHelp(t, { spawnErr: Object.assign(new Error('errrrr'), { code: 'SPAWN_ERR' }), }) - await t.rejects(exec('whoami'), /help process exited with code: SPAWN_ERR/) + await t.rejects(help.exec(['whoami']), /help process exited with code: SPAWN_ERR/) }) t.test('rejects with no code', async t => { - const { exec } = await mockHelp(t, { + const { help } = await mockHelp(t, { spawnErr: new Error('errrrr'), }) - await t.rejects(exec('whoami'), /errrrr/) + await t.rejects(help.exec(['whoami']), /errrrr/) }) diff --git a/test/lib/commands/hook.js b/test/lib/commands/hook.js index 01da9dc720dae..e4e1214b812f3 100644 --- a/test/lib/commands/hook.js +++ b/test/lib/commands/hook.js @@ -51,6 +51,7 @@ const mockHook = async (t, { hookResponse, ...npmOpts } = {}) => { const mock = await mockNpm(t, { ...npmOpts, + command: 'hook', mocks: { libnpmhook, ...npmOpts.mocks, @@ -60,7 +61,6 @@ const mockHook = async (t, { hookResponse, ...npmOpts } = {}) => { return { ...mock, now, - hook: { exec: (args) => mock.npm.exec('hook', args) }, hookArgs: () => hookArgs, } } diff --git a/test/lib/commands/install.js b/test/lib/commands/install.js index 1be42d6e6125f..f40b62edde17c 100644 --- a/test/lib/commands/install.js +++ b/test/lib/commands/install.js @@ -355,31 +355,32 @@ t.test('completion', async t => { t.test('location detection and audit', async (t) => { await t.test('audit false without package.json', async t => { const { npm } = await loadMockNpm(t, { + command: 'install', prefixDir: { // no package.json 'readme.txt': 'just a file', 'other-dir': { a: 'a' }, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), false) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), false) }) await t.test('audit true with package.json', async t => { const { npm } = await loadMockNpm(t, { + command: 'install', prefixDir: { 'package.json': '{ "name": "testpkg", "version": "1.0.0" }', 'readme.txt': 'just a file', }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('audit true without package.json when set', async t => { const { npm } = await loadMockNpm(t, { + command: 'install', prefixDir: { // no package.json 'readme.txt': 'just a file', @@ -389,13 +390,13 @@ t.test('location detection and audit', async (t) => { audit: true, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('audit true in root config without package.json', async t => { const { npm } = await loadMockNpm(t, { + command: 'install', prefixDir: { // no package.json 'readme.txt': 'just a file', @@ -405,13 +406,13 @@ t.test('location detection and audit', async (t) => { otherDirs: { npmrc: 'audit=true' }, npm: ({ other }) => ({ npmRoot: other }), }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) }) await t.test('test for warning when --global & --audit', async t => { const { npm, logs } = await loadMockNpm(t, { + command: 'install', prefixDir: { // no package.json 'readme.txt': 'just a file', @@ -422,9 +423,8 @@ t.test('location detection and audit', async (t) => { global: true, }, }) - const install = await npm.cmd('install') - t.equal(install.npm.config.get('location'), 'user') - t.equal(install.npm.config.get('audit'), true) + t.equal(npm.config.get('location'), 'user') + t.equal(npm.config.get('audit'), true) t.equal(logs.warn[0][0], 'config') t.equal(logs.warn[0][1], 'includes both --global and --audit, which is currently unsupported.') }) diff --git a/test/lib/commands/link.js b/test/lib/commands/link.js index feae75a4b9096..65792fd141acb 100644 --- a/test/lib/commands/link.js +++ b/test/lib/commands/link.js @@ -10,6 +10,7 @@ t.cleanSnapshot = (str) => cleanCwd(str) const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { const mock = await mockNpm(t, { ...opts, + command: 'link', globalPrefixDir, mocks: { ...opts.mocks, @@ -36,10 +37,6 @@ const mockLink = async (t, { globalPrefixDir, ...opts } = {}) => { return { ...mock, - link: { - exec: (args = []) => mock.npm.exec('link', args), - completion: (o) => mock.npm.cmd('link').then(c => c.completion(o)), - }, printLinks, } } diff --git a/test/lib/commands/login.js b/test/lib/commands/login.js index 3c6e33f503ea6..b42d3001ebb90 100644 --- a/test/lib/commands/login.js +++ b/test/lib/commands/login.js @@ -37,8 +37,7 @@ const mockLogin = async (t, { stdin: stdinLines, registry: registryUrl, ...optio } t.test('usage', async t => { - const { npm } = await loadMockNpm(t) - const login = await npm.cmd('login') + const { login } = await loadMockNpm(t, { command: 'login' }) t.match(login.usage, 'login', 'usage has command name in it') }) diff --git a/test/lib/commands/logout.js b/test/lib/commands/logout.js index 0043bb4c57922..4ff21665f3035 100644 --- a/test/lib/commands/logout.js +++ b/test/lib/commands/logout.js @@ -8,6 +8,7 @@ const mockLogout = async (t, { userRc = [], ...npmOpts } = {}) => { let result = null const mock = await mockNpm(t, { + command: 'logout', mocks: { // XXX: refactor to use mock registry 'npm-registry-fetch': Object.assign(async (url, opts) => { @@ -22,7 +23,6 @@ const mockLogout = async (t, { userRc = [], ...npmOpts } = {}) => { return { ...mock, - logout: { exec: (args) => mock.npm.exec('logout', args) }, result: () => result, // get only the message portion of the verbose log from the command logMsg: () => mock.logs.verbose.find(l => l[0] === 'logout')[1], diff --git a/test/lib/commands/org.js b/test/lib/commands/org.js index d3700304328ee..511251e1bb096 100644 --- a/test/lib/commands/org.js +++ b/test/lib/commands/org.js @@ -30,6 +30,7 @@ const mockOrg = async (t, { orgSize = 1, orgList = {}, ...npmOpts } = {}) => { const mock = await mockNpm(t, { ...npmOpts, + command: 'org', mocks: { libnpmorg, ...npmOpts.mocks, @@ -38,11 +39,6 @@ const mockOrg = async (t, { orgSize = 1, orgList = {}, ...npmOpts } = {}) => { return { ...mock, - org: { - exec: (args) => mock.npm.exec('org', args), - completion: (arg) => mock.npm.cmd('org').then(c => c.completion(arg)), - usage: () => mock.npm.cmd('org').then(c => c.usage), - }, setArgs: () => setArgs, rmArgs: () => rmArgs, lsArgs: () => lsArgs, @@ -77,7 +73,7 @@ t.test('completion', async t => { t.test('npm org - invalid subcommand', async t => { const { org } = await mockOrg(t) - await t.rejects(org.exec(['foo']), org.usage()) + await t.rejects(org.exec(['foo']), org.usage) }) t.test('npm org add', async t => { diff --git a/test/lib/commands/outdated.js b/test/lib/commands/outdated.js index 02f2067c5480e..7becc79d62e17 100644 --- a/test/lib/commands/outdated.js +++ b/test/lib/commands/outdated.js @@ -234,6 +234,7 @@ const fixtures = { const mockNpm = async (t, { prefixDir, ...opts } = {}) => { const res = await _mockNpm(t, { + command: 'outdated', mocks: { pacote: { packument, @@ -255,151 +256,150 @@ const mockNpm = async (t, { prefixDir, ...opts } = {}) => { return { ...res, registry, - exec: (args) => res.npm.exec('outdated', args), } } t.test('should display outdated deps', async t => { await t.test('outdated global', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { globalPrefixDir: fixtures.global, config: { global: true }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { color: 'always', }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --omit=dev', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { omit: ['dev'], color: 'always', }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --omit=dev --omit=peer', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { omit: ['dev', 'peer'], color: 'always', }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --omit=prod', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { omit: ['prod'], color: 'always', }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --long', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { long: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --json', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { json: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --json --long', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { json: true, long: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --parseable', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { parseable: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --parseable --long', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { parseable: true, long: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated --all', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, config: { all: true, }, }) - await exec([]) + await outdated.exec([]) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) await t.test('outdated specific dep', async t => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.local, }) - await exec(['cat']) + await outdated.exec(['cat']) t.equal(process.exitCode, 1) t.matchSnapshot(joinedOutput()) }) @@ -424,11 +424,11 @@ t.test('should return if no outdated deps', async t => { }, } - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: testDir, }) - await exec([]) + await outdated.exec([]) t.equal(joinedOutput(), '', 'no logs') }) @@ -451,11 +451,11 @@ t.test('throws if error with a dep', async t => { }, } - const { exec } = await mockNpm(t, { + const { outdated } = await mockNpm(t, { prefixDir: testDir, }) - await t.rejects(exec([]), 'There is an error with this package.') + await t.rejects(outdated.exec([]), 'There is an error with this package.') }) t.test('should skip missing non-prod deps', async t => { @@ -470,11 +470,11 @@ t.test('should skip missing non-prod deps', async t => { node_modules: {}, } - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: testDir, }) - await exec([]) + await outdated.exec([]) t.equal(joinedOutput(), '', 'no logs') }) @@ -498,10 +498,10 @@ t.test('should skip invalid pkg ranges', async t => { }, } - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: testDir, }) - await exec([]) + await outdated.exec([]) t.equal(joinedOutput(), '', 'no logs') }) @@ -524,21 +524,21 @@ t.test('should skip git specs', async t => { }, } - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: testDir, }) - await exec([]) + await outdated.exec([]) t.equal(joinedOutput(), '', 'no logs') }) t.test('workspaces', async t => { const mockWorkspaces = async (t, { exitCode = 1, ...config } = {}) => { - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: fixtures.workspaces, config, }) - await exec([]) + await outdated.exec([]) t.matchSnapshot(joinedOutput(), 'output') t.equal(process.exitCode, exitCode ?? undefined) @@ -603,10 +603,10 @@ t.test('aliases', async t => { }, } - const { exec, joinedOutput } = await mockNpm(t, { + const { outdated, joinedOutput } = await mockNpm(t, { prefixDir: testDir, }) - await exec([]) + await outdated.exec([]) t.matchSnapshot(joinedOutput(), 'should display aliased outdated dep output') t.equal(process.exitCode, 1) diff --git a/test/lib/commands/owner.js b/test/lib/commands/owner.js index f9399a60cdf81..9329e8985e60c 100644 --- a/test/lib/commands/owner.js +++ b/test/lib/commands/owner.js @@ -613,9 +613,10 @@ t.test('workspaces', async t => { }) t.test('completion', async t => { + const mockCompletion = (t, opts) => loadMockNpm(t, { command: 'owner', ...opts }) + t.test('basic commands', async t => { - const { npm } = await loadMockNpm(t) - const owner = await npm.cmd('owner') + const { owner } = await mockCompletion(t) const testComp = async (argv, expect) => { const res = await owner.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, argv.join(' ')) @@ -631,10 +632,9 @@ t.test('completion', async t => { }) t.test('completion npm owner rm', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, owner } = await mockCompletion(t, { prefixDir: { 'package.json': JSON.stringify({ name: packageName }) }, }) - const owner = await npm.cmd('owner') const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry'), @@ -649,26 +649,23 @@ t.test('completion', async t => { }) t.test('completion npm owner rm no cwd package', async t => { - const { npm } = await loadMockNpm(t) - const owner = await npm.cmd('owner') + const { owner } = await mockCompletion(t) const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, [], 'should have no owners to autocomplete if not cwd package') }) t.test('completion npm owner rm global', async t => { - const { npm } = await loadMockNpm(t, { + const { owner } = await mockCompletion(t, { config: { global: true }, }) - const owner = await npm.cmd('owner') const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, [], 'should have no owners to autocomplete if global') }) t.test('completion npm owner rm no owners found', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, owner } = await mockCompletion(t, { prefixDir: { 'package.json': JSON.stringify({ name: packageName }) }, }) - const owner = await npm.cmd('owner') const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry'), diff --git a/test/lib/commands/prefix.js b/test/lib/commands/prefix.js index e8295cf6a5b3c..4fc348843fa25 100644 --- a/test/lib/commands/prefix.js +++ b/test/lib/commands/prefix.js @@ -2,7 +2,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('prefix', async t => { - const { joinedOutput, npm } = await loadMockNpm(t, { load: false }) + const { joinedOutput, npm } = await loadMockNpm(t) await npm.exec('prefix', []) t.equal( joinedOutput(), diff --git a/test/lib/commands/profile.js b/test/lib/commands/profile.js index 1152acfdc5c46..784523f7ccd8a 100644 --- a/test/lib/commands/profile.js +++ b/test/lib/commands/profile.js @@ -24,6 +24,7 @@ const mockProfile = async (t, { npmProfile, readUserInfo, qrcode, config, ...opt const mock = await mockNpm(t, { ...opts, + command: 'profile', config: { color: false, ...config, @@ -37,10 +38,6 @@ const mockProfile = async (t, { npmProfile, readUserInfo, qrcode, config, ...opt return { ...mock, result: () => mock.joinedOutput(), - profile: { - exec: (args) => mock.npm.exec('profile', args), - usage: () => mock.npm.cmd('profile').then(c => c.usage), - }, } } @@ -61,7 +58,7 @@ const userProfile = { t.test('no args', async t => { const { profile } = await mockProfile(t) - await t.rejects(profile.exec([]), await profile.usage()) + await t.rejects(profile.exec([]), await profile.usage) }) t.test('profile get no args', async t => { @@ -1081,8 +1078,7 @@ t.test('unknown subcommand', async t => { t.test('completion', async t => { const testComp = async (t, { argv, expect, title } = {}) => { - const { npm } = await mockProfile(t) - const profile = await npm.cmd('profile') + const { profile } = await mockProfile(t) t.resolveMatch(profile.completion({ conf: { argv: { remain: argv } } }), expect, title) } @@ -1114,8 +1110,7 @@ t.test('completion', async t => { }) t.test('npm profile unknown subcommand autocomplete', async t => { - const { npm } = await mockProfile(t) - const profile = await npm.cmd('profile') + const { profile } = await mockProfile(t) t.rejects( profile.completion({ conf: { argv: { remain: ['npm', 'profile', 'asdf'] } } }), { message: 'asdf not recognized' }, diff --git a/test/lib/commands/prune.js b/test/lib/commands/prune.js index 81245bcfca167..65cfba5e5c00a 100644 --- a/test/lib/commands/prune.js +++ b/test/lib/commands/prune.js @@ -4,7 +4,6 @@ const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should prune using Arborist', async (t) => { t.plan(4) const { npm } = await loadMockNpm(t, { - load: false, mocks: { '@npmcli/arborist': function (args) { t.ok(args, 'gets options object') diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 39696066130f9..0781d94374cc2 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -172,8 +172,7 @@ t.test('dry-run', async t => { }) t.test('shows usage with wrong set of arguments', async t => { - const { npm } = await loadMockNpm(t) - const publish = await npm.cmd('publish') + const { publish } = await loadMockNpm(t, { command: 'publish' }) await t.rejects(publish.exec(['a', 'b', 'c']), publish.usage) }) diff --git a/test/lib/commands/root.js b/test/lib/commands/root.js index a886b30c3ee48..506e2bc04eb84 100644 --- a/test/lib/commands/root.js +++ b/test/lib/commands/root.js @@ -2,7 +2,7 @@ const t = require('tap') const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('prefix', async (t) => { - const { joinedOutput, npm } = await loadMockNpm(t, { load: false }) + const { joinedOutput, npm } = await loadMockNpm(t) await npm.exec('root', []) t.equal( joinedOutput(), diff --git a/test/lib/commands/run-script.js b/test/lib/commands/run-script.js index 6e2bf22adddcf..cb54a7f51e900 100644 --- a/test/lib/commands/run-script.js +++ b/test/lib/commands/run-script.js @@ -11,6 +11,7 @@ const mockRs = async (t, { windows = false, runScript, ...opts } = {}) => { const mock = await mockNpm(t, { ...opts, + command: 'run-script', mocks: { '@npmcli/run-script': Object.assign( async rs => { @@ -28,18 +29,17 @@ const mockRs = async (t, { windows = false, runScript, ...opts } = {}) => { return { ...mock, RUN_SCRIPTS: () => RUN_SCRIPTS, - runScript: { exec: (args) => mock.npm.exec('run-script', args) }, + runScript: mock['run-script'], cleanLogs: () => mock.logs.error.flat().map(v => v.toString()).map(cleanCwd), } } t.test('completion', async t => { const completion = async (t, remain, pkg, isFish = false) => { - const { npm } = await mockRs(t, + const { runScript } = await mockRs(t, pkg ? { prefixDir: { 'package.json': JSON.stringify(pkg) } } : {} ) - const cmd = await npm.cmd('run-script') - return cmd.completion({ conf: { argv: { remain } }, isFish }) + return runScript.completion({ conf: { argv: { remain } }, isFish }) } t.test('already have a script name', async t => { diff --git a/test/lib/commands/stars.js b/test/lib/commands/stars.js index 124d2d344d8da..d92ced950291f 100644 --- a/test/lib/commands/stars.js +++ b/test/lib/commands/stars.js @@ -6,6 +6,8 @@ const noop = () => {} const mockStars = async (t, { npmFetch = noop, exec = true, ...opts }) => { const mock = await mockNpm(t, { + command: 'stars', + exec, mocks: { 'npm-registry-fetch': Object.assign(noop, realFetch, { json: npmFetch }), '{LIB}/utils/get-identity.js': async () => 'foo', @@ -13,16 +15,9 @@ const mockStars = async (t, { npmFetch = noop, exec = true, ...opts }) => { ...opts, }) - const stars = { exec: (args) => mock.npm.exec('stars', args) } - - if (exec) { - await stars.exec(Array.isArray(exec) ? exec : []) - mock.result = mock.joinedOutput() - } - return { ...mock, - stars, + result: mock.stars.output, logs: () => mock.logs.filter(l => l[1] === 'stars').map(l => l[2]), } } @@ -45,7 +40,7 @@ t.test('no args', async t => { } } - const { result } = await mockStars(t, { npmFetch, exec: true }) + const { result } = await mockStars(t, { npmFetch }) t.matchSnapshot( result, @@ -122,7 +117,7 @@ t.test('unexpected error', async t => { t.test('no pkg starred', async t => { const npmFetch = async () => ({ rows: [] }) - const { logs } = await mockStars(t, { npmFetch, exec: true }) + const { logs } = await mockStars(t, { npmFetch }) t.strictSame( logs(), diff --git a/test/lib/commands/team.js b/test/lib/commands/team.js index a13a56d986e35..1a5480293edc9 100644 --- a/test/lib/commands/team.js +++ b/test/lib/commands/team.js @@ -6,6 +6,7 @@ t.cleanSnapshot = s => s.trim().replace(/\n+/g, '\n') const mockTeam = async (t, { libnpmteam, ...opts } = {}) => { const mock = await mockNpm(t, { ...opts, + command: 'team', mocks: { // XXX: this should be refactored to use the mock registry libnpmteam: libnpmteam || { @@ -21,7 +22,6 @@ const mockTeam = async (t, { libnpmteam, ...opts } = {}) => { return { ...mock, - team: { exec: (args) => mock.npm.exec('team', args) }, result: () => mock.joinedOutput(), } } @@ -384,11 +384,10 @@ t.test('team rm ', async t => { }) t.test('completion', async t => { - const { npm } = await mockTeam(t) - const { completion } = await npm.cmd('team') + const { team } = await mockTeam(t) t.test('npm team autocomplete', async t => { - const res = await completion({ + const res = await team.completion({ conf: { argv: { remain: ['npm', 'team'], @@ -405,7 +404,7 @@ t.test('completion', async t => { t.test('npm team autocomplete', async t => { for (const subcmd of ['create', 'destroy', 'add', 'rm', 'ls']) { - const res = await completion({ + const res = await team.completion({ conf: { argv: { remain: ['npm', 'team', subcmd], @@ -421,7 +420,8 @@ t.test('completion', async t => { }) t.test('npm team unknown subcommand autocomplete', async t => { - t.rejects(completion({ conf: { argv: { remain: ['npm', 'team', 'missing-subcommand'] } } }), + t.rejects( + team.completion({ conf: { argv: { remain: ['npm', 'team', 'missing-subcommand'] } } }), { message: 'missing-subcommand not recognized' }, 'should throw a a not recognized error' ) diff --git a/test/lib/commands/token.js b/test/lib/commands/token.js index 1fd686a4427c9..2bc4af4a81a3d 100644 --- a/test/lib/commands/token.js +++ b/test/lib/commands/token.js @@ -14,6 +14,7 @@ const mockToken = async (t, { profile, getCredentialsByURI, readUserInfo, ...opt const mock = await mockNpm(t, { ...opts, + command: 'token', mocks, }) @@ -22,22 +23,14 @@ const mockToken = async (t, { profile, getCredentialsByURI, readUserInfo, ...opt mock.npm.config.getCredentialsByURI = getCredentialsByURI } - const token = { - exec: (args) => mock.npm.exec('token', args), - } - - return { - ...mock, - token, - } + return mock } t.test('completion', async t => { - const { npm } = await mockToken(t) - const { completion } = await npm.cmd('token') + const { token } = await mockToken(t) const testComp = (argv, expect) => { - t.resolveMatch(completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) + t.resolveMatch(token.completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) } testComp(['npm', 'token'], ['list', 'revoke', 'create']) @@ -45,7 +38,7 @@ t.test('completion', async t => { testComp(['npm', 'token', 'revoke'], []) testComp(['npm', 'token', 'create'], []) - t.rejects(completion({ conf: { argv: { remain: ['npm', 'token', 'foobar'] } } }), { + t.rejects(token.completion({ conf: { argv: { remain: ['npm', 'token', 'foobar'] } } }), { message: 'foobar not recognize', }) }) diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index da317fbde8611..6e898bd3d07e4 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -427,13 +427,13 @@ t.test('scoped registry config', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t, { + const { npm, unpublish } = await loadMockNpm(t, { + command: 'unpublish', config: { ...auth, }, }) - const unpublish = await npm.cmd('unpublish') const testComp = async (t, { argv, partialWord, expect, title }) => { const res = await unpublish.completion( diff --git a/test/lib/commands/version.js b/test/lib/commands/version.js index 0f92282f7d1b7..8aa6c088bfc9b 100644 --- a/test/lib/commands/version.js +++ b/test/lib/commands/version.js @@ -7,6 +7,7 @@ const mockGlobals = require('@npmcli/mock-globals') const mockNpm = async (t, opts = {}) => { const res = await _mockNpm(t, { ...opts, + command: 'version', mocks: { ...opts.mocks, '{ROOT}/package.json': { version: '1.0.0' }, @@ -14,7 +15,6 @@ const mockNpm = async (t, opts = {}) => { }) return { ...res, - version: { exec: (args) => res.npm.exec('version', args) }, result: () => res.outputs[0], } } @@ -55,8 +55,7 @@ t.test('node@1', async t => { }) t.test('completion', async t => { - const { npm } = await mockNpm(t) - const version = await npm.cmd('version') + const { version } = await mockNpm(t) const testComp = async (argv, expect) => { const res = await version.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, argv.join(' ')) diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js index 51bc130df24e5..ca07ef9eec2ff 100644 --- a/test/lib/commands/view.js +++ b/test/lib/commands/view.js @@ -576,8 +576,7 @@ t.test('workspaces', async t => { }) t.test('completion', async t => { - const { npm } = await loadMockNpm(t) - const view = await npm.cmd('view') + const { view } = await loadMockNpm(t, { command: 'view' }) const res = await view.completion({ conf: { argv: { remain: ['npm', 'view', 'green@1.0.0'] } }, }) @@ -585,8 +584,7 @@ t.test('completion', async t => { }) t.test('no package completion', async t => { - const { npm } = await loadMockNpm(t) - const view = await npm.cmd('view') + const { view } = await loadMockNpm(t, { command: 'view' }) const res = await view.completion({ conf: { argv: { remain: ['npm', 'view'] } } }) t.notOk(res, 'there is no package completion') t.end() diff --git a/test/lib/docs.js b/test/lib/docs.js index d74cdc5e47988..cc6283c87241c 100644 --- a/test/lib/docs.js +++ b/test/lib/docs.js @@ -45,7 +45,7 @@ t.test('basic usage', async t => { .replace(npm.config.get('userconfig'), '{USERCONFIG}') .split(pkg.version).join('{VERSION}') - t.matchSnapshot(await npm.usage) + t.matchSnapshot(npm.usage) }) t.test('usage', async t => { @@ -80,9 +80,8 @@ t.test('usage', async t => { t.test(cmd, async t => { let output = null if (!bareCommands.includes(cmd)) { - const { npm } = await loadMockNpm(t) - const impl = await npm.cmd(cmd) - output = impl.usage + const mock = await loadMockNpm(t, { command: cmd }) + output = mock[cmd].usage } const usage = docs.usage(docs.TAGS.USAGE, { path: cmd }) diff --git a/test/lib/load-all-commands.js b/test/lib/load-all-commands.js index 1742376a36e69..d3846434489ce 100644 --- a/test/lib/load-all-commands.js +++ b/test/lib/load-all-commands.js @@ -6,20 +6,30 @@ const t = require('tap') const util = require('util') const { load: loadMockNpm } = require('../fixtures/mock-npm.js') const { commands } = require('../../lib/utils/cmd-list.js') +const BaseCommand = require('../../lib/base-command.js') const isAsyncFn = (v) => typeof v === 'function' && /^\[AsyncFunction:/.test(util.inspect(v)) t.test('load each command', async t => { + const counts = { + completion: 0, + ignoreImplicitWorkspace: 0, + workspaces: 0, + noParams: 0, + } + for (const cmd of commands) { - t.test(cmd, async t => { + await t.test(cmd, async t => { const { npm, outputs, cmd: impl } = await loadMockNpm(t, { command: cmd, config: { usage: true }, }) const ctor = impl.constructor - if (impl.completion) { - t.type(impl.completion, 'function', 'completion, if present, is a function') + t.notOk(impl.completion, 'completion is static, not on instance') + if (ctor.completion) { + t.ok(isAsyncFn(ctor.completion), 'completion is async function') + counts.completion++ } // exec fn @@ -28,7 +38,15 @@ t.test('load each command', async t => { // workspaces t.type(ctor.ignoreImplicitWorkspace, 'boolean', 'ctor has ignoreImplictWorkspace boolean') + if (ctor.ignoreImplicitWorkspace !== BaseCommand.ignoreImplicitWorkspace) { + counts.ignoreImplicitWorkspace++ + } + t.type(ctor.workspaces, 'boolean', 'ctor has workspaces boolean') + if (ctor.workspaces !== BaseCommand.workspaces) { + counts.workspaces++ + } + if (ctor.workspaces) { t.ok(isAsyncFn(impl.execWorkspaces), 'execWorkspaces is async') t.ok(impl.exec.length <= 1, 'execWorkspaces fn has 0 or 1 args') @@ -38,13 +56,32 @@ t.test('load each command', async t => { // name/desc t.ok(impl.description, 'implementation has a description') + t.equal(impl.description, ctor.description, 'description is same on instance and ctor') t.ok(impl.name, 'implementation has a name') + t.equal(impl.name, ctor.name, 'name is same on instance and ctor') t.equal(cmd, impl.name, 'command list and name are the same') + // params are optional + if (impl.params) { + t.equal(impl.params, ctor.params, 'params is same on instance and ctor') + t.ok(impl.params, 'implementation has a params') + } else { + counts.noParams++ + } + // usage t.match(impl.usage, cmd, 'usage contains the command') await npm.exec(cmd, []) t.match(outputs[0][0], impl.usage, 'usage is what is output') + t.match(outputs[0][0], ctor.describeUsage, 'usage is what is output') + t.notOk(impl.describeUsage, 'describe usage is only static') }) } + + // make sure refactors dont move or rename these static properties since + // we guard against the tests for them above + t.ok(counts.completion > 0, 'has some completion functions') + t.ok(counts.ignoreImplicitWorkspace > 0, 'has some commands that change ignoreImplicitWorkspace') + t.ok(counts.workspaces > 0, 'has some commands that change workspaces') + t.ok(counts.noParams > 0, 'has some commands that do not have params') }) diff --git a/test/lib/npm.js b/test/lib/npm.js index 2dc3899e0dbe0..162e8c83ca4a4 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -123,7 +123,7 @@ t.test('npm.load', async t => { await t.test('node is a symlink', async t => { const node = process.platform === 'win32' ? 'node.exe' : 'node' - const { npm, logs, outputs, prefix } = await loadMockNpm(t, { + const { Npm, npm, logs, outputs, prefix } = await loadMockNpm(t, { prefixDir: { bin: t.fixture('symlink', dirname(process.execPath)), }, @@ -164,8 +164,8 @@ t.test('npm.load', async t => { t.equal(npm.command, 'll', 'command set to first npm command') t.equal(npm.flatOptions.npmCommand, 'll', 'npmCommand flatOption set') - const ll = await npm.cmd('ll') - t.same(outputs, [[ll.usage]], 'print usage') + const ll = Npm.cmd('ll') + t.same(outputs, [[ll.describeUsage]], 'print usage') npm.config.set('usage', false) outputs.length = 0 @@ -198,7 +198,6 @@ t.test('npm.load', async t => { await t.test('--no-workspaces with --workspace', async t => { const { npm } = await loadMockNpm(t, { - load: false, prefixDir: { packages: { a: { @@ -550,14 +549,14 @@ t.test('output clears progress and console.logs the message', async t => { }) t.test('aliases and typos', async t => { - const { npm } = await loadMockNpm(t, { load: false }) - await t.rejects(npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) - await t.rejects(npm.cmd(''), { code: 'EUNKNOWNCOMMAND' }) - await t.rejects(npm.cmd('birthday'), { code: 'EUNKNOWNCOMMAND' }) - await t.resolves(npm.cmd('it'), { name: 'install-test' }) - await t.resolves(npm.cmd('installTe'), { name: 'install-test' }) - await t.resolves(npm.cmd('access'), { name: 'access' }) - await t.resolves(npm.cmd('auth'), { name: 'owner' }) + const { Npm } = await loadMockNpm(t, { init: false }) + t.throws(() => Npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' }) + t.throws(() => Npm.cmd(''), { code: 'EUNKNOWNCOMMAND' }) + t.throws(() => Npm.cmd('birthday'), { code: 'EUNKNOWNCOMMAND' }) + t.match(Npm.cmd('it').name, 'install-test') + t.match(Npm.cmd('installTe').name, 'install-test') + t.match(Npm.cmd('access').name, 'access') + t.match(Npm.cmd('auth').name, 'owner') }) t.test('explicit workspace rejection', async t => { @@ -660,27 +659,27 @@ t.test('implicit workspace accept', async t => { t.test('usage', async t => { t.test('with browser', async t => { const { npm } = await loadMockNpm(t, { globals: { process: { platform: 'posix' } } }) - const usage = await npm.usage + const usage = npm.usage npm.config.set('viewer', 'browser') - const browserUsage = await npm.usage + const browserUsage = npm.usage t.notMatch(usage, '(in a browser)') t.match(browserUsage, '(in a browser)') }) t.test('windows always uses browser', async t => { const { npm } = await loadMockNpm(t, { globals: { process: { platform: 'win32' } } }) - const usage = await npm.usage + const usage = npm.usage npm.config.set('viewer', 'browser') - const browserUsage = await npm.usage + const browserUsage = npm.usage t.match(usage, '(in a browser)') t.match(browserUsage, '(in a browser)') }) t.test('includes commands', async t => { const { npm } = await loadMockNpm(t) - const usage = await npm.usage + const usage = npm.usage npm.config.set('long', true) - const longUsage = await npm.usage + const longUsage = npm.usage const lastCmd = commands[commands.length - 1] for (const cmd of commands) { @@ -713,7 +712,7 @@ t.test('usage', async t => { for (const width of widths) { t.test(`column width ${width}`, async t => { mockGlobals(t, { 'process.stdout.columns': width }) - const usage = await npm.usage + const usage = npm.usage t.matchSnapshot(usage) }) } diff --git a/test/lib/utils/audit-error.js b/test/lib/utils/audit-error.js index 1cb29a0857d75..f6be56a152f71 100644 --- a/test/lib/utils/audit-error.js +++ b/test/lib/utils/audit-error.js @@ -10,6 +10,8 @@ const auditError = async (t, { command, error, ...config } = {}) => { const mock = await mockNpm(t, { command, config, + exec: true, + prefixDir: { 'package.json': '{}', 'package-lock.json': '{}' }, }) const res = {} @@ -32,7 +34,8 @@ t.test('no error, not audit command', async t => { t.equal(result, false, 'no error') t.notOk(error, 'no error') - t.strictSame(output, '', 'no output') + t.match(output.trim(), /up to date/, 'install output') + t.match(output.trim(), /found 0 vulnerabilities/, 'install output') t.strictSame(logs, [], 'no warnings') }) @@ -53,7 +56,8 @@ t.test('error, not audit command', async t => { t.equal(result, true, 'had error') t.notOk(error, 'no error') - t.strictSame(output, '', 'no output') + t.match(output.trim(), /up to date/, 'install output') + t.match(output.trim(), /found 0 vulnerabilities/, 'install output') t.strictSame(logs, [], 'no warnings') }) @@ -62,7 +66,7 @@ t.test('error, audit command, not json', async t => { command: 'audit', error: { message: 'message', - body: Buffer.from('body'), + body: Buffer.from('body error text'), method: 'POST', uri: 'https://example.com/not/a/registry', headers: { @@ -75,7 +79,7 @@ t.test('error, audit command, not json', async t => { t.equal(result, undefined) t.ok(error, 'throws error') - t.strictSame(output, 'body', 'some output') + t.match(output, 'body error text', 'some output') t.strictSame(logs, [['audit', 'message']], 'some warnings') }) @@ -97,7 +101,7 @@ t.test('error, audit command, json', async t => { t.equal(result, undefined) t.ok(error, 'throws error') - t.strictSame(output, + t.match(output, '{\n' + ' "message": "message",\n' + ' "method": "POST",\n' + diff --git a/test/lib/utils/did-you-mean.js b/test/lib/utils/did-you-mean.js index d3cb3a24f0ae5..d111c2f002960 100644 --- a/test/lib/utils/did-you-mean.js +++ b/test/lib/utils/did-you-mean.js @@ -1,9 +1,8 @@ const t = require('tap') -const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const dym = require('../../../lib/utils/did-you-mean.js') + t.test('did-you-mean', async t => { - const { npm } = await loadMockNpm(t) t.test('with package.json', async t => { const testdir = t.testdir({ 'package.json': JSON.stringify({ @@ -17,27 +16,27 @@ t.test('did-you-mean', async t => { }), }) t.test('nistall', async t => { - const result = await dym(npm, testdir, 'nistall') + const result = await dym(testdir, 'nistall') t.match(result, 'npm install') }) t.test('sttest', async t => { - const result = await dym(npm, testdir, 'sttest') + const result = await dym(testdir, 'sttest') t.match(result, 'npm test') t.match(result, 'npm run posttest') }) t.test('npz', async t => { - const result = await dym(npm, testdir, 'npxx') + const result = await dym(testdir, 'npxx') t.match(result, 'npm exec npx') }) t.test('qwuijbo', async t => { - const result = await dym(npm, testdir, 'qwuijbo') + const result = await dym(testdir, 'qwuijbo') t.match(result, '') }) }) t.test('with no package.json', t => { const testdir = t.testdir({}) t.test('nistall', async t => { - const result = await dym(npm, testdir, 'nistall') + const result = await dym(testdir, 'nistall') t.match(result, 'npm install') }) t.end() @@ -49,7 +48,7 @@ t.test('did-you-mean', async t => { }), }) - const result = await dym(npm, testdir, 'nistall') + const result = await dym(testdir, 'nistall') t.match(result, 'npm install') }) }) diff --git a/test/lib/utils/error-message.js b/test/lib/utils/error-message.js index b0be2c18ec551..1ba5865592edb 100644 --- a/test/lib/utils/error-message.js +++ b/test/lib/utils/error-message.js @@ -46,7 +46,9 @@ const loadMockNpm = async (t, { errorMocks, ...opts } = {}) => { t.test('just simple messages', async t => { const { errorMessage } = await loadMockNpm(t, { + prefixDir: { 'package-lock.json': '{}' }, command: 'audit', + exec: true, }) const codes = [ 'ENOAUDIT', diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index 25adcc29e9b71..f553e1a2ea518 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -40,7 +40,7 @@ t.cleanSnapshot = (path) => cleanDate(cleanCwd(path)) mockGlobals(t, { process: Object.assign(new EventEmitter(), { // these are process properties that are needed in the running code and tests - ...pick(process, 'execPath', 'stdout', 'stderr', 'cwd', 'chdir', 'env', 'umask'), + ...pick(process, 'execPath', 'stdout', 'stderr', 'stdin', 'cwd', 'chdir', 'env', 'umask'), argv: ['/node', ...process.argv.slice(1)], version: 'v1.0.0', kill: () => {}, @@ -53,13 +53,11 @@ mockGlobals(t, { }), }, { replace: true }) -const mockExitHandler = async (t, { init, load, testdir, config, mocks, files } = {}) => { +const mockExitHandler = async (t, { config, mocks, files, ...opts } = {}) => { const errors = [] const { npm, logMocks, ...rest } = await loadMockNpm(t, { - init, - load, - testdir, + ...opts, mocks: { '{ROOT}/package.json': { version: '1.0.0', @@ -592,13 +590,14 @@ t.test('exits uncleanly when only emitting exit event', async (t) => { t.match(logs.error, [['', 'Exit handler never called!']]) t.equal(process.exitCode, 1, 'exitCode coerced to 1') - t.end() }) t.test('do no fancy handling for shellouts', async t => { - const { exitHandler, npm, logs } = await mockExitHandler(t) - - await npm.cmd('exec') + const { exitHandler, logs } = await mockExitHandler(t, { + command: 'exec', + exec: true, + argv: ['-c', 'exit'], + }) const loudNoises = () => logs.filter(([level]) => ['warn', 'error'].includes(level)) @@ -614,7 +613,6 @@ t.test('do no fancy handling for shellouts', async t => { t.equal(process.exitCode, 1, 'got expected exit code') // should log some warnings and errors, because something weird happened t.strictNotSame(loudNoises(), [], 'bring the noise') - t.end() }) t.test('shellout with code=0 (extra weird?)', async t => { @@ -622,6 +620,4 @@ t.test('do no fancy handling for shellouts', async t => { t.equal(process.exitCode, 1, 'got expected exit code') t.strictNotSame(loudNoises(), [], 'bring the noise') }) - - t.end() }) diff --git a/test/lib/utils/reify-output.js b/test/lib/utils/reify-output.js index 5d1d5be47efa3..1c6215ab33bef 100644 --- a/test/lib/utils/reify-output.js +++ b/test/lib/utils/reify-output.js @@ -8,6 +8,7 @@ const mockReify = async (t, reify, { command, ...config } = {}) => { const mock = await mockNpm(t, { command, config, + setCmd: true, }) reifyOutput(mock.npm, reify) diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js index 9c12433a2d117..cc5348a440e0a 100644 --- a/test/lib/utils/update-notifier.js +++ b/test/lib/utils/update-notifier.js @@ -19,7 +19,8 @@ const runUpdateNotifier = async (t, { PACOTE_ERROR, STAT_MTIME = 0, mocks: _mocks = {}, - command = 'view', + command = 'help', + prefixDir, version = CURRENT_VERSION, argv = [], ...config @@ -76,6 +77,8 @@ const runUpdateNotifier = async (t, { command, mocks, config, + exec: true, + prefixDir, argv, }) const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', mocks) @@ -106,6 +109,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating', async t => { const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { command: 'install', + prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, argv: ['npm'], global: true, }) @@ -116,6 +120,7 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating with spec', async t => { const { result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { command: 'install', + prefixDir: { 'package.json': `{"name":"${t.testName}"}` }, argv: ['npm@latest'], global: true, })