From 00ccefdd97b12862b600845a195fb5c8485de7ef Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 24 Feb 2021 15:54:50 -0800 Subject: [PATCH] fix(npm) pass npm context everywhere Instead of files randomly requiring the npm singleton, we pass it where it needs to go so that tests don't need to do so much require mocking everywhere --- lib/access.js | 285 +++++---- lib/adduser.js | 104 ++-- lib/audit.js | 95 +-- lib/bin.js | 32 +- lib/birthday.js | 27 +- lib/bugs.js | 68 ++- lib/cache.js | 184 +++--- lib/ci.js | 107 ++-- lib/completion.js | 200 ++++--- lib/config.js | 372 ++++++------ lib/dedupe.js | 45 +- lib/deprecate.js | 115 ++-- lib/diff.js | 416 ++++++------- lib/dist-tag.js | 181 +++--- lib/docs.js | 59 +- lib/doctor.js | 486 ++++++++-------- lib/edit.js | 68 ++- lib/exec.js | 451 ++++++++------- lib/explain.js | 148 ++--- lib/explore.js | 117 ++-- lib/find-dupes.js | 13 +- lib/fund.js | 346 +++++------ lib/get.js | 27 +- lib/help-search.js | 312 +++++----- lib/help.js | 354 ++++++------ lib/hook.js | 110 ++-- lib/init.js | 156 ++--- lib/install-ci-test.js | 32 +- lib/install-test.js | 34 +- lib/install.js | 232 ++++---- lib/link.js | 256 ++++---- lib/ll.js | 26 +- lib/logout.js | 75 +-- lib/ls.js | 308 +++++----- lib/npm.js | 52 +- lib/org.js | 249 ++++---- lib/outdated.js | 368 ++++++------ lib/owner.js | 212 +++---- lib/pack.js | 62 +- lib/ping.js | 48 +- lib/prefix.js | 24 +- lib/profile.js | 602 +++++++++---------- lib/prune.js | 38 +- lib/publish.js | 210 +++---- lib/rebuild.js | 96 +-- lib/restart.js | 10 +- lib/root.js | 24 +- lib/run-script.js | 231 ++++---- lib/start.js | 10 +- lib/stop.js | 10 +- lib/test.js | 28 +- lib/utils/audit-error.js | 3 +- lib/utils/completion/installed-deep.js | 3 +- lib/utils/completion/installed-shallow.js | 6 +- lib/utils/lifecycle-cmd.js | 21 +- lib/utils/npm-usage.js | 7 +- lib/utils/open-url.js | 40 +- lib/utils/read-local-package.js | 4 +- lib/utils/reify-finish.js | 9 +- lib/utils/reify-output.js | 11 +- lib/utils/usage.js | 4 +- test/lib/access.js | 157 +++-- test/lib/adduser.js | 65 ++- test/lib/audit.js | 68 +-- test/lib/bin.js | 21 +- test/lib/birthday.js | 8 +- test/lib/bugs.js | 11 +- test/lib/cache.js | 25 +- test/lib/ci.js | 111 ++-- test/lib/completion.js | 44 +- test/lib/config.js | 60 +- test/lib/dedupe.js | 43 +- test/lib/deprecate.js | 24 +- test/lib/diff.js | 109 ++-- test/lib/dist-tag.js | 49 +- test/lib/docs.js | 11 +- test/lib/doctor.js | 40 +- test/lib/edit.js | 14 +- test/lib/exec.js | 673 +++++++++++----------- test/lib/explain.js | 140 ++--- test/lib/explore.js | 97 ++-- test/lib/find-dupes.js | 5 +- test/lib/fund.js | 68 +-- test/lib/get.js | 18 +- test/lib/help-search.js | 22 +- test/lib/help.js | 38 +- test/lib/hook.js | 48 +- test/lib/init.js | 34 +- test/lib/install-ci-test.js | 57 ++ test/lib/install-test.js | 57 ++ test/lib/install.js | 92 +-- test/lib/link.js | 23 +- test/lib/ll.js | 29 +- test/lib/logout.js | 20 +- test/lib/ls.js | 244 ++++---- test/lib/org.js | 51 +- test/lib/outdated.js | 53 +- test/lib/owner.js | 56 +- test/lib/pack.js | 76 +-- test/lib/ping.js | 18 +- test/lib/prefix.js | 6 +- test/lib/profile.js | 174 +++--- test/lib/prune.js | 16 +- test/lib/publish.js | 302 +++++----- test/lib/rebuild.js | 16 +- test/lib/restart.js | 17 +- test/lib/root.js | 6 +- test/lib/run-script.js | 445 +++++++------- test/lib/start.js | 17 +- test/lib/stop.js | 17 +- test/lib/test.js | 19 +- 111 files changed, 6389 insertions(+), 5648 deletions(-) create mode 100644 test/lib/install-ci-test.js create mode 100644 test/lib/install-test.js diff --git a/lib/access.js b/lib/access.js index 10b1e21e0c5d7..08f028271685c 100644 --- a/lib/access.js +++ b/lib/access.js @@ -3,25 +3,11 @@ const path = require('path') const libaccess = require('libnpmaccess') const readPackageJson = require('read-package-json-fast') -const npm = require('./npm.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') const usageUtil = require('./utils/usage.js') const getIdentity = require('./utils/get-identity.js') -const usage = usageUtil( - 'npm access', - 'npm access public []\n' + - 'npm access restricted []\n' + - 'npm access grant []\n' + - 'npm access revoke []\n' + - 'npm access 2fa-required []\n' + - 'npm access 2fa-not-required []\n' + - 'npm access ls-packages [||]\n' + - 'npm access ls-collaborators [ []]\n' + - 'npm access edit []' -) - const subcommands = [ 'public', 'restricted', @@ -34,152 +20,195 @@ const subcommands = [ '2fa-not-required', ] -const UsageError = (msg) => - Object.assign(new Error(`\nUsage: ${msg}\n\n` + usage), { - code: 'EUSAGE', - }) - -const cmd = (args, cb) => - access(args) - .then(x => cb(null, x)) - .catch(err => err.code === 'EUSAGE' - ? cb(err.message) - : cb(err) +class Access { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil( + 'npm access', + 'npm access public []\n' + + 'npm access restricted []\n' + + 'npm access grant []\n' + + 'npm access revoke []\n' + + 'npm access 2fa-required []\n' + + 'npm access 2fa-not-required []\n' + + 'npm access ls-packages [||]\n' + + 'npm access ls-collaborators [ []]\n' + + 'npm access edit []' ) + } -const access = async ([cmd, ...args], cb) => { - const fn = subcommands.includes(cmd) && access[cmd] + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) + return subcommands + + switch (argv[2]) { + case 'grant': + if (argv.length === 3) + return ['read-only', 'read-write'] + else + return [] + + case 'public': + case 'restricted': + case 'ls-packages': + case 'ls-collaborators': + case 'edit': + case '2fa-required': + case '2fa-not-required': + case 'revoke': + return [] + default: + throw new Error(argv[2] + ' not recognized') + } + } - if (!cmd) - throw UsageError('Subcommand is required.') + exec (args, cb) { + this.access(args) + .then(x => cb(null, x)) + .catch(err => err.code === 'EUSAGE' + ? cb(err.message) + : cb(err) + ) + } - if (!fn) - throw UsageError(`${cmd} is not a recognized subcommand.`) + async access ([cmd, ...args]) { + if (!cmd) + throw this.usageError('Subcommand is required.') - return fn(args, { ...npm.flatOptions }) -} + if (!subcommands.includes(cmd) || !this[cmd]) + throw this.usageError(`${cmd} is not a recognized subcommand.`) -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv.length === 2) - return subcommands + return this[cmd](args, { ...this.npm.flatOptions }) + } - switch (argv[2]) { - case 'grant': - if (argv.length === 3) - return ['read-only', 'read-write'] - else - return [] + public ([pkg], opts) { + return this.modifyPackage(pkg, opts, libaccess.public) + } - case 'public': - case 'restricted': - case 'ls-packages': - case 'ls-collaborators': - case 'edit': - case '2fa-required': - case '2fa-not-required': - case 'revoke': - return [] - default: - throw new Error(argv[2] + ' not recognized') + restricted ([pkg], opts) { + return this.modifyPackage(pkg, opts, libaccess.restricted) } -} -access.public = ([pkg], opts) => - modifyPackage(pkg, opts, libaccess.public) + async grant ([perms, scopeteam, pkg], opts) { + if (!perms || (perms !== 'read-only' && perms !== 'read-write')) + throw this.usageError('First argument must be either `read-only` or `read-write`.') -access.restricted = ([pkg], opts) => - modifyPackage(pkg, opts, libaccess.restricted) + if (!scopeteam) + throw this.usageError('`` argument is required.') -access.grant = async ([perms, scopeteam, pkg], opts) => { - if (!perms || (perms !== 'read-only' && perms !== 'read-write')) - throw UsageError('First argument must be either `read-only` or `read-write`.') + const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] - if (!scopeteam) - throw UsageError('`` argument is required.') + if (!scope && !team) { + throw this.usageError( + 'Second argument used incorrect format.\n' + + 'Example: @example:developers' + ) + } - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] + return this.modifyPackage(pkg, opts, (pkgName, opts) => + libaccess.grant(pkgName, scopeteam, perms, opts), false) + } - if (!scope && !team) { - throw UsageError( - 'Second argument used incorrect format.\n' + - 'Example: @example:developers' - ) + async revoke ([scopeteam, pkg], opts) { + if (!scopeteam) + throw this.usageError('`` argument is required.') + + const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] + + if (!scope || !team) { + throw this.usageError( + 'First argument used incorrect format.\n' + + 'Example: @example:developers' + ) + } + + return this.modifyPackage(pkg, opts, (pkgName, opts) => + libaccess.revoke(pkgName, scopeteam, opts)) } - return modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.grant(pkgName, scopeteam, perms, opts), false) -} + get ['2fa-required'] () { + return this.tfaRequired + } -access.revoke = async ([scopeteam, pkg], opts) => { - if (!scopeteam) - throw UsageError('`` argument is required.') + tfaRequired ([pkg], opts) { + return this.modifyPackage(pkg, opts, libaccess.tfaRequired, false) + } - const [, scope, team] = scopeteam.match(/^@?([^:]+):(.*)$/) || [] + get ['2fa-not-required'] () { + return this.tfaNotRequired + } - if (!scope || !team) { - throw UsageError( - 'First argument used incorrect format.\n' + - 'Example: @example:developers' - ) + tfaNotRequired ([pkg], opts) { + return this.modifyPackage(pkg, opts, libaccess.tfaNotRequired, false) } - return modifyPackage(pkg, opts, (pkgName, opts) => - libaccess.revoke(pkgName, scopeteam, opts)) -} + get ['ls-packages'] () { + return this.lsPackages + } -access['2fa-required'] = access.tfaRequired = ([pkg], opts) => - modifyPackage(pkg, opts, libaccess.tfaRequired, false) + async lsPackages ([owner], opts) { + if (!owner) + owner = await getIdentity(opts) -access['2fa-not-required'] = access.tfaNotRequired = ([pkg], opts) => - modifyPackage(pkg, opts, libaccess.tfaNotRequired, false) + const pkgs = await libaccess.lsPackages(owner, opts) -access['ls-packages'] = access.lsPackages = async ([owner], opts) => { - if (!owner) - owner = await getIdentity(opts) + // TODO - print these out nicely (breaking change) + output(JSON.stringify(pkgs, null, 2)) + } - const pkgs = await libaccess.lsPackages(owner, opts) + get ['ls-collaborators'] () { + return this.lsCollaborators + } - // TODO - print these out nicely (breaking change) - output(JSON.stringify(pkgs, null, 2)) -} + async lsCollaborators ([pkg, usr], opts) { + const pkgName = await this.getPackage(pkg, false) + const collabs = await libaccess.lsCollaborators(pkgName, usr, opts) -access['ls-collaborators'] = access.lsCollaborators = async ([pkg, usr], opts) => { - const pkgName = await getPackage(pkg, false) - const collabs = await libaccess.lsCollaborators(pkgName, usr, opts) + // TODO - print these out nicely (breaking change) + output(JSON.stringify(collabs, null, 2)) + } - // TODO - print these out nicely (breaking change) - output(JSON.stringify(collabs, null, 2)) -} + async edit () { + throw new Error('edit subcommand is not implemented yet') + } -access.edit = () => - Promise.reject(new Error('edit subcommand is not implemented yet')) - -const modifyPackage = (pkg, opts, fn, requireScope = true) => - getPackage(pkg, requireScope) - .then(pkgName => otplease(opts, opts => fn(pkgName, opts))) - -const getPackage = async (name, requireScope) => { - if (name && name.trim()) - return name.trim() - else { - try { - const pkg = await readPackageJson(path.resolve(npm.prefix, 'package.json')) - name = pkg.name - } catch (err) { - if (err.code === 'ENOENT') { - throw new Error( - 'no package name passed to command and no package.json found' - ) - } else - throw err + modifyPackage (pkg, opts, fn, requireScope = true) { + return this.getPackage(pkg, requireScope) + .then(pkgName => otplease(opts, opts => fn(pkgName, opts))) + } + + async getPackage (name, requireScope) { + if (name && name.trim()) + return name.trim() + else { + try { + const pkg = await readPackageJson(path.resolve(this.npm.prefix, 'package.json')) + name = pkg.name + } catch (err) { + if (err.code === 'ENOENT') { + throw new Error( + 'no package name passed to command and no package.json found' + ) + } else + throw err + } + + if (requireScope && !name.match(/^@[^/]+\/.*$/)) + throw this.usageError('This command is only available for scoped packages.') + else + return name } + } - if (requireScope && !name.match(/^@[^/]+\/.*$/)) - throw UsageError('This command is only available for scoped packages.') - else - return name + usageError (msg) { + return Object.assign(new Error(`\nUsage: ${msg}\n\n` + this.usage), { + code: 'EUSAGE', + }) } } -module.exports = Object.assign(cmd, { usage, completion, subcommands }) +module.exports = Access diff --git a/lib/adduser.js b/lib/adduser.js index c68c2b80f8790..566c768ac4db9 100644 --- a/lib/adduser.js +++ b/lib/adduser.js @@ -1,5 +1,4 @@ const log = require('npmlog') -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') const replaceInfo = require('./utils/replace-info.js') @@ -10,66 +9,75 @@ const authTypes = { sso: require('./auth/sso.js'), } -const usage = usageUtil( - 'adduser', - 'npm adduser [--registry=url] [--scope=@orgname] [--always-auth]' -) +class AddUser { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => adduser(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil( + 'adduser', + 'npm adduser [--registry=url] [--scope=@orgname] [--always-auth]' + ) + } -const getRegistry = ({ scope, registry }) => { - if (scope) { - const scopedRegistry = npm.config.get(`${scope}:registry`) - const cliRegistry = npm.config.get('registry', 'cli') - if (scopedRegistry && !cliRegistry) - return scopedRegistry + exec (args, cb) { + this.adduser(args).then(() => cb()).catch(cb) } - return registry -} -const getAuthType = ({ authType }) => { - const type = authTypes[authType] + async adduser (args) { + const { scope } = this.npm.flatOptions + const registry = this.getRegistry(this.npm.flatOptions) + const auth = this.getAuthType(this.npm.flatOptions) + const creds = this.npm.config.getCredentialsByURI(registry) - if (!type) - throw new Error('no such auth module') + log.disableProgress() - return type -} + log.notice('', `Log in on ${replaceInfo(registry)}`) -const updateConfig = async ({ newCreds, registry, scope }) => { - npm.config.delete('_token', 'user') // prevent legacy pollution + const { message, newCreds } = await auth({ + ...this.npm.flatOptions, + creds, + registry, + scope, + }) - if (scope) - npm.config.set(scope + ':registry', registry, 'user') + await this.updateConfig({ + newCreds, + registry, + scope, + }) - npm.config.setCredentialsByURI(registry, newCreds) - await npm.config.save('user') -} + output(message) + } -const adduser = async (args) => { - const { scope } = npm.flatOptions - const registry = getRegistry(npm.flatOptions) - const auth = getAuthType(npm.flatOptions) - const creds = npm.config.getCredentialsByURI(registry) + getRegistry ({ scope, registry }) { + if (scope) { + const scopedRegistry = this.npm.config.get(`${scope}:registry`) + const cliRegistry = this.npm.config.get('registry', 'cli') + if (scopedRegistry && !cliRegistry) + return scopedRegistry + } + return registry + } - log.disableProgress() + getAuthType ({ authType }) { + const type = authTypes[authType] - log.notice('', `Log in on ${replaceInfo(registry)}`) + if (!type) + throw new Error('no such auth module') - const { message, newCreds } = await auth({ - ...npm.flatOptions, - creds, - registry, - scope, - }) + return type + } - await updateConfig({ - newCreds, - registry, - scope, - }) + async updateConfig ({ newCreds, registry, scope }) { + this.npm.config.delete('_token', 'user') // prevent legacy pollution - output(message) -} + if (scope) + this.npm.config.set(scope + ':registry', registry, 'user') -module.exports = Object.assign(cmd, { usage }) + this.npm.config.setCredentialsByURI(registry, newCreds) + await this.npm.config.save('user') + } +} +module.exports = AddUser diff --git a/lib/audit.js b/lib/audit.js index 1b31401b1a6b0..b3c3b941dc00f 100644 --- a/lib/audit.js +++ b/lib/audit.js @@ -1,55 +1,64 @@ const Arborist = require('@npmcli/arborist') const auditReport = require('npm-audit-report') -const npm = require('./npm.js') const output = require('./utils/output.js') const reifyFinish = require('./utils/reify-finish.js') const auditError = require('./utils/audit-error.js') +const usageUtil = require('./utils/usage') -const audit = async args => { - const arb = new Arborist({ - ...npm.flatOptions, - audit: true, - path: npm.prefix, - }) - const fix = args[0] === 'fix' - await arb.audit({ fix }) - if (fix) - await reifyFinish(arb) - else { - // will throw if there's an error, because this is an audit command - auditError(arb.auditReport) - const reporter = npm.flatOptions.json ? 'json' : 'detail' - const result = auditReport(arb.auditReport, { - ...npm.flatOptions, - reporter, - }) - process.exitCode = process.exitCode || result.exitCode - output(result.report) +class Audit { + constructor (npm) { + this.npm = npm } -} -const cmd = (args, cb) => audit(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil( + 'audit', + 'npm audit [--json] [--production]' + + '\nnpm audit fix ' + + '[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]' + ) + } -const usageUtil = require('./utils/usage') -const usage = usageUtil( - 'audit', - 'npm audit [--json] [--production]' + - '\nnpm audit fix ' + - '[--force|--package-lock-only|--dry-run|--production|--only=(dev|prod)]' -) - -const completion = async (opts) => { - const argv = opts.conf.argv.remain - - if (argv.length === 2) - return ['fix'] - - switch (argv[2]) { - case 'fix': - return [] - default: - throw new Error(argv[2] + ' not recognized') + async completion (opts) { + const argv = opts.conf.argv.remain + + if (argv.length === 2) + return ['fix'] + + switch (argv[2]) { + case 'fix': + return [] + default: + throw new Error(argv[2] + ' not recognized') + } + } + + exec (args, cb) { + this.audit(args).then(() => cb()).catch(cb) + } + + async audit (args) { + const arb = new Arborist({ + ...this.npm.flatOptions, + audit: true, + path: this.npm.prefix, + }) + const fix = args[0] === 'fix' + await arb.audit({ fix }) + if (fix) + await reifyFinish(this.npm, arb) + else { + // will throw if there's an error, because this is an audit command + auditError(this.npm, arb.auditReport) + const reporter = this.npm.flatOptions.json ? 'json' : 'detail' + const result = auditReport(arb.auditReport, { + ...this.npm.flatOptions, + reporter, + }) + process.exitCode = process.exitCode || result.exitCode + output(result.report) + } } } -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = Audit diff --git a/lib/bin.js b/lib/bin.js index e627ce22f13a6..31a9f33b72f6a 100644 --- a/lib/bin.js +++ b/lib/bin.js @@ -1,13 +1,25 @@ -const npm = require('./npm.js') const output = require('./utils/output.js') +const envPath = require('./utils/path.js') const usageUtil = require('./utils/usage.js') -const PATH = require('./utils/path.js') -const cmd = (args, cb) => bin(args).then(() => cb()).catch(cb) -const usage = usageUtil('bin', 'npm bin [-g]') -const bin = async (args, cb) => { - const b = npm.bin - output(b) - if (npm.flatOptions.global && !PATH.includes(b)) - console.error('(not in PATH env variable)') + +class Bin { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('bin', 'npm bin [-g]') + } + + exec (args, cb) { + this.bin(args).then(() => cb()).catch(cb) + } + + async bin (args) { + const b = this.npm.bin + output(b) + if (this.npm.flatOptions.global && !envPath.includes(b)) + console.error('(not in PATH env variable)') + } } -module.exports = Object.assign(cmd, { usage }) +module.exports = Bin diff --git a/lib/birthday.js b/lib/birthday.js index 6c71a9e715668..5ea855512f9f6 100644 --- a/lib/birthday.js +++ b/lib/birthday.js @@ -1,11 +1,18 @@ -const npm = require('./npm.js') -module.exports = (_, cb) => { - Object.defineProperty(npm, 'flatOptions', { - value: { - ...npm.flatOptions, - package: ['@npmcli/npm-birthday'], - yes: true, - }, - }) - return npm.commands.exec(['npm-birthday'], cb) +class Birthday { + constructor (npm) { + this.npm = npm + Object.defineProperty(this.npm, 'flatOptions', { + value: { + ...npm.flatOptions, + package: ['@npmcli/npm-birthday'], + yes: true, + }, + }) + } + + exec (args, cb) { + return this.npm.commands.exec(['npm-birthday'], cb) + } } + +module.exports = Birthday diff --git a/lib/bugs.js b/lib/bugs.js index 09856313ce883..e8607f311d219 100644 --- a/lib/bugs.js +++ b/lib/bugs.js @@ -1,46 +1,54 @@ const log = require('npmlog') const pacote = require('pacote') -const { promisify } = require('util') -const openUrl = promisify(require('./utils/open-url.js')) +const openUrl = require('./utils/open-url.js') const usageUtil = require('./utils/usage.js') -const npm = require('./npm.js') const hostedFromMani = require('./utils/hosted-git-info-from-manifest.js') -const usage = usageUtil('bugs', 'npm bugs []') +class Bugs { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => bugs(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil('bugs', 'npm bugs []') + } -const bugs = async args => { - if (!args || !args.length) - args = ['.'] + exec (args, cb) { + this.bugs(args).then(() => cb()).catch(cb) + } - await Promise.all(args.map(pkg => getBugs(pkg))) -} + async bugs (args) { + if (!args || !args.length) + args = ['.'] -const getBugsUrl = mani => { - if (mani.bugs) { - if (typeof mani.bugs === 'string') - return mani.bugs + await Promise.all(args.map(pkg => this.getBugs(pkg))) + } - if (typeof mani.bugs === 'object' && mani.bugs.url) - return mani.bugs.url + async getBugs (pkg) { + const opts = { ...this.npm.flatOptions, fullMetadata: true } + const mani = await pacote.manifest(pkg, opts) + const url = this.getBugsUrl(mani) + log.silly('bugs', 'url', url) + await openUrl(this.npm, url, `${mani.name} bug list available at the following URL`) } - // try to get it from the repo, if possible - const info = hostedFromMani(mani) - if (info) - return info.bugs() + getBugsUrl (mani) { + if (mani.bugs) { + if (typeof mani.bugs === 'string') + return mani.bugs - // just send them to the website, hopefully that has some info! - return `https://www.npmjs.com/package/${mani.name}` -} + if (typeof mani.bugs === 'object' && mani.bugs.url) + return mani.bugs.url + } -const getBugs = async pkg => { - const opts = { ...npm.flatOptions, fullMetadata: true } - const mani = await pacote.manifest(pkg, opts) - const url = getBugsUrl(mani) - log.silly('bugs', 'url', url) - await openUrl(url, `${mani.name} bug list available at the following URL`) + // try to get it from the repo, if possible + const info = hostedFromMani(mani) + if (info) + return info.bugs() + + // just send them to the website, hopefully that has some info! + return `https://www.npmjs.com/package/${mani.name}` + } } -module.exports = Object.assign(cmd, { usage }) +module.exports = Bugs diff --git a/lib/cache.js b/lib/cache.js index 7b84353b4a19b..8469559764fb3 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -1,62 +1,69 @@ const cacache = require('cacache') const { promisify } = require('util') const log = require('npmlog') -const npm = require('./npm.js') const output = require('./utils/output.js') const pacote = require('pacote') const path = require('path') const rimraf = promisify(require('rimraf')) const usageUtil = require('./utils/usage.js') +class Cache { + constructor (npm) { + this.npm = npm + } -const usage = usageUtil('cache', - 'npm cache add ' + - '\nnpm cache add ' + - '\nnpm cache add ' + - '\nnpm cache add ' + - '\nnpm cache add @' + - '\nnpm cache clean' + - '\nnpm cache verify' -) - -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv.length === 2) - return ['add', 'clean', 'verify'] - - // TODO - eventually... - switch (argv[2]) { - case 'verify': - case 'clean': - case 'add': - return [] + get usage () { + return usageUtil('cache', + 'npm cache add ' + + '\nnpm cache add ' + + '\nnpm cache add ' + + '\nnpm cache add ' + + '\nnpm cache add @' + + '\nnpm cache clean' + + '\nnpm cache verify' + ) } -} -const cmd = (args, cb) => cache(args).then(() => cb()).catch(cb) - -const cache = async (args) => { - const cmd = args.shift() - switch (cmd) { - case 'rm': case 'clear': case 'clean': - return await clean(args) - case 'add': - return await add(args) - case 'verify': case 'check': - return await verify() - default: - throw Object.assign(new Error(usage), { code: 'EUSAGE' }) + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) + return ['add', 'clean', 'verify'] + + // TODO - eventually... + switch (argv[2]) { + case 'verify': + case 'clean': + case 'add': + return [] + } + } + + exec (args, cb) { + this.cache(args).then(() => cb()).catch(cb) + } + + async cache (args) { + const cmd = args.shift() + switch (cmd) { + case 'rm': case 'clear': case 'clean': + return await this.clean(args) + case 'add': + return await this.add(args) + case 'verify': case 'check': + return await this.verify() + default: + throw Object.assign(new Error(this.usage), { code: 'EUSAGE' }) + } } -} -// npm cache clean [pkg]* -const clean = async (args) => { - if (args.length) - throw new Error('npm cache clear does not accept arguments') + // npm cache clean [pkg]* + async clean (args) { + if (args.length) + throw new Error('npm cache clear does not accept arguments') - const cachePath = path.join(npm.cache, '_cacache') - if (!npm.flatOptions.force) { - throw new Error(`As of npm@5, the npm cache self-heals from corruption issues + const cachePath = path.join(this.npm.cache, '_cacache') + if (!this.npm.flatOptions.force) { + throw new Error(`As of npm@5, the npm cache self-heals from corruption issues by treating integrity mismatches as cache misses. As a result, data extracted from the cache is guaranteed to be valid. If you want to make sure everything is consistent, use \`npm cache verify\` @@ -70,52 +77,53 @@ temporary cache instead of nuking the actual one. If you're sure you want to delete the entire cache, rerun this command with --force.`) + } + return rimraf(cachePath) } - return rimraf(cachePath) -} -// npm cache add -// npm cache add -// npm cache add -// npm cache add -const add = async (args) => { - const usage = 'Usage:\n' + - ' npm cache add \n' + - ' npm cache add @\n' + - ' npm cache add \n' + - ' npm cache add \n' - log.silly('cache add', 'args', args) - const spec = args[0] && args[0] + - (args[1] === undefined || args[1] === null ? '' : `@${args[1]}`) - - if (!spec) - throw Object.assign(new Error(usage), { code: 'EUSAGE' }) - - log.silly('cache add', 'spec', spec) - const opts = { ...npm.flatOptions } - - // we ask pacote for the thing, and then just throw the data - // away so that it tee-pipes it into the cache like it does - // for a normal request. - await pacote.tarball.stream(spec, stream => { - stream.resume() - return stream.promise() - }, opts) -} + // npm cache add + // npm cache add + // npm cache add + // npm cache add + async add (args) { + const usage = 'Usage:\n' + + ' npm cache add \n' + + ' npm cache add @\n' + + ' npm cache add \n' + + ' npm cache add \n' + log.silly('cache add', 'args', args) + const spec = args[0] && args[0] + + (args[1] === undefined || args[1] === null ? '' : `@${args[1]}`) + + if (!spec) + throw Object.assign(new Error(usage), { code: 'EUSAGE' }) + + log.silly('cache add', 'spec', spec) + const opts = { ...this.npm.flatOptions } -const verify = async () => { - const cache = path.join(npm.cache, '_cacache') - const prefix = cache.indexOf(process.env.HOME) === 0 - ? `~${cache.substr(process.env.HOME.length)}` - : cache - const stats = await cacache.verify(cache) - output(`Cache verified and compressed (${prefix})`) - output(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`) - stats.badContentCount && output(`Corrupted content removed: ${stats.badContentCount}`) - stats.reclaimedCount && output(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`) - stats.missingContent && output(`Missing content: ${stats.missingContent}`) - output(`Index entries: ${stats.totalEntries}`) - output(`Finished in ${stats.runTime.total / 1000}s`) + // we ask pacote for the thing, and then just throw the data + // away so that it tee-pipes it into the cache like it does + // for a normal request. + await pacote.tarball.stream(spec, stream => { + stream.resume() + return stream.promise() + }, opts) + } + + async verify () { + const cache = path.join(this.npm.cache, '_cacache') + const prefix = cache.indexOf(process.env.HOME) === 0 + ? `~${cache.substr(process.env.HOME.length)}` + : cache + const stats = await cacache.verify(cache) + output(`Cache verified and compressed (${prefix})`) + output(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`) + stats.badContentCount && output(`Corrupted content removed: ${stats.badContentCount}`) + stats.reclaimedCount && output(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`) + stats.missingContent && output(`Missing content: ${stats.missingContent}`) + output(`Index entries: ${stats.totalEntries}`) + output(`Finished in ${stats.runTime.total / 1000}s`) + } } -module.exports = Object.assign(cmd, { completion, usage }) +module.exports = Cache diff --git a/lib/ci.js b/lib/ci.js index 51c165accef7a..7a3b77d8281ce 100644 --- a/lib/ci.js +++ b/lib/ci.js @@ -7,13 +7,8 @@ const fs = require('fs') const readdir = util.promisify(fs.readdir) const log = require('npmlog') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil('ci', 'npm ci') - -const cmd = (args, cb) => ci().then(() => cb()).catch(cb) - const removeNodeModules = async where => { const rimrafOpts = { glob: false } process.emit('time', 'npm-ci:rm') @@ -24,55 +19,69 @@ const removeNodeModules = async where => { process.emit('timeEnd', 'npm-ci:rm') } -const ci = async () => { - if (npm.flatOptions.global) { - const err = new Error('`npm ci` does not work for global packages') - err.code = 'ECIGLOBAL' - throw err +class CI { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('ci', 'npm ci') } - const where = npm.prefix - const { scriptShell, ignoreScripts } = npm.flatOptions - const arb = new Arborist({ ...npm.flatOptions, path: where }) + exec (args, cb) { + this.ci().then(() => cb()).catch(cb) + } + + async ci () { + if (this.npm.flatOptions.global) { + const err = new Error('`npm ci` does not work for global packages') + err.code = 'ECIGLOBAL' + throw err + } + + const where = this.npm.prefix + const { scriptShell, ignoreScripts } = this.npm.flatOptions + const arb = new Arborist({ ...this.npm.flatOptions, path: where }) - await Promise.all([ - arb.loadVirtual().catch(er => { - log.verbose('loadVirtual', er.stack) - const msg = - 'The `npm ci` command can only install with an existing package-lock.json or\n' + - 'npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or\n' + - 'later to generate a package-lock.json file, then try again.' - throw new Error(msg) - }), - removeNodeModules(where), - ]) - // npm ci should never modify the lockfile or package.json - await arb.reify({ ...npm.flatOptions, save: false }) + await Promise.all([ + arb.loadVirtual().catch(er => { + log.verbose('loadVirtual', er.stack) + const msg = + 'The `npm ci` command can only install with an existing package-lock.json or\n' + + 'npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or\n' + + 'later to generate a package-lock.json file, then try again.' + throw new Error(msg) + }), + removeNodeModules(where), + ]) + // npm ci should never modify the lockfile or package.json + await arb.reify({ ...this.npm.flatOptions, save: false }) - // run the same set of scripts that `npm install` runs. - if (!ignoreScripts) { - const scripts = [ - 'preinstall', - 'install', - 'postinstall', - 'prepublish', // XXX should we remove this finally?? - 'preprepare', - 'prepare', - 'postprepare', - ] - for (const event of scripts) { - await runScript({ - path: where, - args: [], - scriptShell, - stdio: 'inherit', - stdioString: true, - banner: log.level !== 'silent', - event, - }) + // run the same set of scripts that `npm install` runs. + if (!ignoreScripts) { + const scripts = [ + 'preinstall', + 'install', + 'postinstall', + 'prepublish', // XXX should we remove this finally?? + 'preprepare', + 'prepare', + 'postprepare', + ] + for (const event of scripts) { + await runScript({ + path: where, + args: [], + scriptShell, + stdio: 'inherit', + stdioString: true, + banner: log.level !== 'silent', + event, + }) + } } + await reifyFinish(arb) } - await reifyFinish(arb) } -module.exports = Object.assign(cmd, {usage}) +module.exports = CI diff --git a/lib/completion.js b/lib/completion.js index b31867d988a69..13fd5f1a41c21 100644 --- a/lib/completion.js +++ b/lib/completion.js @@ -29,7 +29,6 @@ // as an array. // -const npm = require('./npm.js') const { types, shorthands } = require('./utils/config.js') const deref = require('./utils/deref-command.js') const { aliases, cmdList, plumbing } = require('./utils/cmd-list.js') @@ -44,115 +43,126 @@ const output = require('./utils/output.js') const fileExists = require('./utils/file-exists.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil('completion', 'source <(npm completion)') const { promisify } = require('util') -const cmd = (args, cb) => compl(args).then(() => cb()).catch(cb) +class Completion { + constructor (npm) { + this.npm = npm + } -// completion for the completion command -const completion = async (opts) => { - if (opts.w > 2) - return + get usage () { + return usageUtil('completion', 'source <(npm completion)') + } - const { resolve } = require('path') - const [bashExists, zshExists] = await Promise.all([ - fileExists(resolve(process.env.HOME, '.bashrc')), - fileExists(resolve(process.env.HOME, '.zshrc')), - ]) - const out = [] - if (zshExists) - out.push(['>>', '~/.zshrc']) - - if (bashExists) - out.push(['>>', '~/.bashrc']) - - return out -} + // completion for the completion command + async completion (opts) { + if (opts.w > 2) + return -const compl = async args => { - if (isWindowsShell) { - const msg = 'npm completion supported only in MINGW / Git bash on Windows' - throw Object.assign(new Error(msg), { - code: 'ENOTSUP', - }) + const { resolve } = require('path') + const [bashExists, zshExists] = await Promise.all([ + fileExists(resolve(process.env.HOME, '.bashrc')), + fileExists(resolve(process.env.HOME, '.zshrc')), + ]) + const out = [] + if (zshExists) + out.push(['>>', '~/.zshrc']) + + if (bashExists) + out.push(['>>', '~/.bashrc']) + + return out } - const { COMP_CWORD, COMP_LINE, COMP_POINT } = process.env + exec (args, cb) { + this.compl(args).then(() => cb()).catch(cb) + } - // if the COMP_* isn't in the env, then just dump the script. - if (COMP_CWORD === undefined || + async compl (args) { + if (isWindowsShell) { + const msg = 'npm completion supported only in MINGW / Git bash on Windows' + throw Object.assign(new Error(msg), { + code: 'ENOTSUP', + }) + } + + const { COMP_CWORD, COMP_LINE, COMP_POINT } = process.env + + // if the COMP_* isn't in the env, then just dump the script. + if (COMP_CWORD === undefined || COMP_LINE === undefined || COMP_POINT === undefined) - return dumpScript() - - // ok we're actually looking at the envs and outputting the suggestions - // get the partial line and partial word, - // if the point isn't at the end. - // ie, tabbing at: npm foo b|ar - const w = +COMP_CWORD - const words = args.map(unescape) - const word = words[w] - const line = COMP_LINE - const point = +COMP_POINT - const partialLine = line.substr(0, point) - const partialWords = words.slice(0, w) - - // figure out where in that last word the point is. - const partialWordRaw = args[w] - let i = partialWordRaw.length - while (partialWordRaw.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) - i-- - - const partialWord = unescape(partialWordRaw.substr(0, i)) - partialWords.push(partialWord) - - const opts = { - words, - w, - word, - line, - lineLength: line.length, - point, - partialLine, - partialWords, - partialWord, - raw: args, - } + return dumpScript() + + // ok we're actually looking at the envs and outputting the suggestions + // get the partial line and partial word, + // if the point isn't at the end. + // ie, tabbing at: npm foo b|ar + const w = +COMP_CWORD + const words = args.map(unescape) + const word = words[w] + const line = COMP_LINE + const point = +COMP_POINT + const partialLine = line.substr(0, point) + const partialWords = words.slice(0, w) + + // figure out where in that last word the point is. + const partialWordRaw = args[w] + let i = partialWordRaw.length + while (partialWordRaw.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) + i-- + + const partialWord = unescape(partialWordRaw.substr(0, i)) + partialWords.push(partialWord) + + const opts = { + words, + w, + word, + line, + lineLength: line.length, + point, + partialLine, + partialWords, + partialWord, + raw: args, + } - if (partialWords.slice(0, -1).indexOf('--') === -1) { - if (word.charAt(0) === '-') - return wrap(opts, configCompl(opts)) + if (partialWords.slice(0, -1).indexOf('--') === -1) { + if (word.charAt(0) === '-') + return wrap(opts, configCompl(opts)) - if (words[w - 1] && + if (words[w - 1] && words[w - 1].charAt(0) === '-' && !isFlag(words[w - 1])) { - // awaiting a value for a non-bool config. - // don't even try to do this for now - return wrap(opts, configValueCompl(opts)) + // awaiting a value for a non-bool config. + // don't even try to do this for now + return wrap(opts, configValueCompl(opts)) + } } - } - // try to find the npm command. - // it's the first thing after all the configs. - // take a little shortcut and use npm's arg parsing logic. - // don't have to worry about the last arg being implicitly - // boolean'ed, since the last block will catch that. - const parsed = opts.conf = - nopt(types, shorthands, partialWords.slice(0, -1), 0) - // check if there's a command already. - const cmd = parsed.argv.remain[1] - if (!cmd) - return wrap(opts, cmdCompl(opts)) - - Object.keys(parsed).forEach(k => npm.config.set(k, parsed[k])) - - // at this point, if words[1] is some kind of npm command, - // then complete on it. - // otherwise, do nothing - const impl = npm.commands[cmd] - if (impl && impl.completion) { - const comps = await impl.completion(opts) - return wrap(opts, comps) + // try to find the npm command. + // it's the first thing after all the configs. + // take a little shortcut and use npm's arg parsing logic. + // don't have to worry about the last arg being implicitly + // boolean'ed, since the last block will catch that. + const parsed = opts.conf = + nopt(types, shorthands, partialWords.slice(0, -1), 0) + // check if there's a command already. + const cmd = parsed.argv.remain[1] + if (!cmd) + return wrap(opts, cmdCompl(opts)) + + Object.keys(parsed).forEach(k => this.npm.config.set(k, parsed[k])) + + // at this point, if words[1] is some kind of npm command, + // then complete on it. + // otherwise, do nothing + const impl = this.npm.commands[cmd] + if (impl && impl.completion) { + const comps = await impl.completion(opts) + return wrap(opts, comps) + } } } @@ -266,4 +276,4 @@ const cmdCompl = opts => { return fullList } -module.exports = Object.assign(cmd, { completion, usage }) +module.exports = Completion diff --git a/lib/config.js b/lib/config.js index e4da296de8f88..54462ee712b78 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,4 +1,3 @@ -const npm = require('./npm.js') const { defaults, types } = require('./utils/config.js') const usageUtil = require('./utils/usage.js') const output = require('./utils/output.js') @@ -13,165 +12,156 @@ const { spawn } = require('child_process') const { EOL } = require('os') const ini = require('ini') -const usage = usageUtil( - 'config', - 'npm config set = [= ...]' + - '\nnpm config get [ [ ...]]' + - '\nnpm config delete [ ...]' + - '\nnpm config list [--json]' + - '\nnpm config edit' + - '\nnpm set = [= ...]' + - '\nnpm get [ [ ...]]' -) - -const cmd = (args, cb) => config(args).then(() => cb()).catch(cb) - -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv[1] !== 'config') - argv.unshift('config') - - if (argv.length === 2) { - const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit'] - if (opts.partialWord !== 'l') - cmds.push('list') - - return cmds +class Config { + constructor (npm) { + this.npm = npm } - const action = argv[2] - switch (action) { - case 'set': - // todo: complete with valid values, if possible. - if (argv.length > 3) - return [] - - // fallthrough - /* eslint no-fallthrough:0 */ - case 'get': - case 'delete': - case 'rm': - return Object.keys(types) - case 'edit': - case 'list': - case 'ls': - default: - return [] + get usage () { + return usageUtil( + 'config', + 'npm config set = [= ...]' + + '\nnpm config get [ [ ...]]' + + '\nnpm config delete [ ...]' + + '\nnpm config list [--json]' + + '\nnpm config edit' + + '\nnpm set = [= ...]' + + '\nnpm get [ [ ...]]' + ) } -} -const UsageError = () => - Object.assign(new Error(usage), { code: 'EUSAGE' }) + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv[1] !== 'config') + argv.unshift('config') -const config = async ([action, ...args]) => { - npm.log.disableProgress() - try { + if (argv.length === 2) { + const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit'] + if (opts.partialWord !== 'l') + cmds.push('list') + + return cmds + } + + const action = argv[2] switch (action) { case 'set': - await set(args) - break + // todo: complete with valid values, if possible. + if (argv.length > 3) + return [] + + // fallthrough + /* eslint no-fallthrough:0 */ case 'get': - await get(args) - break case 'delete': case 'rm': - case 'del': - await del(args) - break + return Object.keys(types) + case 'edit': case 'list': case 'ls': - await (npm.flatOptions.json ? listJson() : list()) - break - case 'edit': - await edit() - break default: - throw UsageError() + return [] } - } finally { - npm.log.enableProgress() } -} -// take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into -// { key: value, k2: v2, k3: v3 } -const keyValues = args => { - const kv = {} - for (let i = 0; i < args.length; i++) { - const arg = args[i].split('=') - const key = arg.shift() - const val = arg.length ? arg.join('=') - : i < args.length - 1 ? args[++i] - : '' - kv[key.trim()] = val.trim() + exec (args, cb) { + this.config(args).then(() => cb()).catch(cb) } - return kv -} - -const set = async (args) => { - if (!args.length) - throw UsageError() - const where = npm.flatOptions.global ? 'global' : 'user' - for (const [key, val] of Object.entries(keyValues(args))) { - npm.log.info('config', 'set %j %j', key, val) - npm.config.set(key, val || '', where) - if (!npm.config.validate(where)) - npm.log.warn('config', 'omitting invalid config values') + async config ([action, ...args]) { + this.npm.log.disableProgress() + try { + switch (action) { + case 'set': + await this.set(args) + break + case 'get': + await this.get(args) + break + case 'delete': + case 'rm': + case 'del': + await this.del(args) + break + case 'list': + case 'ls': + await (this.npm.flatOptions.json ? this.listJson() : this.list()) + break + case 'edit': + await this.edit() + break + default: + throw this.usageError() + } + } finally { + this.npm.log.enableProgress() + } } - await npm.config.save(where) -} + async set (args) { + if (!args.length) + throw this.usageError() + + const where = this.npm.flatOptions.global ? 'global' : 'user' + for (const [key, val] of Object.entries(keyValues(args))) { + this.npm.log.info('config', 'set %j %j', key, val) + this.npm.config.set(key, val || '', where) + if (!this.npm.config.validate(where)) + this.npm.log.warn('config', 'omitting invalid config values') + } + + await this.npm.config.save(where) + } -const get = async keys => { - if (!keys.length) - return list() + async get (keys) { + if (!keys.length) + return this.list() - const out = [] - for (const key of keys) { - if (!publicVar(key)) - throw `The ${key} option is protected, and cannot be retrieved in this way` + const out = [] + for (const key of keys) { + if (!publicVar(key)) + throw `The ${key} option is protected, and cannot be retrieved in this way` - const pref = keys.length > 1 ? `${key}=` : '' - out.push(pref + npm.config.get(key)) + const pref = keys.length > 1 ? `${key}=` : '' + out.push(pref + this.npm.config.get(key)) + } + output(out.join('\n')) } - output(out.join('\n')) -} -const del = async keys => { - if (!keys.length) - throw UsageError() + async del (keys) { + if (!keys.length) + throw this.usageError() - const where = npm.flatOptions.global ? 'global' : 'user' - for (const key of keys) - npm.config.delete(key, where) - await npm.config.save(where) -} + const where = this.npm.flatOptions.global ? 'global' : 'user' + for (const key of keys) + this.npm.config.delete(key, where) + await this.npm.config.save(where) + } -const edit = async () => { - const { editor: e, global } = npm.flatOptions - const where = global ? 'global' : 'user' - const file = npm.config.data.get(where).source - - // save first, just to make sure it's synced up - // this also removes all the comments from the last time we edited it. - await npm.config.save(where) - - const data = ( - await readFile(file, 'utf8').catch(() => '') - ).replace(/\r\n/g, '\n') - const defData = Object.entries(defaults).reduce((str, [key, val]) => { - const obj = { [key]: val } - const i = ini.stringify(obj) - .replace(/\r\n/g, '\n') // normalizes output from ini.stringify - .replace(/\n$/m, '') - .replace(/^/g, '; ') - .replace(/\n/g, '\n; ') - .split('\n') - return str + '\n' + i - }, '') - - const tmpData = `;;;; + async edit () { + const { editor: e, global } = this.npm.flatOptions + const where = global ? 'global' : 'user' + const file = this.npm.config.data.get(where).source + + // save first, just to make sure it's synced up + // this also removes all the comments from the last time we edited it. + await this.npm.config.save(where) + + const data = ( + await readFile(file, 'utf8').catch(() => '') + ).replace(/\r\n/g, '\n') + const defData = Object.entries(defaults).reduce((str, [key, val]) => { + const obj = { [key]: val } + const i = ini.stringify(obj) + .replace(/\r\n/g, '\n') // normalizes output from ini.stringify + .replace(/\n$/m, '') + .replace(/^/g, '; ') + .replace(/\n/g, '\n; ') + .split('\n') + return str + '\n' + i + }, '') + + const tmpData = `;;;; ; npm ${where}config file: ${file} ; this is a simple ini-formatted file ; lines that start with semi-colons are comments @@ -190,64 +180,84 @@ ${data.split('\n').sort((a, b) => a.localeCompare(b)).join('\n').trim()} ${defData} `.split('\n').join(EOL) - await mkdirp(dirname(file)) - await writeFile(file, tmpData, 'utf8') - await new Promise((resolve, reject) => { - const [bin, ...args] = e.split(/\s+/) - const editor = spawn(bin, [...args, file], { stdio: 'inherit' }) - editor.on('exit', (code) => { - if (code) - return reject(new Error(`editor process exited with code: ${code}`)) - return resolve() + await mkdirp(dirname(file)) + await writeFile(file, tmpData, 'utf8') + await new Promise((resolve, reject) => { + const [bin, ...args] = e.split(/\s+/) + const editor = spawn(bin, [...args, file], { stdio: 'inherit' }) + editor.on('exit', (code) => { + if (code) + return reject(new Error(`editor process exited with code: ${code}`)) + return resolve() + }) }) - }) -} - -const publicVar = k => !/^(\/\/[^:]+:)?_/.test(k) + } -const list = async () => { - const msg = [] - const { long } = npm.flatOptions - for (const [where, { data, source }] of npm.config.data.entries()) { - if (where === 'default' && !long) - continue + async list () { + const msg = [] + const { long } = this.npm.flatOptions + for (const [where, { data, source }] of this.npm.config.data.entries()) { + if (where === 'default' && !long) + continue + + const keys = Object.keys(data).sort((a, b) => a.localeCompare(b)) + if (!keys.length) + continue + + msg.push(`; "${where}" config from ${source}`, '') + for (const k of keys) { + const v = publicVar(k) ? JSON.stringify(data[k]) : '(protected)' + const src = this.npm.config.find(k) + const overridden = src !== where + msg.push((overridden ? '; ' : '') + + `${k} = ${v} ${overridden ? `; overridden by ${src}` : ''}`) + } + msg.push('') + } - const keys = Object.keys(data).sort((a, b) => a.localeCompare(b)) - if (!keys.length) - continue - - msg.push(`; "${where}" config from ${source}`, '') - for (const k of keys) { - const v = publicVar(k) ? JSON.stringify(data[k]) : '(protected)' - const src = npm.config.find(k) - const overridden = src !== where - msg.push((overridden ? '; ' : '') + - `${k} = ${v} ${overridden ? `; overridden by ${src}` : ''}`) + if (!long) { + msg.push( + `; node bin location = ${process.execPath}`, + `; cwd = ${process.cwd()}`, + `; HOME = ${process.env.HOME}`, + '; Run `npm config ls -l` to show all defaults.' + ) } - msg.push('') + + output(msg.join('\n').trim()) } - if (!long) { - msg.push( - `; node bin location = ${process.execPath}`, - `; cwd = ${process.cwd()}`, - `; HOME = ${process.env.HOME}`, - '; Run `npm config ls -l` to show all defaults.' - ) + async listJson () { + const publicConf = {} + for (const key in this.npm.config.list[0]) { + if (!publicVar(key)) + continue + + publicConf[key] = this.npm.config.get(key) + } + output(JSON.stringify(publicConf, null, 2)) } - output(msg.join('\n').trim()) + usageError () { + return Object.assign(new Error(this.usage), { code: 'EUSAGE' }) + } } -const listJson = async () => { - const publicConf = {} - for (const key in npm.config.list[0]) { - if (!publicVar(key)) - continue - - publicConf[key] = npm.config.get(key) +// take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into +// { key: value, k2: v2, k3: v3 } +const keyValues = args => { + const kv = {} + for (let i = 0; i < args.length; i++) { + const arg = args[i].split('=') + const key = arg.shift() + const val = arg.length ? arg.join('=') + : i < args.length - 1 ? args[++i] + : '' + kv[key.trim()] = val.trim() } - output(JSON.stringify(publicConf, null, 2)) + return kv } -module.exports = Object.assign(cmd, { usage, completion }) +const publicVar = k => !/^(\/\/[^:]+:)?_/.test(k) + +module.exports = Config diff --git a/lib/dedupe.js b/lib/dedupe.js index 2211fcac8b481..f4abf48926b86 100644 --- a/lib/dedupe.js +++ b/lib/dedupe.js @@ -1,29 +1,38 @@ // dedupe duplicated packages, or find them in the tree -const npm = require('./npm.js') const Arborist = require('@npmcli/arborist') const usageUtil = require('./utils/usage.js') const reifyFinish = require('./utils/reify-finish.js') -const usage = usageUtil('dedupe', 'npm dedupe') +class Dedupe { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => dedupe(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil('dedupe', 'npm dedupe') + } -const dedupe = async (args) => { - if (npm.flatOptions.global) { - const er = new Error('`npm dedupe` does not work in global mode.') - er.code = 'EDEDUPEGLOBAL' - throw er + exec (args, cb) { + this.dedupe(args).then(() => cb()).catch(cb) } - const dryRun = (args && args.dryRun) || npm.flatOptions.dryRun - const where = npm.prefix - const arb = new Arborist({ - ...npm.flatOptions, - path: where, - dryRun, - }) - await arb.dedupe(npm.flatOptions) - await reifyFinish(arb) + async dedupe (args) { + if (this.npm.flatOptions.global) { + const er = new Error('`npm dedupe` does not work in global mode.') + er.code = 'EDEDUPEGLOBAL' + throw er + } + + const dryRun = (args && args.dryRun) || this.npm.flatOptions.dryRun + const where = this.npm.prefix + const arb = new Arborist({ + ...this.npm.flatOptions, + path: where, + dryRun, + }) + await arb.dedupe(this.npm.flatOptions) + await reifyFinish(arb) + } } -module.exports = Object.assign(cmd, { usage }) +module.exports = Dedupe diff --git a/lib/deprecate.js b/lib/deprecate.js index 42d099b544e31..6de9cefe3f2d0 100644 --- a/lib/deprecate.js +++ b/lib/deprecate.js @@ -1,4 +1,3 @@ -const npm = require('./npm.js') const fetch = require('npm-registry-fetch') const otplease = require('./utils/otplease.js') const npa = require('npm-package-arg') @@ -7,67 +6,77 @@ const getIdentity = require('./utils/get-identity.js') const libaccess = require('libnpmaccess') const usageUtil = require('./utils/usage.js') -const UsageError = () => - Object.assign(new Error(`\nUsage: ${usage}`), { - code: 'EUSAGE', - }) +class Deprecate { + constructor (npm) { + this.npm = npm + } -const usage = usageUtil( - 'deprecate', - 'npm deprecate [@] ' -) + get usage () { + return usageUtil( + 'deprecate', + 'npm deprecate [@] ' + ) + } -const completion = async (opts) => { - if (opts.conf.argv.remain.length > 1) - return [] + async completion (opts) { + if (opts.conf.argv.remain.length > 1) + return [] - const username = await getIdentity(npm.flatOptions) - const packages = await libaccess.lsPackages(username, npm.flatOptions) - return Object.keys(packages) - .filter((name) => - packages[name] === 'write' && - (opts.conf.argv.remain.length === 0 || - name.startsWith(opts.conf.argv.remain[0]))) -} - -const cmd = (args, cb) => - deprecate(args) - .then(() => cb()) - .catch(err => cb(err.code === 'EUSAGE' ? err.message : err)) + const username = await getIdentity(this.npm.flatOptions) + const packages = await libaccess.lsPackages(username, this.npm.flatOptions) + return Object.keys(packages) + .filter((name) => + packages[name] === 'write' && + (opts.conf.argv.remain.length === 0 || + name.startsWith(opts.conf.argv.remain[0]))) + } -const deprecate = async ([pkg, msg]) => { - if (!pkg || !msg) - throw UsageError() + exec (args, cb) { + this.deprecate(args) + .then(() => cb()) + .catch(err => cb(err.code === 'EUSAGE' ? err.message : err)) + } - // fetch the data and make sure it exists. - const p = npa(pkg) - // npa makes the default spec "latest", but for deprecation - // "*" is the appropriate default. - const spec = p.rawSpec === '' ? '*' : p.fetchSpec + async deprecate ([pkg, msg]) { + if (!pkg || !msg) + throw this.usageError() - if (semver.validRange(spec, true) === null) - throw new Error(`invalid version range: ${spec}`) + // fetch the data and make sure it exists. + const p = npa(pkg) + // npa makes the default spec "latest", but for deprecation + // "*" is the appropriate default. + const spec = p.rawSpec === '' ? '*' : p.fetchSpec - const uri = '/' + p.escapedName - const packument = await fetch.json(uri, { - ...npm.flatOptions, - spec: p, - query: { write: true }, - }) + if (semver.validRange(spec, true) === null) + throw new Error(`invalid version range: ${spec}`) - Object.keys(packument.versions) - .filter(v => semver.satisfies(v, spec, { includePrerelease: true })) - .forEach(v => { - packument.versions[v].deprecated = msg + const uri = '/' + p.escapedName + const packument = await fetch.json(uri, { + ...this.npm.flatOptions, + spec: p, + query: { write: true }, }) - return otplease(npm.flatOptions, opts => fetch(uri, { - ...opts, - spec: p, - method: 'PUT', - body: packument, - ignoreBody: true, - })) + Object.keys(packument.versions) + .filter(v => semver.satisfies(v, spec, { includePrerelease: true })) + .forEach(v => { + packument.versions[v].deprecated = msg + }) + + return otplease(this.npm.flatOptions, opts => fetch(uri, { + ...opts, + spec: p, + method: 'PUT', + body: packument, + ignoreBody: true, + })) + } + + usageError () { + return Object.assign(new Error(`\nUsage: ${this.usage}`), { + code: 'EUSAGE', + }) + } } -module.exports = Object.assign(cmd, { completion, usage }) +module.exports = Deprecate diff --git a/lib/diff.js b/lib/diff.js index 9ef5a78a20ce9..ea0340a4909d2 100644 --- a/lib/diff.js +++ b/lib/diff.js @@ -8,258 +8,270 @@ const npmlog = require('npmlog') const pacote = require('pacote') const pickManifest = require('npm-pick-manifest') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const output = require('./utils/output.js') const readLocalPkg = require('./utils/read-local-package.js') -const usage = usageUtil( - 'diff', - 'npm diff [...]' + - '\nnpm diff --diff= [...]' + - '\nnpm diff --diff= [--diff=] [...]' + - '\nnpm diff --diff= [--diff=] [...]' + - '\nnpm diff [--diff-ignore-all-space] [--diff-name-only] [...] [...]' -) - -const cmd = (args, cb) => diff(args).then(() => cb()).catch(cb) - -const where = () => { - const globalTop = resolve(npm.globalDir, '..') - const { global } = npm.flatOptions - return global ? globalTop : npm.prefix -} +class Diff { + constructor (npm) { + this.npm = npm + } -const diff = async (args) => { - const specs = npm.flatOptions.diff.filter(d => d) - if (specs.length > 2) { - throw new TypeError( - 'Can\'t use more than two --diff arguments.\n\n' + - `Usage:\n${usage}` + get usage () { + return usageUtil( + 'diff', + 'npm diff [...]' + + '\nnpm diff --diff= [...]' + + '\nnpm diff --diff= [--diff=] [...]' + + '\nnpm diff --diff= [--diff=] [...]' + + '\nnpm diff [--diff-ignore-all-space] [--diff-name-only] [...] [...]' ) } - const [a, b] = await retrieveSpecs(specs) - npmlog.info('diff', { src: a, dst: b }) - - const res = await libdiff([a, b], { ...npm.flatOptions, diffFiles: args }) - return output(res) -} - -const retrieveSpecs = ([a, b]) => { - // no arguments, defaults to comparing cwd - // to its latest published registry version - if (!a) - return defaultSpec() + get where () { + const globalTop = resolve(this.npm.globalDir, '..') + const { global } = this.npm.flatOptions + return global ? globalTop : this.npm.prefix + } - // single argument, used to compare wanted versions of an - // installed dependency or to compare the cwd to a published version - if (!b) - return transformSingleSpec(a) + exec (args, cb) { + this.diff(args).then(() => cb()).catch(cb) + } - return convertVersionsToSpecs([a, b]) - .then(findVersionsByPackageName) -} + async diff (args) { + const specs = this.npm.flatOptions.diff.filter(d => d) + if (specs.length > 2) { + throw new TypeError( + 'Can\'t use more than two --diff arguments.\n\n' + + `Usage:\n${this.usage}` + ) + } -const defaultSpec = async () => { - let noPackageJson - let pkgName - try { - pkgName = await readLocalPkg() - } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') - noPackageJson = true - } + const [a, b] = await this.retrieveSpecs(specs) + npmlog.info('diff', { src: a, dst: b }) - if (!pkgName || noPackageJson) { - throw new Error( - 'Needs multiple arguments to compare or run from a project dir.\n\n' + - `Usage:\n${usage}` + const res = await libdiff( + [a, b], + { ...this.npm.flatOptions, diffFiles: args } ) + return output(res) } - return [ - `${pkgName}@${npm.flatOptions.defaultTag}`, - `file:${npm.prefix}`, - ] -} + async retrieveSpecs ([a, b]) { + // no arguments, defaults to comparing cwd + // to its latest published registry version + if (!a) + return this.defaultSpec() -const transformSingleSpec = async (a) => { - let noPackageJson - let pkgName - try { - pkgName = await readLocalPkg() - } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') - noPackageJson = true - } - const missingPackageJson = new Error( - 'Needs multiple arguments to compare or run from a project dir.\n\n' + - `Usage:\n${usage}` - ) + // single argument, used to compare wanted versions of an + // installed dependency or to compare the cwd to a published version + if (!b) + return this.transformSingleSpec(a) - const specSelf = () => { - if (noPackageJson) - throw missingPackageJson - - return `file:${npm.prefix}` + const specs = await this.convertVersionsToSpecs([a, b]) + return this.findVersionsByPackageName(specs) } - // using a valid semver range, that means it should just diff - // the cwd against a published version to the registry using the - // same project name and the provided semver range - if (semver.validRange(a)) { - if (!pkgName) - throw missingPackageJson + async defaultSpec () { + let noPackageJson + let pkgName + try { + pkgName = await readLocalPkg(this.npm) + } catch (e) { + npmlog.verbose('diff', 'could not read project dir package.json') + noPackageJson = true + } + + if (!pkgName || noPackageJson) { + throw new Error( + 'Needs multiple arguments to compare or run from a project dir.\n\n' + + `Usage:\n${this.usage}` + ) + } return [ - `${pkgName}@${a}`, - specSelf(), + `${pkgName}@${this.npm.flatOptions.defaultTag}`, + `file:${this.npm.prefix}`, ] } - // when using a single package name as arg and it's part of the current - // install tree, then retrieve the current installed version and compare - // it against the same value `npm outdated` would suggest you to update to - const spec = npa(a) - if (spec.registry) { - let actualTree - let node + async transformSingleSpec (a) { + let noPackageJson + let pkgName try { - const opts = { - ...npm.flatOptions, - path: where(), - } - const arb = new Arborist(opts) - actualTree = await arb.loadActual(opts) - node = actualTree && - actualTree.inventory.query('name', spec.name) - .values().next().value + pkgName = await readLocalPkg(this.npm) } catch (e) { - npmlog.verbose('diff', 'failed to load actual install tree') + npmlog.verbose('diff', 'could not read project dir package.json') + noPackageJson = true + } + const missingPackageJson = new Error( + 'Needs multiple arguments to compare or run from a project dir.\n\n' + + `Usage:\n${this.usage}` + ) + + const specSelf = () => { + if (noPackageJson) + throw missingPackageJson + + return `file:${this.npm.prefix}` } - if (!node || !node.name || !node.package || !node.package.version) { + // using a valid semver range, that means it should just diff + // the cwd against a published version to the registry using the + // same project name and the provided semver range + if (semver.validRange(a)) { + if (!pkgName) + throw missingPackageJson + return [ - `${spec.name}@${spec.fetchSpec}`, + `${pkgName}@${a}`, specSelf(), ] } - const tryRootNodeSpec = () => - (actualTree && actualTree.edgesOut.get(spec.name) || {}).spec - - const tryAnySpec = () => { - for (const edge of node.edgesIn) - return edge.spec - } + // when using a single package name as arg and it's part of the current + // install tree, then retrieve the current installed version and compare + // it against the same value `npm outdated` would suggest you to update to + const spec = npa(a) + if (spec.registry) { + let actualTree + let node + try { + const opts = { + ...this.npm.flatOptions, + path: this.where, + } + const arb = new Arborist(opts) + actualTree = await arb.loadActual(opts) + node = actualTree && + actualTree.inventory.query('name', spec.name) + .values().next().value + } catch (e) { + npmlog.verbose('diff', 'failed to load actual install tree') + } - const aSpec = `file:${node.realpath}` - - // finds what version of the package to compare against, if a exact - // version or tag was passed than it should use that, otherwise - // work from the top of the arborist tree to find the original semver - // range declared in the package that depends on the package. - let bSpec - if (spec.rawSpec) - bSpec = spec.rawSpec - else { - const bTargetVersion = - tryRootNodeSpec() - || tryAnySpec() - - // figure out what to compare against, - // follows same logic to npm outdated "Wanted" results - const packument = await pacote.packument(spec, { - ...npm.flatOptions, - preferOnline: true, - }) - bSpec = pickManifest( - packument, - bTargetVersion, - { ...npm.flatOptions } - ).version - } + if (!node || !node.name || !node.package || !node.package.version) { + return [ + `${spec.name}@${spec.fetchSpec}`, + specSelf(), + ] + } - return [ - `${spec.name}@${aSpec}`, - `${spec.name}@${bSpec}`, - ] - } else if (spec.type === 'directory') { - return [ - `file:${spec.fetchSpec}`, - specSelf(), - ] - } else { - throw new Error( - 'Spec type not supported.\n\n' + - `Usage:\n${usage}` - ) - } -} + const tryRootNodeSpec = () => + (actualTree && actualTree.edgesOut.get(spec.name) || {}).spec -const convertVersionsToSpecs = async ([a, b]) => { - const semverA = semver.validRange(a) - const semverB = semver.validRange(b) + const tryAnySpec = () => { + for (const edge of node.edgesIn) + return edge.spec + } - // both specs are semver versions, assume current project dir name - if (semverA && semverB) { - let pkgName - try { - pkgName = await readLocalPkg() - } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') - } + const aSpec = `file:${node.realpath}` + + // finds what version of the package to compare against, if a exact + // version or tag was passed than it should use that, otherwise + // work from the top of the arborist tree to find the original semver + // range declared in the package that depends on the package. + let bSpec + if (spec.rawSpec) + bSpec = spec.rawSpec + else { + const bTargetVersion = + tryRootNodeSpec() + || tryAnySpec() + + // figure out what to compare against, + // follows same logic to npm outdated "Wanted" results + const packument = await pacote.packument(spec, { + ...this.npm.flatOptions, + preferOnline: true, + }) + bSpec = pickManifest( + packument, + bTargetVersion, + { ...this.npm.flatOptions } + ).version + } - if (!pkgName) { + return [ + `${spec.name}@${aSpec}`, + `${spec.name}@${bSpec}`, + ] + } else if (spec.type === 'directory') { + return [ + `file:${spec.fetchSpec}`, + specSelf(), + ] + } else { throw new Error( - 'Needs to be run from a project dir in order to diff two versions.\n\n' + - `Usage:\n${usage}` + 'Spec type not supported.\n\n' + + `Usage:\n${this.usage}` ) } - return [`${pkgName}@${a}`, `${pkgName}@${b}`] } - // otherwise uses the name from the other arg to - // figure out the spec.name of what to compare - if (!semverA && semverB) - return [a, `${npa(a).name}@${b}`] + async convertVersionsToSpecs ([a, b]) { + const semverA = semver.validRange(a) + const semverB = semver.validRange(b) + + // both specs are semver versions, assume current project dir name + if (semverA && semverB) { + let pkgName + try { + pkgName = await readLocalPkg(this.npm) + } catch (e) { + npmlog.verbose('diff', 'could not read project dir package.json') + } + + if (!pkgName) { + throw new Error( + 'Needs to be run from a project dir in order to diff two versions.\n\n' + + `Usage:\n${this.usage}` + ) + } + return [`${pkgName}@${a}`, `${pkgName}@${b}`] + } - if (semverA && !semverB) - return [`${npa(b).name}@${a}`, b] + // otherwise uses the name from the other arg to + // figure out the spec.name of what to compare + if (!semverA && semverB) + return [a, `${npa(a).name}@${b}`] - // no valid semver ranges used - return [a, b] -} + if (semverA && !semverB) + return [`${npa(b).name}@${a}`, b] -const findVersionsByPackageName = async (specs) => { - let actualTree - try { - const opts = { - ...npm.flatOptions, - path: where(), - } - const arb = new Arborist(opts) - actualTree = await arb.loadActual(opts) - } catch (e) { - npmlog.verbose('diff', 'failed to load actual install tree') + // no valid semver ranges used + return [a, b] } - return specs.map(i => { - const spec = npa(i) - if (spec.rawSpec) - return i + async findVersionsByPackageName (specs) { + let actualTree + try { + const opts = { + ...this.npm.flatOptions, + path: this.where, + } + const arb = new Arborist(opts) + actualTree = await arb.loadActual(opts) + } catch (e) { + npmlog.verbose('diff', 'failed to load actual install tree') + } + + return specs.map(i => { + const spec = npa(i) + if (spec.rawSpec) + return i - const node = actualTree - && actualTree.inventory.query('name', spec.name) - .values().next().value + const node = actualTree + && actualTree.inventory.query('name', spec.name) + .values().next().value - const res = !node || !node.package || !node.package.version - ? spec.fetchSpec - : `file:${node.realpath}` + const res = !node || !node.package || !node.package.version + ? spec.fetchSpec + : `file:${node.realpath}` - return `${spec.name}@${res}` - }) + return `${spec.name}@${res}` + }) + } } -module.exports = Object.assign(cmd, { usage }) +module.exports = Diff diff --git a/lib/dist-tag.js b/lib/dist-tag.js index e958bb7544222..171a88c527e5d 100644 --- a/lib/dist-tag.js +++ b/lib/dist-tag.js @@ -3,69 +3,77 @@ const npa = require('npm-package-arg') const regFetch = require('npm-registry-fetch') const semver = require('semver') -const npm = require('./npm.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') const readLocalPkgName = require('./utils/read-local-package.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'dist-tag', - 'npm dist-tag add @ []' + - '\nnpm dist-tag rm ' + - '\nnpm dist-tag ls []' -) - -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv.length === 2) - return ['add', 'rm', 'ls'] - - switch (argv[2]) { - default: - return [] +class DistTag { + constructor (npm) { + this.npm = npm } -} -const cmd = (args, cb) => distTag(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil( + 'dist-tag', + 'npm dist-tag add @ []' + + '\nnpm dist-tag rm ' + + '\nnpm dist-tag ls []' + ) + } -const distTag = async ([cmdName, pkg, tag]) => { - const opts = npm.flatOptions - const has = (items) => new Set(items).has(cmdName) + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) + return ['add', 'rm', 'ls'] - if (has(['add', 'a', 'set', 's'])) - return add(pkg, tag, opts) + switch (argv[2]) { + default: + return [] + } + } - if (has(['rm', 'r', 'del', 'd', 'remove'])) - return remove(pkg, tag, opts) + exec (args, cb) { + this.distTag(args).then(() => cb()).catch(cb) + } - if (has(['ls', 'l', 'sl', 'list'])) - return list(pkg, opts) + async distTag ([cmdName, pkg, tag]) { + const opts = this.npm.flatOptions + const has = (items) => new Set(items).has(cmdName) - if (!pkg) { - // when only using the pkg name the default behavior - // should be listing the existing tags - return list(cmdName, opts) - } else - throw usage -} + if (has(['add', 'a', 'set', 's'])) + return this.add(pkg, tag, opts) -function add (spec, tag, opts) { - spec = npa(spec || '') - const version = spec.rawSpec - const defaultTag = tag || opts.defaultTag + if (has(['rm', 'r', 'del', 'd', 'remove'])) + return this.remove(pkg, tag, opts) - log.verbose('dist-tag add', defaultTag, 'to', spec.name + '@' + version) + if (has(['ls', 'l', 'sl', 'list'])) + return this.list(pkg, opts) - if (!spec.name || !version || !defaultTag) - throw usage + if (!pkg) { + // when only using the pkg name the default behavior + // should be listing the existing tags + return this.list(cmdName, opts) + } else + throw this.usage + } + + async add (spec, tag, opts) { + spec = npa(spec || '') + const version = spec.rawSpec + const defaultTag = tag || opts.defaultTag + + log.verbose('dist-tag add', defaultTag, 'to', spec.name + '@' + version) - const t = defaultTag.trim() + if (!spec.name || !version || !defaultTag) + throw this.usage - if (semver.validRange(t)) - throw new Error('Tag name must not be a valid SemVer range: ' + t) + const t = defaultTag.trim() - return fetchTags(spec, opts).then(tags => { + if (semver.validRange(t)) + throw new Error('Tag name must not be a valid SemVer range: ' + t) + + const tags = await this.fetchTags(spec, opts) if (tags[t] === version) { log.warn('dist-tag add', t, 'is already set to version', version) return @@ -82,20 +90,18 @@ function add (spec, tag, opts) { }, spec, } - return otplease(reqOpts, reqOpts => regFetch(url, reqOpts)).then(() => { - output(`+${t}: ${spec.name}@${version}`) - }) - }) -} + await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) + output(`+${t}: ${spec.name}@${version}`) + } -function remove (spec, tag, opts) { - spec = npa(spec || '') - log.verbose('dist-tag del', tag, 'from', spec.name) + async remove (spec, tag, opts) { + spec = npa(spec || '') + log.verbose('dist-tag del', tag, 'from', spec.name) - if (!spec.name) - throw usage + if (!spec.name) + throw this.usage - return fetchTags(spec, opts).then(tags => { + const tags = await this.fetchTags(spec, opts) if (!tags[tag]) { log.info('dist-tag del', tag, 'is not a dist-tag on', spec.name) throw new Error(tag + ' is not a dist-tag on ' + spec.name) @@ -109,50 +115,43 @@ function remove (spec, tag, opts) { method: 'DELETE', spec, } - return otplease(reqOpts, reqOpts => regFetch(url, reqOpts)).then(() => { - output(`-${tag}: ${spec.name}@${version}`) - }) - }) -} + await otplease(reqOpts, reqOpts => regFetch(url, reqOpts)) + output(`-${tag}: ${spec.name}@${version}`) + } -function list (spec, opts) { - if (!spec) { - return readLocalPkgName().then(pkg => { + async list (spec, opts) { + if (!spec) { + const pkg = await readLocalPkgName(this.npm) if (!pkg) - throw usage + throw this.usage - return list(pkg, opts) - }) + return this.list(pkg, opts) + } + spec = npa(spec) + + try { + const tags = await this.fetchTags(spec, opts) + const msg = + Object.keys(tags).map(k => `${k}: ${tags[k]}`).sort().join('\n') + output(msg) + return tags + } catch (err) { + log.error('dist-tag ls', "Couldn't get dist-tag data for", spec) + throw err + } } - spec = npa(spec) - - return fetchTags(spec, opts).then(tags => { - const msg = - Object.keys(tags).map(k => `${k}: ${tags[k]}`).sort().join('\n') - output(msg) - return tags - }, err => { - log.error('dist-tag ls', "Couldn't get dist-tag data for", spec) - throw err - }) -} -function fetchTags (spec, opts) { - return regFetch.json( - `/-/package/${spec.escapedName}/dist-tags`, - { - ...opts, - 'prefer-online': true, - spec, - } - ).then(data => { + async fetchTags (spec, opts) { + const data = await regFetch.json( + `/-/package/${spec.escapedName}/dist-tags`, + { ...opts, 'prefer-online': true, spec } + ) if (data && typeof data === 'object') delete data._etag if (!data || !Object.keys(data).length) throw new Error('No dist-tags found for ' + spec.name) return data - }) + } } - -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = DistTag diff --git a/lib/docs.js b/lib/docs.js index fa0adb3d37309..247fc1e43ed9f 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -1,39 +1,46 @@ const log = require('npmlog') const pacote = require('pacote') -const { promisify } = require('util') -const openUrl = promisify(require('./utils/open-url.js')) +const openUrl = require('./utils/open-url.js') const usageUtil = require('./utils/usage.js') -const npm = require('./npm.js') const hostedFromMani = require('./utils/hosted-git-info-from-manifest.js') -const usage = usageUtil('docs', 'npm docs [ [ ...]]') +class Docs { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => docs(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil('docs', 'npm docs [ [ ...]]') + } -const docs = async args => { - if (!args || !args.length) - args = ['.'] + exec (args, cb) { + this.docs(args).then(() => cb()).catch(cb) + } - await Promise.all(args.map(pkg => getDocs(pkg))) -} + async docs (args) { + if (!args || !args.length) + args = ['.'] -const getDocsUrl = mani => { - if (mani.homepage) - return mani.homepage + await Promise.all(args.map(pkg => this.getDocs(pkg))) + } - const info = hostedFromMani(mani) - if (info) - return info.docs() + async getDocs (pkg) { + const opts = { ...this.npm.flatOptions, fullMetadata: true } + const mani = await pacote.manifest(pkg, opts) + const url = this.getDocsUrl(mani) + log.silly('docs', 'url', url) + await openUrl(this.npm, url, `${mani.name} docs available at the following URL`) + } - return 'https://www.npmjs.com/package/' + mani.name -} + getDocsUrl (mani) { + if (mani.homepage) + return mani.homepage -const getDocs = async pkg => { - const opts = { ...npm.flatOptions, fullMetadata: true } - const mani = await pacote.manifest(pkg, opts) - const url = getDocsUrl(mani) - log.silly('docs', 'url', url) - await openUrl(url, `${mani.name} docs available at the following URL`) -} + const info = hostedFromMani(mani) + if (info) + return info.docs() -module.exports = Object.assign(cmd, { usage }) + return 'https://www.npmjs.com/package/' + mani.name + } +} +module.exports = Docs diff --git a/lib/doctor.js b/lib/doctor.js index e149aec1286d5..382efa33ae320 100644 --- a/lib/doctor.js +++ b/lib/doctor.js @@ -1,79 +1,22 @@ -const npm = require('./npm.js') - +const cacache = require('cacache') const chalk = require('chalk') -const ansiTrim = require('./utils/ansi-trim.js') +const fs = require('fs') +const fetch = require('make-fetch-happen') const table = require('text-table') -const output = require('./utils/output.js') -const usageUtil = require('./utils/usage.js') -const usage = usageUtil('doctor', 'npm doctor') -const { resolve } = require('path') - -const ping = require('./utils/ping.js') -const checkPing = async () => { - const tracker = npm.log.newItem('checkPing', 1) - tracker.info('checkPing', 'Pinging registry') - try { - await ping(npm.flatOptions) - return '' - } catch (er) { - if (/^E\d{3}$/.test(er.code || '')) - throw er.code.substr(1) + ' ' + er.message - else - throw er.message - } finally { - tracker.finish() - } -} - +const which = require('which') const pacote = require('pacote') -const getLatestNpmVersion = async () => { - const tracker = npm.log.newItem('getLatestNpmVersion', 1) - tracker.info('getLatestNpmVersion', 'Getting npm package information') - try { - const latest = (await pacote.manifest('npm@latest', npm.flatOptions)).version - if (semver.gte(npm.version, latest)) - return `current: v${npm.version}, latest: v${latest}` - else - throw `Use npm v${latest}` - } finally { - tracker.finish() - } -} - +const { resolve } = require('path') const semver = require('semver') -const fetch = require('make-fetch-happen') -const getLatestNodejsVersion = async () => { - // XXX get the latest in the current major as well - const current = process.version - const currentRange = `^${current}` - const url = 'https://nodejs.org/dist/index.json' - const tracker = npm.log.newItem('getLatestNodejsVersion', 1) - tracker.info('getLatestNodejsVersion', 'Getting Node.js release information') - try { - const res = await fetch(url, { method: 'GET', ...npm.flatOptions }) - const data = await res.json() - let maxCurrent = '0.0.0' - let maxLTS = '0.0.0' - for (const { lts, version } of data) { - if (lts && semver.gt(version, maxLTS)) - maxLTS = version - - if (semver.satisfies(version, currentRange) && - semver.gt(version, maxCurrent)) - maxCurrent = version - } - const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS - if (semver.gte(process.version, recommended)) - return `current: ${current}, recommended: ${recommended}` - else - throw `Use node ${recommended} (current: ${current})` - } finally { - tracker.finish() - } -} - const { promisify } = require('util') -const fs = require('fs') +const ansiTrim = require('./utils/ansi-trim.js') +const isWindows = require('./utils/is-windows.js') +const output = require('./utils/output.js') +const ping = require('./utils/ping.js') +const usageUtil = require('./utils/usage.js') +const { defaults: { registry: defaultRegistry } } = require('./utils/config.js') +const lstat = promisify(fs.lstat) +const readdir = promisify(fs.readdir) +const access = promisify(fs.access) const { R_OK, W_OK, X_OK } = fs.constants const maskLabel = mask => { const label = [] @@ -88,200 +31,267 @@ const maskLabel = mask => { return label.join(', ') } -const lstat = promisify(fs.lstat) -const readdir = promisify(fs.readdir) -const access = promisify(fs.access) -const isWindows = require('./utils/is-windows.js') -const checkFilesPermission = async (root, shouldOwn, mask = null) => { - if (mask === null) - mask = shouldOwn ? R_OK | W_OK : R_OK - - let ok = true - - const tracker = npm.log.newItem(root, 1) - - try { - const uid = process.getuid() - const gid = process.getgid() - const files = new Set([root]) - for (const f of files) { - tracker.silly('checkFilesPermission', f.substr(root.length + 1)) - const st = await lstat(f) - .catch(er => { - ok = false - tracker.warn('checkFilesPermission', 'error getting info for ' + f) - }) - tracker.completeWork(1) - - if (!st) - continue +class Doctor { + constructor (npm) { + this.npm = npm + } - if (shouldOwn && (uid !== st.uid || gid !== st.gid)) { - tracker.warn('checkFilesPermission', 'should be owner of ' + f) - ok = false - } + get usage () { + return usageUtil('doctor', 'npm doctor') + } - if (!st.isDirectory() && !st.isFile()) - continue + exec (args, cb) { + this.doctor(args).then(() => cb()).catch(cb) + } + async doctor (args) { + this.npm.log.info('Running checkup') + + // each message is [title, ok, message] + const messages = [] + + const actions = [ + ['npm ping', 'checkPing', []], + ['npm -v', 'getLatestNpmVersion', []], + ['node -v', 'getLatestNodejsVersion', []], + ['npm config get registry', 'checkNpmRegistry', []], + ['which git', 'getGitPath', []], + ...(isWindows ? [] : [ + ['Perms check on cached files', 'checkFilesPermission', [this.npm.cache, true, R_OK]], + ['Perms check on local node_modules', 'checkFilesPermission', [this.npm.localDir, true]], + ['Perms check on global node_modules', 'checkFilesPermission', [this.npm.globalDir, false]], + ['Perms check on local bin folder', 'checkFilesPermission', [this.npm.localBin, false, R_OK | W_OK | X_OK]], + ['Perms check on global bin folder', 'checkFilesPermission', [this.npm.globalBin, false, X_OK]], + ]), + ['Verify cache contents', 'verifyCachedFiles', [this.npm.flatOptions.cache]], + // TODO: + // - ensure arborist.loadActual() runs without errors and no invalid edges + // - ensure package-lock.json matches loadActual() + // - verify loadActual without hidden lock file matches hidden lockfile + // - verify all local packages have bins linked + ] + + // Do the actual work + for (const [msg, fn, args] of actions) { + const line = [msg] try { - await access(f, mask) + line.push(true, await this[fn](...args)) } catch (er) { - ok = false - const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})` - tracker.error('checkFilesPermission', msg) - continue + line.push(false, er) } + messages.push(line) + } - if (st.isDirectory()) { - const entries = await readdir(f) - .catch(er => { - ok = false - tracker.warn('checkFilesPermission', 'error reading directory ' + f) - return [] - }) - for (const entry of entries) - files.add(resolve(f, entry)) + const outHead = ['Check', 'Value', 'Recommendation/Notes'] + .map(!this.npm.color ? h => h : h => chalk.underline(h)) + let allOk = true + const outBody = messages.map(!this.npm.color + ? item => { + allOk = allOk && item[1] + item[1] = item[1] ? 'ok' : 'not ok' + item[2] = String(item[2]) + return item } + : item => { + allOk = allOk && item[1] + if (!item[1]) { + item[0] = chalk.red(item[0]) + item[2] = chalk.magenta(String(item[2])) + } + item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok') + return item + }) + const outTable = [outHead, ...outBody] + const tableOpts = { + stringLength: s => ansiTrim(s).length, } - } finally { - tracker.finish() - if (!ok) { - throw `Check the permissions of files in ${root}` + - (shouldOwn ? ' (should be owned by current user)' : '') - } else + + const silent = this.npm.log.levels[this.npm.log.level] > + this.npm.log.levels.error + if (!silent) { + output(table(outTable, tableOpts)) + if (!allOk) + console.error('') + } + if (!allOk) + throw 'Some problems found. See above for recommendations.' + } + + async checkPing () { + const tracker = this.npm.log.newItem('checkPing', 1) + tracker.info('checkPing', 'Pinging registry') + try { + await ping(this.npm.flatOptions) return '' + } catch (er) { + if (/^E\d{3}$/.test(er.code || '')) + throw er.code.substr(1) + ' ' + er.message + else + throw er.message + } finally { + tracker.finish() + } } -} -const which = require('which') -const getGitPath = async () => { - const tracker = npm.log.newItem('getGitPath', 1) - tracker.info('getGitPath', 'Finding git in your PATH') - try { - return await which('git').catch(er => { - tracker.warn(er) - throw "Install git and ensure it's in your PATH." - }) - } finally { - tracker.finish() + async getLatestNpmVersion () { + const tracker = this.npm.log.newItem('getLatestNpmVersion', 1) + tracker.info('getLatestNpmVersion', 'Getting npm package information') + try { + const latest = (await pacote.manifest('npm@latest', this.npm.flatOptions)).version + if (semver.gte(this.npm.version, latest)) + return `current: v${this.npm.version}, latest: v${latest}` + else + throw `Use npm v${latest}` + } finally { + tracker.finish() + } } -} -const cacache = require('cacache') -const verifyCachedFiles = async () => { - const tracker = npm.log.newItem('verifyCachedFiles', 1) - tracker.info('verifyCachedFiles', 'Verifying the npm cache') - try { - const stats = await cacache.verify(npm.flatOptions.cache) - const { - badContentCount, - reclaimedCount, - missingContent, - reclaimedSize, - } = stats - if (badContentCount || reclaimedCount || missingContent) { - if (badContentCount) - tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`) - - if (reclaimedCount) - tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`) - - if (missingContent) - tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`) - - tracker.warn('verifyCachedFiles', 'Cache issues have been fixed') + async getLatestNodejsVersion () { + // XXX get the latest in the current major as well + const current = process.version + const currentRange = `^${current}` + const url = 'https://nodejs.org/dist/index.json' + const tracker = this.npm.log.newItem('getLatestNodejsVersion', 1) + tracker.info('getLatestNodejsVersion', 'Getting Node.js release information') + try { + const res = await fetch(url, { method: 'GET', ...this.npm.flatOptions }) + const data = await res.json() + let maxCurrent = '0.0.0' + let maxLTS = '0.0.0' + for (const { lts, version } of data) { + if (lts && semver.gt(version, maxLTS)) + maxLTS = version + + if (semver.satisfies(version, currentRange) && + semver.gt(version, maxCurrent)) + maxCurrent = version + } + const recommended = semver.gt(maxCurrent, maxLTS) ? maxCurrent : maxLTS + if (semver.gte(process.version, recommended)) + return `current: ${current}, recommended: ${recommended}` + else + throw `Use node ${recommended} (current: ${current})` + } finally { + tracker.finish() } - tracker.info('verifyCachedFiles', `Verification complete. Stats: ${ - JSON.stringify(stats, null, 2) - }`) - return `verified ${stats.verifiedContent} tarballs` - } finally { - tracker.finish() } -} -const { defaults: { registry: defaultRegistry } } = require('./utils/config.js') -const checkNpmRegistry = async () => { - if (npm.flatOptions.registry !== defaultRegistry) - throw `Try \`npm config set registry=${defaultRegistry}\`` - else - return `using default registry (${defaultRegistry})` -} + async checkFilesPermission (root, shouldOwn, mask = null) { + if (mask === null) + mask = shouldOwn ? R_OK | W_OK : R_OK + + let ok = true + + const tracker = this.npm.log.newItem(root, 1) -const cmd = (args, cb) => doctor(args).then(() => cb()).catch(cb) - -const doctor = async args => { - npm.log.info('Running checkup') - - // each message is [title, ok, message] - const messages = [] - - const actions = [ - ['npm ping', checkPing, []], - ['npm -v', getLatestNpmVersion, []], - ['node -v', getLatestNodejsVersion, []], - ['npm config get registry', checkNpmRegistry, []], - ['which git', getGitPath, []], - ...(isWindows ? [] : [ - ['Perms check on cached files', checkFilesPermission, [npm.cache, true, R_OK]], - ['Perms check on local node_modules', checkFilesPermission, [npm.localDir, true]], - ['Perms check on global node_modules', checkFilesPermission, [npm.globalDir, false]], - ['Perms check on local bin folder', checkFilesPermission, [npm.localBin, false, R_OK | W_OK | X_OK]], - ['Perms check on global bin folder', checkFilesPermission, [npm.globalBin, false, X_OK]], - ]), - ['Verify cache contents', verifyCachedFiles, [npm.flatOptions.cache]], - // TODO: - // - ensure arborist.loadActual() runs without errors and no invalid edges - // - ensure package-lock.json matches loadActual() - // - verify loadActual without hidden lock file matches hidden lockfile - // - verify all local packages have bins linked - ] - - for (const [msg, fn, args] of actions) { - const line = [msg] try { - line.push(true, await fn(...args)) - } catch (er) { - line.push(false, er) + const uid = process.getuid() + const gid = process.getgid() + const files = new Set([root]) + for (const f of files) { + tracker.silly('checkFilesPermission', f.substr(root.length + 1)) + const st = await lstat(f) + .catch(er => { + ok = false + tracker.warn('checkFilesPermission', 'error getting info for ' + f) + }) + + tracker.completeWork(1) + + if (!st) + continue + + if (shouldOwn && (uid !== st.uid || gid !== st.gid)) { + tracker.warn('checkFilesPermission', 'should be owner of ' + f) + ok = false + } + + if (!st.isDirectory() && !st.isFile()) + continue + + try { + await access(f, mask) + } catch (er) { + ok = false + const msg = `Missing permissions on ${f} (expect: ${maskLabel(mask)})` + tracker.error('checkFilesPermission', msg) + continue + } + + if (st.isDirectory()) { + const entries = await readdir(f) + .catch(er => { + ok = false + tracker.warn('checkFilesPermission', 'error reading directory ' + f) + return [] + }) + for (const entry of entries) + files.add(resolve(f, entry)) + } + } + } finally { + tracker.finish() + if (!ok) { + throw `Check the permissions of files in ${root}` + + (shouldOwn ? ' (should be owned by current user)' : '') + } else + return '' } - messages.push(line) } - const silent = npm.log.levels[npm.log.level] > npm.log.levels.error - - const outHead = ['Check', 'Value', 'Recommendation/Notes'] - .map(!npm.color ? h => h : h => chalk.underline(h)) - let allOk = true - const outBody = messages.map(!npm.color - ? item => { - allOk = allOk && item[1] - item[1] = item[1] ? 'ok' : 'not ok' - item[2] = String(item[2]) - return item + async getGitPath () { + const tracker = this.npm.log.newItem('getGitPath', 1) + tracker.info('getGitPath', 'Finding git in your PATH') + try { + return await which('git').catch(er => { + tracker.warn(er) + throw "Install git and ensure it's in your PATH." + }) + } finally { + tracker.finish() } - : item => { - allOk = allOk && item[1] - if (!item[1]) { - item[0] = chalk.red(item[0]) - item[2] = chalk.magenta(String(item[2])) + } + + async verifyCachedFiles () { + const tracker = this.npm.log.newItem('verifyCachedFiles', 1) + tracker.info('verifyCachedFiles', 'Verifying the npm cache') + try { + const stats = await cacache.verify(this.npm.flatOptions.cache) + const { + badContentCount, + reclaimedCount, + missingContent, + reclaimedSize, + } = stats + if (badContentCount || reclaimedCount || missingContent) { + if (badContentCount) + tracker.warn('verifyCachedFiles', `Corrupted content removed: ${badContentCount}`) + + if (reclaimedCount) + tracker.warn('verifyCachedFiles', `Content garbage-collected: ${reclaimedCount} (${reclaimedSize} bytes)`) + + if (missingContent) + tracker.warn('verifyCachedFiles', `Missing content: ${missingContent}`) + + tracker.warn('verifyCachedFiles', 'Cache issues have been fixed') } - item[1] = item[1] ? chalk.green('ok') : chalk.red('not ok') - return item - }) - const outTable = [outHead, ...outBody] - const tableOpts = { - stringLength: s => ansiTrim(s).length, + tracker.info('verifyCachedFiles', `Verification complete. Stats: ${ + JSON.stringify(stats, null, 2) + }`) + return `verified ${stats.verifiedContent} tarballs` + } finally { + tracker.finish() + } } - if (!silent) { - output(table(outTable, tableOpts)) - if (!allOk) - console.error('') + async checkNpmRegistry () { + if (this.npm.flatOptions.registry !== defaultRegistry) + throw `Try \`npm config set registry=${defaultRegistry}\`` + else + return `using default registry (${defaultRegistry})` } - if (!allOk) - throw 'Some problems found. See above for recommendations.' } -module.exports = Object.assign(cmd, { usage }) +module.exports = Doctor diff --git a/lib/edit.js b/lib/edit.js index 9ae6349262c2d..83361c7194b11 100644 --- a/lib/edit.js +++ b/lib/edit.js @@ -4,33 +4,53 @@ const { resolve } = require('path') const fs = require('graceful-fs') const { spawn } = require('child_process') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const splitPackageNames = require('./utils/split-package-names.js') - -const usage = usageUtil('edit', 'npm edit [/...]') const completion = require('./utils/completion/installed-shallow.js') -function edit (args, cb) { - if (args.length !== 1) - return cb(usage) - - const path = splitPackageNames(args[0]) - const dir = resolve(npm.dir, path) - - fs.lstat(dir, (err) => { - if (err) - return cb(err) - - const [bin, ...args] = npm.config.get('editor').split(/\s+/) - const editor = spawn(bin, [...args, dir], { stdio: 'inherit' }) - editor.on('exit', (code) => { - if (code) - return cb(new Error(`editor process exited with code: ${code}`)) - - npm.commands.rebuild([dir], cb) +class Edit { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('edit', 'npm edit [/...]') + } + + async completion (opts) { + return completion(this.npm, opts) + } + + exec (args, cb) { + this.edit(args).then(() => cb()).catch(cb) + } + + async edit (args) { + if (args.length !== 1) + throw new Error(this.usage) + + const path = splitPackageNames(args[0]) + const dir = resolve(this.npm.dir, path) + + // graceful-fs does not promisify + await new Promise((resolve, reject) => { + fs.lstat(dir, (err) => { + if (err) + return reject(err) + const [bin, ...args] = this.npm.config.get('editor').split(/\s+/) + const editor = spawn(bin, [...args, dir], { stdio: 'inherit' }) + editor.on('exit', (code) => { + if (code) + return reject(new Error(`editor process exited with code: ${code}`)) + this.npm.commands.rebuild([dir], (err) => { + if (err) + return reject(err) + + resolve() + }) + }) + }) }) - }) + } } - -module.exports = Object.assign(edit, { completion, usage }) +module.exports = Edit diff --git a/lib/exec.js b/lib/exec.js index dab65c23a37b2..d1db49128587e 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -1,28 +1,18 @@ -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil('exec', - 'Run a command from a local or remote npm package.\n\n' + - - 'npm exec -- [@] [args...]\n' + - 'npm exec --package=[@] -- [args...]\n' + - 'npm exec -c \' [args...]\'\n' + - 'npm exec --package=foo -c \' [args...]\'\n' + - '\n' + - 'npx [@] [args...]\n' + - 'npx -p [@] [args...]\n' + - 'npx -c \' [args...]\'\n' + - 'npx -p [@] -c \' [args...]\'' + - '\n' + - 'Run without --call or positional args to open interactive subshell\n', - - '\n--package= (may be specified multiple times)\n' + - '-p is a shorthand for --package only when using npx executable\n' + - '-c --call= (may not be mixed with positional arguments)' -) - const { promisify } = require('util') const read = promisify(require('read')) +const mkdirp = require('mkdirp-infer-owner') +const readPackageJson = require('read-package-json-fast') +const Arborist = require('@npmcli/arborist') +const runScript = require('@npmcli/run-script') +const { resolve, delimiter } = require('path') +const ciDetect = require('@npmcli/ci-detect') +const crypto = require('crypto') +const pacote = require('pacote') +const npa = require('npm-package-arg') +const fileExists = require('./utils/file-exists.js') +const PATH = require('./utils/path.js') // it's like this: // @@ -49,237 +39,258 @@ const read = promisify(require('read')) // runScript({ pkg, event: 'npx', ... }) // process.env.npm_lifecycle_event = 'npx' -const mkdirp = require('mkdirp-infer-owner') -const readPackageJson = require('read-package-json-fast') -const Arborist = require('@npmcli/arborist') -const runScript = require('@npmcli/run-script') -const { resolve, delimiter } = require('path') -const ciDetect = require('@npmcli/ci-detect') -const crypto = require('crypto') -const pacote = require('pacote') -const npa = require('npm-package-arg') -const fileExists = require('./utils/file-exists.js') -const PATH = require('./utils/path.js') - -const cmd = (args, cb) => exec(args).then(() => cb()).catch(cb) - -const run = async ({ args, call, pathArr, shell }) => { - // turn list of args into command string - const script = call || args.shift() || shell - - // do the fakey runScript dance - // still should work if no package.json in cwd - const realPkg = await readPackageJson(`${npm.localPrefix}/package.json`) - .catch(() => ({})) - const pkg = { - ...realPkg, - scripts: { - ...(realPkg.scripts || {}), - npx: script, - }, +class Exec { + constructor (npm) { + this.npm = npm } - npm.log.disableProgress() - try { - if (script === shell) { - if (process.stdin.isTTY) { - if (ciDetect()) - return npm.log.warn('exec', 'Interactive mode disabled in CI environment') - output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`) - } - } - return await runScript({ - ...npm.flatOptions, - pkg, - banner: false, - // we always run in cwd, not --prefix - path: process.cwd(), - stdioString: true, - event: 'npx', - args, - env: { - PATH: pathArr.join(delimiter), - }, - stdio: 'inherit', - }) - } finally { - npm.log.enableProgress() + get usage () { + return usageUtil('exec', + 'Run a command from a local or remote npm package.\n\n' + + + 'npm exec -- [@] [args...]\n' + + 'npm exec --package=[@] -- [args...]\n' + + 'npm exec -c \' [args...]\'\n' + + 'npm exec --package=foo -c \' [args...]\'\n' + + '\n' + + 'npx [@] [args...]\n' + + 'npx -p [@] [args...]\n' + + 'npx -c \' [args...]\'\n' + + 'npx -p [@] -c \' [args...]\'' + + '\n' + + 'Run without --call or positional args to open interactive subshell\n', + + '\n--package= (may be specified multiple times)\n' + + '-p is a shorthand for --package only when using npx executable\n' + + '-c --call= (may not be mixed with positional arguments)' + ) } -} - -const exec = async args => { - const { package: packages, call, shell } = npm.flatOptions - if (call && args.length) - throw usage + exec (args, cb) { + this._exec(args).then(() => cb()).catch(cb) + } - const pathArr = [...PATH] + // When commands go async and we can dump the boilerplate exec methods this + // can be named correctly + async _exec (args) { + const { package: packages, call, shell } = this.npm.flatOptions - // nothing to maybe install, skip the arborist dance - if (!call && !args.length && !packages.length) { - return await run({ - args, - call, - shell, - pathArr, - }) - } + if (call && args.length) + throw this.usage - const needPackageCommandSwap = args.length && !packages.length - // if there's an argument and no package has been explicitly asked for - // check the local and global bin paths for a binary named the same as - // the argument and run it if it exists, otherwise fall through to - // the behavior of treating the single argument as a package name - if (needPackageCommandSwap) { - let binExists = false - if (await fileExists(`${npm.localBin}/${args[0]}`)) { - pathArr.unshift(npm.localBin) - binExists = true - } else if (await fileExists(`${npm.globalBin}/${args[0]}`)) { - pathArr.unshift(npm.globalBin) - binExists = true - } + const pathArr = [...PATH] - if (binExists) { - return await run({ + // nothing to maybe install, skip the arborist dance + if (!call && !args.length && !packages.length) { + return await this.run({ args, call, - pathArr, shell, + pathArr, }) } - packages.push(args[0]) - } + const needPackageCommandSwap = args.length && !packages.length + // if there's an argument and no package has been explicitly asked for + // check the local and global bin paths for a binary named the same as + // the argument and run it if it exists, otherwise fall through to + // the behavior of treating the single argument as a package name + if (needPackageCommandSwap) { + let binExists = false + if (await fileExists(`${this.npm.localBin}/${args[0]}`)) { + pathArr.unshift(this.npm.localBin) + binExists = true + } else if (await fileExists(`${this.npm.globalBin}/${args[0]}`)) { + pathArr.unshift(this.npm.globalBin) + binExists = true + } - // If we do `npm exec foo`, and have a `foo` locally, then we'll - // always use that, so we don't really need to fetch the manifest. - // So: run npa on each packages entry, and if it is a name with a - // rawSpec==='', then try to readPackageJson at - // node_modules/${name}/package.json, and only pacote fetch if - // that fails. - const manis = await Promise.all(packages.map(async p => { - const spec = npa(p, npm.localPrefix) - if (spec.type === 'tag' && spec.rawSpec === '') { - // fall through to the pacote.manifest() approach - try { - const pj = resolve(npm.localPrefix, 'node_modules', spec.name) - return await readPackageJson(pj) - } catch (er) {} + if (binExists) { + return await this.run({ + args, + call, + pathArr, + shell, + }) + } + + packages.push(args[0]) } - // Force preferOnline to true so we are making sure to pull in the latest - // This is especially useful if the user didn't give us a version, and - // they expect to be running @latest - return await pacote.manifest(p, { - ...npm.flatOptions, - preferOnline: true, - }) - })) - - if (needPackageCommandSwap) - args[0] = getBinFromManifest(manis[0]) - - // figure out whether we need to install stuff, or if local is fine - const localArb = new Arborist({ - ...npm.flatOptions, - path: npm.localPrefix, - }) - const tree = await localArb.loadActual() - - // do we have all the packages in manifest list? - const needInstall = manis.some(mani => manifestMissing(tree, mani)) - - if (needInstall) { - const installDir = cacheInstallDir(packages) - await mkdirp(installDir) - const arb = new Arborist({ ...npm.flatOptions, path: installDir }) - const tree = await arb.loadActual() - - // at this point, we have to ensure that we get the exact same - // version, because it's something that has only ever been installed - // by npm exec in the cache install directory - const add = manis.filter(mani => manifestMissing(tree, { - ...mani, - _from: `${mani.name}@${mani.version}`, + + // If we do `npm exec foo`, and have a `foo` locally, then we'll + // always use that, so we don't really need to fetch the manifest. + // So: run npa on each packages entry, and if it is a name with a + // rawSpec==='', then try to readPackageJson at + // node_modules/${name}/package.json, and only pacote fetch if + // that fails. + const manis = await Promise.all(packages.map(async p => { + const spec = npa(p, this.npm.localPrefix) + if (spec.type === 'tag' && spec.rawSpec === '') { + // fall through to the pacote.manifest() approach + try { + const pj = resolve(this.npm.localPrefix, 'node_modules', spec.name) + return await readPackageJson(pj) + } catch (er) {} + } + // Force preferOnline to true so we are making sure to pull in the latest + // This is especially useful if the user didn't give us a version, and + // they expect to be running @latest + return await pacote.manifest(p, { + ...this.npm.flatOptions, + preferOnline: true, + }) })) - .map(mani => mani._from) - .sort((a, b) => a.localeCompare(b)) - - // no need to install if already present - if (add.length) { - if (!npm.flatOptions.yes) { - // set -n to always say no - if (npm.flatOptions.yes === false) - throw 'canceled' - - if (!process.stdin.isTTY || ciDetect()) { - npm.log.warn('exec', `The following package${ + + if (needPackageCommandSwap) + args[0] = this.getBinFromManifest(manis[0]) + + // figure out whether we need to install stuff, or if local is fine + const localArb = new Arborist({ + ...this.npm.flatOptions, + path: this.npm.localPrefix, + }) + const tree = await localArb.loadActual() + + // do we have all the packages in manifest list? + const needInstall = manis.some(mani => this.manifestMissing(tree, mani)) + + if (needInstall) { + const installDir = this.cacheInstallDir(packages) + await mkdirp(installDir) + const arb = new Arborist({ ...this.npm.flatOptions, path: installDir }) + const tree = await arb.loadActual() + + // at this point, we have to ensure that we get the exact same + // version, because it's something that has only ever been installed + // by npm exec in the cache install directory + const add = manis.filter(mani => this.manifestMissing(tree, { + ...mani, + _from: `${mani.name}@${mani.version}`, + })) + .map(mani => mani._from) + .sort((a, b) => a.localeCompare(b)) + + // no need to install if already present + if (add.length) { + if (!this.npm.flatOptions.yes) { + // set -n to always say no + if (this.npm.flatOptions.yes === false) + throw 'canceled' + + if (!process.stdin.isTTY || ciDetect()) { + this.npm.log.warn('exec', `The following package${ add.length === 1 ? ' was' : 's were' } not found and will be installed: ${ add.map((pkg) => pkg.replace(/@$/, '')).join(', ') }`) - } else { - const addList = add.map(a => ` ${a.replace(/@$/, '')}`) - .join('\n') + '\n' - const prompt = `Need to install the following packages:\n${ + } else { + const addList = add.map(a => ` ${a.replace(/@$/, '')}`) + .join('\n') + '\n' + const prompt = `Need to install the following packages:\n${ addList }Ok to proceed? ` - const confirm = await read({ prompt, default: 'y' }) - if (confirm.trim().toLowerCase().charAt(0) !== 'y') - throw 'canceled' + const confirm = await read({ prompt, default: 'y' }) + if (confirm.trim().toLowerCase().charAt(0) !== 'y') + throw 'canceled' + } } + await arb.reify({ ...this.npm.flatOptions, add }) } - await arb.reify({ ...npm.flatOptions, add }) + pathArr.unshift(resolve(installDir, 'node_modules/.bin')) } - pathArr.unshift(resolve(installDir, 'node_modules/.bin')) + + return await this.run({ args, call, pathArr, shell }) } - return await run({ args, call, pathArr, shell }) -} + async run ({ args, call, pathArr, shell }) { + // turn list of args into command string + const script = call || args.shift() || shell + + // do the fakey runScript dance + // still should work if no package.json in cwd + const realPkg = await readPackageJson(`${this.npm.localPrefix}/package.json`) + .catch(() => ({})) + const pkg = { + ...realPkg, + scripts: { + ...(realPkg.scripts || {}), + npx: script, + }, + } -const manifestMissing = (tree, mani) => { - // if the tree doesn't have a child by that name/version, return true - // true means we need to install it - const child = tree.children.get(mani.name) - // if no child, we have to load it - if (!child) - return true + this.npm.log.disableProgress() + try { + if (script === shell) { + if (process.stdin.isTTY) { + if (ciDetect()) + return this.npm.log.warn('exec', 'Interactive mode disabled in CI environment') + output(`\nEntering npm script environment\nType 'exit' or ^D when finished\n`) + } + } + return await runScript({ + ...this.npm.flatOptions, + pkg, + banner: false, + // we always run in cwd, not --prefix + path: process.cwd(), + stdioString: true, + event: 'npx', + args, + env: { + PATH: pathArr.join(delimiter), + }, + stdio: 'inherit', + }) + } finally { + this.npm.log.enableProgress() + } + } - // if no version/tag specified, allow whatever's there - if (mani._from === `${mani.name}@`) - return false + manifestMissing (tree, mani) { + // if the tree doesn't have a child by that name/version, return true + // true means we need to install it + const child = tree.children.get(mani.name) + // if no child, we have to load it + if (!child) + return true - // otherwise the version has to match what we WOULD get - return child.version !== mani.version -} + // if no version/tag specified, allow whatever's there + if (mani._from === `${mani.name}@`) + return false -const getBinFromManifest = mani => { - // if we have a bin matching (unscoped portion of) packagename, use that - // otherwise if there's 1 bin or all bin value is the same (alias), use that, - // otherwise fail - const bin = mani.bin || {} - if (new Set(Object.values(bin)).size === 1) - return Object.keys(bin)[0] - - // XXX probably a util to parse this better? - const name = mani.name.replace(/^@[^/]+\//, '') - if (bin[name]) - return name - - // XXX need better error message - throw Object.assign(new Error('could not determine executable to run'), { - pkgid: mani._id, - }) -} + // otherwise the version has to match what we WOULD get + return child.version !== mani.version + } -// only packages not found in ${prefix}/node_modules -const cacheInstallDir = packages => - resolve(npm.config.get('cache'), '_npx', getHash(packages)) + getBinFromManifest (mani) { + // if we have a bin matching (unscoped portion of) packagename, use that + // otherwise if there's 1 bin or all bin value is the same (alias), use + // that, otherwise fail + const bin = mani.bin || {} + if (new Set(Object.values(bin)).size === 1) + return Object.keys(bin)[0] + + // XXX probably a util to parse this better? + const name = mani.name.replace(/^@[^/]+\//, '') + if (bin[name]) + return name + + // XXX need better error message + throw Object.assign(new Error('could not determine executable to run'), { + pkgid: mani._id, + }) + } -const getHash = packages => - crypto.createHash('sha512') - .update(packages.sort((a, b) => a.localeCompare(b)).join('\n')) - .digest('hex') - .slice(0, 16) + cacheInstallDir (packages) { + // only packages not found in ${prefix}/node_modules + return resolve(this.npm.config.get('cache'), '_npx', this.getHash(packages)) + } -module.exports = Object.assign(cmd, { usage }) + getHash (packages) { + return crypto.createHash('sha512') + .update(packages.sort((a, b) => a.localeCompare(b)).join('\n')) + .digest('hex') + .slice(0, 16) + } +} +module.exports = Exec diff --git a/lib/explain.js b/lib/explain.js index a0a4427bccf2c..d8f7ae6beffd6 100644 --- a/lib/explain.js +++ b/lib/explain.js @@ -1,5 +1,4 @@ const usageUtil = require('./utils/usage.js') -const npm = require('./npm.js') const { explainNode } = require('./utils/explain-dep.js') const completion = require('./utils/completion/installed-deep.js') const output = require('./utils/output.js') @@ -9,86 +8,99 @@ const semver = require('semver') const { relative, resolve } = require('path') const validName = require('validate-npm-package-name') -const usage = usageUtil('explain', 'npm explain ') - -const cmd = (args, cb) => explain(args).then(() => cb()).catch(cb) +class Explain { + constructor (npm) { + this.npm = npm + } -const explain = async (args) => { - if (!args.length) - throw usage + get usage () { + return usageUtil('explain', 'npm explain ') + } - const arb = new Arborist({ path: npm.prefix, ...npm.flatOptions }) - const tree = await arb.loadActual() + async completion (opts) { + return completion(this.npm, opts) + } - const nodes = new Set() - for (const arg of args) { - for (const node of getNodes(tree, arg)) - nodes.add(node) + exec (args, cb) { + this.explain(args).then(() => cb()).catch(cb) } - if (nodes.size === 0) - throw `No dependencies found matching ${args.join(', ')}` - const expls = [] - for (const node of nodes) { - const { extraneous, dev, optional, devOptional, peer, inBundle } = node - const expl = node.explain() - if (extraneous) - expl.extraneous = true - else { - expl.dev = dev - expl.optional = optional - expl.devOptional = devOptional - expl.peer = peer - expl.bundled = inBundle + async explain (args) { + if (!args.length) + throw this.usage + + const arb = new Arborist({ path: this.npm.prefix, ...this.npm.flatOptions }) + const tree = await arb.loadActual() + + const nodes = new Set() + for (const arg of args) { + for (const node of this.getNodes(tree, arg)) + nodes.add(node) } - expls.push(expl) - } + if (nodes.size === 0) + throw `No dependencies found matching ${args.join(', ')}` - if (npm.flatOptions.json) - output(JSON.stringify(expls, null, 2)) - else { - output(expls.map(expl => { - return explainNode(expl, Infinity, npm.color) - }).join('\n\n')) + const expls = [] + for (const node of nodes) { + const { extraneous, dev, optional, devOptional, peer, inBundle } = node + const expl = node.explain() + if (extraneous) + expl.extraneous = true + else { + expl.dev = dev + expl.optional = optional + expl.devOptional = devOptional + expl.peer = peer + expl.bundled = inBundle + } + expls.push(expl) + } + + if (this.npm.flatOptions.json) + output(JSON.stringify(expls, null, 2)) + else { + output(expls.map(expl => { + return explainNode(expl, Infinity, this.npm.color) + }).join('\n\n')) + } } -} -const getNodes = (tree, arg) => { - // if it's just a name, return packages by that name - const { validForOldPackages: valid } = validName(arg) - if (valid) - return tree.inventory.query('name', arg) + getNodes (tree, arg) { + // if it's just a name, return packages by that name + const { validForOldPackages: valid } = validName(arg) + if (valid) + return tree.inventory.query('name', arg) - // if it's a location, get that node - const maybeLoc = arg.replace(/\\/g, '/').replace(/\/+$/, '') - const nodeByLoc = tree.inventory.get(maybeLoc) - if (nodeByLoc) - return [nodeByLoc] + // if it's a location, get that node + const maybeLoc = arg.replace(/\\/g, '/').replace(/\/+$/, '') + const nodeByLoc = tree.inventory.get(maybeLoc) + if (nodeByLoc) + return [nodeByLoc] - // maybe a path to a node_modules folder - const maybePath = relative(npm.prefix, resolve(maybeLoc)) - .replace(/\\/g, '/').replace(/\/+$/, '') - const nodeByPath = tree.inventory.get(maybePath) - if (nodeByPath) - return [nodeByPath] + // maybe a path to a node_modules folder + const maybePath = relative(this.npm.prefix, resolve(maybeLoc)) + .replace(/\\/g, '/').replace(/\/+$/, '') + const nodeByPath = tree.inventory.get(maybePath) + if (nodeByPath) + return [nodeByPath] - // otherwise, try to select all matching nodes - try { - return getNodesByVersion(tree, arg) - } catch (er) { - return [] + // otherwise, try to select all matching nodes + try { + return this.getNodesByVersion(tree, arg) + } catch (er) { + return [] + } } -} -const getNodesByVersion = (tree, arg) => { - const spec = npa(arg, npm.prefix) - if (spec.type !== 'version' && spec.type !== 'range') - return [] + getNodesByVersion (tree, arg) { + const spec = npa(arg, this.npm.prefix) + if (spec.type !== 'version' && spec.type !== 'range') + return [] - return tree.inventory.filter(node => { - return node.package.name === spec.name && - semver.satisfies(node.package.version, spec.rawSpec) - }) + return tree.inventory.filter(node => { + return node.package.name === spec.name && + semver.satisfies(node.package.version, spec.rawSpec) + }) + } } - -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = Explain diff --git a/lib/explore.js b/lib/explore.js index e9b09707ec63b..47a792b7522f2 100644 --- a/lib/explore.js +++ b/lib/explore.js @@ -1,69 +1,80 @@ // npm explore [@] // open a subshell to the package folder. -const usageUtil = require('./utils/usage.js') -const completion = require('./utils/completion/installed-shallow.js') -const usage = usageUtil('explore', 'npm explore [ -- ]') const rpj = require('read-package-json-fast') - -const cmd = (args, cb) => explore(args).then(() => cb()).catch(cb) - -const output = require('./utils/output.js') -const npm = require('./npm.js') - const runScript = require('@npmcli/run-script') const { join, resolve, relative } = require('path') +const completion = require('./utils/completion/installed-shallow.js') +const output = require('./utils/output.js') +const usageUtil = require('./utils/usage.js') -const explore = async args => { - if (args.length < 1 || !args[0]) - throw usage +class Explore { + constructor (npm) { + this.npm = npm + } - const pkgname = args.shift() + get usage () { + return usageUtil('explore', 'npm explore [ -- ]') + } - // detect and prevent any .. shenanigans - const path = join(npm.dir, join('/', pkgname)) - if (relative(path, npm.dir) === '') - throw usage + async completion (opts) { + return completion(this.npm, opts) + } - // run as if running a script named '_explore', which we set to either - // the set of arguments, or the shell config, and let @npmcli/run-script - // handle all the escaping and PATH setup stuff. + exec (args, cb) { + this.explore(args).then(() => cb()).catch(cb) + } - const pkg = await rpj(resolve(path, 'package.json')).catch(er => { - npm.log.error('explore', `It doesn't look like ${pkgname} is installed.`) - throw er - }) + async explore (args) { + if (args.length < 1 || !args[0]) + throw this.usage - const { shell } = npm.flatOptions - pkg.scripts = { - ...(pkg.scripts || {}), - _explore: args.join(' ').trim() || shell, - } + const pkgname = args.shift() + + // detect and prevent any .. shenanigans + const path = join(this.npm.dir, join('/', pkgname)) + if (relative(path, this.npm.dir) === '') + throw this.usage - if (!args.length) - output(`\nExploring ${path}\nType 'exit' or ^D when finished\n`) - npm.log.disableProgress() - try { - return await runScript({ - ...npm.flatOptions, - pkg, - banner: false, - path, - stdioString: true, - event: '_explore', - stdio: 'inherit', - }).catch(er => { - process.exitCode = typeof er.code === 'number' && er.code !== 0 ? er.code - : 1 - // if it's not an exit error, or non-interactive, throw it - const isProcExit = er.message === 'command failed' && - (typeof er.code === 'number' || /^SIG/.test(er.signal || '')) - if (args.length || !isProcExit) - throw er + // run as if running a script named '_explore', which we set to either + // the set of arguments, or the shell config, and let @npmcli/run-script + // handle all the escaping and PATH setup stuff. + + const pkg = await rpj(resolve(path, 'package.json')).catch(er => { + this.npm.log.error('explore', `It doesn't look like ${pkgname} is installed.`) + throw er }) - } finally { - npm.log.enableProgress() + + const { shell } = this.npm.flatOptions + pkg.scripts = { + ...(pkg.scripts || {}), + _explore: args.join(' ').trim() || shell, + } + + if (!args.length) + output(`\nExploring ${path}\nType 'exit' or ^D when finished\n`) + this.npm.log.disableProgress() + try { + return await runScript({ + ...this.npm.flatOptions, + pkg, + banner: false, + path, + stdioString: true, + event: '_explore', + stdio: 'inherit', + }).catch(er => { + process.exitCode = typeof er.code === 'number' && er.code !== 0 ? er.code + : 1 + // if it's not an exit error, or non-interactive, throw it + const isProcExit = er.message === 'command failed' && + (typeof er.code === 'number' || /^SIG/.test(er.signal || '')) + if (args.length || !isProcExit) + throw er + }) + } finally { + this.npm.log.enableProgress() + } } } - -module.exports = Object.assign(cmd, { completion, usage }) +module.exports = Explore diff --git a/lib/find-dupes.js b/lib/find-dupes.js index 19e7ea6a7c8cc..10575536977d9 100644 --- a/lib/find-dupes.js +++ b/lib/find-dupes.js @@ -2,7 +2,14 @@ const dedupe = require('./dedupe.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil('find-dupes', 'npm find-dupes') -const cmd = (args, cb) => dedupe({ dryRun: true }, cb) +class FindDupes { + get usage () { + return usageUtil('find-dupes', 'npm find-dupes') + } -module.exports = Object.assign(cmd, { usage }) + exec (args, cb) { + // TODO this should really be this.npm.commands.dedupe + dedupe({ dryRun: true }, cb) + } +} +module.exports = FindDupes diff --git a/lib/fund.js b/lib/fund.js index 41dd48c465342..68d6a8e6a50d2 100644 --- a/lib/fund.js +++ b/lib/fund.js @@ -11,200 +11,212 @@ const { isValidFunding, } = require('libnpmfund') -const npm = require('./npm.js') const completion = require('./utils/completion/installed-deep.js') const output = require('./utils/output.js') const openUrl = require('./utils/open-url.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'fund', - 'npm fund', - 'npm fund [--json] [--browser] [--unicode] [[<@scope>/] [--which=]' -) - -const cmd = (args, cb) => fund(args).then(() => cb()).catch(cb) - -function printJSON (fundingInfo) { - return JSON.stringify(fundingInfo, null, 2) -} - const getPrintableName = ({ name, version }) => { const printableVersion = version ? `@${version}` : '' return `${name}${printableVersion}` } -function printHuman (fundingInfo, { color, unicode }) { - const seenUrls = new Map() - - const tree = obj => - archy(obj, '', { unicode }) - - const result = depth({ - tree: fundingInfo, - - // composes human readable package name - // and creates a new archy item for readable output - visit: ({ name, version, funding }) => { - const [fundingSource] = [] - .concat(normalizeFunding(funding)) - .filter(isValidFunding) - const { url } = fundingSource || {} - const pkgRef = getPrintableName({ name, version }) - let item = { - label: pkgRef, - } - - if (url) { - item.label = tree({ - label: color ? chalk.bgBlack.white(url) : url, - nodes: [pkgRef], - }).trim() - - // stacks all packages together under the same item - if (seenUrls.has(url)) { - item = seenUrls.get(url) - item.label += `, ${pkgRef}` - return null - } else - seenUrls.set(url, item) - } - - return item - }, - - // puts child nodes back into returned archy - // output while also filtering out missing items - leave: (item, children) => { - if (item) - item.nodes = children.filter(Boolean) - - return item - }, - - // turns tree-like object return by libnpmfund - // into children to be properly read by treeverse - getChildren: (node) => - Object.keys(node.dependencies || {}) - .map(key => ({ - name: key, - ...node.dependencies[key], - })), - }) - - const res = tree(result) - return color ? chalk.reset(res) : res -} +class Fund { + constructor (npm) { + this.npm = npm + } -async function openFundingUrl ({ path, tree, spec, fundingSourceNumber }) { - const arg = npa(spec, path) - const retrievePackageMetadata = () => { - if (arg.type === 'directory') { - if (tree.path === arg.fetchSpec) { - // matches cwd, e.g: npm fund . - return tree.package - } else { - // matches any file path within current arborist inventory - for (const item of tree.inventory.values()) { - if (item.path === arg.fetchSpec) - return item.package - } - } - } else { - // tries to retrieve a package from arborist inventory - // by matching resulted package name from the provided spec - const [item] = [...tree.inventory.query('name', arg.name)] - .filter(i => semver.valid(i.package.version)) - .sort((a, b) => semver.rcompare(a.package.version, b.package.version)) - - if (item) - return item.package - } + get usage () { + return usageUtil( + 'fund', + 'npm fund', + 'npm fund [--json] [--browser] [--unicode] [[<@scope>/] [--which=]' + ) } - const { funding } = retrievePackageMetadata() || - await pacote.manifest(arg, npm.flatOptions).catch(() => ({})) - - const validSources = [] - .concat(normalizeFunding(funding)) - .filter(isValidFunding) - - const matchesValidSource = - validSources.length === 1 || - (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length) - - if (matchesValidSource) { - const index = fundingSourceNumber ? fundingSourceNumber - 1 : 0 - const { type, url } = validSources[index] - const typePrefix = type ? `${type} funding` : 'Funding' - const msg = `${typePrefix} available at the following URL` - return new Promise((resolve, reject) => - openUrl(url, msg, err => err - ? reject(err) - : resolve() - )) - } else if (validSources.length && !(fundingSourceNumber >= 1)) { - validSources.forEach(({ type, url }, i) => { - const typePrefix = type ? `${type} funding` : 'Funding' - const msg = `${typePrefix} available at the following URL` - output(`${i + 1}: ${msg}: ${url}`) - }) - output('Run `npm fund [<@scope>/] --which=1`, for example, to open the first funding URL listed in that package') - } else { - const noFundingError = new Error(`No valid funding method available for: ${spec}`) - noFundingError.code = 'ENOFUND' + async completion (opts) { + return completion(this.npm, opts) + } - throw noFundingError + exec (args, cb) { + this.fund(args).then(() => cb()).catch(cb) } -} -const fund = async (args) => { - const opts = npm.flatOptions - const spec = args[0] - const numberArg = opts.which + async fund (args) { + const opts = this.npm.flatOptions + const spec = args[0] + const numberArg = opts.which - const fundingSourceNumber = numberArg && parseInt(numberArg, 10) + const fundingSourceNumber = numberArg && parseInt(numberArg, 10) - const badFundingSourceNumber = - numberArg !== undefined && + const badFundingSourceNumber = + numberArg !== undefined && (String(fundingSourceNumber) !== numberArg || fundingSourceNumber < 1) - if (badFundingSourceNumber) { - const err = new Error('`npm fund [<@scope>/] [--which=fundingSourceNumber]` must be given a positive integer') - err.code = 'EFUNDNUMBER' - throw err + if (badFundingSourceNumber) { + const err = new Error('`npm fund [<@scope>/] [--which=fundingSourceNumber]` must be given a positive integer') + err.code = 'EFUNDNUMBER' + throw err + } + + if (opts.global) { + const err = new Error('`npm fund` does not support global packages') + err.code = 'EFUNDGLOBAL' + throw err + } + + const where = this.npm.prefix + const arb = new Arborist({ ...opts, path: where }) + const tree = await arb.loadActual() + + if (spec) { + await this.openFundingUrl({ + path: where, + tree, + spec, + fundingSourceNumber, + }) + return + } + + const print = opts.json + ? this.printJSON + : this.printHuman + + output( + print( + getFundingInfo(tree), + opts + ) + ) } - if (opts.global) { - const err = new Error('`npm fund` does not support global packages') - err.code = 'EFUNDGLOBAL' - throw err + printJSON (fundingInfo) { + return JSON.stringify(fundingInfo, null, 2) } - const where = npm.prefix - const arb = new Arborist({ ...opts, path: where }) - const tree = await arb.loadActual() + printHuman (fundingInfo, { color, unicode }) { + const seenUrls = new Map() + + const tree = obj => + archy(obj, '', { unicode }) + + const result = depth({ + tree: fundingInfo, + + // composes human readable package name + // and creates a new archy item for readable output + visit: ({ name, version, funding }) => { + const [fundingSource] = [] + .concat(normalizeFunding(funding)) + .filter(isValidFunding) + const { url } = fundingSource || {} + const pkgRef = getPrintableName({ name, version }) + let item = { + label: pkgRef, + } + + if (url) { + item.label = tree({ + label: color ? chalk.bgBlack.white(url) : url, + nodes: [pkgRef], + }).trim() + + // stacks all packages together under the same item + if (seenUrls.has(url)) { + item = seenUrls.get(url) + item.label += `, ${pkgRef}` + return null + } else + seenUrls.set(url, item) + } - if (spec) { - await openFundingUrl({ - path: where, - tree, - spec, - fundingSourceNumber, + return item + }, + + // puts child nodes back into returned archy + // output while also filtering out missing items + leave: (item, children) => { + if (item) + item.nodes = children.filter(Boolean) + + return item + }, + + // turns tree-like object return by libnpmfund + // into children to be properly read by treeverse + getChildren: (node) => + Object.keys(node.dependencies || {}) + .map(key => ({ + name: key, + ...node.dependencies[key], + })), }) - return + + const res = tree(result) + return color ? chalk.reset(res) : res } - const print = opts.json - ? printJSON - : printHuman + async openFundingUrl ({ path, tree, spec, fundingSourceNumber }) { + const arg = npa(spec, path) + const retrievePackageMetadata = () => { + if (arg.type === 'directory') { + if (tree.path === arg.fetchSpec) { + // matches cwd, e.g: npm fund . + return tree.package + } else { + // matches any file path within current arborist inventory + for (const item of tree.inventory.values()) { + if (item.path === arg.fetchSpec) + return item.package + } + } + } else { + // tries to retrieve a package from arborist inventory + // by matching resulted package name from the provided spec + const [item] = [...tree.inventory.query('name', arg.name)] + .filter(i => semver.valid(i.package.version)) + .sort((a, b) => semver.rcompare(a.package.version, b.package.version)) + + if (item) + return item.package + } + } + + const { funding } = retrievePackageMetadata() || + await pacote.manifest(arg, this.npm.flatOptions).catch(() => ({})) - output( - print( - getFundingInfo(tree), - opts - ) - ) -} + const validSources = [] + .concat(normalizeFunding(funding)) + .filter(isValidFunding) + + const matchesValidSource = + validSources.length === 1 || + (fundingSourceNumber > 0 && fundingSourceNumber <= validSources.length) + + if (matchesValidSource) { + const index = fundingSourceNumber ? fundingSourceNumber - 1 : 0 + const { type, url } = validSources[index] + const typePrefix = type ? `${type} funding` : 'Funding' + const msg = `${typePrefix} available at the following URL` + return new Promise((resolve, reject) => + openUrl(this.npm, url, msg, err => err + ? reject(err) + : resolve() + )) + } else if (validSources.length && !(fundingSourceNumber >= 1)) { + validSources.forEach(({ type, url }, i) => { + const typePrefix = type ? `${type} funding` : 'Funding' + const msg = `${typePrefix} available at the following URL` + output(`${i + 1}: ${msg}: ${url}`) + }) + output('Run `npm fund [<@scope>/] --which=1`, for example, to open the first funding URL listed in that package') + } else { + const noFundingError = new Error(`No valid funding method available for: ${spec}`) + noFundingError.code = 'ENOFUND' -module.exports = Object.assign(cmd, { usage, completion }) + throw noFundingError + } + } +} +module.exports = Fund diff --git a/lib/get.js b/lib/get.js index 8a416027d7fba..9a636f87d0bdd 100644 --- a/lib/get.js +++ b/lib/get.js @@ -1,15 +1,24 @@ -const npm = require('./npm.js') const config = require('./config.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'get', - 'npm get [ ...] (See `npm config`)' -) +class Get { + constructor (npm) { + this.npm = npm + } -const completion = config.completion + get usage () { + return usageUtil( + 'get', + 'npm get [ ...] (See `npm config`)' + ) + } -const cmd = (args, cb) => - npm.commands.config(['get'].concat(args), cb) + async completion (opts) { + return config.completion(opts) + } -module.exports = Object.assign(cmd, { usage, completion }) + exec (args, cb) { + this.npm.commands.config(['get'].concat(args), cb) + } +} +module.exports = Get diff --git a/lib/help-search.js b/lib/help-search.js index b184735048043..ed2bc23b9109d 100644 --- a/lib/help-search.js +++ b/lib/help-search.js @@ -1,203 +1,211 @@ const fs = require('fs') const path = require('path') -const npm = require('./npm.js') const color = require('ansicolors') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') +const npmUsage = require('./utils/npm-usage.js') const { promisify } = require('util') const glob = promisify(require('glob')) const readFile = promisify(fs.readFile) const didYouMean = require('./utils/did-you-mean.js') const { cmdList } = require('./utils/cmd-list.js') -const usage = usageUtil('help-search', 'npm help-search ') - -const npmUsage = require('./utils/npm-usage.js') +class HelpSearch { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => helpSearch(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil('help-search', 'npm help-search ') + } -const helpSearch = async args => { - if (!args.length) - throw usage + exec (args, cb) { + this.helpSearch(args).then(() => cb()).catch(cb) + } - const docPath = path.resolve(__dirname, '..', 'docs/content') + async helpSearch (args) { + if (!args.length) + throw this.usage + + const docPath = path.resolve(__dirname, '..', 'docs/content') + + const files = await glob(`${docPath}/*/*.md`) + const data = await this.readFiles(files) + const results = await this.searchFiles(args, data, files) + // if only one result, then just show that help section. + if (results.length === 1) { + return this.npm.commands.help([path.basename(results[0].file, '.md')], er => { + if (er) + throw er + }) + } - const files = await glob(`${docPath}/*/*.md`) - const data = await readFiles(files) - const results = await searchFiles(args, data, files) - // if only one result, then just show that help section. - if (results.length === 1) { - return npm.commands.help([path.basename(results[0].file, '.md')], er => { - if (er) - throw er - }) + const formatted = this.formatResults(args, results) + if (!formatted.trim()) + npmUsage(this.npm, false) + else { + output(formatted) + output(didYouMean(args[0], cmdList)) + } } - const formatted = formatResults(args, results) - if (!formatted.trim()) - npmUsage(false) - else { - output(formatted) - output(didYouMean(args[0], cmdList)) + async readFiles (files) { + const res = {} + await Promise.all(files.map(async file => { + res[file] = (await readFile(file, 'utf8')) + .replace(/^---\n(.*\n)*?---\n/, '').trim() + })) + return res } -} -const readFiles = async files => { - const res = {} - await Promise.all(files.map(async file => { - res[file] = (await readFile(file, 'utf8')) - .replace(/^---\n(.*\n)*?---\n/, '').trim() - })) - return res -} + async searchFiles (args, data, files) { + const results = [] + for (const [file, content] of Object.entries(data)) { + const lowerCase = content.toLowerCase() + // skip if no matches at all + if (!args.some(a => lowerCase.includes(a.toLowerCase()))) + continue + + const lines = content.split(/\n+/) + + // if a line has a search term, then skip it and the next line. + // if the next line has a search term, then skip all 3 + // otherwise, set the line to null. then remove the nulls. + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const nextLine = lines[i + 1] + let match = false + if (nextLine) { + match = args.some(a => + nextLine.toLowerCase().includes(a.toLowerCase())) + if (match) { + // skip over the next line, and the line after it. + i += 2 + continue + } + } + + match = args.some(a => line.toLowerCase().includes(a.toLowerCase())) -const searchFiles = async (args, data, files) => { - const results = [] - for (const [file, content] of Object.entries(data)) { - const lowerCase = content.toLowerCase() - // skip if no matches at all - if (!args.some(a => lowerCase.includes(a.toLowerCase()))) - continue - - const lines = content.split(/\n+/) - - // if a line has a search term, then skip it and the next line. - // if the next line has a search term, then skip all 3 - // otherwise, set the line to null. then remove the nulls. - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const nextLine = lines[i + 1] - let match = false - if (nextLine) { - match = args.some(a => nextLine.toLowerCase().includes(a.toLowerCase())) if (match) { - // skip over the next line, and the line after it. - i += 2 + // skip over the next line + i++ continue } - } - match = args.some(a => line.toLowerCase().includes(a.toLowerCase())) - - if (match) { - // skip over the next line - i++ - continue + lines[i] = null } - lines[i] = null - } - - // now squish any string of nulls into a single null - const pruned = lines.reduce((l, r) => { - if (!(r === null && l[l.length - 1] === null)) - l.push(r) + // now squish any string of nulls into a single null + const pruned = lines.reduce((l, r) => { + if (!(r === null && l[l.length - 1] === null)) + l.push(r) - return l - }, []) + return l + }, []) - if (pruned[pruned.length - 1] === null) - pruned.pop() + if (pruned[pruned.length - 1] === null) + pruned.pop() - if (pruned[0] === null) - pruned.shift() + if (pruned[0] === null) + pruned.shift() - // now count how many args were found - const found = {} - let totalHits = 0 - for (const line of pruned) { - for (const arg of args) { - const hit = (line || '').toLowerCase() - .split(arg.toLowerCase()).length - 1 + // now count how many args were found + const found = {} + let totalHits = 0 + for (const line of pruned) { + for (const arg of args) { + const hit = (line || '').toLowerCase() + .split(arg.toLowerCase()).length - 1 - if (hit > 0) { - found[arg] = (found[arg] || 0) + hit - totalHits += hit + if (hit > 0) { + found[arg] = (found[arg] || 0) + hit + totalHits += hit + } } } + + const cmd = 'npm help ' + + path.basename(file, '.md').replace(/^npm-/, '') + results.push({ + file, + cmd, + lines: pruned, + found: Object.keys(found), + hits: found, + totalHits, + }) } - const cmd = 'npm help ' + - path.basename(file, '.md').replace(/^npm-/, '') - results.push({ - file, - cmd, - lines: pruned, - found: Object.keys(found), - hits: found, - totalHits, - }) + // sort results by number of results found, then by number of hits + // then by number of matching lines + + // coverage is ignored here because the contents of results are + // nondeterministic due to either glob or readFiles or Object.entries + return results.sort(/* istanbul ignore next */ (a, b) => + a.found.length > b.found.length ? -1 + : a.found.length < b.found.length ? 1 + : a.totalHits > b.totalHits ? -1 + : a.totalHits < b.totalHits ? 1 + : a.lines.length > b.lines.length ? -1 + : a.lines.length < b.lines.length ? 1 + : 0).slice(0, 10) } - // sort results by number of results found, then by number of hits - // then by number of matching lines - - // coverage is ignored here because the contents of results are - // nondeterministic due to either glob or readFiles or Object.entries - return results.sort(/* istanbul ignore next */ (a, b) => - a.found.length > b.found.length ? -1 - : a.found.length < b.found.length ? 1 - : a.totalHits > b.totalHits ? -1 - : a.totalHits < b.totalHits ? 1 - : a.lines.length > b.lines.length ? -1 - : a.lines.length < b.lines.length ? 1 - : 0).slice(0, 10) -} - -const formatResults = (args, results) => { - const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 + formatResults (args, results) { + const cols = Math.min(process.stdout.columns || Infinity, 80) + 1 - const out = results.map(res => { - const out = [res.cmd] - const r = Object.keys(res.hits) - .map(k => `${k}:${res.hits[k]}`) - .sort((a, b) => a > b ? 1 : -1) - .join(' ') + const out = results.map(res => { + const out = [res.cmd] + const r = Object.keys(res.hits) + .map(k => `${k}:${res.hits[k]}`) + .sort((a, b) => a > b ? 1 : -1) + .join(' ') - out.push(' '.repeat((Math.max(1, cols - out.join(' ').length - r.length - 1)))) - out.push(r) + out.push(' '.repeat((Math.max(1, cols - out.join(' ').length - r.length - 1)))) + out.push(r) - if (!npm.flatOptions.long) - return out.join('') + if (!this.npm.flatOptions.long) + return out.join('') - out.unshift('\n\n') - out.push('\n') - out.push('-'.repeat(cols - 1) + '\n') - res.lines.forEach((line, i) => { - if (line === null || i > 3) - return + out.unshift('\n\n') + out.push('\n') + out.push('-'.repeat(cols - 1) + '\n') + res.lines.forEach((line, i) => { + if (line === null || i > 3) + return - if (!npm.color) { - out.push(line + '\n') - return - } - const hilitLine = [] - for (const arg of args) { - const finder = line.toLowerCase().split(arg.toLowerCase()) - let p = 0 - for (const f of finder) { - hilitLine.push(line.substr(p, f.length)) - const word = line.substr(p + f.length, arg.length) - const hilit = color.bgBlack(color.red(word)) - hilitLine.push(hilit) - p += f.length + arg.length + if (!this.npm.color) { + out.push(line + '\n') + return } - } - out.push(hilitLine.join('') + '\n') - }) + const hilitLine = [] + for (const arg of args) { + const finder = line.toLowerCase().split(arg.toLowerCase()) + let p = 0 + for (const f of finder) { + hilitLine.push(line.substr(p, f.length)) + const word = line.substr(p + f.length, arg.length) + const hilit = color.bgBlack(color.red(word)) + hilitLine.push(hilit) + p += f.length + arg.length + } + } + out.push(hilitLine.join('') + '\n') + }) - return out.join('') - }).join('\n') + return out.join('') + }).join('\n') - const finalOut = results.length && !npm.flatOptions.long - ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' + + const finalOut = results.length && !this.npm.flatOptions.long + ? 'Top hits for ' + (args.map(JSON.stringify).join(' ')) + '\n' + '—'.repeat(cols - 1) + '\n' + out + '\n' + '—'.repeat(cols - 1) + '\n' + '(run with -l or --long to see more context)' - : out + : out - return finalOut.trim() + return finalOut.trim() + } } - -module.exports = Object.assign(cmd, { usage }) +module.exports = HelpSearch diff --git a/lib/help.js b/lib/help.js index 6f215c76c1ead..012605c34bf22 100644 --- a/lib/help.js +++ b/lib/help.js @@ -1,30 +1,6 @@ - -module.exports = help - -help.completion = async (opts) => { - if (opts.conf.argv.remain.length > 2) - return [] - const g = path.resolve(__dirname, '../man/man[0-9]/*.[0-9]') - const files = await new Promise((resolve, reject) => { - glob(g, function (er, files) { - if (er) - return reject(er) - resolve(files) - }) - }) - - return Object.keys(files.reduce(function (acc, file) { - file = path.basename(file).replace(/\.[0-9]+$/, '') - file = file.replace(/^npm-/, '') - acc[file] = true - return acc - }, { help: true })) -} - const npmUsage = require('./utils/npm-usage.js') const { spawn } = require('child_process') const path = require('path') -const npm = require('./npm.js') const log = require('npmlog') const openUrl = require('./utils/open-url') const glob = require('glob') @@ -32,160 +8,214 @@ const output = require('./utils/output.js') const usage = require('./utils/usage.js') -help.usage = usage('help', 'npm help []') - -function help (args, cb) { - const argv = npm.config.parsedArgv.cooked +class Help { + constructor (npm) { + this.npm = npm + } - let argnum = 0 - if (args.length === 2 && ~~args[0]) - argnum = ~~args.shift() + get usage () { + return usage('help', 'npm help []') + } - // npm help foo bar baz: search topics - if (args.length > 1 && args[0]) - return npm.commands['help-search'](args, cb) + async completion (opts) { + if (opts.conf.argv.remain.length > 2) + return [] + const g = path.resolve(__dirname, '../man/man[0-9]/*.[0-9]') + const files = await new Promise((resolve, reject) => { + glob(g, function (er, files) { + if (er) + return reject(er) + resolve(files) + }) + }) - const affordances = { - 'find-dupes': 'dedupe', + return Object.keys(files.reduce(function (acc, file) { + file = path.basename(file).replace(/\.[0-9]+$/, '') + file = file.replace(/^npm-/, '') + acc[file] = true + return acc + }, { help: true })) } - let section = affordances[args[0]] || npm.deref(args[0]) || args[0] - // npm help : show basic usage - if (!section) { - npmUsage(argv[0] === 'help') - return cb() + exec (args, cb) { + this.help(args).then(() => cb()).catch(cb) } - // npm -h: show command usage - if (npm.config.get('usage') && - npm.commands[section] && - npm.commands[section].usage) { - npm.config.set('loglevel', 'silent') - log.level = 'silent' - output(npm.commands[section].usage) - return cb() + async help (args) { + const argv = this.npm.config.parsedArgv.cooked + + let argnum = 0 + if (args.length === 2 && ~~args[0]) + argnum = ~~args.shift() + + // npm help foo bar baz: search topics + if (args.length > 1 && args[0]) + return this.helpSearch(args) + + const affordances = { + 'find-dupes': 'dedupe', + } + let section = affordances[args[0]] || this.npm.deref(args[0]) || args[0] + + // npm help : show basic usage + if (!section) { + npmUsage(this.npm, argv[0] === 'help') + return + } + + // npm -h: show command usage + if (this.npm.config.get('usage') && + this.npm.commands[section] && + this.npm.commands[section].usage) { + this.npm.config.set('loglevel', 'silent') + log.level = 'silent' + output(this.npm.commands[section].usage) + return + } + + let pref = [1, 5, 7] + if (argnum) + pref = [argnum].concat(pref.filter(n => n !== argnum)) + + // npm help
: Try to find the path + const manroot = path.resolve(__dirname, '..', 'man') + + // legacy + if (section === 'global') + section = 'folders' + else if (section.match(/.*json/)) + section = section.replace('.json', '-json') + + // find either /section.n or /npm-section.n + // The glob is used in the glob. The regexp is used much + // further down. Globs and regexps are different + const compextglob = '.+(gz|bz2|lzma|[FYzZ]|xz)' + const compextre = '\\.(gz|bz2|lzma|[FYzZ]|xz)$' + const f = '+(npm-' + section + '|' + section + ').[0-9]?(' + compextglob + ')' + return new Promise((resolve, reject) => { + glob(manroot + '/*/' + f, async (er, mans) => { + if (er) + return reject(er) + + if (!mans.length) { + await this.helpSearch(args) + resolve() + return + } + + mans = mans.map((man) => { + const ext = path.extname(man) + if (man.match(new RegExp(compextre))) + man = path.basename(man, ext) + + return man + }) + + this.viewMan(this.pickMan(mans, pref), (err) => { + if (err) + return reject(err) + return resolve() + }) + }) + }) } - let pref = [1, 5, 7] - if (argnum) - pref = [argnum].concat(pref.filter(n => n !== argnum)) - - // npm help
: Try to find the path - const manroot = path.resolve(__dirname, '..', 'man') - - // legacy - if (section === 'global') - section = 'folders' - else if (section.match(/.*json/)) - section = section.replace('.json', '-json') - - // find either /section.n or /npm-section.n - // The glob is used in the glob. The regexp is used much - // further down. Globs and regexps are different - const compextglob = '.+(gz|bz2|lzma|[FYzZ]|xz)' - const compextre = '\\.(gz|bz2|lzma|[FYzZ]|xz)$' - const f = '+(npm-' + section + '|' + section + ').[0-9]?(' + compextglob + ')' - return glob(manroot + '/*/' + f, (er, mans) => { - if (er) - return cb(er) - - if (!mans.length) - return npm.commands['help-search'](args, cb) - - mans = mans.map((man) => { - const ext = path.extname(man) - if (man.match(new RegExp(compextre))) - man = path.basename(man, ext) - - return man + helpSearch (args) { + return new Promise((resolve, reject) => { + this.npm.commands['help-search'](args, (err) => { + if (err) + return reject(err) + resolve() + }) }) - - viewMan(pickMan(mans, pref), cb) - }) -} - -function pickMan (mans, pref_) { - const nre = /([0-9]+)$/ - const pref = {} - pref_.forEach((sect, i) => pref[sect] = i) - mans = mans.sort((a, b) => { - const an = a.match(nre)[1] - const bn = b.match(nre)[1] - return an === bn ? (a > b ? -1 : 1) - : pref[an] < pref[bn] ? -1 - : 1 - }) - return mans[0] -} - -function viewMan (man, cb) { - const nre = /([0-9]+)$/ - const num = man.match(nre)[1] - const section = path.basename(man, '.' + num) - - // at this point, we know that the specified man page exists - const manpath = path.join(__dirname, '..', 'man') - const env = {} - Object.keys(process.env).forEach(function (i) { - env[i] = process.env[i] - }) - env.MANPATH = manpath - const viewer = npm.config.get('viewer') - - const opts = { - env, - stdio: 'inherit', } - let bin = 'man' - const args = [] - switch (viewer) { - case 'woman': - bin = 'emacsclient' - args.push('-e', `(woman-find-file '${man}')`) - break - - case 'browser': - bin = false - try { - const url = htmlMan(man) - openUrl(url, 'help available at the following URL', cb) - } catch (err) { - return cb(err) - } - break - - default: - args.push(num, section) - break + pickMan (mans, pref_) { + const nre = /([0-9]+)$/ + const pref = {} + pref_.forEach((sect, i) => pref[sect] = i) + mans = mans.sort((a, b) => { + const an = a.match(nre)[1] + const bn = b.match(nre)[1] + return an === bn ? (a > b ? -1 : 1) + : pref[an] < pref[bn] ? -1 + : 1 + }) + return mans[0] } - if (bin) { - const proc = spawn(bin, args, opts) - proc.on('exit', (code) => { - if (code) - return cb(new Error(`help process exited with code: ${code}`)) - - return cb() + viewMan (man, cb) { + if (!man) { + } + const nre = /([0-9]+)$/ + const num = man.match(nre)[1] + const section = path.basename(man, '.' + num) + + // at this point, we know that the specified man page exists + const manpath = path.join(__dirname, '..', 'man') + const env = {} + Object.keys(process.env).forEach(function (i) { + env[i] = process.env[i] }) + env.MANPATH = manpath + const viewer = this.npm.config.get('viewer') + + const opts = { + env, + stdio: 'inherit', + } + + let bin = 'man' + const args = [] + switch (viewer) { + case 'woman': + bin = 'emacsclient' + args.push('-e', `(woman-find-file '${man}')`) + break + + case 'browser': + bin = false + try { + const url = this.htmlMan(man) + openUrl(url, 'help available at the following URL', cb) + } catch (err) { + return cb(err) + } + break + + default: + args.push(num, section) + break + } + + if (bin) { + const proc = spawn(bin, args, opts) + proc.on('exit', (code) => { + if (code) + return cb(new Error(`help process exited with code: ${code}`)) + + return cb() + }) + } } -} -function htmlMan (man) { - let sect = +man.match(/([0-9]+)$/)[1] - const f = path.basename(man).replace(/[.]([0-9]+)$/, '') - switch (sect) { - case 1: - sect = 'commands' - break - case 5: - sect = 'configuring-npm' - break - case 7: - sect = 'using-npm' - break - default: - throw new Error('invalid man section: ' + sect) + htmlMan (man) { + let sect = +man.match(/([0-9]+)$/)[1] + const f = path.basename(man).replace(/[.]([0-9]+)$/, '') + switch (sect) { + case 1: + sect = 'commands' + break + case 5: + sect = 'configuring-npm' + break + case 7: + sect = 'using-npm' + break + default: + throw new Error('invalid man section: ' + sect) + } + return 'file://' + path.resolve(__dirname, '..', 'docs', 'output', sect, f + '.html') } - return 'file://' + path.resolve(__dirname, '..', 'docs', 'output', sect, f + '.html') } +module.exports = Help diff --git a/lib/hook.js b/lib/hook.js index 7d69ccbf2aa4c..312f542d7cff6 100644 --- a/lib/hook.js +++ b/lib/hook.js @@ -1,53 +1,62 @@ const hookApi = require('libnpmhook') -const npm = require('./npm.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') const relativeDate = require('tiny-relative-date') const Table = require('cli-table3') - const usageUtil = require('./utils/usage.js') -const usage = usageUtil('hook', [ - 'npm hook add [--type=]', - 'npm hook ls [pkg]', - 'npm hook rm ', - 'npm hook update ', -].join('\n')) -const cmd = (args, cb) => hook(args).then(() => cb()).catch(cb) +class Hook { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('hook', [ + 'npm hook add [--type=]', + 'npm hook ls [pkg]', + 'npm hook rm ', + 'npm hook update ', + ].join('\n')) + } + + exec (args, cb) { + this.hook(args).then(() => cb()).catch(cb) + } -const hook = async (args) => otplease(npm.flatOptions, opts => { - switch (args[0]) { - case 'add': - return add(args[1], args[2], args[3], opts) - case 'ls': - return ls(args[1], opts) - case 'rm': - return rm(args[1], opts) - case 'update': - case 'up': - return update(args[1], args[2], args[3], opts) - default: - throw usage + async hook (args) { + return otplease(this.npm.flatOptions, (opts) => { + switch (args[0]) { + case 'add': + return this.add(args[1], args[2], args[3], opts) + case 'ls': + return this.ls(args[1], opts) + case 'rm': + return this.rm(args[1], opts) + case 'update': + case 'up': + return this.update(args[1], args[2], args[3], opts) + default: + throw this.usage + } + }) } -}) -const add = (pkg, uri, secret, opts) => { - hookApi.add(pkg, uri, secret, opts).then(hook => { + async add (pkg, uri, secret, opts) { + const hook = await hookApi.add(pkg, uri, secret, opts) if (opts.json) output(JSON.stringify(hook, null, 2)) else if (opts.parseable) { output(Object.keys(hook).join('\t')) output(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!opts.silent && opts.loglevel !== 'silent') { - output(`+ ${hookName(hook)} ${ + output(`+ ${this.hookName(hook)} ${ opts.unicode ? ' ➜ ' : ' -> ' } ${hook.endpoint}`) } - }) -} + } -const ls = (pkg, opts) => { - return hookApi.ls({ ...opts, package: pkg }).then(hooks => { + async ls (pkg, opts) { + const hooks = await hookApi.ls({ ...opts, package: pkg }) if (opts.json) output(JSON.stringify(hooks, null, 2)) else if (opts.parseable) { @@ -67,7 +76,7 @@ const ls = (pkg, opts) => { hooks.forEach((hook) => { table.push([ { rowSpan: 2, content: hook.id }, - hookName(hook), + this.hookName(hook), hook.endpoint, ]) if (hook.last_delivery) { @@ -83,46 +92,43 @@ const ls = (pkg, opts) => { }) output(table.toString()) } - }) -} + } -const rm = (id, opts) => { - return hookApi.rm(id, opts).then(hook => { + async rm (id, opts) { + const hook = await hookApi.rm(id, opts) if (opts.json) output(JSON.stringify(hook, null, 2)) else if (opts.parseable) { output(Object.keys(hook).join('\t')) output(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!opts.silent && opts.loglevel !== 'silent') { - output(`- ${hookName(hook)} ${ + output(`- ${this.hookName(hook)} ${ opts.unicode ? ' ✘ ' : ' X ' } ${hook.endpoint}`) } - }) -} + } -const update = (id, uri, secret, opts) => { - return hookApi.update(id, uri, secret, opts).then(hook => { + async update (id, uri, secret, opts) { + const hook = await hookApi.update(id, uri, secret, opts) if (opts.json) output(JSON.stringify(hook, null, 2)) else if (opts.parseable) { output(Object.keys(hook).join('\t')) output(Object.keys(hook).map(k => hook[k]).join('\t')) } else if (!opts.silent && opts.loglevel !== 'silent') { - output(`+ ${hookName(hook)} ${ + output(`+ ${this.hookName(hook)} ${ opts.unicode ? ' ➜ ' : ' -> ' } ${hook.endpoint}`) } - }) -} + } -const hookName = (hook) => { - let target = hook.name - if (hook.type === 'scope') - target = '@' + target - if (hook.type === 'owner') - target = '~' + target - return target + hookName (hook) { + let target = hook.name + if (hook.type === 'scope') + target = '@' + target + if (hook.type === 'owner') + target = '~' + target + return target + } } - -module.exports = Object.assign(cmd, { usage }) +module.exports = Hook diff --git a/lib/init.js b/lib/init.js index a029779f89638..ce82afa1a98af 100644 --- a/lib/init.js +++ b/lib/init.js @@ -1,88 +1,96 @@ const initJson = require('init-package-json') const npa = require('npm-package-arg') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const output = require('./utils/output.js') -const usage = usageUtil( - 'init', - '\nnpm init [--force|-f|--yes|-y|--scope]' + - '\nnpm init <@scope> (same as `npx <@scope>/create`)' + - '\nnpm init [<@scope>/] (same as `npx [<@scope>/]create-`)' -) - -const cmd = (args, cb) => init(args).then(() => cb()).catch(cb) +class Init { + constructor (npm) { + this.npm = npm + } -const init = async args => { - // the new npx style way - if (args.length) { - const initerName = args[0] - let packageName = initerName - if (/^@[^/]+$/.test(initerName)) - packageName = initerName + '/create' - else { - const req = npa(initerName) - if (req.type === 'git' && req.hosted) { - const { user, project } = req.hosted - packageName = initerName - .replace(user + '/' + project, user + '/create-' + project) - } else if (req.registry) { - packageName = req.name.replace(/^(@[^/]+\/)?/, '$1create-') - if (req.rawSpec) - packageName += '@' + req.rawSpec - } else { - throw Object.assign(new Error( - 'Unrecognized initializer: ' + initerName + - '\nFor more package binary executing power check out `npx`:' + - '\nhttps://www.npmjs.com/package/npx' - ), { code: 'EUNSUPPORTED' }) - } - } - npm.config.set('package', []) - const newArgs = [packageName, ...args.slice(1)] - return new Promise((res, rej) => { - npm.commands.exec(newArgs, er => er ? rej(er) : res()) - }) + get usage () { + return usageUtil( + 'init', + '\nnpm init [--force|-f|--yes|-y|--scope]' + + '\nnpm init <@scope> (same as `npx <@scope>/create`)' + + '\nnpm init [<@scope>/] (same as `npx [<@scope>/]create-`)' + ) } - // the old way - const dir = process.cwd() - npm.log.pause() - npm.log.disableProgress() - const initFile = npm.config.get('init-module') - if (!npm.flatOptions.yes && !npm.flatOptions.force) { - output([ - 'This utility will walk you through creating a package.json file.', - 'It only covers the most common items, and tries to guess sensible defaults.', - '', - 'See `npm help init` for definitive documentation on these fields', - 'and exactly what they do.', - '', - 'Use `npm install ` afterwards to install a package and', - 'save it as a dependency in the package.json file.', - '', - 'Press ^C at any time to quit.', - ].join('\n')) + exec (args, cb) { + this.init(args).then(() => cb()).catch(cb) } - // XXX promisify init-package-json - await new Promise((res, rej) => { - initJson(dir, initFile, npm.config, (er, data) => { - npm.log.resume() - npm.log.enableProgress() - npm.log.silly('package data', data) - if (er && er.message === 'canceled') { - npm.log.warn('init', 'canceled') - return res() - } - if (er) - rej(er) + + async init (args) { + // the new npx style way + if (args.length) { + const initerName = args[0] + let packageName = initerName + if (/^@[^/]+$/.test(initerName)) + packageName = initerName + '/create' else { - npm.log.info('init', 'written successfully') - res(data) + const req = npa(initerName) + if (req.type === 'git' && req.hosted) { + const { user, project } = req.hosted + packageName = initerName + .replace(user + '/' + project, user + '/create-' + project) + } else if (req.registry) { + packageName = req.name.replace(/^(@[^/]+\/)?/, '$1create-') + if (req.rawSpec) + packageName += '@' + req.rawSpec + } else { + throw Object.assign(new Error( + 'Unrecognized initializer: ' + initerName + + '\nFor more package binary executing power check out `npx`:' + + '\nhttps://www.npmjs.com/package/npx' + ), { code: 'EUNSUPPORTED' }) + } } + this.npm.config.set('package', []) + const newArgs = [packageName, ...args.slice(1)] + return new Promise((res, rej) => { + this.npm.commands.exec(newArgs, er => er ? rej(er) : res()) + }) + } + + // the old way + const dir = process.cwd() + this.npm.log.pause() + this.npm.log.disableProgress() + const initFile = this.npm.config.get('init-module') + if (!this.npm.flatOptions.yes && !this.npm.flatOptions.force) { + output([ + 'This utility will walk you through creating a package.json file.', + 'It only covers the most common items, and tries to guess sensible defaults.', + '', + 'See `npm help init` for definitive documentation on these fields', + 'and exactly what they do.', + '', + 'Use `npm install ` afterwards to install a package and', + 'save it as a dependency in the package.json file.', + '', + 'Press ^C at any time to quit.', + ].join('\n')) + } + // XXX promisify init-package-json + await new Promise((res, rej) => { + initJson(dir, initFile, this.npm.config, (er, data) => { + this.npm.log.resume() + this.npm.log.enableProgress() + this.npm.log.silly('package data', data) + if (er && er.message === 'canceled') { + this.npm.log.warn('init', 'canceled') + return res() + } + if (er) + rej(er) + else { + this.npm.log.info('init', 'written successfully') + res(data) + } + }) }) - }) + } } - -module.exports = Object.assign(cmd, { usage }) +module.exports = Init diff --git a/lib/install-ci-test.js b/lib/install-ci-test.js index 52c41c413a64c..d1740999d4b67 100644 --- a/lib/install-ci-test.js +++ b/lib/install-ci-test.js @@ -1,19 +1,27 @@ // npm install-ci-test // Runs `npm ci` and then runs `npm test` -const ci = require('./ci.js') -const test = require('./test.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'install-ci-test', - 'npm install-ci-test [args]' + - '\nSame args as `npm ci`' -) +class InstallCITest { + constructor (npm) { + this.npm = npm + } -const completion = ci.completion + get usage () { + return usageUtil( + 'install-ci-test', + 'npm install-ci-test [args]' + + '\nSame args as `npm ci`' + ) + } -const ciTest = (args, cb) => - ci(args, er => er ? cb(er) : test([], cb)) - -module.exports = Object.assign(ciTest, { usage, completion }) + exec (args, cb) { + this.npm.commands.ci(args, (er) => { + if (er) + return cb(er) + this.npm.commands.test([], cb) + }) + } +} +module.exports = InstallCITest diff --git a/lib/install-test.js b/lib/install-test.js index 9593361e320b8..487f8da00b6d3 100644 --- a/lib/install-test.js +++ b/lib/install-test.js @@ -1,19 +1,31 @@ // npm install-test // Runs `npm install` and then runs `npm test` -const install = require('./install.js') -const test = require('./test.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'install-test', - 'npm install-test [args]' + - '\nSame args as `npm install`' -) +class InstallTest { + constructor (npm) { + this.npm = npm + } -const completion = install.completion + get usage () { + return usageUtil( + 'install-test', + 'npm install-test [args]' + + '\nSame args as `npm install`' + ) + } -const installTest = (args, cb) => - install(args, er => er ? cb(er) : test([], cb)) + async completion (opts) { + return this.npm.commands.install.completion(opts) + } -module.exports = Object.assign(installTest, { usage, completion }) + exec (args, cb) { + this.npm.commands.install(args, (er) => { + if (er) + return cb(er) + this.npm.commands.test([], cb) + }) + } +} +module.exports = InstallTest diff --git a/lib/install.js b/lib/install.js index 5f0137db1ceac..cc189ab323d54 100644 --- a/lib/install.js +++ b/lib/install.js @@ -3,7 +3,6 @@ const fs = require('fs') const util = require('util') const readdir = util.promisify(fs.readdir) -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const reifyFinish = require('./utils/reify-finish.js') const log = require('npmlog') @@ -11,133 +10,142 @@ const { resolve, join } = require('path') const Arborist = require('@npmcli/arborist') const runScript = require('@npmcli/run-script') -const cmd = async (args, cb) => install(args).then(() => cb()).catch(cb) - -const install = async args => { - // the /path/to/node_modules/.. - const globalTop = resolve(npm.globalDir, '..') - const { ignoreScripts, global: isGlobalInstall } = npm.flatOptions - const where = isGlobalInstall ? globalTop : npm.prefix - - // don't try to install the prefix into itself - args = args.filter(a => resolve(a) !== npm.prefix) - - // `npm i -g` => "install this package globally" - if (where === globalTop && !args.length) - args = ['.'] - - // TODO: Add warnings for other deprecated flags? or remove this one? - if (npm.config.get('dev')) - log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.') - - const arb = new Arborist({ - ...npm.flatOptions, - path: where, - }) +class Install { + constructor (npm) { + this.npm = npm + } - await arb.reify({ - ...npm.flatOptions, - add: args, - }) - if (!args.length && !isGlobalInstall && !ignoreScripts) { - const { scriptShell } = npm.flatOptions - const scripts = [ - 'preinstall', + get usage () { + return usageUtil( 'install', - 'postinstall', - 'prepublish', // XXX should we remove this finally?? - 'preprepare', - 'prepare', - 'postprepare', - ] - for (const event of scripts) { - await runScript({ - path: where, - args: [], - scriptShell, - stdio: 'inherit', - stdioString: true, - banner: log.level !== 'silent', - event, - }) - } + 'npm install (with no args, in package dir)' + + '\nnpm install [<@scope>/]' + + '\nnpm install [<@scope>/]@' + + '\nnpm install [<@scope>/]@' + + '\nnpm install [<@scope>/]@' + + '\nnpm install @npm:' + + '\nnpm install ' + + '\nnpm install ' + + '\nnpm install ' + + '\nnpm install ' + + '\nnpm install /', + '[--save-prod|--save-dev|--save-optional|--save-peer] [--save-exact] [--no-save]' + ) } - await reifyFinish(arb) -} -const usage = usageUtil( - 'install', - 'npm install (with no args, in package dir)' + - '\nnpm install [<@scope>/]' + - '\nnpm install [<@scope>/]@' + - '\nnpm install [<@scope>/]@' + - '\nnpm install [<@scope>/]@' + - '\nnpm install @npm:' + - '\nnpm install ' + - '\nnpm install ' + - '\nnpm install ' + - '\nnpm install ' + - '\nnpm install /', - '[--save-prod|--save-dev|--save-optional|--save-peer] [--save-exact] [--no-save]' -) + 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 + // if it starts with https?://, then just give up, because it's a url + if (/^https?:\/\//.test(partialWord)) { + // do not complete to URLs + return [] + } -const completion = async (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 - // if it starts with https?://, then just give up, because it's a url - if (/^https?:\/\//.test(partialWord)) { - // do not complete to URLs - return [] - } + if (/\//.test(partialWord)) { + // Complete fully to folder if there is exactly one match and it + // is a folder containing a package.json file. If that is not the + // case we return 0 matches, which will trigger the default bash + // complete. + const lastSlashIdx = partialWord.lastIndexOf('/') + const partialName = partialWord.slice(lastSlashIdx + 1) + const partialPath = partialWord.slice(0, lastSlashIdx) || '/' - if (/\//.test(partialWord)) { - // Complete fully to folder if there is exactly one match and it - // is a folder containing a package.json file. If that is not the - // case we return 0 matches, which will trigger the default bash - // complete. - const lastSlashIdx = partialWord.lastIndexOf('/') - const partialName = partialWord.slice(lastSlashIdx + 1) - const partialPath = partialWord.slice(0, lastSlashIdx) || '/' + const annotatePackageDirMatch = async (sibling) => { + const fullPath = join(partialPath, sibling) + if (sibling.slice(0, partialName.length) !== partialName) + return null // not name match - const annotatePackageDirMatch = async (sibling) => { - const fullPath = join(partialPath, sibling) - if (sibling.slice(0, partialName.length) !== partialName) - return null // not name match + try { + const contents = await readdir(fullPath) + return { + fullPath, + isPackage: contents.indexOf('package.json') !== -1, + } + } catch (er) { + return { isPackage: false } + } + } try { - const contents = await readdir(fullPath) - return { - fullPath, - isPackage: contents.indexOf('package.json') !== -1, + const siblings = await readdir(partialPath) + const matches = await Promise.all( + siblings.map(async sibling => { + return await annotatePackageDirMatch(sibling) + }) + ) + const match = matches.filter(el => !el || el.isPackage).pop() + if (match) { + // Success - only one match and it is a package dir + return [match.fullPath] + } else { + // no matches + return [] } } catch (er) { - return { isPackage: false } + return [] // invalid dir: no matching } } + // Note: there used to be registry completion here, + // but it stopped making sense somewhere around + // 50,000 packages on the registry + } + + exec (args, cb) { + this.install(args).then(() => cb()).catch(cb) + } + + async install (args) { + // the /path/to/node_modules/.. + const globalTop = resolve(this.npm.globalDir, '..') + const { ignoreScripts, global: isGlobalInstall } = this.npm.flatOptions + const where = isGlobalInstall ? globalTop : this.npm.prefix - try { - const siblings = await readdir(partialPath) - const matches = await Promise.all( - siblings.map(async sibling => { - return await annotatePackageDirMatch(sibling) + // don't try to install the prefix into itself + args = args.filter(a => resolve(a) !== this.npm.prefix) + + // `npm i -g` => "install this package globally" + if (where === globalTop && !args.length) + args = ['.'] + + // TODO: Add warnings for other deprecated flags? or remove this one? + if (this.npm.config.get('dev')) + log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.') + + const arb = new Arborist({ + ...this.npm.flatOptions, + path: where, + }) + + await arb.reify({ + ...this.npm.flatOptions, + add: args, + }) + if (!args.length && !isGlobalInstall && !ignoreScripts) { + const { scriptShell } = this.npm.flatOptions + const scripts = [ + 'preinstall', + 'install', + 'postinstall', + 'prepublish', // XXX should we remove this finally?? + 'preprepare', + 'prepare', + 'postprepare', + ] + for (const event of scripts) { + await runScript({ + path: where, + args: [], + scriptShell, + stdio: 'inherit', + stdioString: true, + banner: log.level !== 'silent', + event, }) - ) - const match = matches.filter(el => !el || el.isPackage).pop() - if (match) { - // Success - only one match and it is a package dir - return [match.fullPath] - } else { - // no matches - return [] } - } catch (er) { - return [] // invalid dir: no matching } + await reifyFinish(arb) } - // Note: there used to be registry completion here, - // but it stopped making sense somewhere around - // 50,000 packages on the registry } - -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = Install diff --git a/lib/link.js b/lib/link.js index 0bb3d87b5e7d4..4f0bf556a61d9 100644 --- a/lib/link.js +++ b/lib/link.js @@ -8,145 +8,153 @@ const npa = require('npm-package-arg') const rpj = require('read-package-json-fast') const semver = require('semver') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const reifyFinish = require('./utils/reify-finish.js') -const completion = async (opts) => { - const dir = npm.globalDir - const files = await readdir(dir) - return files.filter(f => !/^[._-]/.test(f)) -} +class Link { + constructor (npm) { + this.npm = npm + } -const usage = usageUtil( - 'link', - 'npm link (in package dir)' + - '\nnpm link [<@scope>/][@]' -) - -const cmd = (args, cb) => link(args).then(() => cb()).catch(cb) - -const link = async args => { - if (npm.config.get('global')) { - throw Object.assign( - new Error( - 'link should never be --global.\n' + - 'Please re-run this command with --local' - ), - { code: 'ELINKGLOBAL' } + get usage () { + return usageUtil( + 'link', + 'npm link (in package dir)' + + '\nnpm link [<@scope>/][@]' ) } - // link with no args: symlink the folder to the global location - // link with package arg: symlink the global to the local - args = args.filter(a => resolve(a) !== npm.prefix) - return args.length - ? linkInstall(args) - : linkPkg() -} + async completion (opts) { + const dir = this.npm.globalDir + const files = await readdir(dir) + return files.filter(f => !/^[._-]/.test(f)) + } -// Returns a list of items that can't be fulfilled by -// things found in the current arborist inventory -const missingArgsFromTree = (tree, args) => { - if (tree.isLink) - return missingArgsFromTree(tree.target, args) - - const foundNodes = [] - const missing = args.filter(a => { - const arg = npa(a) - const nodes = tree.children.values() - const argFound = [...nodes].every(node => { - // TODO: write tests for unmatching version specs, this is hard to test - // atm but should be simple once we have a mocked registry again - if (arg.name !== node.name /* istanbul ignore next */ || ( - arg.version && - !semver.satisfies(node.version, arg.version) - )) { - foundNodes.push(node) - return true - } - }) - return argFound - }) + exec (args, cb) { + this.link(args).then(() => cb()).catch(cb) + } - // remote nodes from the loaded tree in order - // to avoid dropping them later when reifying - for (const node of foundNodes) - node.parent = null + async link (args) { + if (this.npm.config.get('global')) { + throw Object.assign( + new Error( + 'link should never be --global.\n' + + 'Please re-run this command with --local' + ), + { code: 'ELINKGLOBAL' } + ) + } + + // link with no args: symlink the folder to the global location + // link with package arg: symlink the global to the local + args = args.filter(a => resolve(a) !== this.npm.prefix) + return args.length + ? this.linkInstall(args) + : this.linkPkg() + } - return missing -} + async linkInstall (args) { + // load current packages from the global space, + // and then add symlinks installs locally + const globalTop = resolve(this.npm.globalDir, '..') + const globalOpts = { + ...this.npm.flatOptions, + path: globalTop, + global: true, + prune: false, + } + const globalArb = new Arborist(globalOpts) + + // get only current top-level packages from the global space + const globals = await globalArb.loadActual({ + filter: (node, kid) => + !node.isRoot || args.some(a => npa(a).name === kid), + }) -const linkInstall = async args => { - // load current packages from the global space, - // and then add symlinks installs locally - const globalTop = resolve(npm.globalDir, '..') - const globalOpts = { - ...npm.flatOptions, - path: globalTop, - global: true, - prune: false, - } - const globalArb = new Arborist(globalOpts) - - // get only current top-level packages from the global space - const globals = await globalArb.loadActual({ - filter: (node, kid) => - !node.isRoot || args.some(a => npa(a).name === kid), - }) - - // any extra arg that is missing from the current - // global space should be reified there first - const missing = missingArgsFromTree(globals, args) - if (missing.length) { - await globalArb.reify({ - ...globalOpts, - add: missing, + // any extra arg that is missing from the current + // global space should be reified there first + const missing = this.missingArgsFromTree(globals, args) + if (missing.length) { + await globalArb.reify({ + ...globalOpts, + add: missing, + }) + } + + // get a list of module names that should be linked in the local prefix + const names = [] + for (const a of args) { + const arg = npa(a) + names.push( + arg.type === 'directory' + ? (await rpj(resolve(arg.fetchSpec, 'package.json'))).name + : arg.name + ) + } + + // npm link should not save=true by default unless you're + // using any of --save-dev or other types + const save = + Boolean(this.npm.config.find('save') !== 'default' || this.npm.flatOptions.saveType) + + // create a new arborist instance for the local prefix and + // reify all the pending names as symlinks there + const localArb = new Arborist({ + ...this.npm.flatOptions, + path: this.npm.prefix, + save, }) + await localArb.reify({ + ...this.npm.flatOptions, + path: this.npm.prefix, + add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), + save, + }) + + await reifyFinish(this.npm, localArb) } - // get a list of module names that should be linked in the local prefix - const names = [] - for (const a of args) { - const arg = npa(a) - names.push( - arg.type === 'directory' - ? (await rpj(resolve(arg.fetchSpec, 'package.json'))).name - : arg.name - ) + async linkPkg () { + const globalTop = resolve(this.npm.globalDir, '..') + const arb = new Arborist({ + ...this.npm.flatOptions, + path: globalTop, + global: true, + }) + await arb.reify({ add: [`file:${this.npm.prefix}`] }) + await reifyFinish(this.npm, arb) } - // npm link should not save=true by default unless you're - // using any of --save-dev or other types - const save = - Boolean(npm.config.find('save') !== 'default' || npm.flatOptions.saveType) - - // create a new arborist instance for the local prefix and - // reify all the pending names as symlinks there - const localArb = new Arborist({ - ...npm.flatOptions, - path: npm.prefix, - save, - }) - await localArb.reify({ - ...npm.flatOptions, - path: npm.prefix, - add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), - save, - }) - - await reifyFinish(localArb) -} + // Returns a list of items that can't be fulfilled by + // things found in the current arborist inventory + missingArgsFromTree (tree, args) { + if (tree.isLink) + return this.missingArgsFromTree(tree.target, args) + + const foundNodes = [] + const missing = args.filter(a => { + const arg = npa(a) + const nodes = tree.children.values() + const argFound = [...nodes].every(node => { + // TODO: write tests for unmatching version specs, this is hard to test + // atm but should be simple once we have a mocked registry again + if (arg.name !== node.name /* istanbul ignore next */ || ( + arg.version && + !semver.satisfies(node.version, arg.version) + )) { + foundNodes.push(node) + return true + } + }) + return argFound + }) -const linkPkg = async () => { - const globalTop = resolve(npm.globalDir, '..') - const arb = new Arborist({ - ...npm.flatOptions, - path: globalTop, - global: true, - }) - await arb.reify({ add: [`file:${npm.prefix}`] }) - await reifyFinish(arb) -} + // remote nodes from the loaded tree in order + // to avoid dropping them later when reifying + for (const node of foundNodes) + node.parent = null -module.exports = Object.assign(cmd, { completion, usage }) + return missing + } +} +module.exports = Link diff --git a/lib/ll.js b/lib/ll.js index 1d5a6217da9c7..171f5cca04e9b 100644 --- a/lib/ll.js +++ b/lib/ll.js @@ -1,9 +1,23 @@ -const { usage, completion } = require('./ls.js') -const npm = require('./npm.js') +const { usage } = require('./ls.js') +const completion = require('./utils/completion/installed-deep.js') -const cmd = (args, cb) => { - npm.config.set('long', true) - return npm.commands.ls(args, cb) +class LL { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usage + } + + async completion (opts) { + return completion(this.npm, opts) + } + + exec (args, cb) { + this.npm.config.set('long', true) + return this.npm.commands.ls(args, cb) + } } -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = LL diff --git a/lib/logout.js b/lib/logout.js index d2762c1ba3e5f..86168b1a0d2f1 100644 --- a/lib/logout.js +++ b/lib/logout.js @@ -1,44 +1,51 @@ -const eu = encodeURIComponent const log = require('npmlog') const getAuth = require('npm-registry-fetch/auth.js') const npmFetch = require('npm-registry-fetch') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'logout', - 'npm logout [--registry=] [--scope=<@scope>]' -) - -const cmd = (args, cb) => logout(args).then(() => cb()).catch(cb) - -const logout = async (args) => { - const { registry, scope } = npm.flatOptions - const regRef = scope ? `${scope}:registry` : 'registry' - const reg = npm.flatOptions[regRef] || registry - - const auth = getAuth(reg, npm.flatOptions) - - if (auth.token) { - log.verbose('logout', `clearing token for ${reg}`) - await npmFetch(`/-/user/token/${eu(auth.token)}`, { - ...npm.flatOptions, - method: 'DELETE', - ignoreBody: true, - }) - } else if (auth.username || auth.password) - log.verbose('logout', `clearing user credentials for ${reg}`) - else { - const msg = `not logged in to ${reg}, so can't log out!` - throw Object.assign(new Error(msg), { code: 'ENEEDAUTH' }) +class Logout { + constructor (npm) { + this.npm = npm } - if (scope) - npm.config.delete(regRef, 'user') + get usage () { + return usageUtil( + 'logout', + 'npm logout [--registry=] [--scope=<@scope>]' + ) + } - npm.config.clearCredentialsByURI(reg) + exec (args, cb) { + this.logout(args).then(() => cb()).catch(cb) + } - await npm.config.save('user') + async logout (args) { + const { registry, scope } = this.npm.flatOptions + const regRef = scope ? `${scope}:registry` : 'registry' + const reg = this.npm.flatOptions[regRef] || registry + + const auth = getAuth(reg, this.npm.flatOptions) + + if (auth.token) { + log.verbose('logout', `clearing token for ${reg}`) + await npmFetch(`/-/user/token/${encodeURIComponent(auth.token)}`, { + ...this.npm.flatOptions, + method: 'DELETE', + ignoreBody: true, + }) + } else if (auth.username || auth.password) + log.verbose('logout', `clearing user credentials for ${reg}`) + else { + const msg = `not logged in to ${reg}, so can't log out!` + throw Object.assign(new Error(msg), { code: 'ENEEDAUTH' }) + } + + if (scope) + this.npm.config.delete(regRef, 'user') + + this.npm.config.clearCredentialsByURI(reg) + + await this.npm.config.save('user') + } } - -module.exports = Object.assign(cmd, { usage }) +module.exports = Logout diff --git a/lib/ls.js b/lib/ls.js index 603c3b412ddc5..cbc9a46122331 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -7,7 +7,6 @@ const Arborist = require('@npmcli/arborist') const { breadth } = require('treeverse') const npa = require('npm-package-arg') -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const completion = require('./utils/completion/installed-deep.js') const output = require('./utils/output.js') @@ -24,20 +23,170 @@ const _problems = Symbol('problems') const _required = Symbol('required') const _type = Symbol('type') -const usage = usageUtil( - 'ls', - 'npm ls [[<@scope>/] ...]' -) +class LS { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil( + 'ls', + 'npm ls [[<@scope>/] ...]' + ) + } + + async completion (opts) { + return completion(this.npm, opts) + } + + exec (args, cb) { + this.ls(args).then(() => cb()).catch(cb) + } -const cmd = (args, cb) => ls(args).then(() => cb()).catch(cb) + async ls (args) { + const { + all, + color, + depth, + json, + long, + global, + parseable, + prefix, + unicode, + } = this.npm.flatOptions + const path = global ? resolve(this.npm.globalDir, '..') : prefix + const dev = this.npm.config.get('dev') + const development = this.npm.config.get('development') + const link = this.npm.config.get('link') + const only = this.npm.config.get('only') + const prod = this.npm.config.get('prod') + const production = this.npm.config.get('production') + + const arb = new Arborist({ + global, + ...this.npm.flatOptions, + legacyPeerDeps: false, + path, + }) + const tree = await this.initTree({ + arb, + args, + global, + json, + }) + + const seenItems = new Set() + const seenNodes = new Map() + const problems = new Set() + + // defines special handling of printed depth when filtering with args + const filterDefaultDepth = depth === null ? Infinity : depth + const depthToPrint = (all || args.length) + ? filterDefaultDepth + : (depth || 0) + + // add root node of tree to list of seenNodes + seenNodes.set(tree.path, tree) + + // tree traversal happens here, using treeverse.breadth + const result = await breadth({ + tree, + // recursive method, `node` is going to be the current elem (starting from + // the `tree` obj) that was just visited in the `visit` method below + // `nodeResult` is going to be the returned `item` from `visit` + getChildren (node, nodeResult) { + const seenPaths = new Set() + const shouldSkipChildren = + !(node instanceof Arborist.Node) || (node[_depth] > depthToPrint) + return (shouldSkipChildren) + ? [] + : [...(node.target || node).edgesOut.values()] + .filter(filterByEdgesTypes({ + dev, + development, + link, + node, + prod, + production, + only, + tree, + })) + .map(mapEdgesToNodes({ seenPaths })) + .concat(appendExtraneousChildren({ node, seenPaths })) + .sort(sortAlphabetically) + .map(augmentNodesWithMetadata({ + args, + currentDepth: node[_depth], + nodeResult, + parseable, + seenNodes, + })) + }, + // visit each `node` of the `tree`, returning an `item` - these are + // the elements that will be used to build the final output + visit (node) { + node[_problems] = getProblems(node, { global }) + + const item = json + ? getJsonOutputItem(node, { global, long }) + : parseable + ? null + : getHumanOutputItem(node, { args, color, global, long }) + + // loop through list of node problems to add them to global list + if (node[_include]) { + for (const problem of node[_problems]) + problems.add(problem) + } + + seenItems.add(item) + + // return a promise so we don't blow the stack + return Promise.resolve(item) + }, + }) + + // handle the special case of a broken package.json in the root folder + const [rootError] = tree.errors.filter(e => + e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json')) + + output( + json + ? jsonOutput({ path, problems, result, rootError, seenItems }) + : parseable + ? parseableOutput({ seenNodes, global, long }) + : humanOutput({ color, result, seenItems, unicode }) + ) -const initTree = async ({ arb, args, json }) => { - const tree = await arb.loadActual() - tree[_include] = args.length === 0 - tree[_depth] = 0 + // if filtering items, should exit with error code on no results + if (result && !result[_include] && args.length) + process.exitCode = 1 - return tree + if (rootError) { + throw Object.assign( + new Error('Failed to parse root package.json'), + { code: 'EJSONPARSE' } + ) + } + + if (problems.size) { + throw Object.assign( + new Error([...problems].join(EOL)), + { code: 'ELSPROBLEMS' } + ) + } + } + + async initTree ({ arb, args, json }) { + const tree = await arb.loadActual() + tree[_include] = args.length === 0 + tree[_depth] = 0 + + return tree + } } +module.exports = LS const isGitNode = (node) => { if (!node.resolved) @@ -359,140 +508,3 @@ const parseableOutput = ({ global, long, seenNodes }) => { } return out.trim() } - -const ls = async (args) => { - const { - all, - color, - depth, - json, - long, - global, - parseable, - prefix, - unicode, - } = npm.flatOptions - const path = global ? resolve(npm.globalDir, '..') : prefix - const dev = npm.config.get('dev') - const development = npm.config.get('development') - const link = npm.config.get('link') - const only = npm.config.get('only') - const prod = npm.config.get('prod') - const production = npm.config.get('production') - - const arb = new Arborist({ - global, - ...npm.flatOptions, - legacyPeerDeps: false, - path, - }) - const tree = await initTree({ - arb, - args, - global, - json, - }) - - const seenItems = new Set() - const seenNodes = new Map() - const problems = new Set() - - // defines special handling of printed depth when filtering with args - const filterDefaultDepth = depth === null ? Infinity : depth - const depthToPrint = (all || args.length) - ? filterDefaultDepth - : (depth || 0) - - // add root node of tree to list of seenNodes - seenNodes.set(tree.path, tree) - - // tree traversal happens here, using treeverse.breadth - const result = await breadth({ - tree, - // recursive method, `node` is going to be the current elem (starting from - // the `tree` obj) that was just visited in the `visit` method below - // `nodeResult` is going to be the returned `item` from `visit` - getChildren (node, nodeResult) { - const seenPaths = new Set() - const shouldSkipChildren = - !(node instanceof Arborist.Node) || (node[_depth] > depthToPrint) - return (shouldSkipChildren) - ? [] - : [...(node.target || node).edgesOut.values()] - .filter(filterByEdgesTypes({ - dev, - development, - link, - node, - prod, - production, - only, - tree, - })) - .map(mapEdgesToNodes({ seenPaths })) - .concat(appendExtraneousChildren({ node, seenPaths })) - .sort(sortAlphabetically) - .map(augmentNodesWithMetadata({ - args, - currentDepth: node[_depth], - nodeResult, - parseable, - seenNodes, - })) - }, - // visit each `node` of the `tree`, returning an `item` - these are - // the elements that will be used to build the final output - visit (node) { - node[_problems] = getProblems(node, { global }) - - const item = json - ? getJsonOutputItem(node, { global, long }) - : parseable - ? null - : getHumanOutputItem(node, { args, color, global, long }) - - // loop through list of node problems to add them to global list - if (node[_include]) { - for (const problem of node[_problems]) - problems.add(problem) - } - - seenItems.add(item) - - // return a promise so we don't blow the stack - return Promise.resolve(item) - }, - }) - - // handle the special case of a broken package.json in the root folder - const [rootError] = tree.errors.filter(e => - e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json')) - - output( - json - ? jsonOutput({ path, problems, result, rootError, seenItems }) - : parseable - ? parseableOutput({ seenNodes, global, long }) - : humanOutput({ color, result, seenItems, unicode }) - ) - - // if filtering items, should exit with error code on no results - if (result && !result[_include] && args.length) - process.exitCode = 1 - - if (rootError) { - throw Object.assign( - new Error('Failed to parse root package.json'), - { code: 'EJSONPARSE' } - ) - } - - if (problems.size) { - throw Object.assign( - new Error([...problems].join(EOL)), - { code: 'ELSPROBLEMS' } - ) - } -} - -module.exports = Object.assign(cmd, { usage, completion }) diff --git a/lib/npm.js b/lib/npm.js index 85dc67a78aac6..93dd5246c4065 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -13,40 +13,20 @@ require('graceful-fs').gracefulify(require('fs')) const procLogListener = require('./utils/proc-log-listener.js') -const hasOwnProperty = (obj, key) => - Object.prototype.hasOwnProperty.call(obj, key) - -// the first time `npm.commands.xyz` is loaded, it gets added -// to the cmds object, so we don't have to load it again. -const proxyCmds = (npm) => { - const cmds = {} - return new Proxy(cmds, { - get: (prop, cmd) => { - if (hasOwnProperty(cmds, cmd)) - return cmds[cmd] - - const actual = deref(cmd) - if (!actual) { - cmds[cmd] = undefined - return cmds[cmd] - } - if (cmds[actual]) { - cmds[cmd] = cmds[actual] - return cmds[cmd] - } - cmds[actual] = makeCmd(actual) - cmds[cmd] = cmds[actual] - return cmds[cmd] - }, - }) -} - -const makeCmd = cmd => { - const impl = require(`./${cmd}.js`) - const fn = (args, cb) => npm[_runCmd](cmd, impl, args, cb) - Object.assign(fn, impl) - return fn -} +const proxyCmds = new Proxy({}, { + get: (target, cmd) => { + const actual = deref(cmd) + if (actual && !target[actual]) { + const Impl = require(`./${actual}.js`) + const impl = new Impl(npm) + target[actual] = Object.assign( + (args, cb) => npm[_runCmd](cmd, impl, args, cb), + impl + ) + } + return target[actual] + }, +}) const { types, defaults, shorthands } = require('./utils/config.js') const { shellouts } = require('./utils/cmd-list.js') @@ -68,7 +48,7 @@ const npm = module.exports = new class extends EventEmitter { } this.started = Date.now() this.command = null - this.commands = proxyCmds(this) + this.commands = proxyCmds procLogListener() process.emit('time', 'npm') this.version = require('../package.json').version @@ -121,7 +101,7 @@ const npm = module.exports = new class extends EventEmitter { console.log(impl.usage) cb() } else { - impl(args, er => { + impl.exec(args, er => { process.emit('timeEnd', `command:${cmd}`) cb(er) }) diff --git a/lib/org.js b/lib/org.js index aa9c97d497bbf..054e1833dba4b 100644 --- a/lib/org.js +++ b/lib/org.js @@ -1,139 +1,148 @@ const liborg = require('libnpmorg') -const npm = require('./npm.js') +const usageUtil = require('./utils/usage.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') const Table = require('cli-table3') -module.exports = org - -org.subcommands = ['set', 'rm', 'ls'] - -org.usage = - 'npm org set orgname username [developer | admin | owner]\n' + - 'npm org rm orgname username\n' + - 'npm org ls orgname []' - -org.completion = async (opts) => { - var argv = opts.conf.argv.remain - if (argv.length === 2) - return org.subcommands +class Org { + constructor (npm) { + this.npm = npm + } - switch (argv[2]) { - case 'ls': - case 'add': - case 'rm': - case 'set': - return [] - default: - throw new Error(argv[2] + ' not recognized') + get usage () { + return usageUtil( + 'org', + 'npm org set orgname username [developer | admin | owner]\n' + + 'npm org rm orgname username\n' + + 'npm org ls orgname []' + ) } -} -function UsageError () { - throw Object.assign(new Error(org.usage), { code: 'EUSAGE' }) -} + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) + return ['set', 'rm', 'ls'] -function org ([cmd, orgname, username, role], cb) { - return otplease(npm.flatOptions, opts => { - switch (cmd) { + switch (argv[2]) { + case 'ls': case 'add': - case 'set': - return orgSet(orgname, username, role, opts) case 'rm': - return orgRm(orgname, username, opts) - case 'ls': - return orgList(orgname, username, opts) + case 'set': + return [] default: - UsageError() + throw new Error(argv[2] + ' not recognized') } - }).then( - x => cb(null, x), - err => cb(err.code === 'EUSAGE' ? err.message : err) - ) -} + } -function orgSet (org, user, role, opts) { - role = role || 'developer' - if (!org) - throw new Error('First argument `orgname` is required.') - - if (!user) - throw new Error('Second argument `username` is required.') - - if (!['owner', 'admin', 'developer'].find(x => x === role)) - throw new Error('Third argument `role` must be one of `owner`, `admin`, or `developer`, with `developer` being the default value if omitted.') - - return liborg.set(org, user, role, opts).then(memDeets => { - if (opts.json) - output(JSON.stringify(memDeets, null, 2)) - else if (opts.parseable) { - output(['org', 'orgsize', 'user', 'role'].join('\t')) - output([ - memDeets.org.name, - memDeets.org.size, - memDeets.user, - memDeets.role, - ].join('\t')) - } else if (!opts.silent && opts.loglevel !== 'silent') - output(`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.`) - - return memDeets - }) -} + exec (args, cb) { + this.org(args) + .then(x => cb(null, x)) + .catch(err => err.code === 'EUSAGE' + ? cb(err.message) + : cb(err) + ) + } -function orgRm (org, user, opts) { - if (!org) - throw new Error('First argument `orgname` is required.') - - if (!user) - throw new Error('Second argument `username` is required.') - - return liborg.rm(org, user, opts).then(() => { - return liborg.ls(org, opts) - }).then(roster => { - user = user.replace(/^[~@]?/, '') - org = org.replace(/^[~@]?/, '') - const userCount = Object.keys(roster).length - if (opts.json) { - output(JSON.stringify({ - user, - org, - userCount, - deleted: true, - })) - } else if (opts.parseable) { - output(['user', 'org', 'userCount', 'deleted'].join('\t')) - output([user, org, userCount, true].join('\t')) - } else if (!opts.silent && opts.loglevel !== 'silent') - output(`Successfully removed ${user} from ${org}. You now have ${userCount} member${userCount === 1 ? '' : 's'} in this org.`) - }) -} + async org ([cmd, orgname, username, role], cb) { + return otplease(this.npm.flatOptions, opts => { + switch (cmd) { + case 'add': + case 'set': + return this.set(orgname, username, role, opts) + case 'rm': + return this.rm(orgname, username, opts) + case 'ls': + return this.ls(orgname, username, opts) + default: + throw Object.assign(new Error(this.usage), { code: 'EUSAGE' }) + } + }) + } -function orgList (org, user, opts) { - if (!org) - throw new Error('First argument `orgname` is required.') + set (org, user, role, opts) { + role = role || 'developer' + if (!org) + throw new Error('First argument `orgname` is required.') + + if (!user) + throw new Error('Second argument `username` is required.') + + if (!['owner', 'admin', 'developer'].find(x => x === role)) + throw new Error('Third argument `role` must be one of `owner`, `admin`, or `developer`, with `developer` being the default value if omitted.') + + return liborg.set(org, user, role, opts).then(memDeets => { + if (opts.json) + output(JSON.stringify(memDeets, null, 2)) + else if (opts.parseable) { + output(['org', 'orgsize', 'user', 'role'].join('\t')) + output([ + memDeets.org.name, + memDeets.org.size, + memDeets.user, + memDeets.role, + ].join('\t')) + } else if (!opts.silent && opts.loglevel !== 'silent') + output(`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.`) + + return memDeets + }) + } - return liborg.ls(org, opts).then(roster => { - if (user) { - const newRoster = {} - if (roster[user]) - newRoster[user] = roster[user] + rm (org, user, opts) { + if (!org) + throw new Error('First argument `orgname` is required.') + + if (!user) + throw new Error('Second argument `username` is required.') + + return liborg.rm(org, user, opts).then(() => { + return liborg.ls(org, opts) + }).then(roster => { + user = user.replace(/^[~@]?/, '') + org = org.replace(/^[~@]?/, '') + const userCount = Object.keys(roster).length + if (opts.json) { + output(JSON.stringify({ + user, + org, + userCount, + deleted: true, + })) + } else if (opts.parseable) { + output(['user', 'org', 'userCount', 'deleted'].join('\t')) + output([user, org, userCount, true].join('\t')) + } else if (!opts.silent && opts.loglevel !== 'silent') + output(`Successfully removed ${user} from ${org}. You now have ${userCount} member${userCount === 1 ? '' : 's'} in this org.`) + }) + } - roster = newRoster - } - if (opts.json) - output(JSON.stringify(roster, null, 2)) - else if (opts.parseable) { - output(['user', 'role'].join('\t')) - Object.keys(roster).forEach(user => { - output([user, roster[user]].join('\t')) - }) - } else if (!opts.silent && opts.loglevel !== 'silent') { - const table = new Table({ head: ['user', 'role'] }) - Object.keys(roster).sort().forEach(user => { - table.push([user, roster[user]]) - }) - output(table.toString()) - } - }) + ls (org, user, opts) { + if (!org) + throw new Error('First argument `orgname` is required.') + + return liborg.ls(org, opts).then(roster => { + if (user) { + const newRoster = {} + if (roster[user]) + newRoster[user] = roster[user] + + roster = newRoster + } + if (opts.json) + output(JSON.stringify(roster, null, 2)) + else if (opts.parseable) { + output(['user', 'role'].join('\t')) + Object.keys(roster).forEach(user => { + output([user, roster[user]].join('\t')) + }) + } else if (!opts.silent && opts.loglevel !== 'silent') { + const table = new Table({ head: ['user', 'role'] }) + Object.keys(roster).sort().forEach(user => { + table.push([user, roster[user]]) + }) + output(table.toString()) + } + }) + } } +module.exports = Org diff --git a/lib/outdated.js b/lib/outdated.js index c10f63a12e3a2..88c25425d873f 100644 --- a/lib/outdated.js +++ b/lib/outdated.js @@ -9,112 +9,134 @@ const pickManifest = require('npm-pick-manifest') const Arborist = require('@npmcli/arborist') -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') const ansiTrim = require('./utils/ansi-trim.js') -const usage = usageUtil('outdated', - 'npm outdated [[<@scope>/] ...]' -) +class Outdated { + constructor (npm) { + this.npm = npm + } -function cmd (args, cb) { - outdated(args) - .then(() => cb()) - .catch(cb) -} + get usage () { + return usageUtil('outdated', + 'npm outdated [[<@scope>/] ...]' + ) + } -async function outdated (args) { - const opts = npm.flatOptions - const global = path.resolve(npm.globalDir, '..') - const where = opts.global - ? global - : npm.prefix - - const arb = new Arborist({ - ...opts, - path: where, - }) - - const tree = await arb.loadActual() - const list = await outdated_(tree, args, opts) - - // sorts list alphabetically - const outdated = list.sort((a, b) => a.name.localeCompare(b.name)) - - // return if no outdated packages - if (outdated.length === 0 && !opts.json) - return - - // display results - if (opts.json) - output(makeJSON(outdated, opts)) - else if (opts.parseable) - output(makeParseable(outdated, opts)) - else { - const outList = outdated.map(x => makePretty(x, opts)) - const outHead = ['Package', - 'Current', - 'Wanted', - 'Latest', - 'Location', - 'Depended by', - ] - - if (opts.long) - outHead.push('Package Type', 'Homepage') - const outTable = [outHead].concat(outList) - - if (opts.color) - outTable[0] = outTable[0].map(heading => styles.underline(heading)) - - const tableOpts = { - align: ['l', 'r', 'r', 'r', 'l'], - stringLength: s => ansiTrim(s).length, - } - output(table(outTable, tableOpts)) + exec (args, cb) { + this.outdated(args).then(() => cb()).catch(cb) } -} -async function outdated_ (tree, deps, opts) { - const list = [] + async outdated (args) { + this.opts = this.npm.flatOptions + + const global = path.resolve(this.npm.globalDir, '..') + const where = this.opts.global + ? global + : this.npm.prefix + + const arb = new Arborist({ + ...this.opts, + path: where, + }) - const edges = new Set() - function getEdges (nodes, type) { - const getEdgesIn = (node) => { - for (const edge of node.edgesIn) - edges.add(edge) + this.edges = new Set() + this.list = [] + this.tree = await arb.loadActual() + + if (args.length !== 0) { + // specific deps + for (let i = 0; i < args.length; i++) { + const nodes = this.tree.inventory.query('name', args[i]) + this.getEdges(nodes, 'edgesIn') + } + } else { + if (this.opts.all) { + // all deps in tree + const nodes = this.tree.inventory.values() + this.getEdges(nodes, 'edgesOut') + } + // top-level deps + this.getEdges() } - const getEdgesOut = (node) => { - if (opts.global) { - for (const child of node.children.values()) - edges.add(child) - } else { - for (const edge of node.edgesOut.values()) - edges.add(edge) + await Promise.all(Array.from(this.edges).map((edge) => { + return this.getOutdatedInfo(edge) + })) + + // sorts list alphabetically + const outdated = this.list.sort((a, b) => a.name.localeCompare(b.name)) + + // return if no outdated packages + if (outdated.length === 0 && !this.opts.json) + return + + // display results + if (this.opts.json) + output(this.makeJSON(outdated)) + else if (this.opts.parseable) + output(this.makeParseable(outdated)) + else { + const outList = outdated.map(x => this.makePretty(x)) + const outHead = ['Package', + 'Current', + 'Wanted', + 'Latest', + 'Location', + 'Depended by', + ] + + if (this.opts.long) + outHead.push('Package Type', 'Homepage') + const outTable = [outHead].concat(outList) + + if (this.opts.color) + outTable[0] = outTable[0].map(heading => styles.underline(heading)) + + const tableOpts = { + align: ['l', 'r', 'r', 'r', 'l'], + stringLength: s => ansiTrim(s).length, } + output(table(outTable, tableOpts)) } + } + getEdges (nodes, type) { if (!nodes) - return getEdgesOut(tree) + return this.getEdgesOut(this.tree) for (const node of nodes) { type === 'edgesOut' - ? getEdgesOut(node) - : getEdgesIn(node) + ? this.getEdgesOut(node) + : this.getEdgesIn(node) + } + } + + getEdgesIn (node) { + for (const edge of node.edgesIn) + this.edges.add(edge) + } + + getEdgesOut (node) { + if (this.opts.global) { + for (const child of node.children.values()) + this.edges.add(child) + } else { + for (const edge of node.edgesOut.values()) + this.edges.add(edge) } } - async function getPackument (spec) { + async getPackument (spec) { const packument = await pacote.packument(spec, { - ...npm.flatOptions, - fullMetadata: npm.flatOptions.long, + ...this.npm.flatOptions, + fullMetadata: this.npm.flatOptions.long, preferOnline: true, }) return packument } - async function getOutdatedInfo (edge) { + async getOutdatedInfo (edge) { const spec = npa(edge.name) const node = edge.to || edge const { path, location } = node @@ -125,7 +147,7 @@ async function outdated_ (tree, deps, opts) { : edge.dev ? 'devDependencies' : 'dependencies' - for (const omitType of opts.omit || []) { + for (const omitType of this.opts.omit || []) { if (node[omitType]) return } @@ -136,7 +158,7 @@ async function outdated_ (tree, deps, opts) { return try { - const packument = await getPackument(spec) + const packument = await this.getPackument(spec) const expected = edge.spec // if it's not a range, version, or tag, skip it try { @@ -145,15 +167,15 @@ async function outdated_ (tree, deps, opts) { } catch (err) { return null } - const wanted = pickManifest(packument, expected, npm.flatOptions) - const latest = pickManifest(packument, '*', npm.flatOptions) + const wanted = pickManifest(packument, expected, this.npm.flatOptions) + const latest = pickManifest(packument, '*', this.npm.flatOptions) if ( !current || current !== wanted.version || wanted.version !== latest.version ) { - list.push({ + this.list.push({ name: edge.name, path, type, @@ -167,7 +189,7 @@ async function outdated_ (tree, deps, opts) { } } catch (err) { // silently catch and ignore ETARGET, E403 & - // E404 errors, deps are just skipped { + // E404 errors, deps are just skipped if (!( err.code === 'ETARGET' || err.code === 'E403' || @@ -177,113 +199,89 @@ async function outdated_ (tree, deps, opts) { } } - const p = [] - if (deps.length !== 0) { - // specific deps - for (let i = 0; i < deps.length; i++) { - const nodes = tree.inventory.query('name', deps[i]) - getEdges(nodes, 'edgesIn') - } - } else { - if (opts.all) { - // all deps in tree - const nodes = tree.inventory.values() - getEdges(nodes, 'edgesOut') - } - // top-level deps - getEdges() - } - - for (const edge of edges) - p.push(getOutdatedInfo(edge)) - - await Promise.all(p) - return list -} - -// formatting functions -function makePretty (dep, opts) { - const { - current = 'MISSING', - location = '-', - homepage = '', - name, - wanted, - latest, - type, - dependent, - } = dep - - const columns = [name, current, wanted, latest, location, dependent] - - if (opts.long) { - columns[6] = type - columns[7] = homepage - } - - if (opts.color) { - columns[0] = color[current === wanted ? 'yellow' : 'red'](columns[0]) // current - columns[2] = color.green(columns[2]) // wanted - columns[3] = color.magenta(columns[3]) // latest - } - - return columns -} - -// --parseable creates output like this: -// :::: -function makeParseable (list, opts) { - return list.map(dep => { + // formatting functions + makePretty (dep) { const { + current = 'MISSING', + location = '-', + homepage = '', name, - current, wanted, latest, - path, - dependent, type, - homepage, - } = dep - const out = [ - path, - name + '@' + wanted, - current ? (name + '@' + current) : 'MISSING', - name + '@' + latest, dependent, - ] - if (opts.long) - out.push(type, homepage) + } = dep - return out.join(':') - }).join(os.EOL) -} + const columns = [name, current, wanted, latest, location, dependent] -function makeJSON (list, opts) { - const out = {} - list.forEach(dep => { - const { - name, - current, - wanted, - latest, - path, - type, - dependent, - homepage, - } = dep - out[name] = { - current, - wanted, - latest, - dependent, - location: path, + if (this.opts.long) { + columns[6] = type + columns[7] = homepage } - if (opts.long) { - out[name].type = type - out[name].homepage = homepage + + if (this.opts.color) { + columns[0] = color[current === wanted ? 'yellow' : 'red'](columns[0]) // current + columns[2] = color.green(columns[2]) // wanted + columns[3] = color.magenta(columns[3]) // latest } - }) - return JSON.stringify(out, null, 2) -} -module.exports = Object.assign(cmd, { usage }) + return columns + } + + // --parseable creates output like this: + // :::: + makeParseable (list) { + return list.map(dep => { + const { + name, + current, + wanted, + latest, + path, + dependent, + type, + homepage, + } = dep + const out = [ + path, + name + '@' + wanted, + current ? (name + '@' + current) : 'MISSING', + name + '@' + latest, + dependent, + ] + if (this.opts.long) + out.push(type, homepage) + + return out.join(':') + }).join(os.EOL) + } + + makeJSON (list) { + const out = {} + list.forEach(dep => { + const { + name, + current, + wanted, + latest, + path, + type, + dependent, + homepage, + } = dep + out[name] = { + current, + wanted, + latest, + dependent, + location: path, + } + if (this.opts.long) { + out[name].type = type + out[name].homepage = homepage + } + }) + return JSON.stringify(out, null, 2) + } +} +module.exports = Outdated diff --git a/lib/owner.js b/lib/owner.js index 6dce3ec70f396..6cb9904880dc2 100644 --- a/lib/owner.js +++ b/lib/owner.js @@ -3,94 +3,138 @@ const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') const pacote = require('pacote') -const npm = require('./npm.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') const readLocalPkg = require('./utils/read-local-package.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'owner', - 'npm owner add [<@scope>/]' + - '\nnpm owner rm [<@scope>/]' + - '\nnpm owner ls [<@scope>/]' -) +class Owner { + constructor (npm) { + this.npm = npm + } -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv.length > 3) - return [] + get usage () { + return usageUtil( + 'owner', + 'npm owner add [<@scope>/]' + + '\nnpm owner rm [<@scope>/]' + + '\nnpm owner ls [<@scope>/]' + ) + } - if (argv[1] !== 'owner') - argv.unshift('owner') + get usageError () { + return Object.assign(new Error(this.usage), { code: 'EUSAGE' }) + } - if (argv.length === 2) - return ['add', 'rm', 'ls'] + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length > 3) + return [] - // reaches registry in order to autocomplete rm - if (argv[2] === 'rm') { - const opts = { - ...npm.flatOptions, - fullMetadata: true, + if (argv[1] !== 'owner') + argv.unshift('owner') + + if (argv.length === 2) + return ['add', 'rm', 'ls'] + + // reaches registry in order to autocomplete rm + if (argv[2] === 'rm') { + const pkgName = await readLocalPkg(this.npm) + if (!pkgName) + return [] + + const spec = npa(pkgName) + const data = await pacote.packument(spec, { + ...this.npm.flatOptions, + fullMetadata: true, + }) + if (data && data.maintainers && data.maintainers.length) + return data.maintainers.map(m => m.name) } - const pkgName = await readLocalPkg() - if (!pkgName) - return [] + return [] + } - const spec = npa(pkgName) - const data = await pacote.packument(spec, opts) - if (data && data.maintainers && data.maintainers.length) - return data.maintainers.map(m => m.name) + exec (args, cb) { + this.owner(args).then(() => cb()).catch(cb) } - return [] -} -const UsageError = () => - Object.assign(new Error(usage), { code: 'EUSAGE' }) - -const cmd = (args, cb) => owner(args).then(() => cb()).catch(cb) - -const owner = async ([action, ...args]) => { - const opts = npm.flatOptions - switch (action) { - case 'ls': - case 'list': - return ls(args[0], opts) - case 'add': - return add(args[0], args[1], opts) - case 'rm': - case 'remove': - return rm(args[0], args[1], opts) - default: - throw UsageError() + async owner ([action, ...args]) { + const opts = this.npm.flatOptions + switch (action) { + case 'ls': + case 'list': + return this.ls(args[0], opts) + case 'add': + return this.add(args[0], args[1], opts) + case 'rm': + case 'remove': + return this.rm(args[0], args[1], opts) + default: + throw this.usageError + } } -} -const ls = async (pkg, opts) => { - if (!pkg) { - const pkgName = await readLocalPkg() - if (!pkgName) - throw UsageError() + async ls (pkg, opts) { + if (!pkg) { + const pkgName = await readLocalPkg(this.npm) + if (!pkgName) + throw this.usageError + + pkg = pkgName + } + + const spec = npa(pkg) + + try { + const packumentOpts = { ...opts, fullMetadata: true } + const { maintainers } = await pacote.packument(spec, packumentOpts) + if (!maintainers || !maintainers.length) + output('no admin found') + else + output(maintainers.map(o => `${o.name} <${o.email}>`).join('\n')) - pkg = pkgName + return maintainers + } catch (err) { + log.error('owner ls', "Couldn't get owner data", pkg) + throw err + } } - const spec = npa(pkg) + async add (user, pkg, opts) { + if (!user) + throw this.usageError - try { - const packumentOpts = { ...opts, fullMetadata: true } - const { maintainers } = await pacote.packument(spec, packumentOpts) - if (!maintainers || !maintainers.length) - output('no admin found') - else - output(maintainers.map(o => `${o.name} <${o.email}>`).join('\n')) + if (!pkg) { + const pkgName = await readLocalPkg(this.npm) + if (!pkgName) + throw this.usageError - return maintainers - } catch (err) { - log.error('owner ls', "Couldn't get owner data", pkg) - throw err + pkg = pkgName + } + log.verbose('owner add', '%s to %s', user, pkg) + + const spec = npa(pkg) + return putOwners(spec, user, opts, validateAddOwner) + } + + async rm (user, pkg, opts) { + if (!user) + throw this.usageError + + if (!pkg) { + const pkgName = await readLocalPkg(this.npm) + if (!pkgName) + throw this.usageError + + pkg = pkgName + } + log.verbose('owner rm', '%s from %s', user, pkg) + + const spec = npa(pkg) + return putOwners(spec, user, opts, validateRmOwner) } } +module.exports = Owner const validateAddOwner = (newOwner, owners) => { owners = owners || [] @@ -109,23 +153,6 @@ const validateAddOwner = (newOwner, owners) => { ] } -const add = async (user, pkg, opts) => { - if (!user) - throw UsageError() - - if (!pkg) { - const pkgName = await readLocalPkg() - if (!pkgName) - throw UsageError() - - pkg = pkgName - } - log.verbose('owner add', '%s to %s', user, pkg) - - const spec = npa(pkg) - return putOwners(spec, user, opts, validateAddOwner) -} - const validateRmOwner = (rmOwner, owners) => { let found = false const m = owners.filter(function (o) { @@ -151,23 +178,6 @@ const validateRmOwner = (rmOwner, owners) => { return m } -const rm = async (user, pkg, opts) => { - if (!user) - throw UsageError() - - if (!pkg) { - const pkgName = await readLocalPkg() - if (!pkgName) - throw UsageError() - - pkg = pkgName - } - log.verbose('owner rm', '%s from %s', user, pkg) - - const spec = npa(pkg) - return putOwners(spec, user, opts, validateRmOwner) -} - const putOwners = async (spec, user, opts, validation) => { const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}` let u = '' @@ -227,5 +237,3 @@ const putOwners = async (spec, user, opts, validation) => { } return res } - -module.exports = Object.assign(cmd, { usage, completion }) diff --git a/lib/pack.js b/lib/pack.js index ff906cc2bd5a1..bcfd96f5ffad5 100644 --- a/lib/pack.js +++ b/lib/pack.js @@ -4,46 +4,52 @@ const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') -const npm = require('./npm.js') const { getContents, logTar } = require('./utils/tar.js') const writeFile = util.promisify(require('fs').writeFile) const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil('pack', 'npm pack [[<@scope>/]...] [--dry-run]') -const cmd = (args, cb) => pack(args).then(() => cb()).catch(cb) +class Pack { + constructor (npm) { + this.npm = npm + } -const pack = async (args) => { - if (args.length === 0) - args = ['.'] + get usage () { + return usageUtil('pack', 'npm pack [[<@scope>/]...] [--dry-run]') + } - const { unicode } = npm.flatOptions + exec (args, cb) { + this.pack(args).then(() => cb()).catch(cb) + } - // clone the opts because pacote mutates it with resolved/integrity - const tarballs = await Promise.all(args.map((arg) => - pack_(arg, { ...npm.flatOptions }))) + async pack (args) { + if (args.length === 0) + args = ['.'] - for (const tar of tarballs) { - logTar(tar, { log, unicode }) - output(tar.filename.replace(/^@/, '').replace(/\//, '-')) - } -} + const { unicode } = this.npm.flatOptions -const pack_ = async (arg, opts) => { - const spec = npa(arg) - const { dryRun } = opts - const manifest = await pacote.manifest(spec, opts) - const filename = `${manifest.name}-${manifest.version}.tgz` - .replace(/^@/, '').replace(/\//, '-') - const tarballData = await libpack(arg, opts) - const pkgContents = await getContents(manifest, tarballData) + // clone the opts because pacote mutates it with resolved/integrity + const tarballs = await Promise.all(args.map(async (arg) => { + const spec = npa(arg) + const { dryRun } = this.npm.flatOptions + const manifest = await pacote.manifest(spec, this.npm.flatOptions) + const filename = `${manifest.name}-${manifest.version}.tgz` + .replace(/^@/, '').replace(/\//, '-') + const tarballData = await libpack(arg, this.npm.flatOptions) + const pkgContents = await getContents(manifest, tarballData) - if (!dryRun) - await writeFile(filename, tarballData) + if (!dryRun) + await writeFile(filename, tarballData) - return pkgContents -} + return pkgContents + })) -module.exports = Object.assign(cmd, { usage }) + for (const tar of tarballs) { + logTar(tar, { log, unicode }) + output(tar.filename.replace(/^@/, '').replace(/\//, '-')) + } + } +} +module.exports = Pack diff --git a/lib/ping.js b/lib/ping.js index efa22631033c9..ac96659cc1fcb 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -1,27 +1,35 @@ const log = require('npmlog') -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') +const pingUtil = require('./utils/ping.js') -const usage = usageUtil('ping', 'npm ping\nping registry') +class Ping { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => ping(args).then(() => cb()).catch(cb) -const pingUtil = require('./utils/ping.js') + get usage () { + return usageUtil('ping', 'npm ping\nping registry') + } -const ping = async args => { - log.notice('PING', npm.flatOptions.registry) - const start = Date.now() - const details = await pingUtil(npm.flatOptions) - const time = Date.now() - start - log.notice('PONG', `${time / 1000}ms`) - if (npm.flatOptions.json) { - output(JSON.stringify({ - registry: npm.flatOptions.registry, - time, - details, - }, null, 2)) - } else if (Object.keys(details).length) - log.notice('PONG', `${JSON.stringify(details, null, 2)}`) -} + exec (args, cb) { + this.ping(args).then(() => cb()).catch(cb) + } -module.exports = Object.assign(cmd, { usage }) + async ping (args) { + log.notice('PING', this.npm.flatOptions.registry) + const start = Date.now() + const details = await pingUtil(this.npm.flatOptions) + const time = Date.now() - start + log.notice('PONG', `${time / 1000}ms`) + if (this.npm.flatOptions.json) { + output(JSON.stringify({ + registry: this.npm.flatOptions.registry, + time, + details, + }, null, 2)) + } else if (Object.keys(details).length) + log.notice('PONG', `${JSON.stringify(details, null, 2)}`) + } +} +module.exports = Ping diff --git a/lib/prefix.js b/lib/prefix.js index d108b9d423afd..a8ed080746fa3 100644 --- a/lib/prefix.js +++ b/lib/prefix.js @@ -1,7 +1,21 @@ -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') -const cmd = (args, cb) => prefix(args).then(() => cb()).catch(cb) -const usage = usageUtil('prefix', 'npm prefix [-g]') -const prefix = async (args, cb) => output(npm.prefix) -module.exports = Object.assign(cmd, { usage }) + +class Prefix { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('prefix', 'npm prefix [-g]') + } + + exec (args, cb) { + this.prefix(args).then(() => cb()).catch(cb) + } + + async prefix (args) { + return output(this.npm.prefix) + } +} +module.exports = Prefix diff --git a/lib/profile.js b/lib/profile.js index 3727ac0c8bdd4..da45def6c473a 100644 --- a/lib/profile.js +++ b/lib/profile.js @@ -6,71 +6,14 @@ const npmProfile = require('npm-profile') const qrcodeTerminal = require('qrcode-terminal') const Table = require('cli-table3') -const npm = require('./npm.js') const otplease = require('./utils/otplease.js') const output = require('./utils/output.js') const pulseTillDone = require('./utils/pulse-till-done.js') const readUserInfo = require('./utils/read-user-info.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'npm profile enable-2fa [auth-only|auth-and-writes]\n', - 'npm profile disable-2fa\n', - 'npm profile get []\n', - 'npm profile set ' -) - -const completion = async (opts) => { - var argv = opts.conf.argv.remain - const subcommands = ['enable-2fa', 'disable-2fa', 'get', 'set'] - - if (!argv[2]) - return subcommands - - switch (argv[2]) { - case 'enable-2fa': - case 'enable-tfa': - return ['auth-and-writes', 'auth-only'] - - case 'disable-2fa': - case 'disable-tfa': - case 'get': - case 'set': - return [] - default: - throw new Error(argv[2] + ' not recognized') - } -} - -const cmd = (args, cb) => profile(args).then(() => cb()).catch(cb) - -const profile = async (args) => { - if (args.length === 0) - throw new Error(usage) - - log.gauge.show('profile') - - const [subcmd, ...opts] = args - - switch (subcmd) { - case 'enable-2fa': - case 'enable-tfa': - case 'enable2fa': - case 'enabletfa': - return enable2fa(opts) - case 'disable-2fa': - case 'disable-tfa': - case 'disable2fa': - case 'disabletfa': - return disable2fa() - case 'get': - return get(opts) - case 'set': - return set(opts) - default: - throw new Error('Unknown profile command: ' + subcmd) - } -} +const qrcode = url => + new Promise((resolve) => qrcodeTerminal.generate(url, resolve)) const knownProfileKeys = [ 'name', @@ -85,64 +28,6 @@ const knownProfileKeys = [ 'updated', ] -const get = async args => { - const tfa = 'two-factor auth' - const conf = { ...npm.flatOptions } - - const info = await pulseTillDone.withPromise(npmProfile.get(conf)) - - if (!info.cidr_whitelist) - delete info.cidr_whitelist - - if (conf.json) { - output(JSON.stringify(info, null, 2)) - return - } - - // clean up and format key/values for output - const cleaned = {} - for (const key of knownProfileKeys) - cleaned[key] = info[key] || '' - - const unknownProfileKeys = Object.keys(info).filter((k) => !(k in cleaned)) - for (const key of unknownProfileKeys) - cleaned[key] = info[key] || '' - - delete cleaned.tfa - delete cleaned.email_verified - cleaned.email += info.email_verified ? ' (verified)' : '(unverified)' - - if (info.tfa && !info.tfa.pending) - cleaned[tfa] = info.tfa.mode - else - cleaned[tfa] = 'disabled' - - if (args.length) { - const values = args // comma or space separated - .join(',') - .split(/,/) - .filter((arg) => arg.trim() !== '') - .map((arg) => cleaned[arg]) - .join('\t') - output(values) - } else { - if (conf.parseable) { - for (const key of Object.keys(info)) { - if (key === 'tfa') - output(`${key}\t${cleaned[tfa]}`) - else - output(`${key}\t${info[key]}`) - } - } else { - const table = new Table() - for (const key of Object.keys(cleaned)) - table.push({ [ansistyles.bright(key)]: cleaned[key] }) - - output(table.toString()) - } - } -} - const writableProfileKeys = [ 'email', 'password', @@ -153,242 +38,363 @@ const writableProfileKeys = [ 'github', ] -const set = async (args) => { - const conf = { ...npm.flatOptions } - const prop = (args[0] || '').toLowerCase().trim() +class Profile { + constructor (npm) { + this.npm = npm + } - let value = args.length > 1 ? args.slice(1).join(' ') : null + get usage () { + return usageUtil( + 'npm profile enable-2fa [auth-only|auth-and-writes]\n', + 'npm profile disable-2fa\n', + 'npm profile get []\n', + 'npm profile set ' + ) + } - const readPasswords = async () => { - const newpassword = await readUserInfo.password('New password: ') - const confirmedpassword = await readUserInfo.password(' Again: ') + async completion (opts) { + var argv = opts.conf.argv.remain - if (newpassword !== confirmedpassword) { - log.warn('profile', 'Passwords do not match, please try again.') - return readPasswords() - } + if (!argv[2]) + return ['enable-2fa', 'disable-2fa', 'get', 'set'] - return newpassword - } + switch (argv[2]) { + case 'enable-2fa': + case 'enable-tfa': + return ['auth-and-writes', 'auth-only'] - if (prop !== 'password' && value === null) - throw new Error('npm profile set ') + case 'disable-2fa': + case 'disable-tfa': + case 'get': + case 'set': + return [] + default: + throw new Error(argv[2] + ' not recognized') + } + } - if (prop === 'password' && value !== null) { - throw new Error( - 'npm profile set password\n' + - 'Do not include your current or new passwords on the command line.') + exec (args, cb) { + this.profile(args).then(() => cb()).catch(cb) } - if (writableProfileKeys.indexOf(prop) === -1) { - throw new Error(`"${prop}" is not a property we can set. ` + - `Valid properties are: ` + writableProfileKeys.join(', ')) + async profile (args) { + if (args.length === 0) + throw new Error(this.usage) + + log.gauge.show('profile') + + const [subcmd, ...opts] = args + + switch (subcmd) { + case 'enable-2fa': + case 'enable-tfa': + case 'enable2fa': + case 'enabletfa': + return this.enable2fa(opts) + case 'disable-2fa': + case 'disable-tfa': + case 'disable2fa': + case 'disabletfa': + return this.disable2fa() + case 'get': + return this.get(opts) + case 'set': + return this.set(opts) + default: + throw new Error('Unknown profile command: ' + subcmd) + } } - if (prop === 'password') { - const current = await readUserInfo.password('Current password: ') - const newpassword = await readPasswords() + async get (args) { + const tfa = 'two-factor auth' + const conf = { ...this.npm.flatOptions } + + const info = await pulseTillDone.withPromise(npmProfile.get(conf)) + + if (!info.cidr_whitelist) + delete info.cidr_whitelist + + if (conf.json) { + output(JSON.stringify(info, null, 2)) + return + } - value = { old: current, new: newpassword } + // clean up and format key/values for output + const cleaned = {} + for (const key of knownProfileKeys) + cleaned[key] = info[key] || '' + + const unknownProfileKeys = Object.keys(info).filter((k) => !(k in cleaned)) + for (const key of unknownProfileKeys) + cleaned[key] = info[key] || '' + + delete cleaned.tfa + delete cleaned.email_verified + cleaned.email += info.email_verified ? ' (verified)' : '(unverified)' + + if (info.tfa && !info.tfa.pending) + cleaned[tfa] = info.tfa.mode + else + cleaned[tfa] = 'disabled' + + if (args.length) { + const values = args // comma or space separated + .join(',') + .split(/,/) + .filter((arg) => arg.trim() !== '') + .map((arg) => cleaned[arg]) + .join('\t') + output(values) + } else { + if (conf.parseable) { + for (const key of Object.keys(info)) { + if (key === 'tfa') + output(`${key}\t${cleaned[tfa]}`) + else + output(`${key}\t${info[key]}`) + } + } else { + const table = new Table() + for (const key of Object.keys(cleaned)) + table.push({ [ansistyles.bright(key)]: cleaned[key] }) + + output(table.toString()) + } + } } - // FIXME: Work around to not clear everything other than what we're setting - const user = await pulseTillDone.withPromise(npmProfile.get(conf)) - const newUser = {} + async set (args) { + const conf = { ...this.npm.flatOptions } + const prop = (args[0] || '').toLowerCase().trim() - for (const key of writableProfileKeys) - newUser[key] = user[key] + let value = args.length > 1 ? args.slice(1).join(' ') : null - newUser[prop] = value + const readPasswords = async () => { + const newpassword = await readUserInfo.password('New password: ') + const confirmedpassword = await readUserInfo.password(' Again: ') - const result = await otplease(conf, conf => npmProfile.set(newUser, conf)) + if (newpassword !== confirmedpassword) { + log.warn('profile', 'Passwords do not match, please try again.') + return readPasswords() + } - if (conf.json) - output(JSON.stringify({ [prop]: result[prop] }, null, 2)) - else if (conf.parseable) - output(prop + '\t' + result[prop]) - else if (result[prop] != null) - output('Set', prop, 'to', result[prop]) - else - output('Set', prop) -} + return newpassword + } -const enable2fa = async (args) => { - if (args.length > 1) - throw new Error('npm profile enable-2fa [auth-and-writes|auth-only]') - - const mode = args[0] || 'auth-and-writes' - if (mode !== 'auth-only' && mode !== 'auth-and-writes') { - throw new Error( - `Invalid two-factor authentication mode "${mode}".\n` + - 'Valid modes are:\n' + - ' auth-only - Require two-factor authentication only when logging in\n' + - ' auth-and-writes - Require two-factor authentication when logging in ' + - 'AND when publishing' - ) - } + if (prop !== 'password' && value === null) + throw new Error('npm profile set ') - const conf = { ...npm.flatOptions } - if (conf.json || conf.parseable) { - throw new Error( - 'Enabling two-factor authentication is an interactive operation and ' + - (conf.json ? 'JSON' : 'parseable') + ' output mode is not available' - ) - } + if (prop === 'password' && value !== null) { + throw new Error( + 'npm profile set password\n' + + 'Do not include your current or new passwords on the command line.') + } - const info = { - tfa: { - mode: mode, - }, - } + if (writableProfileKeys.indexOf(prop) === -1) { + throw new Error(`"${prop}" is not a property we can set. ` + + `Valid properties are: ` + writableProfileKeys.join(', ')) + } - // if they're using legacy auth currently then we have to - // update them to a bearer token before continuing. - const auth = getAuth(conf) + if (prop === 'password') { + const current = await readUserInfo.password('Current password: ') + const newpassword = await readPasswords() - if (!auth.basic && !auth.token) { - throw new Error( - 'You need to be logged in to registry ' + - `${conf.registry} in order to enable 2fa` - ) + value = { old: current, new: newpassword } + } + + // FIXME: Work around to not clear everything other than what we're setting + const user = await pulseTillDone.withPromise(npmProfile.get(conf)) + const newUser = {} + + for (const key of writableProfileKeys) + newUser[key] = user[key] + + newUser[prop] = value + + const result = await otplease(conf, conf => npmProfile.set(newUser, conf)) + + if (conf.json) + output(JSON.stringify({ [prop]: result[prop] }, null, 2)) + else if (conf.parseable) + output(prop + '\t' + result[prop]) + else if (result[prop] != null) + output('Set', prop, 'to', result[prop]) + else + output('Set', prop) } - if (auth.basic) { - log.info('profile', 'Updating authentication to bearer token') - const result = await npmProfile.createToken( - auth.basic.password, false, [], conf - ) + async enable2fa (args) { + if (args.length > 1) + throw new Error('npm profile enable-2fa [auth-and-writes|auth-only]') - if (!result.token) { + const mode = args[0] || 'auth-and-writes' + if (mode !== 'auth-only' && mode !== 'auth-and-writes') { throw new Error( - `Your registry ${conf.registry} does not seem to ` + - 'support bearer tokens. Bearer tokens are required for ' + - 'two-factor authentication' + `Invalid two-factor authentication mode "${mode}".\n` + + 'Valid modes are:\n' + + ' auth-only - Require two-factor authentication only when logging in\n' + + ' auth-and-writes - Require two-factor authentication when logging in ' + + 'AND when publishing' ) } - npm.config.setCredentialsByURI(conf.registry, { token: result.token }) - await npm.config.save('user') - } + const conf = { ...this.npm.flatOptions } + if (conf.json || conf.parseable) { + throw new Error( + 'Enabling two-factor authentication is an interactive operation and ' + + (conf.json ? 'JSON' : 'parseable') + ' output mode is not available' + ) + } - log.notice('profile', 'Enabling two factor authentication for ' + mode) - const password = await readUserInfo.password() - info.tfa.password = password + const info = { + tfa: { + mode: mode, + }, + } - log.info('profile', 'Determine if tfa is pending') - const userInfo = await pulseTillDone.withPromise(npmProfile.get(conf)) + // if they're using legacy auth currently then we have to + // update them to a bearer token before continuing. + const creds = this.npm.config.getCredentialsByURI(conf.registry) + const auth = {} + + if (creds.token) + auth.token = creds.token + else if (creds.username) + auth.basic = { username: creds.username, password: creds.password } + else if (creds.auth) { + const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2) + auth.basic = { username: basic[0], password: basic[1] } + } - if (userInfo && userInfo.tfa && userInfo.tfa.pending) { - log.info('profile', 'Resetting two-factor authentication') - await pulseTillDone.withPromise( - npmProfile.set({ tfa: { password, mode: 'disable' } }, conf) - ) - } else if (userInfo && userInfo.tfa) { if (conf.otp) - conf.otp = conf.otp - else { - const otp = await readUserInfo.otp( - 'Enter one-time password from your authenticator app: ' + auth.otp = conf.otp + + if (!auth.basic && !auth.token) { + throw new Error( + 'You need to be logged in to registry ' + + `${conf.registry} in order to enable 2fa` ) - conf.otp = otp } - } - log.info('profile', 'Setting two-factor authentication to ' + mode) - const challenge = await pulseTillDone.withPromise(npmProfile.set(info, conf)) + if (auth.basic) { + log.info('profile', 'Updating authentication to bearer token') + const result = await npmProfile.createToken( + auth.basic.password, false, [], conf + ) - if (challenge.tfa === null) { - output('Two factor authentication mode changed to: ' + mode) - return - } + if (!result.token) { + throw new Error( + `Your registry ${conf.registry} does not seem to ` + + 'support bearer tokens. Bearer tokens are required for ' + + 'two-factor authentication' + ) + } - const badResponse = typeof challenge.tfa !== 'string' - || !/^otpauth:[/][/]/.test(challenge.tfa) - if (badResponse) { - throw new Error( - 'Unknown error enabling two-factor authentication. Expected otpauth URL' + - ', got: ' + inspect(challenge.tfa) - ) - } + this.npm.config.setCredentialsByURI( + conf.registry, + { token: result.token } + ) + await this.npm.config.save('user') + } - const otpauth = new URL(challenge.tfa) - const secret = otpauth.searchParams.get('secret') - const code = await qrcode(challenge.tfa) + log.notice('profile', 'Enabling two factor authentication for ' + mode) + const password = await readUserInfo.password() + info.tfa.password = password - output( - 'Scan into your authenticator app:\n' + code + '\n Or enter code:', secret - ) + log.info('profile', 'Determine if tfa is pending') + const userInfo = await pulseTillDone.withPromise(npmProfile.get(conf)) - const interactiveOTP = - await readUserInfo.otp('And an OTP code from your authenticator: ') + if (userInfo && userInfo.tfa && userInfo.tfa.pending) { + log.info('profile', 'Resetting two-factor authentication') + await pulseTillDone.withPromise( + npmProfile.set({ tfa: { password, mode: 'disable' } }, conf) + ) + } else if (userInfo && userInfo.tfa) { + if (conf.otp) + conf.otp = conf.otp + else { + const otp = await readUserInfo.otp( + 'Enter one-time password from your authenticator app: ' + ) + conf.otp = otp + } + } - log.info('profile', 'Finalizing two-factor authentication') + log.info('profile', 'Setting two-factor authentication to ' + mode) + const challenge = await pulseTillDone.withPromise( + npmProfile.set(info, conf) + ) - const result = await npmProfile.set({ tfa: [interactiveOTP] }, conf) + if (challenge.tfa === null) { + output('Two factor authentication mode changed to: ' + mode) + return + } - output( - '2FA successfully enabled. Below are your recovery codes, ' + - 'please print these out.' - ) - output( - 'You will need these to recover access to your account ' + - 'if you lose your authentication device.' - ) + const badResponse = typeof challenge.tfa !== 'string' + || !/^otpauth:[/][/]/.test(challenge.tfa) + if (badResponse) { + throw new Error( + 'Unknown error enabling two-factor authentication. Expected otpauth URL' + + ', got: ' + inspect(challenge.tfa) + ) + } - for (const tfaCode of result.tfa) - output('\t' + tfaCode) -} + const otpauth = new URL(challenge.tfa) + const secret = otpauth.searchParams.get('secret') + const code = await qrcode(challenge.tfa) -const getAuth = conf => { - const creds = npm.config.getCredentialsByURI(conf.registry) - const auth = {} - - if (creds.token) - auth.token = creds.token - else if (creds.username) - auth.basic = { username: creds.username, password: creds.password } - else if (creds.auth) { - const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2) - auth.basic = { username: basic[0], password: basic[1] } - } + output( + 'Scan into your authenticator app:\n' + code + '\n Or enter code:', secret + ) - if (conf.otp) - auth.otp = conf.otp + const interactiveOTP = + await readUserInfo.otp('And an OTP code from your authenticator: ') - return auth -} + log.info('profile', 'Finalizing two-factor authentication') -const disable2fa = async args => { - const conf = { ...npm.flatOptions } - const info = await pulseTillDone.withPromise(npmProfile.get(conf)) + const result = await npmProfile.set({ tfa: [interactiveOTP] }, conf) - if (!info.tfa || info.tfa.pending) { - output('Two factor authentication not enabled.') - return + output( + '2FA successfully enabled. Below are your recovery codes, ' + + 'please print these out.' + ) + output( + 'You will need these to recover access to your account ' + + 'if you lose your authentication device.' + ) + + for (const tfaCode of result.tfa) + output('\t' + tfaCode) } - const password = await readUserInfo.password() + async disable2fa (args) { + const conf = { ...this.npm.flatOptions } + const info = await pulseTillDone.withPromise(npmProfile.get(conf)) - if (!conf.otp) { - const msg = 'Enter one-time password from your authenticator app: ' - conf.otp = await readUserInfo.otp(msg) - } + if (!info.tfa || info.tfa.pending) { + output('Two factor authentication not enabled.') + return + } - log.info('profile', 'disabling tfa') + const password = await readUserInfo.password() - await pulseTillDone.withPromise(npmProfile.set({ - tfa: { password: password, mode: 'disable' }, - }, conf)) + if (!conf.otp) { + const msg = 'Enter one-time password from your authenticator app: ' + conf.otp = await readUserInfo.otp(msg) + } - if (conf.json) - output(JSON.stringify({ tfa: false }, null, 2)) - else if (conf.parseable) - output('tfa\tfalse') - else - output('Two factor authentication disabled.') -} + log.info('profile', 'disabling tfa') -const qrcode = url => - new Promise((resolve) => qrcodeTerminal.generate(url, resolve)) + await pulseTillDone.withPromise(npmProfile.set({ + tfa: { password: password, mode: 'disable' }, + }, conf)) -module.exports = Object.assign(cmd, { usage, completion }) + if (conf.json) + output(JSON.stringify({ tfa: false }, null, 2)) + else if (conf.parseable) + output('tfa\tfalse') + else + output('Two factor authentication disabled.') + } +} +module.exports = Profile diff --git a/lib/prune.js b/lib/prune.js index 228fd3eebb178..c4bdc0672821f 100644 --- a/lib/prune.js +++ b/lib/prune.js @@ -2,23 +2,31 @@ const npm = require('./npm.js') const Arborist = require('@npmcli/arborist') const usageUtil = require('./utils/usage.js') - const reifyFinish = require('./utils/reify-finish.js') -const usage = usageUtil('prune', - 'npm prune [[<@scope>/]...] [--production]' -) +class Prune { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => prune().then(() => cb()).catch(cb) + get usage () { + return usageUtil('prune', + 'npm prune [[<@scope>/]...] [--production]' + ) + } -const prune = async () => { - const where = npm.prefix - const arb = new Arborist({ - ...npm.flatOptions, - path: where, - }) - await arb.prune(npm.flatOptions) - await reifyFinish(arb) -} + exec (args, cb) { + this.prune().then(() => cb()).catch(cb) + } -module.exports = Object.assign(cmd, { usage }) + async prune () { + const where = this.npm.prefix + const arb = new Arborist({ + ...this.npm.flatOptions, + path: where, + }) + await arb.prune(npm.flatOptions) + await reifyFinish(this.npm, arb) + } +} +module.exports = Prune diff --git a/lib/publish.js b/lib/publish.js index 5ec66d42fa9a7..c8e82c44c5a3c 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -8,9 +8,10 @@ const pacote = require('pacote') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') -const npm = require('./npm.js') +const { flatten } = require('./utils/flat-options.js') const output = require('./utils/output.js') const otplease = require('./utils/otplease.js') +const usageUtil = require('./utils/usage.js') const { getContents, logTar } = require('./utils/tar.js') // this is the only case in the CLI where we use the old full slow @@ -18,122 +19,125 @@ const { getContents, logTar } = require('./utils/tar.js') // defaults and metadata, like git sha's and default scripts and all that. const readJson = util.promisify(require('read-package-json')) -const usageUtil = require('./utils/usage.js') -const usage = usageUtil('publish', - 'npm publish [] [--tag ] [--access ] [--dry-run]' + - '\n\nPublishes \'.\' if no argument supplied' + - '\nSets tag `latest` if no --tag specified') +class Publish { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => publish(args).then(() => cb()).catch(cb) + get usage () { + return usageUtil('publish', + 'npm publish [] [--tag ] [--access ] [--dry-run]' + + '\n\nPublishes \'.\' if no argument supplied' + + '\nSets tag `latest` if no --tag specified') + } -const publish = async args => { - if (args.length === 0) - args = ['.'] - if (args.length !== 1) - throw usage + exec (args, cb) { + this.publish(args).then(() => cb()).catch(cb) + } - log.verbose('publish', args) + async publish (args) { + if (args.length === 0) + args = ['.'] + if (args.length !== 1) + throw this.usage - const opts = { ...npm.flatOptions } - const { json, defaultTag } = opts + log.verbose('publish', args) - if (semver.validRange(defaultTag)) - throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim()) + const opts = { ...this.npm.flatOptions } + const { unicode, dryRun, json, defaultTag } = opts - const tarball = await publish_(args[0], opts) - const silent = log.level === 'silent' - if (!silent && json) - output(JSON.stringify(tarball, null, 2)) - else if (!silent) - output(`+ ${tarball.id}`) + if (semver.validRange(defaultTag)) + throw new Error('Tag name must not be a valid SemVer range: ' + defaultTag.trim()) - return tarball -} + // you can publish name@version, ./foo.tgz, etc. + // even though the default is the 'file:.' cwd. + const spec = npa(args[0]) + let manifest = await this.getManifest(spec, opts) -// if it's a directory, read it from the file system -// otherwise, get the full metadata from whatever it is -const getManifest = (spec, opts) => - spec.type === 'directory' ? readJson(`${spec.fetchSpec}/package.json`) - : pacote.manifest(spec, { ...opts, fullMetadata: true }) + if (manifest.publishConfig) + Object.assign(opts, this.publishConfigToOpts(manifest.publishConfig)) -// for historical reasons, publishConfig in package.json can contain -// ANY config keys that npm supports in .npmrc files and elsewhere. -// We *may* want to revisit this at some point, and have a minimal set -// that's a SemVer-major change that ought to get a RFC written on it. -const { flatten } = require('./utils/flat-options.js') -const publishConfigToOpts = publishConfig => - // create a new object that inherits from the config stack - // then squash the css-case into camelCase opts, like we do - flatten(Object.assign(Object.create(npm.config.list[0]), publishConfig)) - -const publish_ = async (arg, opts) => { - const { unicode, dryRun, json } = opts - // you can publish name@version, ./foo.tgz, etc. - // even though the default is the 'file:.' cwd. - const spec = npa(arg) - - let manifest = await getManifest(spec, opts) - - if (manifest.publishConfig) - Object.assign(opts, publishConfigToOpts(manifest.publishConfig)) - - // only run scripts for directory type publishes - if (spec.type === 'directory') { - await runScript({ - event: 'prepublishOnly', - path: spec.fetchSpec, - stdio: 'inherit', - pkg: manifest, - banner: log.level !== 'silent', - }) - } + // only run scripts for directory type publishes + if (spec.type === 'directory') { + await runScript({ + event: 'prepublishOnly', + path: spec.fetchSpec, + stdio: 'inherit', + pkg: manifest, + banner: log.level !== 'silent', + }) + } + + const tarballData = await pack(spec, opts) + const pkgContents = await getContents(manifest, tarballData) + + // The purpose of re-reading the manifest is in case it changed, + // so that we send the latest and greatest thing to the registry + // note that publishConfig might have changed as well! + manifest = await this.getManifest(spec, opts) + if (manifest.publishConfig) + Object.assign(opts, this.publishConfigToOpts(manifest.publishConfig)) + + // note that logTar calls npmlog.notice(), so if we ARE in silent mode, + // this will do nothing, but we still want it in the debuglog if it fails. + if (!json) + logTar(pkgContents, { log, unicode }) + + if (!dryRun) { + const resolved = npa.resolve(manifest.name, manifest.version) + const registry = npmFetch.pickRegistry(resolved, opts) + const creds = this.npm.config.getCredentialsByURI(registry) + if (!creds.token && !creds.username) { + throw Object.assign(new Error('This command requires you to be logged in.'), { + code: 'ENEEDAUTH', + }) + } + await otplease(opts, opts => libpub(manifest, tarballData, opts)) + } + + if (spec.type === 'directory') { + await runScript({ + event: 'publish', + path: spec.fetchSpec, + stdio: 'inherit', + pkg: manifest, + banner: log.level !== 'silent', + }) - const tarballData = await pack(spec, opts) - const pkgContents = await getContents(manifest, tarballData) - - // The purpose of re-reading the manifest is in case it changed, - // so that we send the latest and greatest thing to the registry - // note that publishConfig might have changed as well! - manifest = await getManifest(spec, opts) - if (manifest.publishConfig) - Object.assign(opts, publishConfigToOpts(manifest.publishConfig)) - - // note that logTar calls npmlog.notice(), so if we ARE in silent mode, - // this will do nothing, but we still want it in the debuglog if it fails. - if (!json) - logTar(pkgContents, { log, unicode }) - - if (!dryRun) { - const resolved = npa.resolve(manifest.name, manifest.version) - const registry = npmFetch.pickRegistry(resolved, opts) - const creds = npm.config.getCredentialsByURI(registry) - if (!creds.token && !creds.username) { - throw Object.assign(new Error('This command requires you to be logged in.'), { - code: 'ENEEDAUTH', + await runScript({ + event: 'postpublish', + path: spec.fetchSpec, + stdio: 'inherit', + pkg: manifest, + banner: log.level !== 'silent', }) } - await otplease(opts, opts => libpub(manifest, tarballData, opts)) + + const silent = log.level === 'silent' + if (!silent && json) + output(JSON.stringify(pkgContents, null, 2)) + else if (!silent) + output(`+ ${pkgContents.id}`) + + return pkgContents } - if (spec.type === 'directory') { - await runScript({ - event: 'publish', - path: spec.fetchSpec, - stdio: 'inherit', - pkg: manifest, - banner: log.level !== 'silent', - }) - - await runScript({ - event: 'postpublish', - path: spec.fetchSpec, - stdio: 'inherit', - pkg: manifest, - banner: log.level !== 'silent', - }) + // if it's a directory, read it from the file system + // otherwise, get the full metadata from whatever it is + getManifest (spec, opts) { + if (spec.type === 'directory') + return readJson(`${spec.fetchSpec}/package.json`) + return pacote.manifest(spec, { ...opts, fullMetadata: true }) } - return pkgContents + // for historical reasons, publishConfig in package.json can contain + // ANY config keys that npm supports in .npmrc files and elsewhere. + // We *may* want to revisit this at some point, and have a minimal set + // that's a SemVer-major change that ought to get a RFC written on it. + publishConfigToOpts (publishConfig) { + // create a new object that inherits from the config stack + // then squash the css-case into camelCase opts, like we do + return flatten({...this.npm.config.list[0], ...publishConfig}) + } } - -module.exports = Object.assign(cmd, { usage }) +module.exports = Publish diff --git a/lib/rebuild.js b/lib/rebuild.js index ab34b7f3dfb51..fa0360c3e42e2 100644 --- a/lib/rebuild.js +++ b/lib/rebuild.js @@ -2,64 +2,72 @@ const { resolve } = require('path') const Arborist = require('@npmcli/arborist') const npa = require('npm-package-arg') const semver = require('semver') - -const npm = require('./npm.js') const usageUtil = require('./utils/usage.js') const output = require('./utils/output.js') +const completion = require('./utils/completion/installed-deep.js') -const cmd = (args, cb) => rebuild(args).then(() => cb()).catch(cb) +class Rebuild { + constructor (npm) { + this.npm = npm + } -const usage = usageUtil('rebuild', 'npm rebuild [[<@scope>/][@] ...]') + get usage () { + return usageUtil('rebuild', 'npm rebuild [[<@scope>/][@] ...]') + } -const completion = require('./utils/completion/installed-deep.js') + async completion (opts) { + return completion(this.npm, opts) + } -const rebuild = async args => { - const globalTop = resolve(npm.globalDir, '..') - const where = npm.flatOptions.global ? globalTop : npm.prefix - const arb = new Arborist({ - ...npm.flatOptions, - path: where, - }) + exec (args, cb) { + this.rebuild(args).then(() => cb()).catch(cb) + } - if (args.length) { - // get the set of nodes matching the name that we want rebuilt - const tree = await arb.loadActual() - const filter = getFilterFn(args) - await arb.rebuild({ - nodes: tree.inventory.filter(filter), + async rebuild (args) { + const globalTop = resolve(this.npm.globalDir, '..') + const where = this.npm.flatOptions.global ? globalTop : this.npm.prefix + const arb = new Arborist({ + ...this.npm.flatOptions, + path: where, }) - } else - await arb.rebuild() - output('rebuilt dependencies successfully') -} + if (args.length) { + // get the set of nodes matching the name that we want rebuilt + const tree = await arb.loadActual() + const specs = args.map(arg => { + const spec = npa(arg) + if (spec.type === 'tag' && spec.rawSpec === '') + return spec -const getFilterFn = args => { - const specs = args.map(arg => { - const spec = npa(arg) - if (spec.type === 'tag' && spec.rawSpec === '') - return spec + if (spec.type !== 'range' && spec.type !== 'version' && spec.type !== 'directory') + throw new Error('`npm rebuild` only supports SemVer version/range specifiers') - if (spec.type !== 'range' && spec.type !== 'version' && spec.type !== 'directory') - throw new Error('`npm rebuild` only supports SemVer version/range specifiers') + return spec + }) + const nodes = tree.inventory.filter(node => this.isNode(specs, node)) - return spec - }) + await arb.rebuild({ nodes }) + } else + await arb.rebuild() - return node => specs.some(spec => { - if (spec.type === 'directory') - return node.path === spec.fetchSpec + output('rebuilt dependencies successfully') + } - if (spec.name !== node.name) - return false + isNode (specs, node) { + return specs.some(spec => { + if (spec.type === 'directory') + return node.path === spec.fetchSpec - if (spec.rawSpec === '' || spec.rawSpec === '*') - return true + if (spec.name !== node.name) + return false - const { version } = node.package - // TODO: add tests for a package with missing version - return semver.satisfies(version, spec.fetchSpec) - }) -} + if (spec.rawSpec === '' || spec.rawSpec === '*') + return true -module.exports = Object.assign(cmd, { usage, completion }) + const { version } = node.package + // TODO: add tests for a package with missing version + return semver.satisfies(version, spec.fetchSpec) + }) + } +} +module.exports = Rebuild diff --git a/lib/restart.js b/lib/restart.js index 1462cf6051d0f..8ca9f334d90ae 100644 --- a/lib/restart.js +++ b/lib/restart.js @@ -1,2 +1,8 @@ -const npm = require('./npm.js') -module.exports = require('./utils/lifecycle-cmd.js')(npm, 'restart') +const LifecycleCmd = require('./utils/lifecycle-cmd.js') + +class Restart extends LifecycleCmd { + constructor (npm) { + super(npm, 'restart') + } +} +module.exports = Restart diff --git a/lib/root.js b/lib/root.js index 631aef83867d1..13698841cd903 100644 --- a/lib/root.js +++ b/lib/root.js @@ -1,7 +1,21 @@ -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') -const cmd = (args, cb) => root(args).then(() => cb()).catch(cb) -const usage = usageUtil('root', 'npm root [-g]') -const root = async (args, cb) => output(npm.dir) -module.exports = Object.assign(cmd, { usage }) + +class Root { + constructor (npm) { + this.npm = npm + } + + get usage () { + return usageUtil('root', 'npm root [-g]') + } + + exec (args, cb) { + this.root(args).then(() => cb()).catch(cb) + } + + async root () { + output(this.npm.dir) + } +} +module.exports = Root diff --git a/lib/run-script.js b/lib/run-script.js index 4dfb854cad9fa..e158895697e8d 100644 --- a/lib/run-script.js +++ b/lib/run-script.js @@ -6,139 +6,152 @@ const { resolve } = require('path') const output = require('./utils/output.js') const log = require('npmlog') const usageUtil = require('./utils/usage') -const didYouMean = require('./utils/did-you-mean') +const didYouMean = require('./utils/did-you-mean.js') const isWindowsShell = require('./utils/is-windows-shell.js') -const usage = usageUtil( - 'run-script', - 'npm run-script [-- ]' -) - -const completion = async (opts) => { - const argv = opts.conf.argv.remain - if (argv.length === 2) { - // find the script name - const json = resolve(npm.localPrefix, 'package.json') - const { scripts = {} } = await readJson(json).catch(er => ({})) - return Object.keys(scripts) +const cmdList = [ + 'publish', + 'install', + 'uninstall', + 'test', + 'stop', + 'start', + 'restart', + 'version', +].reduce((l, p) => l.concat(['pre' + p, p, 'post' + p]), []) + +class RunScript { + constructor (npm) { + this.npm = npm } -} -const cmd = (args, cb) => { - const fn = args.length ? doRun : list - return fn(args).then(() => cb()).catch(cb) -} + get usage () { + return usageUtil( + 'run-script', + 'npm run-script [-- ]' + ) + } -const doRun = async (args) => { - const path = npm.localPrefix - const event = args.shift() - const { scriptShell } = npm.flatOptions + async completion (opts) { + const argv = opts.conf.argv.remain + if (argv.length === 2) { + // find the script name + const json = resolve(npm.localPrefix, 'package.json') + const { scripts = {} } = await readJson(json).catch(er => ({})) + return Object.keys(scripts) + } + } - const pkg = await readJson(`${path}/package.json`) - const { scripts = {} } = pkg + exec (args, cb) { + if (args.length) + this.run(args).then(() => cb()).catch(cb) + else + this.list(args).then(() => cb()).catch(cb) + } - if (event === 'restart' && !scripts.restart) - scripts.restart = 'npm stop --if-present && npm start' - else if (event === 'env' && !scripts.env) - scripts.env = isWindowsShell ? 'SET' : 'env' + async run (args) { + const path = this.npm.localPrefix + const event = args.shift() + const { scriptShell } = this.npm.flatOptions - pkg.scripts = scripts + const pkg = await readJson(`${path}/package.json`) + const { scripts = {} } = pkg - if (!Object.prototype.hasOwnProperty.call(scripts, event) && !(event === 'start' && await isServerPackage(path))) { - if (npm.config.get('if-present')) - return + if (event === 'restart' && !scripts.restart) + scripts.restart = 'npm stop --if-present && npm start' + else if (event === 'env' && !scripts.env) + scripts.env = isWindowsShell ? 'SET' : 'env' - const suggestions = didYouMean(event, Object.keys(scripts)) - throw new Error(`missing script: ${event}${ - suggestions ? `\n${suggestions}` : ''}`) - } + pkg.scripts = scripts - // positional args only added to the main event, not pre/post - const events = [[event, args]] - if (!npm.flatOptions.ignoreScripts) { - if (scripts[`pre${event}`]) - events.unshift([`pre${event}`, []]) + if ( + !Object.prototype.hasOwnProperty.call(scripts, event) && + !(event === 'start' && await isServerPackage(path)) + ) { + if (this.npm.config.get('if-present')) + return - if (scripts[`post${event}`]) - events.push([`post${event}`, []]) - } + const suggestions = didYouMean(event, Object.keys(scripts)) + throw new Error(`missing script: ${event}${ + suggestions ? `\n${suggestions}` : ''}`) + } - const opts = { - path, - args, - scriptShell, - stdio: 'inherit', - stdioString: true, - pkg, - banner: log.level !== 'silent', - } + // positional args only added to the main event, not pre/post + const events = [[event, args]] + if (!this.npm.flatOptions.ignoreScripts) { + if (scripts[`pre${event}`]) + events.unshift([`pre${event}`, []]) + + if (scripts[`post${event}`]) + events.push([`post${event}`, []]) + } - for (const [event, args] of events) { - await runScript({ - ...opts, - event, + const opts = { + path, args, - }) + scriptShell, + stdio: 'inherit', + stdioString: true, + pkg, + banner: log.level !== 'silent', + } + + for (const [event, args] of events) { + await runScript({ + ...opts, + event, + args, + }) + } } -} -const list = async () => { - const path = npm.localPrefix - const { scripts, name } = await readJson(`${path}/package.json`) - const cmdList = [ - 'publish', - 'install', - 'uninstall', - 'test', - 'stop', - 'start', - 'restart', - 'version', - ].reduce((l, p) => l.concat(['pre' + p, p, 'post' + p]), []) - - if (!scripts) - return [] - - const allScripts = Object.keys(scripts) - if (log.level === 'silent') - return allScripts + async list () { + const path = this.npm.localPrefix + const { scripts, name } = await readJson(`${path}/package.json`) - if (npm.flatOptions.json) { - output(JSON.stringify(scripts, null, 2)) - return allScripts - } + if (!scripts) + return [] - if (npm.flatOptions.parseable) { - for (const [script, cmd] of Object.entries(scripts)) - output(`${script}:${cmd}`) + const allScripts = Object.keys(scripts) + if (log.level === 'silent') + return allScripts - return allScripts - } + if (this.npm.flatOptions.json) { + output(JSON.stringify(scripts, null, 2)) + return allScripts + } - const indent = '\n ' - const prefix = ' ' - const cmds = [] - const runScripts = [] - for (const script of allScripts) { - const list = cmdList.includes(script) ? cmds : runScripts - list.push(script) - } + if (this.npm.flatOptions.parseable) { + for (const [script, cmd] of Object.entries(scripts)) + output(`${script}:${cmd}`) - if (cmds.length) - output(`Lifecycle scripts included in ${name}:`) + return allScripts + } - for (const script of cmds) - output(prefix + script + indent + scripts[script]) + const indent = '\n ' + const prefix = ' ' + const cmds = [] + const runScripts = [] + for (const script of allScripts) { + const list = cmdList.includes(script) ? cmds : runScripts + list.push(script) + } - if (!cmds.length && runScripts.length) - output(`Scripts available in ${name} via \`npm run-script\`:`) - else if (runScripts.length) - output('\navailable via `npm run-script`:') + if (cmds.length) + output(`Lifecycle scripts included in ${name}:`) - for (const script of runScripts) - output(prefix + script + indent + scripts[script]) + for (const script of cmds) + output(prefix + script + indent + scripts[script]) - return allScripts -} + if (!cmds.length && runScripts.length) + output(`Scripts available in ${name} via \`npm run-script\`:`) + else if (runScripts.length) + output('\navailable via `npm run-script`:') + + for (const script of runScripts) + output(prefix + script + indent + scripts[script]) -module.exports = Object.assign(cmd, { completion, usage }) + return allScripts + } +} +module.exports = RunScript diff --git a/lib/start.js b/lib/start.js index 9fa076d5e35f5..73cb96af88531 100644 --- a/lib/start.js +++ b/lib/start.js @@ -1,2 +1,8 @@ -const npm = require('./npm.js') -module.exports = require('./utils/lifecycle-cmd.js')(npm, 'start') +const LifecycleCmd = require('./utils/lifecycle-cmd.js') + +class Start extends LifecycleCmd { + constructor (npm) { + super(npm, 'start') + } +} +module.exports = Start diff --git a/lib/stop.js b/lib/stop.js index 827d414d1384c..7f82aeeb8ffe2 100644 --- a/lib/stop.js +++ b/lib/stop.js @@ -1,2 +1,8 @@ -const npm = require('./npm.js') -module.exports = require('./utils/lifecycle-cmd.js')(npm, 'stop') +const LifecycleCmd = require('./utils/lifecycle-cmd.js') + +class Stop extends LifecycleCmd { + constructor (npm) { + super(npm, 'stop') + } +} +module.exports = Stop diff --git a/lib/test.js b/lib/test.js index ea5914ea38a1a..1b3c36d1ce5d5 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,12 +1,18 @@ -const npm = require('./npm.js') -const testCmd = require('./utils/lifecycle-cmd.js')(npm, 'test') -const { completion, usage } = testCmd -const cmd = (args, cb) => testCmd(args, er => { - if (er && er.code === 'ELIFECYCLE') { - /* eslint-disable standard/no-callback-literal */ - cb('Test failed. See above for more details.') - } else - cb(er) -}) +const LifecycleCmd = require('./utils/lifecycle-cmd.js') -module.exports = Object.assign(cmd, { completion, usage }) +class Test extends LifecycleCmd { + constructor (npm) { + super(npm, 'test') + } + + exec (args, cb) { + super.exec(args, er => { + if (er && er.code === 'ELIFECYCLE') { + /* eslint-disable standard/no-callback-literal */ + cb('Test failed. See above for more details.') + } else + cb(er) + }) + } +} +module.exports = Test diff --git a/lib/utils/audit-error.js b/lib/utils/audit-error.js index c7423c4472d5d..ae0749ff6f0be 100644 --- a/lib/utils/audit-error.js +++ b/lib/utils/audit-error.js @@ -4,8 +4,7 @@ // returns 'true' if there was an error, false otherwise const output = require('./output.js') -const npm = require('../npm.js') -const auditError = (report) => { +const auditError = (npm, report) => { if (!report || !report.error) return false diff --git a/lib/utils/completion/installed-deep.js b/lib/utils/completion/installed-deep.js index f464bb9a9d7a3..b65c17e41d743 100644 --- a/lib/utils/completion/installed-deep.js +++ b/lib/utils/completion/installed-deep.js @@ -1,8 +1,7 @@ const { resolve } = require('path') const Arborist = require('@npmcli/arborist') -const npm = require('../../npm.js') -const installedDeep = async () => { +const installedDeep = async (npm) => { const { depth, global, diff --git a/lib/utils/completion/installed-shallow.js b/lib/utils/completion/installed-shallow.js index c9c680e7dd293..1c9b8ef5acb0f 100644 --- a/lib/utils/completion/installed-shallow.js +++ b/lib/utils/completion/installed-shallow.js @@ -1,10 +1,8 @@ -const npm = require('../../npm.js') const { promisify } = require('util') const readdir = promisify(require('readdir-scoped-modules')) -const names = global => readdir(global ? npm.globalDir : npm.localDir) - -const installedShallow = async (opts) => { +const installedShallow = async (npm, opts) => { + const names = global => readdir(global ? npm.globalDir : npm.localDir) const { conf: { argv: { remain } } } = opts if (remain.length > 3) return null diff --git a/lib/utils/lifecycle-cmd.js b/lib/utils/lifecycle-cmd.js index 94b109942aa0b..8be9b5a12f9a3 100644 --- a/lib/utils/lifecycle-cmd.js +++ b/lib/utils/lifecycle-cmd.js @@ -1,10 +1,19 @@ // The implementation of commands that are just "run a script" -// test, start, stop, restart - +// restart, start, stop, test const usageUtil = require('./usage.js') -module.exports = (npm, stage) => { - const cmd = (args, cb) => npm.commands['run-script']([stage, ...args], cb) - const usage = usageUtil(stage, `npm ${stage} [-- ]`) - return Object.assign(cmd, { usage }) +class LifecycleCmd { + constructor (npm, stage) { + this.npm = npm + this.stage = stage + } + + get usage () { + return usageUtil(this.stage, `npm ${this.stage} [-- ]`) + } + + exec (args, cb) { + this.npm.commands['run-script']([this.stage, ...args], cb) + } } +module.exports = LifecycleCmd diff --git a/lib/utils/npm-usage.js b/lib/utils/npm-usage.js index d4261f79dcb71..6350b026e984b 100644 --- a/lib/utils/npm-usage.js +++ b/lib/utils/npm-usage.js @@ -1,10 +1,9 @@ -const npm = require('../npm.js') const didYouMean = require('./did-you-mean.js') const { dirname } = require('path') const output = require('./output.js') const { cmdList } = require('./cmd-list') -module.exports = (valid = true) => { +module.exports = (npm, valid = true) => { npm.config.set('loglevel', 'silent') const usesBrowser = npm.config.get('viewer') === 'browser' ? ' (in a browser)' : '' @@ -22,7 +21,7 @@ npm help search for help on ${usesBrowser} npm help npm more involved overview${usesBrowser} All commands: -${npm.config.get('long') ? usages() : ('\n ' + wrap(cmdList))} +${npm.config.get('long') ? usages(npm) : ('\n ' + wrap(cmdList))} Specify configs in the ini-formatted file: ${npm.config.get('userconfig')} @@ -59,7 +58,7 @@ const wrap = (arr) => { return out.join('\n ').substr(2) } -const usages = () => { +const usages = (npm) => { // return a string of : let maxLen = 0 return cmdList.reduce((set, c) => { diff --git a/lib/utils/open-url.js b/lib/utils/open-url.js index 28c2d038a47f3..c259d955b9ab6 100644 --- a/lib/utils/open-url.js +++ b/lib/utils/open-url.js @@ -1,21 +1,14 @@ -const npm = require('../npm.js') const output = require('./output.js') const opener = require('opener') const { URL } = require('url') -const isUrlValid = url => { - try { - return /^(https?|file):$/.test(new URL(url).protocol) - } catch (_) { - return false - } -} - // attempt to open URL in web-browser, print address otherwise: -module.exports = function open (url, errMsg, cb, browser = npm.config.get('browser')) { +const open = async (npm, url, errMsg) => { + const browser = npm.config.get('browser') + function printAlternateMsg () { - const json = npm.config.get('json') + const json = this.npm.config.get('json') const alternateMsg = json ? JSON.stringify({ title: errMsg, @@ -28,18 +21,25 @@ module.exports = function open (url, errMsg, cb, browser = npm.config.get('brows if (browser === false) { printAlternateMsg() - return cb() + return } - if (!isUrlValid(url)) - return cb(new Error('Invalid URL: ' + url)) + try { + /^(https?|file):$/.test(new URL(url).protocol) + } catch (_) { + throw new Error('Invalid URL: ' + url) + } const command = browser === true ? null : browser - opener(url, { command }, (er) => { - if (er && er.code === 'ENOENT') { - printAlternateMsg() - return cb() - } else - return cb(er) + await new Promise((resolve, reject) => { + opener(url, { command }, (err) => { + if (err && err.code === 'ENOENT') { + printAlternateMsg() + resolve() + } else + reject(err) + }) }) } + +module.exports = open diff --git a/lib/utils/read-local-package.js b/lib/utils/read-local-package.js index 7ab130c1f31b0..c31bca994704c 100644 --- a/lib/utils/read-local-package.js +++ b/lib/utils/read-local-package.js @@ -1,8 +1,6 @@ const { resolve } = require('path') const readJson = require('read-package-json-fast') -const npm = require('../npm.js') - -async function readLocalPackageName (cb) { +async function readLocalPackageName (npm) { if (npm.flatOptions.global) return diff --git a/lib/utils/reify-finish.js b/lib/utils/reify-finish.js index 9c95e9fcff24e..1c02b93a41294 100644 --- a/lib/utils/reify-finish.js +++ b/lib/utils/reify-finish.js @@ -1,17 +1,16 @@ const reifyOutput = require('./reify-output.js') -const npm = require('../npm.js') const ini = require('ini') const util = require('util') const fs = require('fs') const { writeFile } = fs.promises || { writeFile: util.promisify(fs.writeFile) } const {resolve} = require('path') -const reifyFinish = async arb => { - await saveBuiltinConfig(arb) - reifyOutput(arb) +const reifyFinish = async (npm, arb) => { + await saveBuiltinConfig(npm, arb) + reifyOutput(npm, arb) } -const saveBuiltinConfig = async arb => { +const saveBuiltinConfig = async (npm, arb) => { const { options: { global }, actualTree } = arb if (!global) return diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index 4abaadc2ec7aa..983a02811a39e 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -9,7 +9,6 @@ // found 37 vulnerabilities (5 low, 7 moderate, 25 high) // run `npm audit fix` to fix them, or `npm audit` for details -const npm = require('../npm.js') const log = require('npmlog') const output = require('./output.js') const { depth } = require('treeverse') @@ -19,7 +18,7 @@ const { readTree: getFundingInfo } = require('libnpmfund') const auditError = require('./audit-error.js') // TODO: output JSON if flatOptions.json is true -const reifyOutput = arb => { +const reifyOutput = (npm, arb) => { // don't print any info in --silent mode if (log.levels[log.level] > log.levels.error) return @@ -75,9 +74,9 @@ const reifyOutput = arb => { } output(JSON.stringify(summary, 0, 2)) } else { - packagesChangedMessage(summary) + packagesChangedMessage(npm, summary) packagesFundingMessage(summary) - printAuditReport(auditReport) + printAuditReport(npm, auditReport) } } @@ -85,7 +84,7 @@ const reifyOutput = arb => { // at the end if there's still stuff, because it's silly for `npm audit` // to tell you to run `npm audit` for details. otherwise, use the summary // report. if we get here, we know it's not quiet or json. -const printAuditReport = report => { +const printAuditReport = (npm, report) => { if (!report) return @@ -102,7 +101,7 @@ const printAuditReport = report => { output('\n' + res.report) } -const packagesChangedMessage = ({ added, removed, changed, audited }) => { +const packagesChangedMessage = (npm, { added, removed, changed, audited }) => { const msg = ['\n'] if (added === 0 && removed === 0 && changed === 0) { msg.push('up to date') diff --git a/lib/utils/usage.js b/lib/utils/usage.js index ddcbd708b7e84..5f4eca73ea5d3 100644 --- a/lib/utils/usage.js +++ b/lib/utils/usage.js @@ -1,7 +1,7 @@ -var aliases = require('../utils/cmd-list').aliases +const aliases = require('../utils/cmd-list').aliases module.exports = function usage (cmd, txt, opt) { - var post = Object.keys(aliases).reduce(function (p, c) { + const post = Object.keys(aliases).reduce(function (p, c) { var val = aliases[c] if (val !== cmd) return p diff --git a/test/lib/access.js b/test/lib/access.js index fb799f2df29d8..3a732ad0aac37 100644 --- a/test/lib/access.js +++ b/test/lib/access.js @@ -1,17 +1,12 @@ const { test } = require('tap') const requireInject = require('require-inject') -const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { - flatOptions: {}, - }, -}) +const Access = require('../../lib/access.js') test('completion', t => { - const { completion } = access - + const access = new Access({ flatOptions: {} }) const testComp = (argv, expect) => { - const res = completion({conf: {argv: {remain: argv}}}) + const res = access.completion({conf: {argv: {remain: argv}}}) t.resolves(res, expect, argv.join(' ')) } @@ -32,7 +27,7 @@ test('completion', t => { testComp(['npm', 'access', 'revoke'], []) t.rejects( - completion({conf: {argv: {remain: ['npm', 'access', 'foobar']}}}), + access.completion({conf: {argv: {remain: ['npm', 'access', 'foobar']}}}), { message: 'foobar not recognized' } ) @@ -40,14 +35,16 @@ test('completion', t => { }) test('subcommand required', t => { - access([], (err) => { + const access = new Access({ flatOptions: {} }) + access.exec([], (err) => { t.equal(err, '\nUsage: Subcommand is required.\n\n' + access.usage) t.end() }) }) test('unrecognized subcommand', (t) => { - access(['blerg'], (err) => { + const access = new Access({ flatOptions: {} }) + access.exec(['blerg'], (err) => { t.match( err, /Usage: blerg is not a recognized subcommand/, @@ -58,7 +55,8 @@ test('unrecognized subcommand', (t) => { }) test('edit', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'edit', '@scoped/another', ], (err) => { @@ -77,10 +75,8 @@ test('access public on unscoped package', (t) => { name: 'npm-access-public-pkg', }), }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'public', ], (err) => { t.match( @@ -98,7 +94,7 @@ test('access public on scoped package', (t) => { const prefix = t.testdir({ 'package.json': JSON.stringify({ name }), }) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { public: (pkg, { registry }) => { t.equal(pkg, name, 'should use pkg name ref') @@ -110,14 +106,12 @@ test('access public on scoped package', (t) => { return true }, }, - '../../lib/npm.js': { - flatOptions: { - registry: 'https://registry.npmjs.org', - }, - prefix, - }, }) - access([ + const access = new Access({ + flatOptions: { registry: 'https://registry.npmjs.org' }, + prefix, + }) + access.exec([ 'public', ], (err) => { t.ifError(err, 'npm access') @@ -129,10 +123,8 @@ test('access public on missing package.json', (t) => { const prefix = t.testdir({ node_modules: {}, }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'public', ], (err) => { t.match( @@ -149,10 +141,8 @@ test('access public on invalid package.json', (t) => { 'package.json': '{\n', node_modules: {}, }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'public', ], (err) => { t.match( @@ -170,10 +160,8 @@ test('access restricted on unscoped package', (t) => { name: 'npm-access-restricted-pkg', }), }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'restricted', ], (err) => { t.match( @@ -191,7 +179,7 @@ test('access restricted on scoped package', (t) => { const prefix = t.testdir({ 'package.json': JSON.stringify({ name }), }) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { restricted: (pkg, { registry }) => { t.equal(pkg, name, 'should use pkg name ref') @@ -203,14 +191,12 @@ test('access restricted on scoped package', (t) => { return true }, }, - '../../lib/npm.js': { - flatOptions: { - registry: 'https://registry.npmjs.org', - }, - prefix, - }, }) - access([ + const access = new Access({ + flatOptions: { registry: 'https://registry.npmjs.org' }, + prefix, + }) + access.exec([ 'restricted', ], (err) => { t.ifError(err, 'npm access') @@ -222,10 +208,8 @@ test('access restricted on missing package.json', (t) => { const prefix = t.testdir({ node_modules: {}, }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'restricted', ], (err) => { t.match( @@ -242,10 +226,8 @@ test('access restricted on invalid package.json', (t) => { 'package.json': '{\n', node_modules: {}, }) - const access = requireInject('../../lib/access.js', { - '../../lib/npm.js': { prefix }, - }) - access([ + const access = new Access({ prefix }) + access.exec([ 'restricted', ], (err) => { t.match( @@ -259,7 +241,7 @@ test('access restricted on invalid package.json', (t) => { test('access grant read-only', (t) => { t.plan(5) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { grant: (spec, team, permissions) => { t.equal(spec, '@scoped/another', 'should use expected spec') @@ -268,9 +250,9 @@ test('access grant read-only', (t) => { return true }, }, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'grant', 'read-only', 'myorg:myteam', @@ -283,7 +265,7 @@ test('access grant read-only', (t) => { test('access grant read-write', (t) => { t.plan(5) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { grant: (spec, team, permissions) => { t.equal(spec, '@scoped/another', 'should use expected spec') @@ -292,9 +274,9 @@ test('access grant read-write', (t) => { return true }, }, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'grant', 'read-write', 'myorg:myteam', @@ -312,7 +294,7 @@ test('access grant current cwd', (t) => { name: 'yargs', }), }) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { grant: (spec, team, permissions) => { t.equal(spec, 'yargs', 'should use expected spec') @@ -321,9 +303,9 @@ test('access grant current cwd', (t) => { return true }, }, - '../../lib/npm.js': { prefix }, }) - access([ + const access = new Access({ prefix }) + access.exec([ 'grant', 'read-write', 'myorg:myteam', @@ -334,7 +316,8 @@ test('access grant current cwd', (t) => { }) test('access grant others', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'grant', 'rerere', 'myorg:myteam', @@ -350,7 +333,8 @@ test('access grant others', (t) => { }) test('access grant missing team args', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'grant', 'read-only', undefined, @@ -366,7 +350,8 @@ test('access grant missing team args', (t) => { }) test('access grant malformed team arg', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'grant', 'read-only', 'foo', @@ -383,7 +368,7 @@ test('access grant malformed team arg', (t) => { test('access 2fa-required/2fa-not-required', t => { t.plan(2) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { tfaRequired: (spec) => { t.equal(spec, '@scope/pkg', 'should use expected spec') @@ -394,15 +379,15 @@ test('access 2fa-required/2fa-not-required', t => { return true }, }, - '../../lib/npm.js': {}, }) + const access = new Access({}) - access(['2fa-required', '@scope/pkg'], er => { + access.exec(['2fa-required', '@scope/pkg'], er => { if (er) throw er }) - access(['2fa-not-required', 'unscoped-pkg'], er => { + access.exec(['2fa-not-required', 'unscoped-pkg'], er => { if (er) throw er }) @@ -410,7 +395,7 @@ test('access 2fa-required/2fa-not-required', t => { test('access revoke', (t) => { t.plan(4) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { revoke: (spec, team) => { t.equal(spec, '@scoped/another', 'should use expected spec') @@ -418,9 +403,9 @@ test('access revoke', (t) => { return true }, }, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'revoke', 'myorg:myteam', '@scoped/another', @@ -431,7 +416,8 @@ test('access revoke', (t) => { }) test('access revoke missing team args', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'revoke', undefined, '@scoped/another', @@ -446,7 +432,8 @@ test('access revoke missing team args', (t) => { }) test('access revoke malformed team arg', (t) => { - access([ + const access = new Access({ flatOptions: {} }) + access.exec([ 'revoke', 'foo', '@scoped/another', @@ -462,7 +449,7 @@ test('access revoke malformed team arg', (t) => { test('npm access ls-packages with no team', (t) => { t.plan(3) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { lsPackages: (entity) => { t.equal(entity, 'foo', 'should use expected entity') @@ -471,9 +458,9 @@ test('npm access ls-packages with no team', (t) => { }, '../../lib/utils/get-identity.js': () => Promise.resolve('foo'), '../../lib/utils/output.js': () => null, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'ls-packages', ], (err) => { t.ifError(err, 'npm access') @@ -483,7 +470,7 @@ test('npm access ls-packages with no team', (t) => { test('access ls-packages on team', (t) => { t.plan(3) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { lsPackages: (entity) => { t.equal(entity, 'myorg:myteam', 'should use expected entity') @@ -491,9 +478,9 @@ test('access ls-packages on team', (t) => { }, }, '../../lib/utils/output.js': () => null, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'ls-packages', 'myorg:myteam', ], (err) => { @@ -509,7 +496,7 @@ test('access ls-collaborators on current', (t) => { name: 'yargs', }), }) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { lsCollaborators: (spec) => { t.equal(spec, 'yargs', 'should use expected spec') @@ -517,9 +504,9 @@ test('access ls-collaborators on current', (t) => { }, }, '../../lib/utils/output.js': () => null, - '../../lib/npm.js': { prefix }, }) - access([ + const access = new Access({ prefix }) + access.exec([ 'ls-collaborators', ], (err) => { t.ifError(err, 'npm access') @@ -529,7 +516,7 @@ test('access ls-collaborators on current', (t) => { test('access ls-collaborators on spec', (t) => { t.plan(3) - const access = requireInject('../../lib/access.js', { + const Access = requireInject('../../lib/access.js', { libnpmaccess: { lsCollaborators: (spec) => { t.equal(spec, 'yargs', 'should use expected spec') @@ -537,9 +524,9 @@ test('access ls-collaborators on spec', (t) => { }, }, '../../lib/utils/output.js': () => null, - '../../lib/npm.js': {}, }) - access([ + const access = new Access({}) + access.exec([ 'ls-collaborators', 'yargs', ], (err) => { diff --git a/test/lib/adduser.js b/test/lib/adduser.js index 36f59e0857902..d370e38d0b2c2 100644 --- a/test/lib/adduser.js +++ b/test/lib/adduser.js @@ -37,46 +37,49 @@ const deleteMock = (key, where) => { [key]: where, } } -const adduser = requireInject('../../lib/adduser.js', { +const npm = { + flatOptions: _flatOptions, + config: { + delete: deleteMock, + get (key, where) { + if (!where || where === 'user') + return _flatOptions[key] + }, + getCredentialsByURI, + async save () { + if (failSave) + throw new Error('error saving user config') + }, + set (key, value, where) { + setConfig = { + ...setConfig, + [key]: { + value, + where, + }, + } + }, + setCredentialsByURI, + }, +} + +const AddUser = requireInject('../../lib/adduser.js', { npmlog: { disableProgress: () => null, notice: (_, msg) => { registryOutput = msg }, }, - '../../lib/npm.js': { - flatOptions: _flatOptions, - config: { - delete: deleteMock, - get (key, where) { - if (!where || where === 'user') - return _flatOptions[key] - }, - getCredentialsByURI, - async save () { - if (failSave) - throw new Error('error saving user config') - }, - set (key, value, where) { - setConfig = { - ...setConfig, - [key]: { - value, - where, - }, - } - }, - setCredentialsByURI, - }, - }, '../../lib/utils/output.js': msg => { result = msg }, '../../lib/auth/legacy.js': authDummy, }) +const adduser = new AddUser(npm) + test('simple login', (t) => { - adduser([], (err) => { + adduser.exec([], (err) => { t.ifError(err, 'npm adduser') t.equal( @@ -129,7 +132,7 @@ test('simple login', (t) => { test('bad auth type', (t) => { _flatOptions.authType = 'foo' - adduser([], (err) => { + adduser.exec([], (err) => { t.match( err, /Error: no such auth module/, @@ -147,7 +150,7 @@ test('bad auth type', (t) => { test('scoped login', (t) => { _flatOptions.scope = '@myscope' - adduser([], (err) => { + adduser.exec([], (err) => { t.ifError(err, 'npm adduser') t.deepEqual( @@ -168,7 +171,7 @@ test('scoped login with valid scoped registry config', (t) => { _flatOptions['@myscope:registry'] = 'https://diff-registry.npmjs.com/' _flatOptions.scope = '@myscope' - adduser([], (err) => { + adduser.exec([], (err) => { t.ifError(err, 'npm adduser') t.deepEqual( @@ -189,7 +192,7 @@ test('scoped login with valid scoped registry config', (t) => { test('save config failure', (t) => { failSave = true - adduser([], (err) => { + adduser.exec([], (err) => { t.match( err, /error saving user config/, diff --git a/test/lib/audit.js b/test/lib/audit.js index 3d6296bac6486..6fd9c8a2c9b8f 100644 --- a/test/lib/audit.js +++ b/test/lib/audit.js @@ -1,6 +1,5 @@ const t = require('tap') const requireInject = require('require-inject') -const audit = require('../../lib/audit.js') t.test('should audit using Arborist', t => { let ARB_ARGS = null @@ -10,13 +9,13 @@ t.test('should audit using Arborist', t => { let OUTPUT_CALLED = false let ARB_OBJ = null - const audit = requireInject('../../lib/audit.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - json: false, - }, + const npm = { + prefix: 'foo', + flatOptions: { + json: false, }, + } + const Audit = requireInject('../../lib/audit.js', { 'npm-audit-report': () => { AUDIT_REPORT_CALLED = true return { @@ -32,7 +31,7 @@ t.test('should audit using Arborist', t => { this.auditReport = {} } }, - '../../lib/utils/reify-finish.js': arb => { + '../../lib/utils/reify-finish.js': (npm, arb) => { if (arb !== ARB_OBJ) throw new Error('got wrong object passed to reify-output') @@ -43,8 +42,10 @@ t.test('should audit using Arborist', t => { }, }) + const audit = new Audit(npm) + t.test('audit', t => { - audit([], () => { + audit.exec([], () => { t.match(ARB_ARGS, { audit: true, path: 'foo' }) t.equal(AUDIT_CALLED, true, 'called audit') t.equal(AUDIT_REPORT_CALLED, true, 'called audit report') @@ -54,7 +55,7 @@ t.test('should audit using Arborist', t => { }) t.test('audit fix', t => { - audit(['fix'], () => { + audit.exec(['fix'], () => { t.equal(REIFY_FINISH_CALLED, true, 'called reify output') t.end() }) @@ -64,13 +65,14 @@ t.test('should audit using Arborist', t => { }) t.test('should audit - json', t => { - const audit = requireInject('../../lib/audit.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - json: true, - }, + const npm = { + prefix: 'foo', + flatOptions: { + json: true, }, + } + + const Audit = requireInject('../../lib/audit.js', { 'npm-audit-report': () => ({ report: 'there are vulnerabilities', exitCode: 0, @@ -83,8 +85,9 @@ t.test('should audit - json', t => { '../../lib/utils/reify-output.js': () => {}, '../../lib/utils/output.js': () => {}, }) + const audit = new Audit(npm) - audit([], (err) => { + audit.exec([], (err) => { t.notOk(err, 'no errors') t.end() }) @@ -95,17 +98,17 @@ t.test('report endpoint error', t => { t.test(`json=${json}`, t => { const OUTPUT = [] const LOGS = [] - const mocks = { - '../../lib/npm.js': { - prefix: 'foo', - command: 'audit', - flatOptions: { - json, - }, - log: { - warn: (...warning) => LOGS.push(warning), - }, + const npm = { + prefix: 'foo', + command: 'audit', + flatOptions: { + json, }, + log: { + warn: (...warning) => LOGS.push(warning), + }, + } + const Audit = requireInject('../../lib/audit.js', { 'npm-audit-report': () => { throw new Error('should not call audit report when there are errors') }, @@ -130,15 +133,10 @@ t.test('report endpoint error', t => { '../../lib/utils/output.js': (...msg) => { OUTPUT.push(msg) }, - } - // have to pass mocks to both to get the npm and output set right - const auditError = requireInject('../../lib/utils/audit-error.js', mocks) - const audit = requireInject('../../lib/audit.js', { - ...mocks, - '../../lib/utils/audit-error.js': auditError, }) + const audit = new Audit(npm) - audit([], (err) => { + audit.exec([], (err) => { t.equal(err, 'audit endpoint returned an error') t.strictSame(OUTPUT, [ [ @@ -168,6 +166,8 @@ t.test('report endpoint error', t => { }) t.test('completion', t => { + const Audit = require('../../lib/audit.js') + const audit = new Audit({}) t.test('fix', async t => { t.resolveMatch(audit.completion({ conf: { argv: { remain: ['npm', 'audit'] } } }), ['fix'], 'completes to fix') t.end() diff --git a/test/lib/bin.js b/test/lib/bin.js index c5ed2a91b9831..7f183e65653b7 100644 --- a/test/lib/bin.js +++ b/test/lib/bin.js @@ -5,14 +5,16 @@ test('bin', (t) => { t.plan(3) const dir = '/bin/dir' - const bin = requireInject('../../lib/bin.js', { - '../../lib/npm.js': { bin: dir, flatOptions: { global: false } }, + const Bin = requireInject('../../lib/bin.js', { '../../lib/utils/output.js': (output) => { + console.log('output is called') t.equal(output, dir, 'prints the correct directory') }, }) - bin([], (err) => { + const bin = new Bin({ bin: dir, flatOptions: { global: false } }) + + bin.exec([], (err) => { t.ifError(err, 'npm bin') t.ok('should have printed directory') }) @@ -30,15 +32,16 @@ test('bin -g', (t) => { } const dir = '/bin/dir' - const bin = requireInject('../../lib/bin.js', { - '../../lib/npm.js': { bin: dir, flatOptions: { global: true } }, + const Bin = requireInject('../../lib/bin.js', { '../../lib/utils/path.js': [dir], '../../lib/utils/output.js': (output) => { t.equal(output, dir, 'prints the correct directory') }, }) - bin([], (err) => { + const bin = new Bin({ bin: dir, flatOptions: { global: true } }) + + bin.exec([], (err) => { t.ifError(err, 'npm bin') t.ok('should have printed directory') }) @@ -56,15 +59,15 @@ test('bin -g (not in path)', (t) => { } const dir = '/bin/dir' - const bin = requireInject('../../lib/bin.js', { - '../../lib/npm.js': { bin: dir, flatOptions: { global: true } }, + const Bin = requireInject('../../lib/bin.js', { '../../lib/utils/path.js': ['/not/my/dir'], '../../lib/utils/output.js': (output) => { t.equal(output, dir, 'prints the correct directory') }, }) + const bin = new Bin({ bin: dir, flatOptions: { global: true } }) - bin([], (err) => { + bin.exec([], (err) => { t.ifError(err, 'npm bin') t.ok('should have printed directory') }) diff --git a/test/lib/birthday.js b/test/lib/birthday.js index 3b8110fc8f3bc..c818223fb51e5 100644 --- a/test/lib/birthday.js +++ b/test/lib/birthday.js @@ -1,5 +1,4 @@ const t = require('tap') -const requireInject = require('require-inject') const npm = { flatOptions: { yes: false, @@ -17,10 +16,9 @@ const npm = { }, } -const birthday = requireInject('../../lib/birthday.js', { - '../../lib/npm.js': npm, -}) +const Birthday = require('../../lib/birthday.js') +const birthday = new Birthday(npm) let calledCb = false -birthday([], () => calledCb = true) +birthday.exec([], () => calledCb = true) t.equal(calledCb, true, 'called the callback') diff --git a/test/lib/bugs.js b/test/lib/bugs.js index 992bd9f614686..7ea78fb320586 100644 --- a/test/lib/bugs.js +++ b/test/lib/bugs.js @@ -43,17 +43,18 @@ const pacote = { // keep a tally of which urls got opened const opened = {} -const openUrl = (url, errMsg, cb) => { +const openUrl = (npm, url, errMsg) => { opened[url] = opened[url] || 0 opened[url]++ - process.nextTick(cb) } -const bugs = requireInject('../../lib/bugs.js', { +const Bugs = requireInject('../../lib/bugs.js', { pacote, '../../lib/utils/open-url.js': openUrl, }) +const bugs = new Bugs({ flatOptions: {} }) + t.test('open bugs urls', t => { const expect = { nobugs: 'https://www.npmjs.com/package/nobugs', @@ -68,7 +69,7 @@ t.test('open bugs urls', t => { t.plan(keys.length) keys.forEach(pkg => { t.test(pkg, t => { - bugs([pkg], (er) => { + bugs.exec([pkg], (er) => { if (er) throw er t.equal(opened[expect[pkg]], 1, 'opened expected url', {opened}) @@ -79,7 +80,7 @@ t.test('open bugs urls', t => { }) t.test('open default package if none specified', t => { - bugs([], (er) => { + bugs.exec([], (er) => { if (er) throw er t.equal(opened['https://example.com'], 2, 'opened expected url', {opened}) diff --git a/test/lib/cache.js b/test/lib/cache.js index 05d269dd4dca7..67499f37e9f30 100644 --- a/test/lib/cache.js +++ b/test/lib/cache.js @@ -58,27 +58,26 @@ const cacache = { }, } -const mocks = { +const Cache = requireInject('../../lib/cache.js', { cacache, npmlog, pacote, rimraf, - '../../lib/npm.js': npm, '../../lib/utils/output.js': output, '../../lib/utils/usage.js': usageUtil, -} +}) -const cache = requireInject('../../lib/cache.js', mocks) +const cache = new Cache(npm) t.test('cache no args', t => { - cache([], err => { + cache.exec([], err => { t.equal(err.message, 'usage instructions', 'should throw usage instructions') t.end() }) }) t.test('cache clean', t => { - cache(['clean'], err => { + cache.exec(['clean'], err => { t.match(err.message, 'the npm cache self-heals', 'should throw warning') t.end() }) @@ -91,7 +90,7 @@ t.test('cache clean (force)', t => { flatOptions.force = false }) - cache(['clear'], err => { + cache.exec(['clear'], err => { t.ifError(err) t.equal(rimrafPath, path.join(npm.cache, '_cacache')) t.end() @@ -99,7 +98,7 @@ t.test('cache clean (force)', t => { }) t.test('cache clean with arg', t => { - cache(['rm', 'pkg'], err => { + cache.exec(['rm', 'pkg'], err => { t.match(err.message, 'does not accept arguments', 'should throw error') t.end() }) @@ -110,7 +109,7 @@ t.test('cache add no arg', t => { logOutput = [] }) - cache(['add'], err => { + cache.exec(['add'], err => { t.strictSame(logOutput, [ ['silly', 'cache add', 'args', []], ], 'logs correctly') @@ -126,7 +125,7 @@ t.test('cache add pkg only', t => { tarballStreamOpts = {} }) - cache(['add', 'mypkg'], err => { + cache.exec(['add', 'mypkg'], err => { t.ifError(err) t.strictSame(logOutput, [ ['silly', 'cache add', 'args', ['mypkg']], @@ -145,7 +144,7 @@ t.test('cache add pkg w/ spec modifier', t => { tarballStreamOpts = {} }) - cache(['add', 'mypkg', 'latest'], err => { + cache.exec(['add', 'mypkg', 'latest'], err => { t.ifError(err) t.strictSame(logOutput, [ ['silly', 'cache add', 'args', ['mypkg', 'latest']], @@ -162,7 +161,7 @@ t.test('cache verify', t => { outputOutput = [] }) - cache(['verify'], err => { + cache.exec(['verify'], err => { t.ifError(err) t.match(outputOutput, [ `Cache verified and compressed (${path.join(npm.cache, '_cacache')})`, @@ -189,7 +188,7 @@ t.test('cache verify w/ extra output', t => { delete cacacheVerifyStats.missingContent }) - cache(['check'], err => { + cache.exec(['check'], err => { t.ifError(err) t.match(outputOutput, [ `Cache verified and compressed (~${path.join('/fake/path', '_cacache')})`, diff --git a/test/lib/ci.js b/test/lib/ci.js index 28c66b056cf31..3419218ef9d8b 100644 --- a/test/lib/ci.js +++ b/test/lib/ci.js @@ -9,19 +9,8 @@ const requireInject = require('require-inject') test('should ignore scripts with --ignore-scripts', (t) => { const SCRIPTS = [] let REIFY_CALLED = false - const ci = requireInject('../../lib/ci.js', { + const CI = requireInject('../../lib/ci.js', { '../../lib/utils/reify-finish.js': async () => {}, - '../../lib/npm.js': { - globalDir: 'path/to/node_modules/', - prefix: 'foo', - flatOptions: { - global: false, - ignoreScripts: true, - }, - config: { - get: () => false, - }, - }, '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -32,7 +21,20 @@ test('should ignore scripts with --ignore-scripts', (t) => { } }, }) - ci([], er => { + + const ci = new CI({ + globalDir: 'path/to/node_modules/', + prefix: 'foo', + flatOptions: { + global: false, + ignoreScripts: true, + }, + config: { + get: () => false, + }, + }) + + ci.exec([], er => { if (er) throw er t.equal(REIFY_CALLED, true, 'called reify') @@ -87,13 +89,7 @@ test('should use Arborist and run-script', (t) => { const expectRimrafs = 3 let actualRimrafs = 0 - const ci = requireInject('../../lib/ci.js', { - '../../lib/npm.js': { - prefix: path, - flatOptions: { - global: false, - }, - }, + const CI = requireInject('../../lib/ci.js', { '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/run-script': opts => { t.match(opts, { event: scripts.shift() }) @@ -118,7 +114,15 @@ test('should use Arborist and run-script', (t) => { t.ok(arb, 'gets arborist tree') }, }) - ci(null, er => { + + const ci = new CI({ + prefix: path, + flatOptions: { + global: false, + }, + }) + + ci.exec(null, er => { if (er) throw er for (const [msg, result] of Object.entries(timers)) @@ -131,13 +135,7 @@ test('should use Arborist and run-script', (t) => { }) test('should pass flatOptions to Arborist.reify', (t) => { - const ci = requireInject('../../lib/ci.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - production: true, - }, - }, + const CI = requireInject('../../lib/ci.js', { '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/run-script': opts => {}, '@npmcli/arborist': function () { @@ -148,7 +146,13 @@ test('should pass flatOptions to Arborist.reify', (t) => { } }, }) - ci(null, er => { + const ci = new CI({ + prefix: 'foo', + flatOptions: { + production: true, + }, + }) + ci.exec(null, er => { if (er) throw er }) @@ -160,13 +164,7 @@ test('should throw if package-lock.json or npm-shrinkwrap missing', (t) => { 'package.json': 'some info', }) - const ci = requireInject('../../lib/ci.js', { - '../../lib/npm.js': { - prefix: testDir, - flatOptions: { - global: false, - }, - }, + const CI = requireInject('../../lib/ci.js', { '@npmcli/run-script': opts => {}, '../../lib/utils/reify-finish.js': async () => {}, npmlog: { @@ -175,7 +173,13 @@ test('should throw if package-lock.json or npm-shrinkwrap missing', (t) => { }, }, }) - ci(null, (err, res) => { + const ci = new CI({ + prefix: testDir, + flatOptions: { + global: false, + }, + }) + ci.exec(null, (err, res) => { t.ok(err, 'throws error when there is no package-lock') t.notOk(res) t.end() @@ -183,17 +187,17 @@ test('should throw if package-lock.json or npm-shrinkwrap missing', (t) => { }) test('should throw ECIGLOBAL', (t) => { - const ci = requireInject('../../lib/ci.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - global: true, - }, - }, + const CI = requireInject('../../lib/ci.js', { '@npmcli/run-script': opts => {}, '../../lib/utils/reify-finish.js': async () => {}, }) - ci(null, (err, res) => { + const ci = new CI({ + prefix: 'foo', + flatOptions: { + global: true, + }, + }) + ci.exec(null, (err, res) => { t.equals(err.code, 'ECIGLOBAL', 'throws error with global packages') t.notOk(res) t.end() @@ -207,13 +211,7 @@ test('should remove existing node_modules before installing', (t) => { }, }) - const ci = requireInject('../../lib/ci.js', { - '../../lib/npm.js': { - prefix: testDir, - flatOptions: { - global: false, - }, - }, + const CI = requireInject('../../lib/ci.js', { '@npmcli/run-script': opts => {}, '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/arborist': function () { @@ -229,7 +227,14 @@ test('should remove existing node_modules before installing', (t) => { }, }) - ci(null, er => { + const ci = new CI({ + prefix: testDir, + flatOptions: { + global: false, + }, + }) + + ci.exec(null, er => { if (er) throw er }) diff --git a/test/lib/completion.js b/test/lib/completion.js index 19f70df20eec6..89e8134ebb303 100644 --- a/test/lib/completion.js +++ b/test/lib/completion.js @@ -75,8 +75,7 @@ const deref = (cmd) => { return cmd } -const completion = requireInject('../../lib/completion.js', { - '../../lib/npm.js': npm, +const Completion = requireInject('../../lib/completion.js', { '../../lib/utils/cmd-list.js': cmdList, '../../lib/utils/config.js': config, '../../lib/utils/deref-command.js': deref, @@ -85,6 +84,7 @@ const completion = requireInject('../../lib/completion.js', { output.push(line) }, }) +const completion = new Completion(npm) test('completion completion', async t => { const home = process.env.HOME @@ -125,11 +125,13 @@ test('completion completion wrong word count', async t => { }) test('completion errors in windows without bash', t => { - const compl = requireInject('../../lib/completion.js', { + const Compl = requireInject('../../lib/completion.js', { '../../lib/utils/is-windows-shell.js': true, }) - compl({}, (err) => { + const compl = new Compl() + + compl.exec({}, (err) => { t.match(err, { code: 'ENOTSUP', message: /completion supported only in MINGW/, @@ -162,7 +164,7 @@ test('dump script when completion is not being attempted', t => { }) } - completion({}, (err) => { + completion.exec({}, (err) => { if (err) throw err @@ -195,7 +197,7 @@ test('dump script exits correctly when EPIPE is emitted on stdout', t => { }) } - completion({}, (err) => { + completion.exec({}, (err) => { if (err) throw err @@ -228,7 +230,7 @@ test('non EPIPE errors cause failures', t => { }) } - completion({}, (err) => { + completion.exec({}, (err) => { t.equal(err.errno, 'ESOMETHINGELSE', 'propagated error') t.equal(data, completionScript, 'wrote the completion script') t.end() @@ -248,7 +250,7 @@ test('completion completes single command name', t => { output.length = 0 }) - completion(['npm', 'c'], (err, res) => { + completion.exec(['npm', 'c'], (err, res) => { if (err) throw err @@ -270,7 +272,7 @@ test('completion completes command names', t => { output.length = 0 }) - completion(['npm', 'a'], (err, res) => { + completion.exec(['npm', 'a'], (err, res) => { if (err) throw err @@ -292,7 +294,7 @@ test('completion of invalid command name does nothing', t => { output.length = 0 }) - completion(['npm', 'compute'], (err, res) => { + completion.exec(['npm', 'compute'], (err, res) => { if (err) throw err @@ -314,7 +316,7 @@ test('handles async completion function', t => { output.length = 0 }) - completion(['npm', 'promise', ''], (err, res) => { + completion.exec(['npm', 'promise', ''], (err, res) => { if (err) throw err @@ -343,7 +345,7 @@ test('completion triggers command completions', t => { output.length = 0 }) - completion(['npm', 'access', ''], (err, res) => { + completion.exec(['npm', 'access', ''], (err, res) => { if (err) throw err @@ -372,7 +374,7 @@ test('completion triggers filtered command completions', t => { output.length = 0 }) - completion(['npm', 'access', 'p'], (err, res) => { + completion.exec(['npm', 'access', 'p'], (err, res) => { if (err) throw err @@ -401,7 +403,7 @@ test('completions for commands that return nested arrays are joined', t => { output.length = 0 }) - completion(['npm', 'completion', ''], (err, res) => { + completion.exec(['npm', 'completion', ''], (err, res) => { if (err) throw err @@ -430,7 +432,7 @@ test('completions for commands that return nothing work correctly', t => { output.length = 0 }) - completion(['npm', 'donothing', ''], (err, res) => { + completion.exec(['npm', 'donothing', ''], (err, res) => { if (err) throw err @@ -459,7 +461,7 @@ test('completions for commands that return a single item work correctly', t => { output.length = 0 }) - completion(['npm', 'driveaboat', ''], (err, res) => { + completion.exec(['npm', 'driveaboat', ''], (err, res) => { if (err) throw err @@ -489,7 +491,7 @@ test('command completion for commands with no completion return no results', t = }) // quotes around adduser are to ensure coverage when unescaping commands - completion(['npm', '\'adduser\'', ''], (err, res) => { + completion.exec(['npm', '\'adduser\'', ''], (err, res) => { if (err) throw err @@ -520,7 +522,7 @@ test('command completion errors propagate', t => { accessCompletionError = false }) - completion(['npm', 'access', ''], (err, res) => { + completion.exec(['npm', 'access', ''], (err, res) => { t.match(err, /access completion failed/, 'catches the appropriate error') t.strictSame(npmConfig, { argv: { @@ -547,7 +549,7 @@ test('completion can complete flags', t => { output.length = 0 }) - completion(['npm', 'install', '--'], (err, res) => { + completion.exec(['npm', 'install', '--'], (err, res) => { if (err) throw err @@ -570,7 +572,7 @@ test('double dashes escape from flag completion', t => { output.length = 0 }) - completion(['npm', '--', 'install', '--'], (err, res) => { + completion.exec(['npm', '--', 'install', '--'], (err, res) => { if (err) throw err @@ -593,7 +595,7 @@ test('completion cannot complete options that take a value in mid-command', t => output.length = 0 }) - completion(['npm', '--registry', 'install'], (err, res) => { + completion.exec(['npm', '--registry', 'install'], (err, res) => { if (err) throw err diff --git a/test/lib/config.js b/test/lib/config.js index edaa6486cdc95..c2420aefb4a00 100644 --- a/test/lib/config.js +++ b/test/lib/config.js @@ -68,17 +68,17 @@ const usageUtil = () => 'usage instructions' const mocks = { '../../lib/utils/config.js': { defaults, types }, - '../../lib/npm.js': npm, '../../lib/utils/output.js': msg => { result = msg }, '../../lib/utils/usage.js': usageUtil, } -const config = requireInject('../../lib/config.js', mocks) +const Config = requireInject('../../lib/config.js', mocks) +const config = new Config(npm) t.test('config no args', t => { - config([], (err) => { + config.exec([], (err) => { t.match(err, /usage instructions/, 'should not error out on empty locations') t.end() }) @@ -94,7 +94,7 @@ t.test('config list', t => { delete npm.config.find }) - config(['list'], (err) => { + config.exec(['list'], (err) => { t.ifError(err, 'npm config list') t.matchSnapshot(result, 'should list configs') }) @@ -120,7 +120,7 @@ t.test('config list overrides', t => { delete npm.config.find }) - config(['list'], (err) => { + config.exec(['list'], (err) => { t.ifError(err, 'npm config list') t.matchSnapshot(result, 'should list overridden configs') }) @@ -138,7 +138,7 @@ t.test('config list --long', t => { result = '' }) - config(['list'], (err) => { + config.exec(['list'], (err) => { t.ifError(err, 'npm config list --long') t.matchSnapshot(result, 'should list all configs') }) @@ -163,7 +163,7 @@ t.test('config list --json', t => { result = '' }) - config(['list'], (err) => { + config.exec(['list'], (err) => { t.ifError(err, 'npm config list --json') t.deepEqual( JSON.parse(result), @@ -179,7 +179,7 @@ t.test('config list --json', t => { }) t.test('config delete no args', t => { - config(['delete'], (err) => { + config.exec(['delete'], (err) => { t.equal( err.message, 'usage instructions', @@ -202,7 +202,7 @@ t.test('config delete key', t => { t.equal(where, 'user', 'should save user config post-delete') } - config(['delete', 'foo'], (err) => { + config.exec(['delete', 'foo'], (err) => { t.ifError(err, 'npm config delete key') }) @@ -229,7 +229,7 @@ t.test('config delete multiple key', t => { t.equal(where, 'user', 'should save user config post-delete') } - config(['delete', 'foo', 'bar'], (err) => { + config.exec(['delete', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config delete keys') }) @@ -252,7 +252,7 @@ t.test('config delete key --global', t => { } flatOptions.global = true - config(['delete', 'foo'], (err) => { + config.exec(['delete', 'foo'], (err) => { t.ifError(err, 'npm config delete key --global') }) @@ -264,7 +264,7 @@ t.test('config delete key --global', t => { }) t.test('config set no args', t => { - config(['set'], (err) => { + config.exec(['set'], (err) => { t.equal( err.message, 'usage instructions', @@ -287,7 +287,7 @@ t.test('config set key', t => { t.equal(where, 'user', 'should save user config') } - config(['set', 'foo', 'bar'], (err) => { + config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set key') }) @@ -310,7 +310,7 @@ t.test('config set key=val', t => { t.equal(where, 'user', 'should save user config') } - config(['set', 'foo=bar'], (err) => { + config.exec(['set', 'foo=bar'], (err) => { t.ifError(err, 'npm config set key') }) @@ -341,7 +341,7 @@ t.test('config set multiple keys', t => { t.equal(where, 'user', 'should save user config') } - config(['set', ...args], (err) => { + config.exec(['set', ...args], (err) => { t.ifError(err, 'npm config set key') }) @@ -364,7 +364,7 @@ t.test('config set key to empty value', t => { t.equal(where, 'user', 'should save user config') } - config(['set', 'foo'], (err) => { + config.exec(['set', 'foo'], (err) => { t.ifError(err, 'npm config set key to empty value') }) @@ -392,7 +392,7 @@ t.test('config set invalid key', t => { delete npm.log.warn }) - config(['set', 'foo', 'bar'], (err) => { + config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set invalid key') }) }) @@ -411,7 +411,7 @@ t.test('config set key --global', t => { } flatOptions.global = true - config(['set', 'foo', 'bar'], (err) => { + config.exec(['set', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config set key --global') }) @@ -432,7 +432,7 @@ t.test('config get no args', t => { delete npm.config.find }) - config(['get'], (err) => { + config.exec(['get'], (err) => { t.ifError(err, 'npm config get no args') t.matchSnapshot(result, 'should list configs on config get no args') }) @@ -451,7 +451,7 @@ t.test('config get key', t => { throw new Error('should not save') } - config(['get', 'foo'], (err) => { + config.exec(['get', 'foo'], (err) => { t.ifError(err, 'npm config get key') }) @@ -479,7 +479,7 @@ t.test('config get multiple keys', t => { throw new Error('should not save') } - config(['get', 'foo', 'bar'], (err) => { + config.exec(['get', 'foo', 'bar'], (err) => { t.ifError(err, 'npm config get multiple keys') t.equal(result, 'foo=asdf\nbar=asdf') }) @@ -492,7 +492,7 @@ t.test('config get multiple keys', t => { }) t.test('config get private key', t => { - config(['get', '//private-reg.npmjs.org/:_authThoken'], (err) => { + config.exec(['get', '//private-reg.npmjs.org/:_authThoken'], (err) => { t.match( err, /The \/\/private-reg.npmjs.org\/:_authThoken option is protected, and cannot be retrieved in this way/, @@ -538,16 +538,19 @@ sign-git-commit=true` }, }, } - const config = requireInject('../../lib/config.js', editMocks) - config(['edit'], (err) => { + const Config = requireInject('../../lib/config.js', editMocks) + const config = new Config(npm) + + config.exec(['edit'], (err) => { t.ifError(err, 'npm config edit') // test no config file result editMocks.fs.readFile = (p, e, cb) => { cb(new Error('ERR')) } - const config = requireInject('../../lib/config.js', editMocks) - config(['edit'], (err) => { + const Config = requireInject('../../lib/config.js', editMocks) + const config = new Config(npm) + config.exec(['edit'], (err) => { t.ifError(err, 'npm config edit') }) }) @@ -594,8 +597,9 @@ t.test('config edit --global', t => { }, }, } - const config = requireInject('../../lib/config.js', editMocks) - config(['edit'], (err) => { + const Config = requireInject('../../lib/config.js', editMocks) + const config = new Config(npm) + config.exec(['edit'], (err) => { t.match(err, /exited with code: 137/, 'propagated exit code from editor') }) diff --git a/test/lib/dedupe.js b/test/lib/dedupe.js index b14185525bbea..27cd583dd2c9a 100644 --- a/test/lib/dedupe.js +++ b/test/lib/dedupe.js @@ -2,28 +2,17 @@ const { test } = require('tap') const requireInject = require('require-inject') test('should throw in global mode', (t) => { - const dedupe = requireInject('../../lib/dedupe.js', { - '../../lib/npm.js': { - flatOptions: { - global: true, - }, - }, - }) + const Dedupe = requireInject('../../lib/dedupe.js') + const dedupe = new Dedupe({ flatOptions: { global: true } }) - dedupe([], er => { + dedupe.exec([], er => { t.match(er, { code: 'EDEDUPEGLOBAL' }, 'throws EDEDUPEGLOBAL') t.end() }) }) test('should remove dupes using Arborist', (t) => { - const dedupe = requireInject('../../lib/dedupe.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - dryRun: 'false', - }, - }, + const Dedupe = requireInject('../../lib/dedupe.js', { '@npmcli/arborist': function (args) { t.ok(args, 'gets options object') t.ok(args.path, 'gets path option') @@ -36,7 +25,13 @@ test('should remove dupes using Arborist', (t) => { t.ok(arb, 'gets arborist tree') }, }) - dedupe({ dryRun: true }, er => { + const dedupe = new Dedupe({ + prefix: 'foo', + flatOptions: { + dryRun: 'false', + }, + }) + dedupe.exec({ dryRun: true }, er => { if (er) throw er t.ok(true, 'callback is called') @@ -45,20 +40,20 @@ test('should remove dupes using Arborist', (t) => { }) test('should remove dupes using Arborist - no arguments', (t) => { - const dedupe = requireInject('../../lib/dedupe.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - dryRun: 'true', - }, - }, + const Dedupe = requireInject('../../lib/dedupe.js', { '@npmcli/arborist': function (args) { t.ok(args.dryRun, 'gets dryRun from flatOptions') this.dedupe = () => {} }, '../../lib/utils/reify-output.js': () => {}, }) - dedupe(null, () => { + const dedupe = new Dedupe({ + prefix: 'foo', + flatOptions: { + dryRun: 'true', + }, + }) + dedupe.exec(null, () => { t.end() }) }) diff --git a/test/lib/deprecate.js b/test/lib/deprecate.js index fd563de1209dd..03100166a012c 100644 --- a/test/lib/deprecate.js +++ b/test/lib/deprecate.js @@ -18,10 +18,7 @@ npmFetch.json = async (uri, opts) => { } } -const deprecate = requireInject('../../lib/deprecate.js', { - '../../lib/npm.js': { - flatOptions: { registry: 'https://registry.npmjs.org' }, - }, +const Deprecate = requireInject('../../lib/deprecate.js', { '../../lib/utils/get-identity.js': async () => getIdentityImpl(), '../../lib/utils/otplease.js': async (opts, fn) => fn(opts), libnpmaccess: { @@ -30,16 +27,19 @@ const deprecate = requireInject('../../lib/deprecate.js', { 'npm-registry-fetch': npmFetch, }) +const deprecate = new Deprecate({ + flatOptions: { registry: 'https://registry.npmjs.org' }, +}) + test('completion', async t => { const defaultIdentityImpl = getIdentityImpl t.teardown(() => { getIdentityImpl = defaultIdentityImpl }) - const { completion } = deprecate - const testComp = async (argv, expect) => { - const res = await completion({ conf: { argv: { remain: argv } } }) + const res = + await deprecate.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, `completion: ${argv}`) } @@ -59,21 +59,21 @@ test('completion', async t => { }) test('no args', t => { - deprecate([], (err) => { + deprecate.exec([], (err) => { t.match(err, /Usage: npm deprecate/, 'logs usage') t.end() }) }) test('only one arg', t => { - deprecate(['foo'], (err) => { + deprecate.exec(['foo'], (err) => { t.match(err, /Usage: npm deprecate/, 'logs usage') t.end() }) }) test('invalid semver range', t => { - deprecate(['foo@notaversion', 'this will fail'], (err) => { + deprecate.exec(['foo@notaversion', 'this will fail'], (err) => { t.match(err, /invalid version range/, 'logs semver error') t.end() }) @@ -84,7 +84,7 @@ test('deprecates given range', t => { npmFetchBody = null }) - deprecate(['foo@1.0.0', 'this version is deprecated'], (err) => { + deprecate.exec(['foo@1.0.0', 'this version is deprecated'], (err) => { if (err) throw err @@ -110,7 +110,7 @@ test('deprecates all versions when no range is specified', t => { npmFetchBody = null }) - deprecate(['foo', 'this version is deprecated'], (err) => { + deprecate.exec(['foo', 'this version is deprecated'], (err) => { if (err) throw err diff --git a/test/lib/diff.js b/test/lib/diff.js index 926c54fdf1848..5e60f125cec3d 100644 --- a/test/lib/diff.js +++ b/test/lib/diff.js @@ -28,7 +28,6 @@ const mocks = { npmlog: { info: noop, verbose: noop }, libnpmdiff: (...args) => libnpmdiff(...args), 'npm-registry-fetch': async () => ({}), - '../../lib/npm.js': npm, '../../lib/utils/output.js': noop, '../../lib/utils/read-local-package.js': async () => rlp(), '../../lib/utils/usage.js': () => 'usage instructions', @@ -42,7 +41,8 @@ t.afterEach(cb => { cb() }) -const diff = requireInject('../../lib/diff.js', mocks) +const Diff = requireInject('../../lib/diff.js', mocks) +const diff = new Diff(npm) t.test('no args', t => { t.test('in a project dir', t => { @@ -56,7 +56,7 @@ t.test('no args', t => { } npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -65,7 +65,7 @@ t.test('no args', t => { t.test('no args, missing package.json name in cwd', t => { rlp = () => undefined - diff([], err => { + diff.exec([], err => { t.match( err, /Needs multiple arguments to compare or run from a project dir./, @@ -80,7 +80,7 @@ t.test('no args', t => { throw new Error('ERR') } - diff([], err => { + diff.exec([], err => { t.match( err, /Needs multiple arguments to compare or run from a project dir./, @@ -106,7 +106,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['foo@1.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -120,7 +120,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['foo@1.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { t.match( err, /Needs multiple arguments to compare or run from a project dir./, @@ -142,7 +142,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['foo@~1.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -160,7 +160,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['2.1.4'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -172,7 +172,7 @@ t.test('single arg', t => { } npm.flatOptions.diff = ['2.1.4'] - diff([], err => { + diff.exec([], err => { t.match( err, /Needs multiple arguments to compare or run from a project dir./, @@ -200,7 +200,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['2.1.4'] npm.flatOptions.prefix = path - diff(['./foo.js', './bar.js'], err => { + diff.exec(['./foo.js', './bar.js'], err => { if (err) throw err }) @@ -224,7 +224,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['bar@1.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -250,7 +250,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['simple-output'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -264,7 +264,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['bar'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { t.match( err, /Needs multiple arguments to compare or run from a project dir./, @@ -297,7 +297,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['bar'] npm.flatOptions.prefix = path - const diff = requireInject('../../lib/diff.js', { + const Diff = requireInject('../../lib/diff.js', { ...mocks, pacote: { packument: (spec) => { @@ -313,8 +313,9 @@ t.test('single arg', t => { t.equal(b, 'bar@1.8.10', 'should have possible semver range spec') }, }) + const diff = new Diff(npm) - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -359,7 +360,7 @@ t.test('single arg', t => { npm.flatOptions.prefix = resolve(path, 'project') npm.globalDir = resolve(path, 'globalDir/lib/node_modules') - const diff = requireInject('../../lib/diff.js', { + const Diff = requireInject('../../lib/diff.js', { ...mocks, pacote: { packument: (spec) => { @@ -375,8 +376,9 @@ t.test('single arg', t => { t.equal(b, 'lorem@2.1.0', 'should have possible semver range spec') }, }) + const diff = new Diff(npm) - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -410,7 +412,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['bar@2.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -445,7 +447,7 @@ t.test('single arg', t => { }), }) - const diff = requireInject('../../lib/diff.js', { + const Diff = requireInject('../../lib/diff.js', { ...mocks, '../../lib/utils/read-local-package.js': async () => 'my-project', pacote: { @@ -462,11 +464,12 @@ t.test('single arg', t => { t.equal(b, 'lorem@2.2.2', 'should have expected target spec') }, }) + const diff = new Diff(npm) npm.flatOptions.diff = ['lorem'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -481,7 +484,7 @@ t.test('single arg', t => { }), }) - const diff = requireInject('../../lib/diff.js', { + const Diff = requireInject('../../lib/diff.js', { ...mocks, '../../lib/utils/read-local-package.js': async () => 'my-project', '@npmcli/arborist': class { @@ -494,11 +497,12 @@ t.test('single arg', t => { t.equal(b, `file:${path}`, 'should target current cwd') }, }) + const diff = new Diff(npm) npm.flatOptions.diff = ['lorem'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -517,7 +521,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['bar'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -535,7 +539,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['my-project'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -553,7 +557,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['/path/to/other-dir'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -564,7 +568,7 @@ t.test('single arg', t => { npm.flatOptions.diff = ['git+https://github.com/user/foo'] - diff([], err => { + diff.exec([], err => { t.match( err, /Spec type not supported./, @@ -588,7 +592,7 @@ t.test('first arg is a qualified spec', t => { } npm.flatOptions.diff = ['bar@1.0.0', 'bar@^2.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -622,7 +626,7 @@ t.test('first arg is a qualified spec', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar@2.0.0', 'bar'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -638,7 +642,7 @@ t.test('first arg is a qualified spec', t => { t.equal(b, 'bar@2.0.0', 'should use name from first arg') } - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -653,7 +657,7 @@ t.test('first arg is a qualified spec', t => { } npm.flatOptions.diff = ['bar@1.0.0', 'bar-fork'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -691,7 +695,7 @@ t.test('first arg is a known dependency name', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar', 'bar@2.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -731,7 +735,7 @@ t.test('first arg is a known dependency name', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar', 'bar-fork'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -765,7 +769,7 @@ t.test('first arg is a known dependency name', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar', '2.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -799,7 +803,7 @@ t.test('first arg is a known dependency name', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar', 'bar-fork'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -819,7 +823,7 @@ t.test('first arg is a valid semver range', t => { t.equal(b, 'bar@2.0.0', 'should use expected spec') } - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -853,7 +857,7 @@ t.test('first arg is a valid semver range', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['1.0.0', 'bar'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -869,7 +873,7 @@ t.test('first arg is a valid semver range', t => { } npm.flatOptions.diff = ['1.0.0', '2.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -883,7 +887,7 @@ t.test('first arg is a valid semver range', t => { npm.flatOptions.diff = ['1.0.0', '2.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { t.match( err, /Needs to be run from a project dir in order to diff two versions./, @@ -903,7 +907,7 @@ t.test('first arg is a valid semver range', t => { } npm.flatOptions.diff = ['1.0.0', 'bar'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -918,7 +922,7 @@ t.test('first arg is a valid semver range', t => { }), }) - const diff = requireInject('../../lib/diff.js', { + const Diff = requireInject('../../lib/diff.js', { ...mocks, '../../lib/utils/read-local-package.js': async () => 'my-project', '@npmcli/arborist': class { @@ -931,11 +935,12 @@ t.test('first arg is a valid semver range', t => { t.equal(b, 'lorem@2.0.0', 'should target expected spec') }, }) + const diff = new Diff(npm) npm.flatOptions.diff = ['1.0.0', 'lorem@2.0.0'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -955,7 +960,7 @@ t.test('first arg is an unknown dependency name', t => { } npm.flatOptions.diff = ['bar', 'bar@2.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -989,7 +994,7 @@ t.test('first arg is an unknown dependency name', t => { npm.flatOptions.prefix = path npm.flatOptions.diff = ['bar-fork', 'bar'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1004,7 +1009,7 @@ t.test('first arg is an unknown dependency name', t => { } npm.flatOptions.diff = ['bar', '^1.0.0'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1019,7 +1024,7 @@ t.test('first arg is an unknown dependency name', t => { } npm.flatOptions.diff = ['bar', 'bar-fork'] - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1040,7 +1045,7 @@ t.test('first arg is an unknown dependency name', t => { npm.flatOptions.diff = ['bar', 'bar-fork'] npm.flatOptions.prefix = path - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1062,7 +1067,7 @@ t.test('various options', t => { }, 'should forward nameOnly=true option') } - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1085,7 +1090,7 @@ t.test('various options', t => { }, 'should forward diffFiles values') } - diff(['./foo.js', './bar.js'], err => { + diff.exec(['./foo.js', './bar.js'], err => { if (err) throw err }) @@ -1109,7 +1114,7 @@ t.test('various options', t => { } npm.flatOptions.prefix = path - diff(['./foo.js', './bar.js'], err => { + diff.exec(['./foo.js', './bar.js'], err => { if (err) throw err }) @@ -1137,7 +1142,7 @@ t.test('various options', t => { }, 'should forward diff options') } - diff([], err => { + diff.exec([], err => { if (err) throw err }) @@ -1148,7 +1153,7 @@ t.test('various options', t => { t.test('too many args', t => { npm.flatOptions.diff = ['a', 'b', 'c'] - diff([], err => { + diff.exec([], err => { t.match( err, /Can't use more than two --diff arguments./, diff --git a/test/lib/dist-tag.js b/test/lib/dist-tag.js index c189352302e9a..b761fb103cda8 100644 --- a/test/lib/dist-tag.js +++ b/test/lib/dist-tag.js @@ -48,7 +48,7 @@ const logger = (...msgs) => { log += '\n' } -const distTag = requireInject('../../lib/dist-tag.js', { +const DistTag = requireInject('../../lib/dist-tag.js', { npmlog: { error: logger, info: logger, @@ -58,26 +58,27 @@ const distTag = requireInject('../../lib/dist-tag.js', { get 'npm-registry-fetch' () { return npmRegistryFetchMock }, - '../../lib/npm.js': { - flatOptions: _flatOptions, - config: { - get (key) { - return _flatOptions[key] - }, - }, - }, '../../lib/utils/output.js': msg => { result = msg }, }) +const distTag = new DistTag({ + flatOptions: _flatOptions, + config: { + get (key) { + return _flatOptions[key] + }, + }, +}) + test('ls in current package', (t) => { prefix = t.testdir({ 'package.json': JSON.stringify({ name: '@scoped/pkg', }), }) - distTag(['ls'], (err) => { + distTag.exec(['ls'], (err) => { t.ifError(err, 'npm dist-tags ls') t.matchSnapshot( result, @@ -95,7 +96,7 @@ test('no args in current package', (t) => { name: '@scoped/pkg', }), }) - distTag([], (err) => { + distTag.exec([], (err) => { t.ifError(err, 'npm dist-tags ls') t.matchSnapshot( result, @@ -109,7 +110,7 @@ test('no args in current package', (t) => { test('borked cmd usage', (t) => { prefix = t.testdir({}) - distTag(['borked', '@scoped/pkg'], (err) => { + distTag.exec(['borked', '@scoped/pkg'], (err) => { t.matchSnapshot(err, 'should show usage error') result = '' log = '' @@ -119,7 +120,7 @@ test('borked cmd usage', (t) => { test('ls on named package', (t) => { prefix = t.testdir({}) - distTag(['ls', '@scoped/another'], (err) => { + distTag.exec(['ls', '@scoped/another'], (err) => { t.ifError(err, 'npm dist-tags ls') t.matchSnapshot( result, @@ -133,7 +134,7 @@ test('ls on named package', (t) => { test('ls on missing package', (t) => { prefix = t.testdir({}) - distTag(['ls', 'foo'], (err) => { + distTag.exec(['ls', 'foo'], (err) => { t.matchSnapshot( log, 'should log no dist-tag found msg' @@ -154,7 +155,7 @@ test('ls on missing name in current package', (t) => { version: '1.0.0', }), }) - distTag(['ls'], (err) => { + distTag.exec(['ls'], (err) => { t.matchSnapshot( err, 'should throw usage error message' @@ -167,7 +168,7 @@ test('ls on missing name in current package', (t) => { test('only named package arg', (t) => { prefix = t.testdir({}) - distTag(['@scoped/another'], (err) => { + distTag.exec(['@scoped/another'], (err) => { t.ifError(err, 'npm dist-tags ls') t.matchSnapshot( result, @@ -186,7 +187,7 @@ test('add new tag', (t) => { t.equal(opts.body, '7.7.7', 'should point to expected version') } prefix = t.testdir({}) - distTag(['add', '@scoped/another@7.7.7', 'c'], (err) => { + distTag.exec(['add', '@scoped/another@7.7.7', 'c'], (err) => { t.ifError(err, 'npm dist-tags add') t.matchSnapshot( result, @@ -201,7 +202,7 @@ test('add new tag', (t) => { test('add using valid semver range as name', (t) => { prefix = t.testdir({}) - distTag(['add', '@scoped/another@7.7.7', '1.0.0'], (err) => { + distTag.exec(['add', '@scoped/another@7.7.7', '1.0.0'], (err) => { t.match( err, /Error: Tag name must not be a valid SemVer range: 1.0.0/, @@ -219,7 +220,7 @@ test('add using valid semver range as name', (t) => { test('add missing args', (t) => { prefix = t.testdir({}) - distTag(['add', '@scoped/another@7.7.7'], (err) => { + distTag.exec(['add', '@scoped/another@7.7.7'], (err) => { t.matchSnapshot(err, 'should exit usage error message') result = '' log = '' @@ -229,7 +230,7 @@ test('add missing args', (t) => { test('add missing pkg name', (t) => { prefix = t.testdir({}) - distTag(['add', null], (err) => { + distTag.exec(['add', null], (err) => { t.matchSnapshot(err, 'should exit usage error message') result = '' log = '' @@ -239,7 +240,7 @@ test('add missing pkg name', (t) => { test('set existing version', (t) => { prefix = t.testdir({}) - distTag(['set', '@scoped/another@0.6.0', 'b'], (err) => { + distTag.exec(['set', '@scoped/another@0.6.0', 'b'], (err) => { t.ifError(err, 'npm dist-tags set') t.matchSnapshot( log, @@ -256,7 +257,7 @@ test('remove existing tag', (t) => { t.equal(opts.method, 'DELETE', 'should trigger request to remove tag') } prefix = t.testdir({}) - distTag(['rm', '@scoped/another', 'c'], (err) => { + distTag.exec(['rm', '@scoped/another', 'c'], (err) => { t.ifError(err, 'npm dist-tags rm') t.matchSnapshot(log, 'should log remove info') t.matchSnapshot(result, 'should return success msg') @@ -269,7 +270,7 @@ test('remove existing tag', (t) => { test('remove non-existing tag', (t) => { prefix = t.testdir({}) - distTag(['rm', '@scoped/another', 'nonexistent'], (err) => { + distTag.exec(['rm', '@scoped/another', 'nonexistent'], (err) => { t.match( err, /Error: nonexistent is not a dist-tag on @scoped\/another/, @@ -284,7 +285,7 @@ test('remove non-existing tag', (t) => { test('remove missing pkg name', (t) => { prefix = t.testdir({}) - distTag(['rm', null], (err) => { + distTag.exec(['rm', null], (err) => { t.matchSnapshot(err, 'should exit usage error message') result = '' log = '' diff --git a/test/lib/docs.js b/test/lib/docs.js index 8a59ed7cc0b09..52191db469b02 100644 --- a/test/lib/docs.js +++ b/test/lib/docs.js @@ -33,17 +33,18 @@ const pacote = { // keep a tally of which urls got opened const opened = {} -const openUrl = (url, errMsg, cb) => { +const openUrl = (npm, url, errMsg) => { opened[url] = opened[url] || 0 opened[url]++ - process.nextTick(cb) } -const docs = requireInject('../../lib/docs.js', { +const Docs = requireInject('../../lib/docs.js', { pacote, '../../lib/utils/open-url.js': openUrl, }) +const docs = new Docs({ flatOptions: {} }) + t.test('open docs urls', t => { const expect = { nodocs: 'https://www.npmjs.com/package/nodocs', @@ -56,7 +57,7 @@ t.test('open docs urls', t => { t.plan(keys.length) keys.forEach(pkg => { t.test(pkg, t => { - docs([pkg], (er) => { + docs.exec([pkg], (er) => { if (er) throw er const url = expect[pkg] @@ -68,7 +69,7 @@ t.test('open docs urls', t => { }) t.test('open default package if none specified', t => { - docs([], (er) => { + docs.exec([], (er) => { if (er) throw er t.equal(opened['https://example.com'], 2, 'opened expected url', {opened}) diff --git a/test/lib/doctor.js b/test/lib/doctor.js index f5e6fd062a331..8200493478fa1 100644 --- a/test/lib/doctor.js +++ b/test/lib/doctor.js @@ -120,18 +120,18 @@ const cacache = { }, } -const doctor = requireInject('../../lib/doctor.js', { +const Doctor = requireInject('../../lib/doctor.js', { '../../lib/utils/is-windows.js': false, '../../lib/utils/ping.js': ping, '../../lib/utils/output.js': (data) => { output.push(data) }, - '../../lib/npm.js': npm, cacache, pacote, 'make-fetch-happen': fetch, which, }) +const doctor = new Doctor(npm) const origVersion = process.version test('node versions', t => { @@ -162,7 +162,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { if (err) { st.fail(output) return st.end() @@ -211,7 +211,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { if (err) { st.fail(err) return st.end() @@ -255,7 +255,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the ping error') st.match(logs, { checkPing: { finished: true }, @@ -282,18 +282,18 @@ test('node versions', t => { }) vt.test('npm doctor skips some tests in windows', st => { - const winDoctor = requireInject('../../lib/doctor.js', { + const WinDoctor = requireInject('../../lib/doctor.js', { '../../lib/utils/is-windows.js': true, '../../lib/utils/ping.js': ping, '../../lib/utils/output.js': (data) => { output.push(data) }, - '../../lib/npm.js': npm, cacache, pacote, 'make-fetch-happen': fetch, which, }) + const winDoctor = new WinDoctor(npm) const dir = st.testdir() npm.cache = npm.flatOptions.cache = dir @@ -312,7 +312,7 @@ test('node versions', t => { clearLogs() }) - winDoctor([], (err) => { + winDoctor.exec([], (err) => { if (err) { st.fail(output) return st.end() @@ -360,7 +360,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the ping error') st.match(logs, { checkPing: { finished: true }, @@ -409,7 +409,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the ping error') st.match(logs, { checkPing: { finished: true }, @@ -458,7 +458,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the out of date npm') st.match(logs, { checkPing: { finished: true }, @@ -563,19 +563,19 @@ test('node versions', t => { } } - const doctor = requireInject('../../lib/doctor.js', { + const Doctor = requireInject('../../lib/doctor.js', { '../../lib/utils/is-windows.js': false, '../../lib/utils/ping.js': ping, '../../lib/utils/output.js': (data) => { output.push(data) }, - '../../lib/npm.js': npm, cacache, pacote, 'make-fetch-happen': fetch, which, fs, }) + const doctor = new Doctor(npm) // it's necessary to allow tests in node 10.x to not mark 12.x as lted npm.cache = npm.flatOptions.cache = join(dir, 'cache') @@ -600,7 +600,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'identified problems') st.match(logs, { checkPing: { finished: true }, @@ -653,7 +653,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the missing git') st.match(logs, { checkPing: { finished: true }, @@ -706,7 +706,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { // cache verification problems get fixed and so do not throw an error if (err) { st.fail(output) @@ -765,7 +765,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { // cache verification problems get fixed and so do not throw an error if (err) { st.fail(output) @@ -823,7 +823,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { // cache verification problems get fixed and so do not throw an error if (err) { st.fail(output) @@ -878,7 +878,7 @@ test('node versions', t => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { // cache verification problems get fixed and so do not throw an error st.match(err, /Some problems found/, 'detected the non-default registry') st.match(logs, { @@ -942,7 +942,7 @@ test('outdated node version', vt => { clearLogs() }) - doctor([], (err) => { + doctor.exec([], (err) => { st.match(err, /Some problems found/, 'detected the out of date nodejs') st.match(logs, { checkPing: { finished: true }, diff --git a/test/lib/edit.js b/test/lib/edit.js index 0d3bbb4c57e71..fe4835c60a583 100644 --- a/test/lib/edit.js +++ b/test/lib/edit.js @@ -38,11 +38,11 @@ const npm = { } const gracefulFs = require('graceful-fs') -const edit = requireInject('../../lib/edit.js', { - '../../lib/npm.js': npm, +const Edit = requireInject('../../lib/edit.js', { child_process: childProcess, 'graceful-fs': gracefulFs, }) +const edit = new Edit(npm) test('npm edit', t => { t.teardown(() => { @@ -52,7 +52,7 @@ test('npm edit', t => { editorOpts = null }) - return edit(['semver'], (err) => { + return edit.exec(['semver'], (err) => { if (err) throw err @@ -75,7 +75,7 @@ test('npm edit editor has flags', t => { EDITOR = 'vim' }) - return edit(['semver'], (err) => { + return edit.exec(['semver'], (err) => { if (err) throw err @@ -89,7 +89,7 @@ test('npm edit editor has flags', t => { }) test('npm edit no args', t => { - return edit([], (err) => { + return edit.exec([], (err) => { t.match(err, /npm edit/, 'throws usage error') t.end() }) @@ -104,7 +104,7 @@ test('npm edit lstat error propagates', t => { gracefulFs.lstat = _lstat }) - return edit(['semver'], (err) => { + return edit.exec(['semver'], (err) => { t.match(err, /lstat failed/, 'user received correct error') t.end() }) @@ -116,7 +116,7 @@ test('npm edit editor exit code error propagates', t => { EDITOR_CODE = 0 }) - return edit(['semver'], (err) => { + return edit.exec(['semver'], (err) => { t.match(err, /exited with code: 137/, 'user received correct error') t.end() }) diff --git a/test/lib/exec.js b/test/lib/exec.js index ac813ade7b7e2..4dc7f31cc31f4 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -90,13 +90,13 @@ const mocks = { '@npmcli/arborist': Arborist, '@npmcli/run-script': runScript, '@npmcli/ci-detect': () => CI_NAME, - '../../lib/npm.js': npm, pacote, read, 'mkdirp-infer-owner': mkdirp, '../../lib/utils/output.js': output, } -const exec = requireInject('../../lib/exec.js', mocks) +const Exec = requireInject('../../lib/exec.js', mocks) +const exec = new Exec(npm) t.afterEach(cb => { MKDIRPS.length = 0 @@ -116,7 +116,7 @@ t.afterEach(cb => { cb() }) -t.test('npx foo, bin already exists locally', async t => { +t.test('npx foo, bin already exists locally', t => { const path = t.testdir({ foo: 'just some file', }) @@ -124,24 +124,25 @@ t.test('npx foo, bin already exists locally', async t => { PROGRESS_IGNORED = true npm.localBin = path - await exec(['foo', 'one arg', 'two arg'], er => { + exec.exec(['foo', 'one arg', 'two arg'], er => { t.ifError(er, 'npm exec') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' }}, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { + PATH: [path, ...PATH].join(delimiter), + }, + stdio: 'inherit', + }]) + t.end() }) - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' }}, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [path, ...PATH].join(delimiter), - }, - stdio: 'inherit', - }]) }) -t.test('npx foo, bin already exists globally', async t => { +t.test('npx foo, bin already exists globally', t => { const path = t.testdir({ foo: 'just some file', }) @@ -149,24 +150,25 @@ t.test('npx foo, bin already exists globally', async t => { PROGRESS_IGNORED = true npm.globalBin = path - await exec(['foo', 'one arg', 'two arg'], er => { + exec.exec(['foo', 'one arg', 'two arg'], er => { t.ifError(er, 'npm exec') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' }}, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { + PATH: [path, ...PATH].join(delimiter), + }, + stdio: 'inherit', + }]) + t.end() }) - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' }}, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { - PATH: [path, ...PATH].join(delimiter), - }, - stdio: 'inherit', - }]) }) -t.test('npm exec foo, already present locally', async t => { +t.test('npm exec foo, already present locally', t => { const path = t.testdir() npm.localPrefix = path ARB_ACTUAL_TREE[path] = { @@ -180,94 +182,103 @@ t.test('npm exec foo, already present locally', async t => { }, _from: 'foo@', } - await exec(['foo', 'one arg', 'two arg'], er => { + exec.exec(['foo', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec , run interactive shell', async t => { +t.test('npm exec , run interactive shell', t => { CI_NAME = null const { isTTY } = process.stdin process.stdin.isTTY = true t.teardown(() => process.stdin.isTTY = isTTY) - const run = async (t, doRun = true) => { + const run = (t, doRun, cb) => { LOG_WARN.length = 0 ARB_CTOR.length = 0 MKDIRPS.length = 0 ARB_REIFY.length = 0 OUTPUT.length = 0 - await exec([], er => { + exec.exec([], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist') + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + if (doRun) { + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'shell-cmd' } }, + args: [], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + } else + t.strictSame(RUN_SCRIPTS, []) + + RUN_SCRIPTS.length = 0 + cb() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.strictSame(ARB_CTOR, [], 'no need to instantiate arborist') - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - if (doRun) { - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'shell-cmd' } }, - args: [], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) - } else - t.strictSame(RUN_SCRIPTS, []) - RUN_SCRIPTS.length = 0 } - t.test('print message when tty and not in CI', async t => { + t.test('print message when tty and not in CI', t => { CI_NAME = null process.stdin.isTTY = true - await run(t) - t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [ - ['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'], - ], 'printed message about interactive shell') + run(t, true, () => { + t.strictSame(LOG_WARN, []) + t.strictSame(OUTPUT, [ + ['\nEntering npm script environment\nType \'exit\' or ^D when finished\n'], + ], 'printed message about interactive shell') + t.end() + }) }) - t.test('no message when not TTY', async t => { + t.test('no message when not TTY', t => { CI_NAME = null process.stdin.isTTY = false - await run(t) - t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [], 'no message about interactive shell') + run(t, true, () => { + t.strictSame(LOG_WARN, []) + t.strictSame(OUTPUT, [], 'no message about interactive shell') + t.end() + }) }) - t.test('print warning when in CI and interactive', async t => { + t.test('print warning when in CI and interactive', t => { CI_NAME = 'travis-ci' process.stdin.isTTY = true - await run(t, false) - t.strictSame(LOG_WARN, [ - ['exec', 'Interactive mode disabled in CI environment'], - ]) - t.strictSame(OUTPUT, [], 'no message about interactive shell') + run(t, false, () => { + t.strictSame(LOG_WARN, [ + ['exec', 'Interactive mode disabled in CI environment'], + ]) + t.strictSame(OUTPUT, [], 'no message about interactive shell') + t.end() + }) }) t.end() }) -t.test('npm exec foo, not present locally or in central loc', async t => { +t.test('npm exec foo, not present locally or in central loc', t => { const path = t.testdir() const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') npm.localPrefix = path @@ -285,28 +296,29 @@ t.test('npm exec foo, not present locally or in central loc', async t => { }, _from: 'foo@', } - await exec(['foo', 'one arg', 'two arg'], er => { + exec.exec(['foo', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.match(ARB_REIFY, [{add: ['foo@'], legacyPeerDeps: false}], 'need to install foo@') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.match(ARB_REIFY, [{add: ['foo@'], legacyPeerDeps: false}], 'need to install foo@') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec foo, not present locally but in central loc', async t => { +t.test('npm exec foo, not present locally but in central loc', t => { const path = t.testdir() const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') npm.localPrefix = path @@ -324,28 +336,29 @@ t.test('npm exec foo, not present locally but in central loc', async t => { }, _from: 'foo@', } - await exec(['foo', 'one arg', 'two arg'], er => { + exec.exec(['foo', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.match(ARB_REIFY, [], 'no need to install again, already there') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.match(ARB_REIFY, [], 'no need to install again, already there') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec foo, present locally but wrong version', async t => { +t.test('npm exec foo, present locally but wrong version', t => { const path = t.testdir() const installDir = resolve('cache-dir/_npx/2badf4630f1cfaad') npm.localPrefix = path @@ -363,28 +376,29 @@ t.test('npm exec foo, present locally but wrong version', async t => { }, _from: 'foo@2.x', } - await exec(['foo@2.x', 'one arg', 'two arg'], er => { + exec.exec(['foo@2.x', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.match(ARB_REIFY, [{ add: ['foo@2.x'], legacyPeerDeps: false }], 'need to add foo@2.x') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foo' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.match(ARB_REIFY, [{ add: ['foo@2.x'], legacyPeerDeps: false }], 'need to add foo@2.x') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foo' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec --package=foo bar', async t => { +t.test('npm exec --package=foo bar', t => { const path = t.testdir() npm.localPrefix = path ARB_ACTUAL_TREE[path] = { @@ -399,27 +413,28 @@ t.test('npm exec --package=foo bar', async t => { _from: 'foo@', } npm.flatOptions.package = ['foo'] - await exec(['bar', 'one arg', 'two arg'], er => { + exec.exec(['bar', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'bar' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'bar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => { +t.test('npm exec @foo/bar -- --some=arg, locally installed', t => { const foobarManifest = { name: '@foo/bar', version: '1.2.3', @@ -440,27 +455,28 @@ t.test('npm exec @foo/bar -- --some=arg, locally installed', async t => { children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]]), } MANIFESTS['@foo/bar'] = foobarManifest - await exec(['@foo/bar', '--some=arg'], er => { + exec.exec(['@foo/bar', '--some=arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [{ package: ['@foo/bar'], path }]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'bar' } }, + args: ['--some=arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ package: ['@foo/bar'], path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'bar' } }, - args: ['--some=arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) }) -t.test('npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', async t => { +t.test('npm exec @foo/bar, with same bin alias and no unscoped named bin, locally installed', t => { const foobarManifest = { name: '@foo/bar', version: '1.2.3', @@ -482,24 +498,25 @@ t.test('npm exec @foo/bar, with same bin alias and no unscoped named bin, locall children: new Map([['@foo/bar', { name: '@foo/bar', version: '1.2.3' }]]), } MANIFESTS['@foo/bar'] = foobarManifest - await exec(['@foo/bar', 'one arg', 'two arg'], er => { + exec.exec(['@foo/bar', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [{ package: ['@foo/bar'], path }]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'baz' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ package: ['@foo/bar'], path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'baz' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) }) t.test('npm exec @foo/bar, with different bin alias and no unscoped named bin, locally installed', t => { @@ -519,12 +536,12 @@ t.test('npm exec @foo/bar, with different bin alias and no unscoped named bin, l _from: 'foo@', _id: '@foo/bar@1.2.3', } - return t.rejects(exec(['@foo/bar'], er => { - if (er) - throw er - }), { - message: 'could not determine executable to run', - pkgid: '@foo/bar@1.2.3', + exec.exec(['@foo/bar'], er => { + t.match(er, { + message: 'could not determine executable to run', + pkgid: '@foo/bar@1.2.3', + }) + t.end() }) }) @@ -534,7 +551,7 @@ t.test('run command with 2 packages, need install, verify sort', t => { const cases = [['foo', 'bar'], ['bar', 'foo']] t.plan(cases.length) for (const packages of cases) { - t.test(packages.join(', '), async t => { + t.test(packages.join(', '), t => { npm.flatOptions.package = packages const add = packages.map(p => `${p}@`).sort((a, b) => a.localeCompare(b)) const path = t.testdir() @@ -562,25 +579,26 @@ t.test('run command with 2 packages, need install, verify sort', t => { }, _from: 'bar@', } - await exec(['foobar', 'one arg', 'two arg'], er => { + exec.exec(['foobar', 'one arg', 'two arg'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foobar' } }, + args: ['one arg', 'two arg'], + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foobar' } }, - args: ['one arg', 'two arg'], - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) }) } }) @@ -597,12 +615,12 @@ t.test('npm exec foo, no bin in package', t => { _from: 'foo@', _id: 'foo@1.2.3', } - return t.rejects(exec(['foo'], er => { - if (er) - throw er - }), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', + exec.exec(['foo'], er => { + t.match(er, { + message: 'could not determine executable to run', + pkgid: 'foo@1.2.3', + }) + t.end() }) }) @@ -622,16 +640,16 @@ t.test('npm exec foo, many bins in package, none named foo', t => { _from: 'foo@', _id: 'foo@1.2.3', } - return t.rejects(exec(['foo'], er => { - if (er) - throw er - }), { - message: 'could not determine executable to run', - pkgid: 'foo@1.2.3', + exec.exec(['foo'], er => { + t.match(er, { + message: 'could not determine executable to run', + pkgid: 'foo@1.2.3', + }) + t.end() }) }) -t.test('npm exec -p foo -c "ls -laF"', async t => { +t.test('npm exec -p foo -c "ls -laF"', t => { const path = t.testdir() npm.localPrefix = path npm.flatOptions.package = ['foo'] @@ -644,31 +662,35 @@ t.test('npm exec -p foo -c "ls -laF"', async t => { version: '1.2.3', _from: 'foo@', } - await exec([], er => { + exec.exec([], er => { if (er) throw er + t.strictSame(MKDIRPS, [], 'no need to make any dirs') + t.match(ARB_CTOR, [{ package: ['foo'], path }]) + t.strictSame(ARB_REIFY, [], 'no need to reify anything') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'ls -laF' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH: process.env.PATH }, + stdio: 'inherit', + }]) + t.end() }) - t.strictSame(MKDIRPS, [], 'no need to make any dirs') - t.match(ARB_CTOR, [{ package: ['foo'], path }]) - t.strictSame(ARB_REIFY, [], 'no need to reify anything') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'ls -laF' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH: process.env.PATH }, - stdio: 'inherit', - }]) }) t.test('positional args and --call together is an error', t => { npm.flatOptions.call = 'true' - return exec(['foo'], er => t.equal(er, exec.usage)) + exec.exec(['foo'], er => { + t.equal(er, exec.usage) + t.end() + }) }) -t.test('prompt when installs are needed if not already present and shell is a TTY', async t => { +t.test('prompt when installs are needed if not already present and shell is a TTY', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -712,31 +734,32 @@ t.test('prompt when installs are needed if not already present and shell is a TT }, _from: 'bar@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foobar' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.strictSame(READ, [{ + prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', + default: 'y', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) - t.strictSame(READ, [{ - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }]) }) -t.test('skip prompt when installs are needed if not already present and shell is not a tty (multiple packages)', async t => { +t.test('skip prompt when installs are needed if not already present and shell is not a tty (multiple packages)', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -780,29 +803,30 @@ t.test('skip prompt when installs are needed if not already present and shell is }, _from: 'bar@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foobar' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.strictSame(READ, [], 'should not have prompted') + t.strictSame(LOG_WARN, [['exec', 'The following packages were not found and will be installed: bar, foo']], 'should have printed a warning') + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install both packages') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame(LOG_WARN, [['exec', 'The following packages were not found and will be installed: bar, foo']], 'should have printed a warning') }) -t.test('skip prompt when installs are needed if not already present and shell is not a tty (single package)', async t => { +t.test('skip prompt when installs are needed if not already present and shell is not a tty (single package)', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -838,29 +862,30 @@ t.test('skip prompt when installs are needed if not already present and shell is }, _from: 'foo@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { if (er) throw er + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install the package') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` + t.match(RUN_SCRIPTS, [{ + pkg: { scripts: { npx: 'foobar' } }, + banner: false, + path: process.cwd(), + stdioString: true, + event: 'npx', + env: { PATH }, + stdio: 'inherit', + }]) + t.strictSame(READ, [], 'should not have prompted') + t.strictSame(LOG_WARN, [['exec', 'The following package was not found and will be installed: foo']], 'should have printed a warning') + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.match(ARB_REIFY, [{add, legacyPeerDeps: false}], 'need to install the package') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - const PATH = `${resolve(installDir, 'node_modules', '.bin')}${delimiter}${process.env.PATH}` - t.match(RUN_SCRIPTS, [{ - pkg: { scripts: { npx: 'foobar' } }, - banner: false, - path: process.cwd(), - stdioString: true, - event: 'npx', - env: { PATH }, - stdio: 'inherit', - }]) - t.strictSame(READ, [], 'should not have prompted') - t.strictSame(LOG_WARN, [['exec', 'The following package was not found and will be installed: foo']], 'should have printed a warning') }) -t.test('abort if prompt rejected', async t => { +t.test('abort if prompt rejected', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -903,21 +928,22 @@ t.test('abort if prompt rejected', async t => { }, _from: 'bar@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { t.equal(er, 'canceled', 'should be canceled') + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.strictSame(ARB_REIFY, [], 'no install performed') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.strictSame(RUN_SCRIPTS, []) + t.strictSame(READ, [{ + prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', + default: 'y', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [{ - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }]) }) -t.test('abort if prompt false', async t => { +t.test('abort if prompt false', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -960,21 +986,22 @@ t.test('abort if prompt false', async t => { }, _from: 'bar@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { t.equal(er, 'canceled', 'should be canceled') + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.strictSame(ARB_REIFY, [], 'no install performed') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.strictSame(RUN_SCRIPTS, []) + t.strictSame(READ, [{ + prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', + default: 'y', + }]) + t.end() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, [{ - prompt: 'Need to install the following packages:\n bar\n foo\nOk to proceed? ', - default: 'y', - }]) }) -t.test('abort if -n provided', async t => { +t.test('abort if -n provided', t => { const stdoutTTY = process.stdout.isTTY const stdinTTY = process.stdin.isTTY t.teardown(() => { @@ -1016,18 +1043,19 @@ t.test('abort if -n provided', async t => { }, _from: 'bar@', } - await exec(['foobar'], er => { + exec.exec(['foobar'], er => { t.equal(er, 'canceled', 'should be canceled') + t.strictSame(MKDIRPS, [installDir], 'need to make install dir') + t.match(ARB_CTOR, [{ package: packages, path }]) + t.strictSame(ARB_REIFY, [], 'no install performed') + t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') + t.strictSame(RUN_SCRIPTS, []) + t.strictSame(READ, []) + t.done() }) - t.strictSame(MKDIRPS, [installDir], 'need to make install dir') - t.match(ARB_CTOR, [{ package: packages, path }]) - t.strictSame(ARB_REIFY, [], 'no install performed') - t.equal(PROGRESS_ENABLED, true, 'progress re-enabled') - t.strictSame(RUN_SCRIPTS, []) - t.strictSame(READ, []) }) -t.test('forward legacyPeerDeps opt', async t => { +t.test('forward legacyPeerDeps opt', t => { const path = t.testdir() const installDir = resolve('cache-dir/_npx/f7fbba6e0636f890') npm.localPrefix = path @@ -1047,9 +1075,10 @@ t.test('forward legacyPeerDeps opt', async t => { } npm.flatOptions.yes = true npm.flatOptions.legacyPeerDeps = true - await exec(['foo'], er => { + exec.exec(['foo'], er => { if (er) throw er + t.match(ARB_REIFY, [{add: ['foo@'], legacyPeerDeps: true}], 'need to install foo@ using legacyPeerDeps opt') + t.done() }) - t.match(ARB_REIFY, [{add: ['foo@'], legacyPeerDeps: true}], 'need to install foo@ using legacyPeerDeps opt') }) diff --git a/test/lib/explain.js b/test/lib/explain.js index 1eeca8c4c4f5d..22bfb8639ecff 100644 --- a/test/lib/explain.js +++ b/test/lib/explain.js @@ -9,9 +9,7 @@ const { resolve } = require('path') const OUTPUT = [] -const explain = requireInject('../../lib/explain.js', { - '../../lib/npm.js': npm, - +const Explain = requireInject('../../lib/explain.js', { '../../lib/utils/output.js': (...args) => { OUTPUT.push(args) }, @@ -23,44 +21,40 @@ const explain = requireInject('../../lib/explain.js', { }, }, }) +const explain = new Explain(npm) -t.test('no args throws usage', async t => { +t.test('no args throws usage', t => { t.plan(1) - try { - await explain([], er => { - throw er - }) - } catch (er) { + explain.exec([], er => { t.equal(er, explain.usage) - } + t.done() + }) }) -t.test('no match throws not found', async t => { +t.test('no match throws not found', t => { npm.prefix = t.testdir() t.plan(1) - try { - await explain(['foo@1.2.3', 'node_modules/baz'], er => { - throw er - }) - } catch (er) { + explain.exec(['foo@1.2.3', 'node_modules/baz'], er => { t.equal(er, 'No dependencies found matching foo@1.2.3, node_modules/baz') - } + }) }) -t.test('invalid package name throws not found', async t => { +t.test('invalid package name throws not found', t => { npm.prefix = t.testdir() t.plan(1) const badName = ' not a valid package name ' - try { - await explain([`${badName}@1.2.3`], er => { - throw er - }) - } catch (er) { + explain.exec([`${badName}@1.2.3`], er => { t.equal(er, `No dependencies found matching ${badName}@1.2.3`) - } + }) }) -t.test('explain some nodes', async t => { +t.test('explain some nodes', t => { + t.afterEach((cb) => { + OUTPUT.length = 0 + npm.flatOptions.json = false + cb() + }) + npm.prefix = t.testdir({ node_modules: { foo: { @@ -111,61 +105,75 @@ t.test('explain some nodes', async t => { }), }) - // works with either a full actual path or the location - const p = 'node_modules/foo' - for (const path of [p, resolve(npm.prefix, p)]) { - await explain([path], er => { + t.test('works with the location', t => { + const path = 'node_modules/foo' + explain.exec([path], er => { if (er) throw er + t.strictSame(OUTPUT, [['foo@1.2.3 depth=Infinity color=true']]) + t.end() }) - t.strictSame(OUTPUT, [['foo@1.2.3 depth=Infinity color=true']]) - OUTPUT.length = 0 - } + }) + t.test('works with a full actual path', t => { + const path = resolve(npm.prefix, 'node_modules/foo') + explain.exec([path], er => { + if (er) + throw er + t.strictSame(OUTPUT, [['foo@1.2.3 depth=Infinity color=true']]) + t.end() + }) + }) - // finds all nodes by name - await explain(['bar'], er => { - if (er) - throw er + t.test('finds all nodes by name', t => { + explain.exec(['bar'], er => { + if (er) + throw er + t.strictSame(OUTPUT, [[ + 'bar@1.2.3 depth=Infinity color=true\n\n' + + 'bar@2.3.4 depth=Infinity color=true', + ]]) + t.end() + }) }) - t.strictSame(OUTPUT, [[ - 'bar@1.2.3 depth=Infinity color=true\n\n' + - 'bar@2.3.4 depth=Infinity color=true', - ]]) - OUTPUT.length = 0 - // finds only nodes that match the spec - await explain(['bar@1'], er => { - if (er) - throw er + t.test('finds only nodes that match the spec', t => { + explain.exec(['bar@1'], er => { + if (er) + throw er + t.strictSame(OUTPUT, [['bar@1.2.3 depth=Infinity color=true']]) + t.end() + }) }) - t.strictSame(OUTPUT, [['bar@1.2.3 depth=Infinity color=true']]) - OUTPUT.length = 0 - // finds extraneous nodes - await explain(['extra'], er => { - if (er) - throw er + t.test('finds extraneous nodes', t => { + explain.exec(['extra'], er => { + if (er) + throw er + t.strictSame(OUTPUT, [['extra@99.9999.999999 depth=Infinity color=true']]) + t.end() + }) }) - t.strictSame(OUTPUT, [['extra@99.9999.999999 depth=Infinity color=true']]) - OUTPUT.length = 0 - npm.flatOptions.json = true - await explain(['node_modules/foo'], er => { - if (er) - throw er + t.test('json output', t => { + npm.flatOptions.json = true + explain.exec(['node_modules/foo'], er => { + if (er) + throw er + t.match(JSON.parse(OUTPUT[0][0]), [{ + name: 'foo', + version: '1.2.3', + dependents: Array, + }]) + t.end() + }) }) - t.match(JSON.parse(OUTPUT[0][0]), [{ - name: 'foo', - version: '1.2.3', - dependents: Array, - }]) - OUTPUT.length = 0 - npm.flatOptions.json = false - t.test('report if no nodes found', async t => { + t.test('report if no nodes found', t => { t.plan(1) - await explain(['asdf/foo/bar', 'quux@1.x'], er => { + explain.exec(['asdf/foo/bar', 'quux@1.x'], er => { t.equal(er, 'No dependencies found matching asdf/foo/bar, quux@1.x') + t.done() }) }) + t.end() }) diff --git a/test/lib/explore.js b/test/lib/explore.js index 23eab1172a05e..6f1f3bb47f240 100644 --- a/test/lib/explore.js +++ b/test/lib/explore.js @@ -46,14 +46,20 @@ const mockRunScript = ({ pkg, banner, path, event, stdio }) => { const output = [] let ERROR_HANDLER_CALLED = null const logs = [] -const getExplore = windows => requireInject('../../lib/explore.js', { - '../../lib/utils/is-windows.js': windows, - path: require('path')[windows ? 'win32' : 'posix'], - '../../lib/utils/error-handler.js': er => { - ERROR_HANDLER_CALLED = er - }, - 'read-package-json-fast': mockRPJ, - '../../lib/npm.js': { +const getExplore = (windows) => { + const Explore = requireInject('../../lib/explore.js', { + '../../lib/utils/is-windows.js': windows, + path: require('path')[windows ? 'win32' : 'posix'], + '../../lib/utils/error-handler.js': er => { + ERROR_HANDLER_CALLED = er + }, + 'read-package-json-fast': mockRPJ, + '@npmcli/run-script': mockRunScript, + '../../lib/utils/output.js': out => { + output.push(out) + }, + }) + const npm = { dir: windows ? 'c:\\npm\\dir' : '/npm/dir', log: { error: (...msg) => logs.push(msg), @@ -63,12 +69,9 @@ const getExplore = windows => requireInject('../../lib/explore.js', { flatOptions: { shell: 'shell-command', }, - }, - '@npmcli/run-script': mockRunScript, - '../../lib/utils/output.js': out => { - output.push(out) - }, -}) + } + return new Explore(npm) +} const windowsExplore = getExplore(true) const posixExplore = getExplore(false) @@ -79,7 +82,7 @@ t.test('basic interactive', t => { cb() }) - t.test('windows', t => windowsExplore(['pkg'], er => { + t.test('windows', t => windowsExplore.exec(['pkg'], er => { if (er) throw er @@ -95,9 +98,10 @@ t.test('basic interactive', t => { t.strictSame(output, [ "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n", ]) + t.end() })) - t.test('posix', t => posixExplore(['pkg'], er => { + t.test('posix', t => posixExplore.exec(['pkg'], er => { if (er) throw er @@ -113,6 +117,7 @@ t.test('basic interactive', t => { t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) + t.end() })) t.end() @@ -132,7 +137,7 @@ t.test('interactive tracks exit code', t => { cb() }) - t.test('windows', t => windowsExplore(['pkg'], er => { + t.test('windows', t => windowsExplore.exec(['pkg'], er => { if (er) throw er @@ -149,9 +154,10 @@ t.test('interactive tracks exit code', t => { "\nExploring c:\\npm\\dir\\pkg\nType 'exit' or ^D when finished\n", ]) t.equal(process.exitCode, 99) + t.end() })) - t.test('posix', t => posixExplore(['pkg'], er => { + t.test('posix', t => posixExplore.exec(['pkg'], er => { if (er) throw er @@ -168,18 +174,20 @@ t.test('interactive tracks exit code', t => { "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) t.equal(process.exitCode, 99) + t.end() })) t.test('posix spawn fail', t => { RUN_SCRIPT_ERROR = Object.assign(new Error('glorb'), { code: 33, }) - return posixExplore(['pkg'], er => { + posixExplore.exec(['pkg'], er => { t.match(er, { message: 'glorb', code: 33 }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) t.equal(process.exitCode, 33) + t.end() }) }) @@ -187,12 +195,13 @@ t.test('interactive tracks exit code', t => { RUN_SCRIPT_ERROR = Object.assign(new Error('glorb'), { code: 0, }) - return posixExplore(['pkg'], er => { + posixExplore.exec(['pkg'], er => { t.match(er, { message: 'glorb', code: 0 }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) t.equal(process.exitCode, 1) + t.end() }) }) @@ -200,12 +209,13 @@ t.test('interactive tracks exit code', t => { RUN_SCRIPT_ERROR = Object.assign(new Error('command failed'), { code: 'EPROBLEM', }) - return posixExplore(['pkg'], er => { + posixExplore.exec(['pkg'], er => { t.match(er, { message: 'command failed', code: 'EPROBLEM' }) t.strictSame(output, [ "\nExploring /npm/dir/pkg\nType 'exit' or ^D when finished\n", ]) t.equal(process.exitCode, 1) + t.end() }) }) @@ -218,7 +228,7 @@ t.test('basic non-interactive', t => { cb() }) - t.test('windows', t => windowsExplore(['pkg', 'ls'], er => { + t.test('windows', t => windowsExplore.exec(['pkg', 'ls'], er => { if (er) throw er @@ -232,9 +242,10 @@ t.test('basic non-interactive', t => { RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) + t.end() })) - t.test('posix', t => posixExplore(['pkg', 'ls'], er => { + t.test('posix', t => posixExplore.exec(['pkg', 'ls'], er => { if (er) throw er @@ -248,6 +259,7 @@ t.test('basic non-interactive', t => { RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) + t.end() })) t.end() @@ -272,7 +284,7 @@ t.test('signal fails non-interactive', t => { cb() }) - t.test('windows', t => windowsExplore(['pkg', 'ls'], er => { + t.test('windows', t => windowsExplore.exec(['pkg', 'ls'], er => { t.match(er, { message: 'command failed', signal: 'SIGPROBLEM', @@ -286,9 +298,10 @@ t.test('signal fails non-interactive', t => { RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) + t.end() })) - t.test('posix', t => posixExplore(['pkg', 'ls'], er => { + t.test('posix', t => posixExplore.exec(['pkg', 'ls'], er => { t.match(er, { message: 'command failed', signal: 'SIGPROBLEM', @@ -302,6 +315,7 @@ t.test('signal fails non-interactive', t => { RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) + t.end() })) t.end() @@ -322,29 +336,28 @@ t.test('usage if no pkg provided', t => { ] t.plan(noPkg.length) for (const args of noPkg) { - t.test(JSON.stringify(args), t => posixExplore(args, er => { - t.equal(er, 'npm explore [ -- ]') - t.strictSame({ - ERROR_HANDLER_CALLED: null, - RPJ_CALLED, - RUN_SCRIPT_EXEC, - }, { - ERROR_HANDLER_CALLED: null, - RPJ_CALLED: '/npm/dir/pkg/package.json', - RUN_SCRIPT_EXEC: 'ls', + t.test(JSON.stringify(args), t => { + posixExplore.exec(args, er => { + t.equal(er, 'npm explore [ -- ]') + t.strictSame({ + ERROR_HANDLER_CALLED: null, + RPJ_CALLED, + RUN_SCRIPT_EXEC, + }, { + ERROR_HANDLER_CALLED: null, + RPJ_CALLED: '/npm/dir/pkg/package.json', + RUN_SCRIPT_EXEC: 'ls', + }) + t.end() }) - })) + }) } }) t.test('pkg not installed', t => { RPJ_ERROR = new Error('plurple') - t.plan(2) - - posixExplore(['pkg', 'ls'], er => { - if (er) - throw er + posixExplore.exec(['pkg', 'ls'], er => { t.strictSame({ ERROR_HANDLER_CALLED, RPJ_CALLED, @@ -355,9 +368,9 @@ t.test('pkg not installed', t => { RUN_SCRIPT_EXEC: 'ls', }) t.strictSame(output, []) - }).catch(er => { t.match(er, { message: 'plurple' }) t.match(logs, [['explore', `It doesn't look like pkg is installed.`]]) + t.end() logs.length = 0 }) }) diff --git a/test/lib/find-dupes.js b/test/lib/find-dupes.js index 73c8fa2dc2793..ae179ec2be926 100644 --- a/test/lib/find-dupes.js +++ b/test/lib/find-dupes.js @@ -2,13 +2,14 @@ const { test } = require('tap') const requireInject = require('require-inject') test('should run dedupe in dryRun mode', (t) => { - const findDupes = requireInject('../../lib/find-dupes.js', { + const FindDupes = requireInject('../../lib/find-dupes.js', { '../../lib/dedupe.js': function (args, cb) { t.ok(args.dryRun, 'dryRun is true') cb() }, }) - findDupes(null, () => { + const findDupes = new FindDupes() + findDupes.exec(null, () => { t.ok(true, 'callback is called') t.end() }) diff --git a/test/lib/fund.js b/test/lib/fund.js index 73f639b6ce658..9af8a8f5280bd 100644 --- a/test/lib/fund.js +++ b/test/lib/fund.js @@ -188,7 +188,7 @@ const _flatOptions = { unicode: false, which: undefined, } -const openUrl = (url, msg, cb) => { +const openUrl = (npm, url, msg, cb) => { if (url === 'http://npmjs.org') { cb(new Error('ERROR')) return @@ -203,13 +203,7 @@ const openUrl = (url, msg, cb) => { cb() } -const fund = requireInject('../../lib/fund.js', { - '../../lib/npm.js': { - flatOptions: _flatOptions, - get prefix () { - return _flatOptions.prefix - }, - }, +const Fund = requireInject('../../lib/fund.js', { '../../lib/utils/open-url.js': openUrl, '../../lib/utils/output.js': msg => { result += msg + '\n' @@ -222,6 +216,12 @@ const fund = requireInject('../../lib/fund.js', { : Promise.reject(new Error('ERROR')), }, }) +const fund = new Fund({ + flatOptions: _flatOptions, + get prefix () { + return _flatOptions.prefix + }, +}) test('fund with no package containing funding', t => { _flatOptions.prefix = t.testdir({ @@ -231,7 +231,7 @@ test('fund with no package containing funding', t => { }), }) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should print empty funding info') result = '' @@ -242,7 +242,7 @@ test('fund with no package containing funding', t => { test('fund in which same maintainer owns all its deps', t => { _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should print stack packages together') result = '' @@ -254,7 +254,7 @@ test('fund in which same maintainer owns all its deps, using --json option', t = _flatOptions.json = true _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( JSON.parse(result), @@ -292,7 +292,7 @@ test('fund in which same maintainer owns all its deps, using --json option', t = test('fund containing multi-level nested deps with no funding', t => { _flatOptions.prefix = t.testdir(nestedNoFundingPackages) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot( result, @@ -308,7 +308,7 @@ test('fund containing multi-level nested deps with no funding, using --json opti _flatOptions.prefix = t.testdir(nestedNoFundingPackages) _flatOptions.json = true - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( JSON.parse(result), @@ -340,7 +340,7 @@ test('fund containing multi-level nested deps with no funding, using --json opti _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) _flatOptions.json = true - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( JSON.parse(result), @@ -397,7 +397,7 @@ test('fund does not support global', t => { _flatOptions.prefix = t.testdir({}) _flatOptions.global = true - fund([], (err) => { + fund.exec([], (err) => { t.match(err.code, 'EFUNDGLOBAL', 'should throw EFUNDGLOBAL error') result = '' @@ -409,7 +409,7 @@ test('fund does not support global', t => { test('fund using package argument', t => { _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(printUrl, 'should open funding url') @@ -423,7 +423,7 @@ test('fund does not support global, using --json option', t => { _flatOptions.global = true _flatOptions.json = true - fund([], (err) => { + fund.exec([], (err) => { t.equal(err.code, 'EFUNDGLOBAL', 'should use EFUNDGLOBAL error code') t.equal( err.message, @@ -446,7 +446,7 @@ test('fund using string shorthand', t => { }), }) - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(printUrl, 'should open string-only url') @@ -458,7 +458,7 @@ test('fund using string shorthand', t => { test('fund using nested packages with multiple sources', t => { _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should prompt with all available URLs') @@ -486,7 +486,7 @@ test('fund using symlink ref', t => { }) // using symlinked ref - fund(['./node_modules/a'], (err) => { + fund.exec(['./node_modules/a'], (err) => { t.ifError(err, 'should not error out') t.match( printUrl, @@ -497,7 +497,7 @@ test('fund using symlink ref', t => { printUrl = '' // using target ref - fund(['./a'], (err) => { + fund.exec(['./a'], (err) => { t.ifError(err, 'should not error out') t.match( @@ -547,7 +547,7 @@ test('fund using data from actual tree', t => { }) // using symlinked ref - fund(['a'], (err) => { + fund.exec(['a'], (err) => { t.ifError(err, 'should not error out') t.match( printUrl, @@ -564,7 +564,7 @@ test('fund using nested packages with multiple sources, with a source number', t _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) _flatOptions.which = '1' - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(printUrl, 'should open the numbered URL') @@ -578,7 +578,7 @@ test('fund using pkg name while having conflicting versions', t => { _flatOptions.prefix = t.testdir(conflictingFundingPackages) _flatOptions.which = '1' - fund(['foo'], (err) => { + fund.exec(['foo'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(printUrl, 'should open greatest version') @@ -591,7 +591,7 @@ test('fund using package argument with no browser, using --json option', t => { _flatOptions.prefix = t.testdir(maintainerOwnsAllDeps) _flatOptions.json = true - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.deepEqual( JSON.parse(printUrl), @@ -611,7 +611,7 @@ test('fund using package argument with no browser, using --json option', t => { test('fund using package info fetch from registry', t => { _flatOptions.prefix = t.testdir({}) - fund(['ntl'], (err) => { + fund.exec(['ntl'], (err) => { t.ifError(err, 'should not error out') t.match( printUrl, @@ -627,7 +627,7 @@ test('fund using package info fetch from registry', t => { test('fund tries to use package info fetch from registry but registry has nothing', t => { _flatOptions.prefix = t.testdir({}) - fund(['foo'], (err) => { + fund.exec(['foo'], (err) => { t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code') t.equal( err.message, @@ -643,7 +643,7 @@ test('fund tries to use package info fetch from registry but registry has nothin test('fund but target module has no funding info', t => { _flatOptions.prefix = t.testdir(nestedNoFundingPackages) - fund(['foo'], (err) => { + fund.exec(['foo'], (err) => { t.equal(err.code, 'ENOFUND', 'should have ENOFUND error code') t.equal( err.message, @@ -660,7 +660,7 @@ test('fund using bad which value', t => { _flatOptions.prefix = t.testdir(nestedMultipleFundingPackages) _flatOptions.which = 3 - fund(['bar'], (err) => { + fund.exec(['bar'], (err) => { t.equal(err.code, 'EFUNDNUMBER', 'should have EFUNDNUMBER error code') t.equal( err.message, @@ -682,7 +682,7 @@ test('fund pkg missing version number', t => { }), }) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should print name only') result = '' @@ -699,7 +699,7 @@ test('fund a package throws on openUrl', t => { }), }) - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.equal(err.message, 'ERROR', 'should throw unknown error') result = '' t.end() @@ -723,7 +723,7 @@ test('fund a package with type and multiple sources', t => { }), }) - fund(['.'], (err) => { + fund.exec(['.'], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should print prompt select message') @@ -787,7 +787,7 @@ test('fund colors', t => { }) _flatOptions.color = true - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should print output with color info') @@ -837,7 +837,7 @@ test('sub dep with fund info and a parent with no funding info', t => { }, }) - fund([], (err) => { + fund.exec([], (err) => { t.ifError(err, 'should not error out') t.matchSnapshot(result, 'should nest sub dep as child of root') diff --git a/test/lib/get.js b/test/lib/get.js index 5260c00bae795..c40d8d3eec81e 100644 --- a/test/lib/get.js +++ b/test/lib/get.js @@ -2,17 +2,17 @@ const { test } = require('tap') const requireInject = require('require-inject') test('should retrieve values from npm.commands.config', (t) => { - const get = requireInject('../../lib/get.js', { - '../../lib/npm.js': { - commands: { - config: ([action, arg]) => { - t.equal(action, 'get', 'should use config get action') - t.equal(arg, 'foo', 'should use expected key') - t.end() - }, + const Get = requireInject('../../lib/get.js', { + }) + const get = new Get({ + commands: { + config: ([action, arg]) => { + t.equal(action, 'get', 'should use config get action') + t.equal(arg, 'foo', 'should use expected key') + t.end() }, }, }) - get(['foo']) + get.exec(['foo']) }) diff --git a/test/lib/help-search.js b/test/lib/help-search.js index f74e2f1efeb9c..8b1ecd46eb774 100644 --- a/test/lib/help-search.js +++ b/test/lib/help-search.js @@ -24,7 +24,7 @@ const npm = { } let npmUsageArg = null -const npmUsage = (arg) => { +const npmUsage = (npm, arg) => { npmUsageArg = arg } @@ -43,12 +43,12 @@ const globDir = { const glob = (p, cb) => cb(null, Object.keys(globDir).map((file) => join(globRoot, file))) -const helpSearch = requireInject('../../lib/help-search.js', { - '../../lib/npm.js': npm, +const HelpSearch = requireInject('../../lib/help-search.js', { '../../lib/utils/npm-usage.js': npmUsage, '../../lib/utils/output.js': output, glob, }) +const helpSearch = new HelpSearch(npm) test('npm help-search', t => { globRoot = t.testdir(globDir) @@ -57,7 +57,7 @@ test('npm help-search', t => { globRoot = null }) - return helpSearch(['exec'], (err) => { + return helpSearch.exec(['exec'], (err) => { if (err) throw err @@ -74,7 +74,7 @@ test('npm help-search multiple terms', t => { globRoot = null }) - return helpSearch(['run', 'script'], (err) => { + return helpSearch.exec(['run', 'script'], (err) => { if (err) throw err @@ -92,7 +92,7 @@ test('npm help-search single result prints full section', t => { globRoot = null }) - return helpSearch(['does not exist in'], (err) => { + return helpSearch.exec(['does not exist in'], (err) => { if (err) throw err @@ -111,7 +111,7 @@ test('npm help-search single result propagates error', t => { globRoot = null }) - return helpSearch(['does not exist in'], (err) => { + return helpSearch.exec(['does not exist in'], (err) => { t.strictSame(npmHelpArgs, ['npm-install'], 'identified the correct man page and called help with it') t.match(err, /help broke/, 'propagated the error from help') t.end() @@ -127,7 +127,7 @@ test('npm help-search long output', t => { globRoot = null }) - return helpSearch(['exec'], (err) => { + return helpSearch.exec(['exec'], (err) => { if (err) throw err @@ -147,7 +147,7 @@ test('npm help-search long output with color', t => { globRoot = null }) - return helpSearch(['help-search'], (err) => { + return helpSearch.exec(['help-search'], (err) => { if (err) throw err @@ -158,7 +158,7 @@ test('npm help-search long output with color', t => { }) test('npm help-search no args', t => { - return helpSearch([], (err) => { + return helpSearch.exec([], (err) => { t.match(err, /npm help-search/, 'throws usage') t.end() }) @@ -172,7 +172,7 @@ test('npm help-search no matches', t => { globRoot = null }) - return helpSearch(['asdfasdf'], (err) => { + return helpSearch.exec(['asdfasdf'], (err) => { if (err) throw err diff --git a/test/lib/help.js b/test/lib/help.js index fc4a32e07beaa..5276db03b3e15 100644 --- a/test/lib/help.js +++ b/test/lib/help.js @@ -3,7 +3,7 @@ const requireInject = require('require-inject') const { EventEmitter } = require('events') let npmUsageArg = null -const npmUsage = (arg) => { +const npmUsage = (npm, arg) => { npmUsageArg = arg } @@ -72,8 +72,7 @@ const openUrl = (url, msg, cb) => { return cb() } -const help = requireInject('../../lib/help.js', { - '../../lib/npm.js': npm, +const Help = requireInject('../../lib/help.js', { '../../lib/utils/npm-usage.js': npmUsage, '../../lib/utils/open-url.js': openUrl, '../../lib/utils/output.js': output, @@ -82,13 +81,14 @@ const help = requireInject('../../lib/help.js', { }, glob, }) +const help = new Help(npm) test('npm help', t => { t.teardown(() => { npmUsageArg = null }) - return help([], (err) => { + return help.exec([], (err) => { if (err) throw err @@ -117,7 +117,7 @@ test('npm help -h', t => { OUTPUT.length = 0 }) - return help(['help'], (err) => { + return help.exec(['help'], (err) => { if (err) throw err @@ -131,7 +131,7 @@ test('npm help multiple args calls search', t => { helpSearchArgs = null }) - return help(['run', 'script'], (err) => { + return help.exec(['run', 'script'], (err) => { if (err) throw err @@ -147,7 +147,7 @@ test('npm help no matches calls search', t => { globResult = globDefaults }) - return help(['asdfasdf'], (err) => { + return help.exec(['asdfasdf'], (err) => { if (err) throw err @@ -164,7 +164,7 @@ test('npm help glob errors propagate', t => { spawnArgs = null }) - return help(['whoami'], (err) => { + return help.exec(['whoami'], (err) => { t.match(err, /glob failed/, 'glob error propagates') t.end() }) @@ -178,7 +178,7 @@ test('npm help whoami', t => { spawnArgs = null }) - return help(['whoami'], (err) => { + return help.exec(['whoami'], (err) => { if (err) throw err @@ -202,7 +202,7 @@ test('npm help 1 install', t => { spawnArgs = null }) - return help(['1', 'install'], (err) => { + return help.exec(['1', 'install'], (err) => { if (err) throw err @@ -225,7 +225,7 @@ test('npm help 5 install', t => { spawnArgs = null }) - return help(['5', 'install'], (err) => { + return help.exec(['5', 'install'], (err) => { if (err) throw err @@ -247,7 +247,7 @@ test('npm help 7 config', t => { spawnArgs = null }) - return help(['7', 'config'], (err) => { + return help.exec(['7', 'config'], (err) => { if (err) throw err @@ -270,7 +270,7 @@ test('npm help with browser viewer and invalid section throws', t => { spawnArgs = null }) - return help(['9', 'config'], (err) => { + return help.exec(['9', 'config'], (err) => { t.match(err, /invalid man section: 9/, 'throws appropriate error') t.end() }) @@ -284,7 +284,7 @@ test('npm help global redirects to folders', t => { spawnArgs = null }) - return help(['global'], (err) => { + return help.exec(['global'], (err) => { if (err) throw err @@ -302,7 +302,7 @@ test('npm help package.json redirects to package-json', t => { spawnArgs = null }) - return help(['package.json'], (err) => { + return help.exec(['package.json'], (err) => { if (err) throw err @@ -325,7 +325,7 @@ test('npm help ?(un)star', t => { spawnArgs = null }) - return help(['?(un)star'], (err) => { + return help.exec(['?(un)star'], (err) => { if (err) throw err @@ -350,7 +350,7 @@ test('npm help - woman viewer propagates errors', t => { spawnArgs = null }) - return help(['?(un)star'], (err) => { + return help.exec(['?(un)star'], (err) => { t.match(err, /help process exited with code: 1/, 'received the correct error') t.equal(spawnBin, 'emacsclient', 'maps woman to emacs correctly') t.strictSame(spawnArgs, ['-e', `(woman-find-file '/root/man/man1/npm-unstar.1')`], 'passes the correct arguments') @@ -370,7 +370,7 @@ test('npm help un*', t => { spawnArgs = null }) - return help(['un*'], (err) => { + return help.exec(['un*'], (err) => { if (err) throw err @@ -394,7 +394,7 @@ test('npm help - man viewer propagates errors', t => { spawnArgs = null }) - return help(['un*'], (err) => { + return help.exec(['un*'], (err) => { t.match(err, /help process exited with code: 1/, 'received correct error') t.equal(spawnBin, 'man', 'calls man by default') t.strictSame(spawnArgs, ['1', 'npm-unstar'], 'passes the correct arguments') diff --git a/test/lib/hook.js b/test/lib/hook.js index 3599042021f38..923f86e81ddf3 100644 --- a/test/lib/hook.js +++ b/test/lib/hook.js @@ -52,17 +52,17 @@ const libnpmhook = { } const output = [] -const hook = requireInject('../../lib/hook.js', { - '../../lib/npm.js': npm, +const Hook = requireInject('../../lib/hook.js', { '../../lib/utils/otplease.js': async (opts, fn) => fn(opts), '../../lib/utils/output.js': (msg) => { output.push(msg) }, libnpmhook, }) +const hook = new Hook(npm) test('npm hook no args', t => { - return hook([], (err) => { + return hook.exec([], (err) => { t.match(err, /npm hook add/, 'throws usage with no arguments') t.end() }) @@ -74,7 +74,7 @@ test('npm hook add', t => { output.length = 0 }) - return hook(['add', 'semver', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['add', 'semver', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -97,7 +97,7 @@ test('npm hook add - unicode output', t => { output.length = 0 }) - return hook(['add', 'semver', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['add', 'semver', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -120,7 +120,7 @@ test('npm hook add - json output', t => { output.length = 0 }) - return hook(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -148,7 +148,7 @@ test('npm hook add - parseable output', t => { output.length = 0 }) - return hook(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -176,7 +176,7 @@ test('npm hook add - silent output', t => { output.length = 0 }) - return hook(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['add', '@npmcli', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -197,7 +197,7 @@ test('npm hook ls', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -222,7 +222,7 @@ test('npm hook ls, no results', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -249,7 +249,7 @@ test('npm hook ls, single result', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -272,7 +272,7 @@ test('npm hook ls - json output', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -309,7 +309,7 @@ test('npm hook ls - parseable output', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -335,7 +335,7 @@ test('npm hook ls - silent output', t => { output.length = 0 }) - return hook(['ls'], (err) => { + return hook.exec(['ls'], (err) => { if (err) throw err @@ -354,7 +354,7 @@ test('npm hook rm', t => { output.length = 0 }) - return hook(['rm', '1'], (err) => { + return hook.exec(['rm', '1'], (err) => { if (err) throw err @@ -377,7 +377,7 @@ test('npm hook rm - unicode output', t => { output.length = 0 }) - return hook(['rm', '1'], (err) => { + return hook.exec(['rm', '1'], (err) => { if (err) throw err @@ -400,7 +400,7 @@ test('npm hook rm - silent output', t => { output.length = 0 }) - return hook(['rm', '1'], (err) => { + return hook.exec(['rm', '1'], (err) => { if (err) throw err @@ -421,7 +421,7 @@ test('npm hook rm - json output', t => { output.length = 0 }) - return hook(['rm', '1'], (err) => { + return hook.exec(['rm', '1'], (err) => { if (err) throw err @@ -447,7 +447,7 @@ test('npm hook rm - parseable output', t => { output.length = 0 }) - return hook(['rm', '1'], (err) => { + return hook.exec(['rm', '1'], (err) => { if (err) throw err @@ -469,7 +469,7 @@ test('npm hook update', t => { output.length = 0 }) - return hook(['update', '1', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['update', '1', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -494,7 +494,7 @@ test('npm hook update - unicode', t => { output.length = 0 }) - return hook(['update', '1', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['update', '1', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -519,7 +519,7 @@ test('npm hook update - json output', t => { output.length = 0 }) - return hook(['update', '1', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['update', '1', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -547,7 +547,7 @@ test('npm hook update - parseable output', t => { output.length = 0 }) - return hook(['update', '1', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['update', '1', 'https://google.com', 'some-secret'], (err) => { if (err) throw err @@ -573,7 +573,7 @@ test('npm hook update - silent output', t => { output.length = 0 }) - return hook(['update', '1', 'https://google.com', 'some-secret'], (err) => { + return hook.exec(['update', '1', 'https://google.com', 'some-secret'], (err) => { if (err) throw err diff --git a/test/lib/init.js b/test/lib/init.js index e73cc4b30988c..db5411ba76bf8 100644 --- a/test/lib/init.js +++ b/test/lib/init.js @@ -17,13 +17,13 @@ const npm = { } const mocks = { 'init-package-json': (dir, initFile, config, cb) => cb(null, 'data'), - '../../lib/npm.js': npm, '../../lib/utils/usage.js': () => 'usage instructions', '../../lib/utils/output.js': (...msg) => { result += msg.join('\n') }, } -const init = requireInject('../../lib/init.js', mocks) +const Init = requireInject('../../lib/init.js', mocks) +const init = new Init(npm) t.afterEach(cb => { result = '' @@ -40,7 +40,7 @@ t.test('classic npm init no args', t => { return '~/.npm-init.js' }, } - init([], err => { + init.exec([], err => { t.ifError(err, 'npm init no args') t.matchSnapshot(result, 'should print helper info') t.end() @@ -65,7 +65,7 @@ t.test('classic npm init -y', t => { t.equal(title, 'init', 'should print title') t.equal(msg, 'written successfully', 'should print done info') } - init([], err => { + init.exec([], err => { t.ifError(err, 'npm init -y') t.equal(result, '') }) @@ -87,7 +87,7 @@ t.test('npm init ', t => { ) cb() } - init(['react-app'], err => { + init.exec(['react-app'], err => { t.ifError(err, 'npm init react-app') }) }) @@ -102,7 +102,7 @@ t.test('npm init @scope/name', t => { ) cb() } - init(['@npmcli/something'], err => { + init.exec(['@npmcli/something'], err => { t.ifError(err, 'npm init init @scope/name') }) }) @@ -117,7 +117,7 @@ t.test('npm init git spec', t => { ) cb() } - init(['npm/something'], err => { + init.exec(['npm/something'], err => { t.ifError(err, 'npm init init @scope/name') }) }) @@ -132,13 +132,13 @@ t.test('npm init @scope', t => { ) cb() } - init(['@npmcli'], err => { + init.exec(['@npmcli'], err => { t.ifError(err, 'npm init init @scope/create') }) }) t.test('npm init tgz', t => { - init(['something.tgz'], err => { + init.exec(['something.tgz'], err => { t.match( err, /Error: Unrecognized initializer: something.tgz/, @@ -158,7 +158,7 @@ t.test('npm init @next', t => { ) cb() } - init(['something@next'], err => { + init.exec(['something@next'], err => { t.ifError(err, 'npm init init something@next') }) }) @@ -167,7 +167,7 @@ t.test('npm init exec error', t => { npm.commands.exec = (arr, cb) => { cb(new Error('ERROR')) } - init(['something@next'], err => { + init.exec(['something@next'], err => { t.match( err, /ERROR/, @@ -199,37 +199,39 @@ t.test('should not rewrite flatOptions', t => { ) cb() } - init(['react-app', 'my-app'], err => { + init.exec(['react-app', 'my-app'], err => { t.ifError(err, 'npm init react-app') }) }) t.test('npm init cancel', t => { t.plan(3) - const init = requireInject('../../lib/init.js', { + const Init = requireInject('../../lib/init.js', { ...mocks, 'init-package-json': (dir, initFile, config, cb) => cb( new Error('canceled') ), }) + const init = new Init(npm) npm.log = { ...npm.log } npm.log.warn = (title, msg) => { t.equal(title, 'init', 'should have init title') t.equal(msg, 'canceled', 'should log canceled') } - init([], err => { + init.exec([], err => { t.ifError(err, 'npm init cancel') }) }) t.test('npm init error', t => { - const init = requireInject('../../lib/init.js', { + const Init = requireInject('../../lib/init.js', { ...mocks, 'init-package-json': (dir, initFile, config, cb) => cb( new Error('Unknown Error') ), }) - init([], err => { + const init = new Init(npm) + init.exec([], err => { t.match(err, /Unknown Error/, 'should throw error') t.end() }) diff --git a/test/lib/install-ci-test.js b/test/lib/install-ci-test.js new file mode 100644 index 0000000000000..5f30efcabf259 --- /dev/null +++ b/test/lib/install-ci-test.js @@ -0,0 +1,57 @@ +const t = require('tap') + +const InstallCITest = require('../../lib/install-ci-test.js') + +let ciArgs = null +let ciCalled = false +let testArgs = null +let testCalled = false +let ciError = null + +const installCITest = new InstallCITest({ + commands: { + ci: (args, cb) => { + ciArgs = args + ciCalled = true + cb(ciError) + }, + test: (args, cb) => { + testArgs = args + testCalled = true + cb() + }, + }, +}) + +t.test('the install-ci-test command', t => { + t.afterEach(cb => { + ciArgs = null + ciCalled = false + testArgs = null + testCalled = false + ciError = null + cb() + }) + + t.test('ci and test', t => { + installCITest.exec(['extra'], () => { + t.equal(ciCalled, true) + t.equal(testCalled, true) + t.match(ciArgs, ['extra']) + t.match(testArgs, []) + t.end() + }) + }) + + t.test('ci fails', t => { + ciError = new Error('test fail') + installCITest.exec(['extra'], (err) => { + t.equal(ciCalled, true) + t.equal(testCalled, false) + t.match(ciArgs, ['extra']) + t.match(err, { message: 'test fail' }) + t.end() + }) + }) + t.end() +}) diff --git a/test/lib/install-test.js b/test/lib/install-test.js new file mode 100644 index 0000000000000..0c52bd5e3c012 --- /dev/null +++ b/test/lib/install-test.js @@ -0,0 +1,57 @@ +const t = require('tap') + +const InstallTest = require('../../lib/install-test.js') + +let installArgs = null +let installCalled = false +let testArgs = null +let testCalled = false +let installError = null + +const installTest = new InstallTest({ + commands: { + install: (args, cb) => { + installArgs = args + installCalled = true + cb(installError) + }, + test: (args, cb) => { + testArgs = args + testCalled = true + cb() + }, + }, +}) + +t.test('the install-test command', t => { + t.afterEach(cb => { + installArgs = null + installCalled = false + testArgs = null + testCalled = false + installError = null + cb() + }) + + t.test('install and test', t => { + installTest.exec(['extra'], () => { + t.equal(installCalled, true) + t.equal(testCalled, true) + t.match(installArgs, ['extra']) + t.match(testArgs, []) + t.end() + }) + }) + + t.test('install fails', t => { + installError = new Error('test fail') + installTest.exec(['extra'], (err) => { + t.equal(installCalled, true) + t.equal(testCalled, false) + t.match(installArgs, ['extra']) + t.match(err, { message: 'test fail' }) + t.end() + }) + }) + t.end() +}) diff --git a/test/lib/install.js b/test/lib/install.js index 859a4bdaaaafd..5277c40f23c4c 100644 --- a/test/lib/install.js +++ b/test/lib/install.js @@ -1,6 +1,6 @@ const { test } = require('tap') -const install = require('../../lib/install.js') +const Install = require('../../lib/install.js') const requireInject = require('require-inject') test('should install using Arborist', (t) => { @@ -9,17 +9,7 @@ test('should install using Arborist', (t) => { let REIFY_CALLED = false let ARB_OBJ = null - const install = requireInject('../../lib/install.js', { - '../../lib/npm.js': { - globalDir: 'path/to/node_modules/', - prefix: 'foo', - flatOptions: { - global: false, - }, - config: { - get: () => true, - }, - }, + const Install = requireInject('../../lib/install.js', { '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -38,9 +28,19 @@ test('should install using Arborist', (t) => { throw new Error('got wrong object passed to reify-finish') }, }) + const install = new Install({ + globalDir: 'path/to/node_modules/', + prefix: 'foo', + flatOptions: { + global: false, + }, + config: { + get: () => true, + }, + }) t.test('with args', t => { - install(['fizzbuzz'], er => { + install.exec(['fizzbuzz'], er => { if (er) throw er t.match(ARB_ARGS, { global: false, path: 'foo' }) @@ -51,7 +51,7 @@ test('should install using Arborist', (t) => { }) t.test('just a local npm install', t => { - install([], er => { + install.exec([], er => { if (er) throw er t.match(ARB_ARGS, { global: false, path: 'foo' }) @@ -75,19 +75,8 @@ test('should install using Arborist', (t) => { test('should ignore scripts with --ignore-scripts', (t) => { const SCRIPTS = [] let REIFY_CALLED = false - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, - '../../lib/npm.js': { - globalDir: 'path/to/node_modules/', - prefix: 'foo', - flatOptions: { - global: false, - ignoreScripts: true, - }, - config: { - get: () => false, - }, - }, '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -97,7 +86,18 @@ test('should ignore scripts with --ignore-scripts', (t) => { } }, }) - install([], er => { + const install = new Install({ + globalDir: 'path/to/node_modules/', + prefix: 'foo', + flatOptions: { + global: false, + ignoreScripts: true, + }, + config: { + get: () => false, + }, + }) + install.exec([], er => { if (er) throw er t.equal(REIFY_CALLED, true, 'called reify') @@ -107,23 +107,23 @@ test('should ignore scripts with --ignore-scripts', (t) => { }) test('should install globally using Arborist', (t) => { - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, - '../../lib/npm.js': { - globalDir: 'path/to/node_modules/', - prefix: 'foo', - flatOptions: { - global: true, - }, - config: { - get: () => false, - }, - }, '@npmcli/arborist': function () { this.reify = () => {} }, }) - install([], er => { + const install = new Install({ + globalDir: 'path/to/node_modules/', + prefix: 'foo', + flatOptions: { + global: true, + }, + config: { + get: () => false, + }, + }) + install.exec([], er => { if (er) throw er t.end() @@ -131,7 +131,7 @@ test('should install globally using Arborist', (t) => { }) test('completion to folder', async t => { - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, util: { promisify: (fn) => fn, @@ -145,6 +145,7 @@ test('completion to folder', async t => { }, }, }) + const install = new Install({}) const res = await install.completion({ partialWord: '/ar' }) const expect = process.platform === 'win32' ? '\\arborist' : '/arborist' t.strictSame(res, [expect], 'package dir match') @@ -152,7 +153,7 @@ test('completion to folder', async t => { }) test('completion to folder - invalid dir', async t => { - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, util: { promisify: (fn) => fn, @@ -163,13 +164,14 @@ test('completion to folder - invalid dir', async t => { }, }, }) + const install = new Install({}) const res = await install.completion({ partialWord: 'path/to/folder' }) t.strictSame(res, [], 'invalid dir: no matching') t.end() }) test('completion to folder - no matches', async t => { - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, util: { promisify: (fn) => fn, @@ -180,13 +182,14 @@ test('completion to folder - no matches', async t => { }, }, }) + const install = new Install({}) const res = await install.completion({ partialWord: '/pa' }) t.strictSame(res, [], 'no name match') t.end() }) test('completion to folder - match is not a package', async t => { - const install = requireInject('../../lib/install.js', { + const Install = requireInject('../../lib/install.js', { '../../lib/utils/reify-finish.js': async () => {}, util: { promisify: (fn) => fn, @@ -200,18 +203,21 @@ test('completion to folder - match is not a package', async t => { }, }, }) + const install = new Install({}) const res = await install.completion({ partialWord: '/ar' }) t.strictSame(res, [], 'no name match') t.end() }) test('completion to url', async t => { + const install = new Install({}) const res = await install.completion({ partialWord: 'http://path/to/url' }) t.strictSame(res, []) t.end() }) test('completion', async t => { + const install = new Install({}) const res = await install.completion({ partialWord: 'toto' }) t.notOk(res) t.end() diff --git a/test/lib/link.js b/test/lib/link.js index b1048427d78f3..be7af3f524019 100644 --- a/test/lib/link.js +++ b/test/lib/link.js @@ -40,11 +40,11 @@ const printLinks = async (opts) => { } const mocks = { - '../../lib/npm.js': npm, '../../lib/utils/reify-output.js': () => reifyOutput(), } -const link = requireInject('../../lib/link.js', mocks) +const Link = requireInject('../../lib/link.js', mocks) +const link = new Link(npm) t.test('link to globalDir when in current working dir of pkg and no args', (t) => { t.plan(2) @@ -83,7 +83,7 @@ t.test('link to globalDir when in current working dir of pkg and no args', (t) = t.matchSnapshot(links, 'should create a global link to current pkg') } - link([], (err) => { + link.exec([], (err) => { t.ifError(err, 'should not error out') }) }) @@ -185,7 +185,7 @@ t.test('link global linked pkg to local nm when using args', (t) => { // - @myscope/bar: prev installed scoped package available in globalDir // - a: prev installed package available in globalDir // - file:./link-me-too: pkg that needs to be reified in globalDir first - link([ + link.exec([ 'test-pkg-link', '@myscope/linked', '@myscope/bar', @@ -254,7 +254,7 @@ t.test('link pkg already in global space', (t) => { // - @myscope/bar: prev installed scoped package available in globalDir // - a: prev installed package available in globalDir // - file:./link-me-too: pkg that needs to be reified in globalDir first - link(['@myscope/linked'], (err) => { + link.exec(['@myscope/linked'], (err) => { t.ifError(err, 'should not error out') }) }) @@ -312,7 +312,7 @@ t.test('link pkg already in global space when prefix is a symlink', (t) => { t.matchSnapshot(links, 'should create a local symlink to global pkg') } - link(['@myscope/linked'], (err) => { + link.exec(['@myscope/linked'], (err) => { t.ifError(err, 'should not error out') }) }) @@ -341,21 +341,18 @@ t.test('completion', async t => { t.end() }) -t.test('--global option', async t => { +t.test('--global option', t => { const _config = npm.config npm.config = { get () { return true } } - try { - await link([]) - t.fail('should not get here') - } catch (err) { + link.exec([], (err) => { npm.config = _config t.match( err.message, /link should never be --global/, 'should throw an useful error' ) - } - t.end() + t.end() + }) }) diff --git a/test/lib/ll.js b/test/lib/ll.js index 7d4e2b94f2b7e..5d751a13844b2 100644 --- a/test/lib/ll.js +++ b/test/lib/ll.js @@ -1,29 +1,26 @@ const t = require('tap') -const requireInject = require('require-inject') const configs = {} let lsCalled = false -const ll = requireInject('../../lib/ll.js', { - '../../lib/npm.js': { - config: { - set: (k, v) => { - configs[k] = v - }, +const LL = require('../../lib/ll.js') +const ll = new LL({ + config: { + set: (k, v) => { + configs[k] = v }, - commands: { - ls: (args, cb) => { - lsCalled = true - cb() - }, + }, + commands: { + ls: (args, cb) => { + lsCalled = true + cb() }, }, }) const ls = require('../../lib/ls.js') -const { usage, completion } = ls -t.equal(ll.usage, usage) -t.equal(ll.completion.toString(), completion.toString()) +const { usage } = ls t.test('the ll command', t => { - ll([], () => { + t.equal(ll.usage, usage) + ll.exec([], () => { t.equal(lsCalled, true) t.strictSame(configs, { long: true }) t.end() diff --git a/test/lib/logout.js b/test/lib/logout.js index 96b1bcc7fe8c4..b00fa641d8c16 100644 --- a/test/lib/logout.js +++ b/test/lib/logout.js @@ -17,13 +17,13 @@ const npmFetch = (url, opts) => { const mocks = { npmlog, 'npm-registry-fetch': npmFetch, - '../../lib/npm.js': { - flatOptions: _flatOptions, - config, - }, } -const logout = requireInject('../../lib/logout.js', mocks) +const Logout = requireInject('../../lib/logout.js', mocks) +const logout = new Logout({ + flatOptions: _flatOptions, + config, +}) test('token logout', async (t) => { t.plan(6) @@ -52,7 +52,7 @@ test('token logout', async (t) => { } await new Promise((res, rej) => { - logout([], (err) => { + logout.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( @@ -121,7 +121,7 @@ test('token scoped logout', async (t) => { } await new Promise((res, rej) => { - logout([], (err) => { + logout.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( @@ -174,7 +174,7 @@ test('user/pass logout', async (t) => { config.save = () => null await new Promise((res, rej) => { - logout([], (err) => { + logout.exec([], (err) => { t.ifError(err, 'should not error out') delete _flatOptions.username @@ -189,7 +189,7 @@ test('user/pass logout', async (t) => { }) test('missing credentials', (t) => { - logout([], (err) => { + logout.exec([], (err) => { t.match( err.message, /not logged in to https:\/\/registry.npmjs.org\/, so can't log out!/, @@ -228,7 +228,7 @@ test('ignore invalid scoped registry config', async (t) => { config.save = () => null await new Promise((res, rej) => { - logout([], (err) => { + logout.exec([], (err) => { t.ifError(err, 'should not error out') t.deepEqual( diff --git a/test/lib/ls.js b/test/lib/ls.js index b1df9067c3d57..bd81776d5f3df 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -106,28 +106,28 @@ const _flatOptions = { }, production: false, } -const ls = requireInject('../../lib/ls.js', { - '../../lib/npm.js': { - flatOptions: _flatOptions, - limit: { - fetch: 3, - }, - get prefix () { - return _flatOptions.prefix - }, - get globalDir () { - return globalDir - }, - config: { - get (key) { - return _flatOptions[key] - }, - }, - }, +const LS = requireInject('../../lib/ls.js', { '../../lib/utils/output.js': msg => { result = msg }, }) +const ls = new LS({ + flatOptions: _flatOptions, + limit: { + fetch: 3, + }, + get prefix () { + return _flatOptions.prefix + }, + get globalDir () { + return globalDir + }, + config: { + get (key) { + return _flatOptions[key] + }, + }, +}) const redactCwd = res => res && res.replace(/\\+/g, '/').replace(new RegExp(__dirname.replace(/\\+/g, '/'), 'gi'), '{CWD}') @@ -155,7 +155,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree representation of dependencies structure') t.end() @@ -166,7 +166,7 @@ t.test('ls', (t) => { prefix = t.testdir({ ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.matchSnapshot( redactCwd(err.message), @@ -188,7 +188,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal(err.code, 'ELSPROBLEMS', 'should have error code') t.equal( redactCwd(err.message), @@ -213,7 +213,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls(['lorem'], (err) => { + ls.exec(['lorem'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree contaning only occurrences of filtered by package and colored output') _flatOptions.color = false @@ -235,7 +235,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls(['.'], (err) => { + ls.exec(['.'], (err) => { t.ifError(err, 'should not throw on missing dep above current level') t.matchSnapshot(redactCwd(result), 'should output tree contaning only occurrences of filtered by package and colored output') _flatOptions.all = true @@ -256,7 +256,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls(['bar'], (err) => { + ls.exec(['bar'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree contaning only occurrences of filtered package and its ancestors') t.end() @@ -284,7 +284,7 @@ t.test('ls', (t) => { }, }, }) - ls(['bar@*', 'lorem@1.0.0'], (err) => { + ls.exec(['bar@*', 'lorem@1.0.0'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree contaning only occurrences of multiple filtered packages and their ancestors') t.end() @@ -303,7 +303,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls(['notadep'], (err) => { + ls.exec(['notadep'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing no dependencies info') t.equal( @@ -330,7 +330,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing only top-level dependencies') _flatOptions.all = true @@ -353,7 +353,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing only top-level dependencies') _flatOptions.all = true @@ -414,7 +414,7 @@ t.test('ls', (t) => { }, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing top-level deps and their deps only') _flatOptions.all = true @@ -435,7 +435,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal(err.code, 'ELSPROBLEMS', 'should have error code') t.equal( redactCwd(err.message).replace(/\r\n/g, '\n'), @@ -462,7 +462,7 @@ t.test('ls', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal(err.code, 'ELSPROBLEMS', 'should have error code') t.matchSnapshot(redactCwd(result), 'should output tree containing color info') _flatOptions.color = false @@ -492,7 +492,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing dev deps') _flatOptions.dev = false t.end() @@ -521,7 +521,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing only development deps') _flatOptions.only = null t.end() @@ -560,7 +560,7 @@ t.test('ls', (t) => { ...diffDepTypesNmFixture.node_modules, }, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing linked deps') _flatOptions.link = false t.end() @@ -596,7 +596,7 @@ t.test('ls', (t) => { b: t.fixture('symlink', '../b'), }, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing linked deps') _flatOptions.link = false t.end() @@ -625,7 +625,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing production deps') _flatOptions.production = false t.end() @@ -654,7 +654,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing only prod deps') _flatOptions.only = null t.end() @@ -683,7 +683,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree info with descriptions') _flatOptions.long = true t.end() @@ -714,7 +714,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing top-level deps with descriptions') _flatOptions.all = true _flatOptions.depth = Infinity @@ -727,7 +727,7 @@ t.test('ls', (t) => { prefix = t.testdir({ 'package.json': '{broken json', }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'EJSONPARSE' }, 'should throw EJSONPARSE error') t.matchSnapshot(redactCwd(result), 'should print empty result') t.end() @@ -736,7 +736,7 @@ t.test('ls', (t) => { t.test('empty location', (t) => { prefix = t.testdir({}) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'should not error out on empty locations') t.matchSnapshot(redactCwd(result), 'should print empty result') t.end() @@ -764,7 +764,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree signaling mismatching peer dep in problems') t.end() }) @@ -799,7 +799,7 @@ t.test('ls', (t) => { }, }, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree signaling mismatching peer dep in problems') _flatOptions.color = false t.end() @@ -828,7 +828,7 @@ t.test('ls', (t) => { }, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.match(err.message, /missing: b@\^1.0.0/, 'should list missing dep problem') t.matchSnapshot(redactCwd(result), 'should output parseable signaling missing peer dep in problems') @@ -846,7 +846,7 @@ t.test('ls', (t) => { }, }), }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.match(err.message, 'missing: peer-dep@*, required by test-npm-ls@1.0.0', 'should have missing peer-dep error msg') t.matchSnapshot(redactCwd(result), 'should output tree signaling missing peer dep in problems') @@ -877,7 +877,7 @@ t.test('ls', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.match(err.message, /invalid: optional-dep@1.0.0/, 'should have invalid dep error msg') t.matchSnapshot(redactCwd(result), 'should output tree with empty entry for missing optional deps') @@ -916,7 +916,7 @@ t.test('ls', (t) => { }, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') t.end() @@ -954,7 +954,7 @@ t.test('ls', (t) => { }, }, }) - ls(['a'], (err) => { + ls.exec(['a'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') _flatOptions.color = false @@ -1002,7 +1002,7 @@ t.test('ls', (t) => { }, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') t.end() @@ -1051,7 +1051,7 @@ t.test('ls', (t) => { }, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') _flatOptions.all = true @@ -1101,7 +1101,7 @@ t.test('ls', (t) => { }, }, }) - ls(['@npmcli/b'], (err) => { + ls.exec(['@npmcli/b'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') _flatOptions.color = false @@ -1149,7 +1149,7 @@ t.test('ls', (t) => { }, }, }) - ls(['@npmcli/c'], (err) => { + ls.exec(['@npmcli/c'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should print tree output containing deduped ref') t.end() @@ -1193,7 +1193,7 @@ t.test('ls', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing aliases') t.end() }) @@ -1239,7 +1239,7 @@ t.test('ls', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing git refs') t.end() @@ -1283,7 +1283,7 @@ t.test('ls', (t) => { }, }), }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should NOT print git refs in output tree') t.end() @@ -1338,7 +1338,7 @@ t.test('ls', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should not be printed in tree output') t.end() }) @@ -1374,7 +1374,7 @@ t.test('ls', (t) => { // mimics lib/npm.js globalDir getter but pointing to fixtures globalDir = resolve(fixtures, 'node_modules') - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should print tree and not mark top-level items extraneous') globalDir = 'MISSING_GLOBAL_DIR' _flatOptions.global = false @@ -1427,7 +1427,7 @@ t.test('ls', (t) => { }, }) - ls(['c'], (err) => { + ls.exec(['c'], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.matchSnapshot(redactCwd(result), 'should print tree and not duplicate child of missing items') t.end() @@ -1471,12 +1471,12 @@ t.test('ls', (t) => { }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'should NOT have ELSPROBLEMS error code') t.matchSnapshot(redactCwd(result), 'should list workspaces properly') // should also be able to filter out one of the workspaces - ls(['a'], (err) => { + ls.exec(['a'], (err) => { t.ifError(err, 'should NOT have ELSPROBLEMS error code when filter') t.matchSnapshot(redactCwd(result), 'should filter single workspace') @@ -1534,17 +1534,17 @@ t.test('ls', (t) => { }) t.plan(6) - ls(['a'], (err) => { + ls.exec(['a'], (err) => { t.ifError(err, 'should NOT have ELSPROBLEMS error code') t.matchSnapshot(redactCwd(result), 'should list a in top-level only') - ls(['d'], (err) => { + ls.exec(['d'], (err) => { t.ifError(err, 'should NOT have ELSPROBLEMS error code when filter') t.matchSnapshot(redactCwd(result), 'should print empty results msg') // if no --depth config is defined, should print path to dep _flatOptions.depth = null // default config value - ls(['d'], (err) => { + ls.exec(['d'], (err) => { t.ifError(err, 'should NOT have ELSPROBLEMS error code when filter') t.matchSnapshot(redactCwd(result), 'should print expected result') }) @@ -1576,7 +1576,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable representation of dependencies structure') t.end() @@ -1587,7 +1587,7 @@ t.test('ls --parseable', (t) => { prefix = t.testdir({ ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.matchSnapshot( redactCwd(err.message), @@ -1609,7 +1609,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal(err.code, 'ELSPROBLEMS', 'should have error code') t.matchSnapshot(redactCwd(result), 'should output containing problems info') t.end() @@ -1628,7 +1628,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls(['lorem'], (err) => { + ls.exec(['lorem'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable contaning only occurrences of filtered by package') t.end() @@ -1647,7 +1647,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls(['bar'], (err) => { + ls.exec(['bar'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable contaning only occurrences of filtered package') t.end() @@ -1675,7 +1675,7 @@ t.test('ls --parseable', (t) => { }, }, }) - ls(['bar@*', 'lorem@1.0.0'], (err) => { + ls.exec(['bar@*', 'lorem@1.0.0'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable contaning only occurrences of multiple filtered packages and their ancestors') t.end() @@ -1694,7 +1694,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls(['notadep'], (err) => { + ls.exec(['notadep'], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable output containing no dependencies info') t.equal( @@ -1721,7 +1721,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable output containing only top-level dependencies') _flatOptions.all = true @@ -1744,7 +1744,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output tree containing only top-level dependencies') _flatOptions.all = true @@ -1767,7 +1767,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable containing top-level deps and their deps only') _flatOptions.all = true @@ -1788,7 +1788,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') t.matchSnapshot(redactCwd(result), 'should output parseable containing top-level deps and their deps only') t.end() @@ -1817,7 +1817,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing dev deps') _flatOptions.dev = false t.end() @@ -1846,7 +1846,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing only development deps') _flatOptions.only = null t.end() @@ -1885,7 +1885,7 @@ t.test('ls --parseable', (t) => { ...diffDepTypesNmFixture.node_modules, }, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing linked deps') _flatOptions.link = false t.end() @@ -1914,7 +1914,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing production deps') _flatOptions.production = false t.end() @@ -1943,7 +1943,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing only prod deps') _flatOptions.only = null t.end() @@ -1972,7 +1972,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree info with descriptions') _flatOptions.long = true t.end() @@ -1990,7 +1990,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal(err.code, 'ELSPROBLEMS', 'should have error code') t.match(redactCwd(err.message), 'extraneous: lorem@1.0.0 {CWD}/ls-ls-parseable--long-with-extraneous-deps/node_modules/lorem', 'should have error code') t.matchSnapshot(redactCwd(result), 'should output long parseable output with extraneous info') @@ -2011,7 +2011,7 @@ t.test('ls --parseable', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') t.matchSnapshot(redactCwd(result), 'should output parseable result containing EXTRANEOUS/INVALID labels') _flatOptions.long = false @@ -2051,7 +2051,7 @@ t.test('ls --parseable', (t) => { ...diffDepTypesNmFixture.node_modules, }, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.matchSnapshot(redactCwd(result), 'should output parseable results with symlink targets') _flatOptions.long = false @@ -2083,7 +2083,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing top-level deps with descriptions') _flatOptions.all = true _flatOptions.depth = Infinity @@ -2096,7 +2096,7 @@ t.test('ls --parseable', (t) => { prefix = t.testdir({ 'package.json': '{broken json', }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'EJSONPARSE' }, 'should throw EJSONPARSE error') t.matchSnapshot(redactCwd(result), 'should print empty result') t.end() @@ -2105,7 +2105,7 @@ t.test('ls --parseable', (t) => { t.test('empty location', (t) => { prefix = t.testdir({}) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'should not error out on empty locations') t.matchSnapshot(redactCwd(result), 'should print empty result') t.end() @@ -2133,7 +2133,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output parseable signaling missing peer dep in problems') t.end() }) @@ -2161,7 +2161,7 @@ t.test('ls --parseable', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.match(err.message, /invalid: optional-dep@1.0.0/, 'should have invalid dep error msg') t.matchSnapshot(redactCwd(result), 'should output parseable with empty entry for missing optional deps') @@ -2199,7 +2199,7 @@ t.test('ls --parseable', (t) => { }, }, }) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should print tree output omitting deduped ref') t.end() }) @@ -2238,7 +2238,7 @@ t.test('ls --parseable', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing aliases') t.end() }) @@ -2283,7 +2283,7 @@ t.test('ls --parseable', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should output tree containing git refs') t.end() }) @@ -2337,7 +2337,7 @@ t.test('ls --parseable', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should not be printed in tree output') t.end() }) @@ -2373,7 +2373,7 @@ t.test('ls --parseable', (t) => { // mimics lib/npm.js globalDir getter but pointing to fixtures globalDir = resolve(fixtures, 'node_modules') - ls([], () => { + ls.exec([], () => { t.matchSnapshot(redactCwd(result), 'should print parseable output for global deps') globalDir = 'MISSING_GLOBAL_DIR' _flatOptions.global = false @@ -2400,7 +2400,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2431,7 +2431,7 @@ t.test('ls --json', (t) => { prefix = t.testdir({ ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') t.deepEqual( jsonParse(result), @@ -2487,7 +2487,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.equal( redactCwd(err.message), 'extraneous: lorem@1.0.0 {CWD}/ls-ls-json-extraneous-deps/node_modules/lorem', @@ -2542,7 +2542,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls(['lorem'], (err) => { + ls.exec(['lorem'], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2578,7 +2578,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls(['bar'], (err) => { + ls.exec(['bar'], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2623,7 +2623,7 @@ t.test('ls --json', (t) => { }, }, }) - ls(['bar@*', 'lorem@1.0.0'], (err) => { + ls.exec(['bar@*', 'lorem@1.0.0'], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2662,7 +2662,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls(['notadep'], (err) => { + ls.exec(['notadep'], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2696,7 +2696,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2734,7 +2734,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2772,7 +2772,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'npm ls') t.deepEqual( jsonParse(result), @@ -2813,7 +2813,7 @@ t.test('ls --json', (t) => { }), ...simpleNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') t.deepEqual( jsonParse(result), @@ -2882,7 +2882,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -2929,7 +2929,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -2986,7 +2986,7 @@ t.test('ls --json', (t) => { ...diffDepTypesNmFixture.node_modules, }, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3028,7 +3028,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3069,7 +3069,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3182,7 +3182,7 @@ t.test('ls --json', (t) => { }, }), }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3239,7 +3239,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3377,7 +3377,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3460,7 +3460,7 @@ t.test('ls --json', (t) => { prefix = t.testdir({ 'package.json': '{broken json', }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.message, 'Failed to parse root package.json', 'should have missin root package.json msg') t.match(err.code, 'EJSONPARSE', 'should have EJSONPARSE error code') t.deepEqual( @@ -3479,7 +3479,7 @@ t.test('ls --json', (t) => { t.test('empty location', (t) => { prefix = t.testdir({}) - ls([], (err) => { + ls.exec([], (err) => { t.ifError(err, 'should not error out on empty locations') t.deepEqual( jsonParse(result), @@ -3511,7 +3511,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'Should have ELSPROBLEMS error code') t.deepEqual( jsonParse(result), @@ -3571,7 +3571,7 @@ t.test('ls --json', (t) => { }), ...diffDepTypesNmFixture, }) - ls([], (err) => { + ls.exec([], (err) => { t.match(err.code, 'ELSPROBLEMS', 'should have ELSPROBLEMS error code') t.match(err.message, /invalid: optional-dep@1.0.0/, 'should have invalid dep error msg') t.deepEqual( @@ -3643,7 +3643,7 @@ t.test('ls --json', (t) => { }, }, }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3701,7 +3701,7 @@ t.test('ls --json', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3761,7 +3761,7 @@ t.test('ls --json', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3845,7 +3845,7 @@ t.test('ls --json', (t) => { }, }) touchHiddenPackageLock(prefix) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3870,7 +3870,7 @@ t.test('ls --json', (t) => { version: '1.0.0', }), }) - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { @@ -3913,7 +3913,7 @@ t.test('ls --json', (t) => { // mimics lib/npm.js globalDir getter but pointing to fixtures globalDir = resolve(fixtures, 'node_modules') - ls([], () => { + ls.exec([], () => { t.deepEqual( jsonParse(result), { diff --git a/test/lib/org.js b/test/lib/org.js index 1e8aabc1d7edd..df8ff60dd26da 100644 --- a/test/lib/org.js +++ b/test/lib/org.js @@ -39,14 +39,14 @@ const libnpmorg = { }, } -const org = requireInject('../../lib/org.js', { - '../../lib/npm.js': npm, +const Org = requireInject('../../lib/org.js', { '../../lib/utils/otplease.js': async (opts, fn) => fn(opts), '../../lib/utils/output.js': (msg) => { output.push(msg) }, libnpmorg, }) +const org = new Org(npm) test('completion', async t => { const completion = (argv) => @@ -67,7 +67,8 @@ test('completion', async t => { }) test('npm org - invalid subcommand', t => { - return org(['foo'], (err) => { + org.exec(['foo'], (err) => { + console.log(err) t.match(err, /npm org set/, 'prints usage information') t.end() }) @@ -79,7 +80,7 @@ test('npm org add', t => { output.length = 0 }) - return org(['add', 'orgname', 'username'], (err) => { + org.exec(['add', 'orgname', 'username'], (err) => { if (err) throw err @@ -100,7 +101,7 @@ test('npm org add - no org', t => { output.length = 0 }) - return org(['add', '', 'username'], (err) => { + org.exec(['add', '', 'username'], (err) => { t.match(err, /`orgname` is required/, 'returns the correct error') t.end() }) @@ -112,7 +113,7 @@ test('npm org add - no user', t => { output.length = 0 }) - return org(['add', 'orgname', ''], (err) => { + org.exec(['add', 'orgname', ''], (err) => { t.match(err, /`username` is required/, 'returns the correct error') t.end() }) @@ -124,7 +125,7 @@ test('npm org add - invalid role', t => { output.length = 0 }) - return org(['add', 'orgname', 'username', 'person'], (err) => { + org.exec(['add', 'orgname', 'username', 'person'], (err) => { t.match(err, /`role` must be one of/, 'returns the correct error') t.end() }) @@ -138,7 +139,7 @@ test('npm org add - more users', t => { output.length = 0 }) - return org(['add', 'orgname', 'username'], (err) => { + org.exec(['add', 'orgname', 'username'], (err) => { if (err) throw err @@ -161,7 +162,7 @@ test('npm org add - json output', t => { output.length = 0 }) - return org(['add', 'orgname', 'username'], (err) => { + org.exec(['add', 'orgname', 'username'], (err) => { if (err) throw err @@ -191,7 +192,7 @@ test('npm org add - parseable output', t => { output.length = 0 }) - return org(['add', 'orgname', 'username'], (err) => { + org.exec(['add', 'orgname', 'username'], (err) => { if (err) throw err @@ -217,7 +218,7 @@ test('npm org add - silent output', t => { output.length = 0 }) - return org(['add', 'orgname', 'username'], (err) => { + org.exec(['add', 'orgname', 'username'], (err) => { if (err) throw err @@ -239,7 +240,7 @@ test('npm org rm', t => { output.length = 0 }) - return org(['rm', 'orgname', 'username'], (err) => { + org.exec(['rm', 'orgname', 'username'], (err) => { if (err) throw err @@ -264,7 +265,7 @@ test('npm org rm - no org', t => { output.length = 0 }) - return org(['rm', '', 'username'], (err) => { + org.exec(['rm', '', 'username'], (err) => { t.match(err, /`orgname` is required/, 'threw the correct error') t.end() }) @@ -277,7 +278,7 @@ test('npm org rm - no user', t => { output.length = 0 }) - return org(['rm', 'orgname'], (err) => { + org.exec(['rm', 'orgname'], (err) => { t.match(err, /`username` is required/, 'threw the correct error') t.end() }) @@ -295,7 +296,7 @@ test('npm org rm - one user left', t => { output.length = 0 }) - return org(['rm', 'orgname', 'username'], (err) => { + org.exec(['rm', 'orgname', 'username'], (err) => { if (err) throw err @@ -322,7 +323,7 @@ test('npm org rm - json output', t => { output.length = 0 }) - return org(['rm', 'orgname', 'username'], (err) => { + org.exec(['rm', 'orgname', 'username'], (err) => { if (err) throw err @@ -354,7 +355,7 @@ test('npm org rm - parseable output', t => { output.length = 0 }) - return org(['rm', 'orgname', 'username'], (err) => { + org.exec(['rm', 'orgname', 'username'], (err) => { if (err) throw err @@ -384,7 +385,7 @@ test('npm org rm - silent output', t => { output.length = 0 }) - return org(['rm', 'orgname', 'username'], (err) => { + org.exec(['rm', 'orgname', 'username'], (err) => { if (err) throw err @@ -414,7 +415,7 @@ test('npm org ls', t => { output.length = 0 }) - return org(['ls', 'orgname'], (err) => { + org.exec(['ls', 'orgname'], (err) => { if (err) throw err @@ -441,7 +442,7 @@ test('npm org ls - user filter', t => { output.length = 0 }) - return org(['ls', 'orgname', 'username'], (err) => { + org.exec(['ls', 'orgname', 'username'], (err) => { if (err) throw err @@ -466,7 +467,7 @@ test('npm org ls - user filter, missing user', t => { output.length = 0 }) - return org(['ls', 'orgname', 'username'], (err) => { + org.exec(['ls', 'orgname', 'username'], (err) => { if (err) throw err @@ -487,7 +488,7 @@ test('npm org ls - no org', t => { output.length = 0 }) - return org(['ls'], (err) => { + org.exec(['ls'], (err) => { t.match(err, /`orgname` is required/, 'throws the correct error') t.end() }) @@ -507,7 +508,7 @@ test('npm org ls - json output', t => { output.length = 0 }) - return org(['ls', 'orgname'], (err) => { + org.exec(['ls', 'orgname'], (err) => { if (err) throw err @@ -534,7 +535,7 @@ test('npm org ls - parseable output', t => { output.length = 0 }) - return org(['ls', 'orgname'], (err) => { + org.exec(['ls', 'orgname'], (err) => { if (err) throw err @@ -566,7 +567,7 @@ test('npm org ls - silent output', t => { output.length = 0 }) - return org(['ls', 'orgname'], (err) => { + org.exec(['ls', 'orgname'], (err) => { if (err) throw err diff --git a/test/lib/outdated.js b/test/lib/outdated.js index da53b6031d6f9..aa8a1bcb6b3a5 100644 --- a/test/lib/outdated.js +++ b/test/lib/outdated.js @@ -92,19 +92,18 @@ const globalDir = t.testdir({ }, }) -const outdated = (dir, opts) => requireInject( - '../../lib/outdated.js', - { - '../../lib/npm.js': { - prefix: dir, - globalDir: `${globalDir}/node_modules`, - flatOptions: opts, - }, +const outdated = (dir, opts) => { + const Outdated = requireInject('../../lib/outdated.js', { pacote: { packument, }, - } -) + }) + return new Outdated({ + prefix: dir, + globalDir: `${globalDir}/node_modules`, + flatOptions: opts, + }) +} t.beforeEach(cleanLogs) @@ -180,7 +179,7 @@ t.test('should display outdated deps', t => { t.test('outdated global', t => { outdated(null, { global: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -190,7 +189,7 @@ t.test('should display outdated deps', t => { outdated(testDir, { global: false, color: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -201,7 +200,7 @@ t.test('should display outdated deps', t => { global: false, color: true, omit: ['dev'], - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -212,7 +211,7 @@ t.test('should display outdated deps', t => { global: false, color: true, omit: ['dev', 'peer'], - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -223,7 +222,7 @@ t.test('should display outdated deps', t => { global: false, color: true, omit: ['prod'], - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -233,7 +232,7 @@ t.test('should display outdated deps', t => { outdated(testDir, { global: false, long: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -243,7 +242,7 @@ t.test('should display outdated deps', t => { outdated(testDir, { global: false, json: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -254,7 +253,7 @@ t.test('should display outdated deps', t => { global: false, json: true, long: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -264,7 +263,7 @@ t.test('should display outdated deps', t => { outdated(testDir, { global: false, parseable: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -275,7 +274,7 @@ t.test('should display outdated deps', t => { global: false, parseable: true, long: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -284,7 +283,7 @@ t.test('should display outdated deps', t => { t.test('outdated --all', t => { outdated(testDir, { all: true, - })([], () => { + }).exec([], () => { t.matchSnapshot(logs) t.end() }) @@ -293,7 +292,7 @@ t.test('should display outdated deps', t => { t.test('outdated specific dep', t => { outdated(testDir, { global: false, - })(['alpha'], () => { + }).exec(['alpha'], () => { t.matchSnapshot(logs) t.end() }) @@ -323,7 +322,7 @@ t.test('should return if no outdated deps', t => { outdated(testDir, { global: false, - })([], () => { + }).exec([], () => { t.equals(logs.length, 0, 'no logs') t.end() }) @@ -350,7 +349,7 @@ t.test('throws if error with a dep', t => { outdated(testDir, { global: false, - })([], (err) => { + }).exec([], (err) => { t.equals(err.message, 'There is an error with this package.') t.end() }) @@ -370,7 +369,7 @@ t.test('should skip missing non-prod deps', t => { outdated(testDir, { global: false, - })([], () => { + }).exec([], () => { t.equals(logs.length, 0, 'no logs') t.end() }) @@ -395,7 +394,7 @@ t.test('should skip invalid pkg ranges', t => { }, }) - outdated(testDir, {})([], () => { + outdated(testDir, {}).exec([], () => { t.equals(logs.length, 0, 'no logs') t.end() }) @@ -420,7 +419,7 @@ t.test('should skip git specs', t => { }, }) - outdated(testDir, {})([], () => { + outdated(testDir, {}).exec([], () => { t.equals(logs.length, 0, 'no logs') t.end() }) diff --git a/test/lib/owner.js b/test/lib/owner.js index aa5e3ee63798e..4f8f430886b7e 100644 --- a/test/lib/owner.js +++ b/test/lib/owner.js @@ -15,7 +15,6 @@ const mocks = { npmlog, 'npm-registry-fetch': npmFetch, pacote, - '../../lib/npm.js': npm, '../../lib/utils/output.js': (...msg) => { result += msg.join('\n') }, @@ -31,7 +30,8 @@ const npmcliMaintainers = [ { email: 'i@izs.me', name: 'isaacs' }, ] -const owner = requireInject('../../lib/owner.js', mocks) +const Owner = requireInject('../../lib/owner.js', mocks) +const owner = new Owner(npm) t.test('owner no args', t => { result = '' @@ -39,7 +39,7 @@ t.test('owner no args', t => { result = '' }) - owner([], err => { + owner.exec([], err => { t.equal( err.message, 'usage instructions', @@ -73,7 +73,7 @@ t.test('owner ls no args', t => { readLocalPkgResponse = null }) - owner(['ls'], err => { + owner.exec(['ls'], err => { t.ifError(err, 'npm owner ls no args') t.matchSnapshot(result, 'should output owners of cwd package') }) @@ -86,7 +86,7 @@ t.test('owner ls no args no cwd package', t => { npmlog.error = noop }) - owner(['ls'], err => { + owner.exec(['ls'], err => { t.equal( err.message, 'usage instructions', @@ -115,7 +115,7 @@ t.test('owner ls fails to retrieve packument', t => { pacote.packument = noop }) - owner(['ls'], err => { + owner.exec(['ls'], err => { t.match( err, /ERR/, @@ -145,7 +145,7 @@ t.test('owner ls ', t => { pacote.packument = noop }) - owner(['ls', '@npmcli/map-workspaces'], err => { + owner.exec(['ls', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner ls ') t.matchSnapshot(result, 'should output owners of ') }) @@ -161,7 +161,7 @@ t.test('owner ls no maintainers', t => { pacote.packument = noop }) - owner(['ls', '@npmcli/map-workspaces'], err => { + owner.exec(['ls', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner ls no maintainers') t.equal(result, 'no admin found', 'should output no admint found msg') t.end() @@ -232,7 +232,7 @@ t.test('owner add ', t => { pacote.packument = noop }) - owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + owner.exec(['add', 'foo', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner add ') t.equal(result, '+ foo (@npmcli/map-workspaces)', 'should output add result') }) @@ -265,7 +265,7 @@ t.test('owner add cwd package', t => { pacote.packument = noop }) - owner(['add', 'foo'], err => { + owner.exec(['add', 'foo'], err => { t.ifError(err, 'npm owner add cwd package') t.equal(result, '+ foo (@npmcli/map-workspaces)', 'should output add result') t.end() @@ -308,7 +308,7 @@ t.test('owner add already an owner', t => { pacote.packument = noop }) - owner(['add', 'ruyadorno', '@npmcli/map-workspaces'], err => { + owner.exec(['add', 'ruyadorno', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner add already an owner') }) }) @@ -336,7 +336,7 @@ t.test('owner add fails to retrieve user', t => { pacote.packument = noop }) - owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + owner.exec(['add', 'foo', '@npmcli/map-workspaces'], err => { t.match( err, /Error: Couldn't get user data for foo: {"ok":false}/, @@ -377,7 +377,7 @@ t.test('owner add fails to PUT updates', t => { pacote.packument = noop }) - owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + owner.exec(['add', 'foo', '@npmcli/map-workspaces'], err => { t.match( err.message, /Failed to update package/, @@ -417,7 +417,7 @@ t.test('owner add fails to retrieve user info', t => { pacote.packument = noop }) - owner(['add', 'foo', '@npmcli/map-workspaces'], err => { + owner.exec(['add', 'foo', '@npmcli/map-workspaces'], err => { t.match( err.message, "I'm a teapot", @@ -453,7 +453,7 @@ t.test('owner add no previous maintainers property from server', t pacote.packument = noop }) - owner(['add', 'foo', '@npmcli/no-owners-pkg'], err => { + owner.exec(['add', 'foo', '@npmcli/no-owners-pkg'], err => { t.ifError(err, 'npm owner add ') t.equal(result, '+ foo (@npmcli/no-owners-pkg)', 'should output add result') t.end() @@ -466,7 +466,7 @@ t.test('owner add no user', t => { result = '' }) - owner(['add'], err => { + owner.exec(['add'], err => { t.equal( err.message, 'usage instructions', @@ -482,7 +482,7 @@ t.test('owner add no cwd package', t => { result = '' }) - owner(['add', 'foo'], err => { + owner.exec(['add', 'foo'], err => { t.equal( err.message, 'usage instructions', @@ -549,7 +549,7 @@ t.test('owner rm ', t => { pacote.packument = noop }) - owner(['rm', 'ruyadorno', '@npmcli/map-workspaces'], err => { + owner.exec(['rm', 'ruyadorno', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner rm ') t.equal(result, '- ruyadorno (@npmcli/map-workspaces)', 'should output rm result') }) @@ -589,7 +589,7 @@ t.test('owner rm not a current owner', t => { pacote.packument = noop }) - owner(['rm', 'foo', '@npmcli/map-workspaces'], err => { + owner.exec(['rm', 'foo', '@npmcli/map-workspaces'], err => { t.ifError(err, 'npm owner rm not a current owner') }) }) @@ -621,7 +621,7 @@ t.test('owner rm cwd package', t => { pacote.packument = noop }) - owner(['rm', 'ruyadorno'], err => { + owner.exec(['rm', 'ruyadorno'], err => { t.ifError(err, 'npm owner rm cwd package') t.equal(result, '- ruyadorno (@npmcli/map-workspaces)', 'should output rm result') t.end() @@ -656,7 +656,7 @@ t.test('owner rm only user', t => { pacote.packument = noop }) - owner(['rm', 'ruyadorno'], err => { + owner.exec(['rm', 'ruyadorno'], err => { t.equal( err.message, 'Cannot remove all owners of a package. Add someone else first.', @@ -673,7 +673,7 @@ t.test('owner rm no user', t => { result = '' }) - owner(['rm'], err => { + owner.exec(['rm'], err => { t.equal( err.message, 'usage instructions', @@ -689,7 +689,7 @@ t.test('owner rm no cwd package', t => { result = '' }) - owner(['rm', 'foo'], err => { + owner.exec(['rm', 'foo'], err => { t.equal( err.message, 'usage instructions', @@ -700,10 +700,8 @@ t.test('owner rm no cwd package', t => { }) t.test('completion', async t => { - const { completion } = owner - const testComp = async (argv, expect) => { - const res = await completion({ conf: { argv: { remain: argv } } }) + const res = await owner.completion({ conf: { argv: { remain: argv } } }) t.strictSame(res, expect, argv.join(' ')) } @@ -730,7 +728,7 @@ t.test('completion', async t => { pacote.packument = noop }) - const res = await completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) + const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, ['nlf', 'ruyadorno', 'darcyclarke', 'isaacs'], 'should return list of current owners' @@ -738,7 +736,7 @@ t.test('completion', async t => { }) t.test('completion npm owner rm no cwd package', async t => { - const res = await completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) + 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.end() }) @@ -757,7 +755,7 @@ t.test('completion', async t => { pacote.packument = noop }) - const res = await completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) + const res = await owner.completion({ conf: { argv: { remain: ['npm', 'owner', 'rm'] } } }) t.strictSame(res, [], 'should return no owners if not found') }) diff --git a/test/lib/pack.js b/test/lib/pack.js index 851174d259cb3..73a19baa3ef73 100644 --- a/test/lib/pack.js +++ b/test/lib/pack.js @@ -17,15 +17,8 @@ t.afterEach(cb => { }) t.test('should pack current directory with no arguments', (t) => { - const pack = requireInject('../../lib/pack.js', { + const Pack = requireInject('../../lib/pack.js', { '../../lib/utils/output.js': output, - '../../lib/npm.js': { - flatOptions: { - unicode: false, - json: false, - dryRun: false, - }, - }, libnpmpack, npmlog: { notice: () => {}, @@ -33,13 +26,21 @@ t.test('should pack current directory with no arguments', (t) => { clearProgress: () => {}, }, }) + const pack = new Pack({ + flatOptions: { + unicode: false, + json: false, + dryRun: false, + }, + }) - return pack([], er => { + pack.exec([], er => { if (er) throw er const filename = `npm-${require('../../package.json').version}.tgz` t.strictSame(OUTPUT, [[filename]]) + t.end() }) }) @@ -51,15 +52,8 @@ t.test('should pack given directory', (t) => { }, null, 2), }) - const pack = requireInject('../../lib/pack.js', { + const Pack = requireInject('../../lib/pack.js', { '../../lib/utils/output.js': output, - '../../lib/npm.js': { - flatOptions: { - unicode: true, - json: true, - dryRun: true, - }, - }, libnpmpack, npmlog: { notice: () => {}, @@ -67,13 +61,21 @@ t.test('should pack given directory', (t) => { clearProgress: () => {}, }, }) + const pack = new Pack({ + flatOptions: { + unicode: true, + json: true, + dryRun: true, + }, + }) - return pack([testDir], er => { + pack.exec([testDir], er => { if (er) throw er const filename = 'my-cool-pkg-1.0.0.tgz' t.strictSame(OUTPUT, [[filename]]) + t.end() }) }) @@ -85,15 +87,8 @@ t.test('should pack given directory for scoped package', (t) => { }, null, 2), }) - const pack = requireInject('../../lib/pack.js', { + const Pack = requireInject('../../lib/pack.js', { '../../lib/utils/output.js': output, - '../../lib/npm.js': { - flatOptions: { - unicode: true, - json: true, - dryRun: true, - }, - }, libnpmpack, npmlog: { notice: () => {}, @@ -101,18 +96,26 @@ t.test('should pack given directory for scoped package', (t) => { clearProgress: () => {}, }, }) + const pack = new Pack({ + flatOptions: { + unicode: true, + json: true, + dryRun: true, + }, + }) - return pack([testDir], er => { + return pack.exec([testDir], er => { if (er) throw er const filename = 'cool-my-pkg-1.0.0.tgz' t.strictSame(OUTPUT, [[filename]]) + t.end() }) }) t.test('should log pack contents', (t) => { - const pack = requireInject('../../lib/pack.js', { + const Pack = requireInject('../../lib/pack.js', { '../../lib/utils/output.js': output, '../../lib/utils/tar.js': { ...require('../../lib/utils/tar.js'), @@ -120,13 +123,6 @@ t.test('should log pack contents', (t) => { t.ok(true, 'logTar is called') }, }, - '../../lib/npm.js': { - flatOptions: { - unicode: false, - json: false, - dryRun: false, - }, - }, libnpmpack, npmlog: { notice: () => {}, @@ -134,12 +130,20 @@ t.test('should log pack contents', (t) => { clearProgress: () => {}, }, }) + const pack = new Pack({ + flatOptions: { + unicode: false, + json: false, + dryRun: false, + }, + }) - return pack([], er => { + pack.exec([], er => { if (er) throw er const filename = `npm-${require('../../package.json').version}.tgz` t.strictSame(OUTPUT, [[filename]]) + t.end() }) }) diff --git a/test/lib/ping.js b/test/lib/ping.js index a185919dddc33..cf47530749b33 100644 --- a/test/lib/ping.js +++ b/test/lib/ping.js @@ -6,8 +6,7 @@ test('pings', (t) => { const flatOptions = { registry: 'https://registry.npmjs.org' } let noticeCalls = 0 - const ping = requireInject('../../lib/ping.js', { - '../../lib/npm.js': { flatOptions }, + const Ping = requireInject('../../lib/ping.js', { '../../lib/utils/ping.js': function (spec) { t.equal(spec, flatOptions, 'passes flatOptions') return {} @@ -25,8 +24,9 @@ test('pings', (t) => { }, }, }) + const ping = new Ping({ flatOptions }) - ping([], (err) => { + ping.exec([], (err) => { t.equal(noticeCalls, 2, 'should have logged 2 lines') t.ifError(err, 'npm ping') t.ok('should be able to ping') @@ -39,8 +39,7 @@ test('pings and logs details', (t) => { const flatOptions = { registry: 'https://registry.npmjs.org' } const details = { extra: 'data' } let noticeCalls = 0 - const ping = requireInject('../../lib/ping.js', { - '../../lib/npm.js': { flatOptions }, + const Ping = requireInject('../../lib/ping.js', { '../../lib/utils/ping.js': function (spec) { t.equal(spec, flatOptions, 'passes flatOptions') return details @@ -62,8 +61,9 @@ test('pings and logs details', (t) => { }, }, }) + const ping = new Ping({ flatOptions }) - ping([], (err) => { + ping.exec([], (err) => { t.equal(noticeCalls, 3, 'should have logged 3 lines') t.ifError(err, 'npm ping') t.ok('should be able to ping') @@ -76,8 +76,7 @@ test('pings and returns json', (t) => { const flatOptions = { registry: 'https://registry.npmjs.org', json: true } const details = { extra: 'data' } let noticeCalls = 0 - const ping = requireInject('../../lib/ping.js', { - '../../lib/npm.js': { flatOptions }, + const Ping = requireInject('../../lib/ping.js', { '../../lib/utils/ping.js': function (spec) { t.equal(spec, flatOptions, 'passes flatOptions') return details @@ -101,8 +100,9 @@ test('pings and returns json', (t) => { }, }, }) + const ping = new Ping({ flatOptions }) - ping([], (err) => { + ping.exec([], (err) => { t.equal(noticeCalls, 2, 'should have logged 2 lines') t.ifError(err, 'npm ping') t.ok('should be able to ping') diff --git a/test/lib/prefix.js b/test/lib/prefix.js index 83e2d63680859..dfb50f174f5db 100644 --- a/test/lib/prefix.js +++ b/test/lib/prefix.js @@ -5,14 +5,14 @@ test('prefix', (t) => { t.plan(3) const dir = '/prefix/dir' - const prefix = requireInject('../../lib/prefix.js', { - '../../lib/npm.js': { prefix: dir }, + const Prefix = requireInject('../../lib/prefix.js', { '../../lib/utils/output.js': (output) => { t.equal(output, dir, 'prints the correct directory') }, }) + const prefix = new Prefix({ prefix: dir }) - prefix([], (err) => { + prefix.exec([], (err) => { t.ifError(err, 'npm prefix') t.ok('should have printed directory') }) diff --git a/test/lib/profile.js b/test/lib/profile.js index 3b2e140036c7e..743ba2d6872e1 100644 --- a/test/lib/profile.js +++ b/test/lib/profile.js @@ -32,7 +32,6 @@ const mocks = { .join('\n') } }, - '../../lib/npm.js': npm, '../../lib/utils/output.js': (...msg) => { result += msg.join('\n') }, @@ -68,10 +67,11 @@ t.afterEach(cb => { cb() }) -const profile = requireInject('../../lib/profile.js', mocks) +const Profile = requireInject('../../lib/profile.js', mocks) +const profile = new Profile(npm) t.test('no args', t => { - profile([], err => { + profile.exec([], err => { t.match( err, /usage instructions/, @@ -88,13 +88,14 @@ t.test('profile get no args', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) t.test('default output', t => { - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -109,7 +110,7 @@ t.test('profile get no args', t => { t.test('--json', t => { npm.flatOptions.json = true - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -125,7 +126,7 @@ t.test('profile get no args', t => { t.test('--parseable', t => { npm.flatOptions.parseable = true - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -147,12 +148,13 @@ t.test('profile get no args', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -174,12 +176,13 @@ t.test('profile get no args', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -201,12 +204,13 @@ t.test('profile get no args', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['get'], err => { + profile.exec(['get'], err => { if (err) throw err @@ -228,13 +232,14 @@ t.test('profile get ', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) t.test('default output', t => { - profile(['get', 'name'], err => { + profile.exec(['get', 'name'], err => { if (err) throw err @@ -250,7 +255,7 @@ t.test('profile get ', t => { t.test('--json', t => { npm.flatOptions.json = true - profile(['get', 'name'], err => { + profile.exec(['get', 'name'], err => { if (err) throw err @@ -266,7 +271,7 @@ t.test('profile get ', t => { t.test('--parseable', t => { npm.flatOptions.parseable = true - profile(['get', 'name'], err => { + profile.exec(['get', 'name'], err => { if (err) throw err @@ -288,13 +293,14 @@ t.test('profile get multiple args', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) t.test('default output', t => { - profile(['get', 'name', 'email', 'github'], err => { + profile.exec(['get', 'name', 'email', 'github'], err => { if (err) throw err @@ -309,7 +315,7 @@ t.test('profile get multiple args', t => { t.test('--json', t => { npm.flatOptions.json = true - profile(['get', 'name', 'email', 'github'], err => { + profile.exec(['get', 'name', 'email', 'github'], err => { if (err) throw err @@ -325,7 +331,7 @@ t.test('profile get multiple args', t => { t.test('--parseable', t => { npm.flatOptions.parseable = true - profile(['get', 'name', 'email', 'github'], err => { + profile.exec(['get', 'name', 'email', 'github'], err => { if (err) throw err @@ -338,7 +344,7 @@ t.test('profile get multiple args', t => { }) t.test('comma separated', t => { - profile(['get', 'name,email,github'], err => { + profile.exec(['get', 'name,email,github'], err => { if (err) throw err @@ -374,7 +380,7 @@ t.test('profile set ', t => { }) t.test('no key', t => { - profile(['set'], err => { + profile.exec(['set'], err => { t.match( err, /npm profile set /, @@ -385,7 +391,7 @@ t.test('profile set ', t => { }) t.test('no value', t => { - profile(['set', 'email'], err => { + profile.exec(['set', 'email'], err => { t.match( err, /npm profile set /, @@ -396,7 +402,7 @@ t.test('profile set ', t => { }) t.test('set password', t => { - profile(['set', 'password', '1234'], err => { + profile.exec(['set', 'password', '1234'], err => { t.match( err, /Do not include your current or new passwords on the command line./, @@ -407,7 +413,7 @@ t.test('profile set ', t => { }) t.test('unwritable key', t => { - profile(['set', 'name', 'foo'], err => { + profile.exec(['set', 'name', 'foo'], err => { t.match( err, /"name" is not a property we can set./, @@ -421,12 +427,13 @@ t.test('profile set ', t => { t.test('default output', t => { t.plan(2) - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) + const profile = new Profile(npm) - profile(['set', 'fullname', 'Lorem Ipsum'], err => { + profile.exec(['set', 'fullname', 'Lorem Ipsum'], err => { if (err) throw err @@ -443,12 +450,13 @@ t.test('profile set ', t => { npm.flatOptions.json = true - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) + const profile = new Profile(npm) - profile(['set', 'fullname', 'Lorem Ipsum'], err => { + profile.exec(['set', 'fullname', 'Lorem Ipsum'], err => { if (err) throw err @@ -467,12 +475,13 @@ t.test('profile set ', t => { npm.flatOptions.parseable = true - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), }) + const profile = new Profile(npm) - profile(['set', 'fullname', 'Lorem Ipsum'], err => { + profile.exec(['set', 'fullname', 'Lorem Ipsum'], err => { if (err) throw err @@ -513,12 +522,13 @@ t.test('profile set ', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['set', 'email', 'foo@npmjs.com'], err => { + profile.exec(['set', 'email', 'foo@npmjs.com'], err => { if (err) throw err @@ -576,13 +586,14 @@ t.test('profile set ', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['set', 'password'], err => { + profile.exec(['set', 'password'], err => { if (err) throw err @@ -643,14 +654,15 @@ t.test('profile set ', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, npmlog, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['set', 'password'], err => { + profile.exec(['set', 'password'], err => { if (err) throw err @@ -668,7 +680,7 @@ t.test('profile set ', t => { t.test('enable-2fa', t => { t.test('invalid args', t => { - profile(['enable-2fa', 'foo', 'bar'], err => { + profile.exec(['enable-2fa', 'foo', 'bar'], err => { t.match( err, /npm profile enable-2fa \[auth-and-writes|auth-only\]/, @@ -679,7 +691,7 @@ t.test('enable-2fa', t => { }) t.test('invalid two factor auth mode', t => { - profile(['enable-2fa', 'foo'], err => { + profile.exec(['enable-2fa', 'foo'], err => { t.match( err, /Invalid two-factor authentication mode "foo"/, @@ -692,7 +704,7 @@ t.test('enable-2fa', t => { t.test('no support for --json output', t => { npm.flatOptions.json = true - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err.message, 'Enabling two-factor authentication is an interactive ' + @@ -706,7 +718,7 @@ t.test('enable-2fa', t => { t.test('no support for --parseable output', t => { npm.flatOptions.parseable = true - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err.message, 'Enabling two-factor authentication is an interactive ' + @@ -733,12 +745,13 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err.message, 'Your registry https://registry.npmjs.org/ does ' + @@ -761,12 +774,13 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err.message, 'Your registry https://registry.npmjs.org/ does ' + @@ -781,11 +795,12 @@ t.test('enable-2fa', t => { t.test('no auth found', t => { npm.config.getCredentialsByURI = () => ({}) - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err.message, 'You need to be logged in to registry ' + @@ -861,13 +876,14 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { if (err) throw err @@ -964,14 +980,15 @@ t.test('enable-2fa', t => { generate: (url, cb) => cb('qrcode'), } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, 'qrcode-terminal': qrcode, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { if (err) throw err @@ -1017,13 +1034,14 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { t.match( err, /Unknown error enabling two-factor authentication./, @@ -1063,13 +1081,14 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-and-writes'], err => { + profile.exec(['enable-2fa', 'auth-and-writes'], err => { if (err) throw err @@ -1113,13 +1132,14 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa', 'auth-only'], err => { + profile.exec(['enable-2fa', 'auth-only'], err => { if (err) throw err @@ -1163,13 +1183,14 @@ t.test('enable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['enable-2fa'], err => { + profile.exec(['enable-2fa'], err => { if (err) throw err @@ -1196,12 +1217,13 @@ t.test('disable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, }) + const profile = new Profile(npm) - profile(['disable-2fa'], err => { + profile.exec(['disable-2fa'], err => { if (err) throw err @@ -1257,13 +1279,14 @@ t.test('disable-2fa', t => { }) t.test('default output', t => { - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../lib/utils/read-user-info.js': readUserInfo(t), }) + const profile = new Profile(npm) - profile(['disable-2fa'], err => { + profile.exec(['disable-2fa'], err => { if (err) throw err @@ -1279,13 +1302,14 @@ t.test('disable-2fa', t => { t.test('--json', t => { npm.flatOptions.json = true - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../lib/utils/read-user-info.js': readUserInfo(t), }) + const profile = new Profile(npm) - profile(['disable-2fa'], err => { + profile.exec(['disable-2fa'], err => { if (err) throw err @@ -1301,13 +1325,14 @@ t.test('disable-2fa', t => { t.test('--parseable', t => { npm.flatOptions.parseable = true - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile(t), '../../lib/utils/read-user-info.js': readUserInfo(t), }) + const profile = new Profile(npm) - profile(['disable-2fa'], err => { + profile.exec(['disable-2fa'], err => { if (err) throw err @@ -1363,13 +1388,14 @@ t.test('disable-2fa', t => { }, } - const profile = requireInject('../../lib/profile.js', { + const Profile = requireInject('../../lib/profile.js', { ...mocks, 'npm-profile': npmProfile, '../../lib/utils/read-user-info.js': readUserInfo, }) + const profile = new Profile(npm) - profile(['disable-2fa'], err => { + profile.exec(['disable-2fa'], err => { if (err) throw err @@ -1385,7 +1411,7 @@ t.test('disable-2fa', t => { }) t.test('unknown subcommand', t => { - profile(['asfd'], err => { + profile.exec(['asfd'], err => { t.match( err, /Unknown profile command: asfd/, @@ -1396,11 +1422,9 @@ t.test('unknown subcommand', t => { }) t.test('completion', t => { - const { completion } = profile - const testComp = async ({ t, argv, expect, title }) => { t.resolveMatch( - completion({ conf: { argv: { remain: argv } } }), + profile.completion({ conf: { argv: { remain: argv } } }), expect, title ) @@ -1444,7 +1468,7 @@ t.test('completion', t => { t.test('npm profile unknown subcommand autocomplete', async t => { t.rejects( - completion({ conf: { argv: { remain: ['npm', 'profile', 'asdf'] } } }), + profile.completion({ conf: { argv: { remain: ['npm', 'profile', 'asdf'] } } }), { message: 'asdf not recognized' }, 'should throw unknown cmd error' ) t.end() diff --git a/test/lib/prune.js b/test/lib/prune.js index 074f4eac6eeee..8cd148806e352 100644 --- a/test/lib/prune.js +++ b/test/lib/prune.js @@ -2,13 +2,7 @@ const { test } = require('tap') const requireInject = require('require-inject') test('should prune using Arborist', (t) => { - const prune = requireInject('../../lib/prune.js', { - '../../lib/npm.js': { - prefix: 'foo', - flatOptions: { - foo: 'bar', - }, - }, + const Prune = requireInject('../../lib/prune.js', { '@npmcli/arborist': function (args) { t.ok(args, 'gets options object') t.ok(args.path, 'gets path option') @@ -20,7 +14,13 @@ test('should prune using Arborist', (t) => { t.ok(arb, 'gets arborist tree') }, }) - prune(null, er => { + const prune = new Prune({ + prefix: 'foo', + flatOptions: { + foo: 'bar', + }, + }) + prune.exec(null, er => { if (er) throw er t.ok(true, 'callback is called') diff --git a/test/lib/publish.js b/test/lib/publish.js index 5243b5254201e..0e857fafddfe2 100644 --- a/test/lib/publish.js +++ b/test/lib/publish.js @@ -34,19 +34,7 @@ t.test('should publish with libnpmpublish, passing through flatOptions and respe }, null, 2), }) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - customValue: true, - }, - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { // verify that we do NOT remove publishConfig if it was there originally // and then removed during the script/pack process libnpmpack: async () => { @@ -66,11 +54,24 @@ t.test('should publish with libnpmpublish, passing through flatOptions and respe }, }, }) + const publish = new Publish({ + flatOptions: { + customValue: true, + }, + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) @@ -85,16 +86,7 @@ t.test('re-loads publishConfig.registry if added during script process', (t) => }, null, 2), }) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { libnpmpack: async () => { fs.writeFileSync(`${testDir}/package.json`, JSON.stringify({ name: 'my-cool-pkg', @@ -112,11 +104,21 @@ t.test('re-loads publishConfig.registry if added during script process', (t) => }, }, }) + const publish = new Publish({ + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) @@ -131,19 +133,7 @@ t.test('if loglevel=info and json, should not output package contents', (t) => { }) log.level = 'info' - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - json: true, - }, - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, defaults.registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { '../../lib/utils/output.js': () => { t.pass('output is called') }, @@ -161,11 +151,24 @@ t.test('if loglevel=info and json, should not output package contents', (t) => { }, }, }) + const publish = new Publish({ + flatOptions: { + json: true, + }, + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, defaults.registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) @@ -180,18 +183,7 @@ t.test('if loglevel=silent and dry-run, should not output package contents or pu }) log.level = 'silent' - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - dryRun: true, - }, - config: { - ...config, - getCredentialsByURI: () => { - throw new Error('should not call getCredentialsByURI in dry run') - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { '../../lib/utils/output.js': () => { throw new Error('should not output in dry run mode') }, @@ -209,11 +201,23 @@ t.test('if loglevel=silent and dry-run, should not output package contents or pu }, }, }) + const publish = new Publish({ + flatOptions: { + dryRun: true, + }, + config: { + ...config, + getCredentialsByURI: () => { + throw new Error('should not call getCredentialsByURI in dry run') + }, + }, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) @@ -228,17 +232,7 @@ t.test('if loglevel=info and dry-run, should not publish, should log package con }) log.level = 'info' - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - dryRun: true, - }, - config: { - ...config, - getCredentialsByURI: () => { - throw new Error('should not call getCredentialsByURI in dry run') - }}, - }, + const Publish = requireInject('../../lib/publish.js', { '../../lib/utils/tar.js': { getContents: () => ({ id: 'someid', @@ -256,37 +250,52 @@ t.test('if loglevel=info and dry-run, should not publish, should log package con }, }, }) + const publish = new Publish({ + flatOptions: { + dryRun: true, + }, + config: { + ...config, + getCredentialsByURI: () => { + throw new Error('should not call getCredentialsByURI in dry run') + }}, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) t.test('shows usage with wrong set of arguments', (t) => { t.plan(1) - const publish = requireInject('../../lib/publish.js') + const Publish = requireInject('../../lib/publish.js') + const publish = new Publish({}) - return publish(['a', 'b', 'c'], (er) => t.matchSnapshot(er, 'should print usage')) + publish.exec(['a', 'b', 'c'], (er) => { + t.matchSnapshot(er, 'should print usage') + t.end() + }) }) t.test('throws when invalid tag', (t) => { t.plan(1) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - defaultTag: '0.0.13', - }, - config, + const Publish = requireInject('../../lib/publish.js') + const publish = new Publish({ + flatOptions: { + defaultTag: '0.0.13', }, + config, }) - return publish([], (err) => { + publish.exec([], (err) => { t.match(err, { message: /Tag name must not be a valid SemVer range: /, }, 'throws when tag name is a valid SemVer range') + t.end() }) }) @@ -310,16 +319,7 @@ t.test('can publish a tarball', t => { }, ['package']) const tarFile = fs.readFileSync(`${testDir}/tarball/package.tgz`) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, defaults.registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { libnpmpublish: { publish: (manifest, tarData, opts) => { t.match(manifest, { @@ -330,63 +330,73 @@ t.test('can publish a tarball', t => { }, }, }) + const publish = new Publish({ + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, defaults.registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) - return publish([`${testDir}/tarball/package.tgz`], (er) => { + publish.exec([`${testDir}/tarball/package.tgz`], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) -t.test('should check auth for default registry', async t => { +t.test('should check auth for default registry', t => { t.plan(2) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, defaults.registry, 'gets credentials for expected registry') - return {} - }, + const Publish = requireInject('../../lib/publish.js') + const publish = new Publish({ + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, defaults.registry, 'gets credentials for expected registry') + return {} }, }, }) - return publish([], (err) => { + publish.exec([], (err) => { t.match(err, { message: 'This command requires you to be logged in.', code: 'ENEEDAUTH', }, 'throws when not logged in') + t.end() }) }) -t.test('should check auth for configured registry', async t => { +t.test('should check auth for configured registry', t => { t.plan(2) const registry = 'https://some.registry' - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - registry, - }, - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return {} - }, + const Publish = requireInject('../../lib/publish.js') + const publish = new Publish({ + flatOptions: { + registry, + }, + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return {} }, }, }) - return publish([], (err) => { + publish.exec([], (err) => { t.match(err, { message: 'This command requires you to be logged in.', code: 'ENEEDAUTH', }, 'throws when not logged in') + t.end() }) }) -t.test('should check auth for scope specific registry', async t => { +t.test('should check auth for scope specific registry', t => { t.plan(2) const registry = 'https://some.registry' const testDir = t.testdir({ @@ -396,26 +406,26 @@ t.test('should check auth for scope specific registry', async t => { }, null, 2), }) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - '@npm:registry': registry, - }, - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return {} - }, + const Publish = requireInject('../../lib/publish.js') + const publish = new Publish({ + flatOptions: { + '@npm:registry': registry, + }, + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return {} }, }, }) - return publish([testDir], (err) => { + publish.exec([testDir], (err) => { t.match(err, { message: 'This command requires you to be logged in.', code: 'ENEEDAUTH', }, 'throws when not logged in') + t.end() }) }) @@ -429,19 +439,7 @@ t.test('should use auth for scope specific registry', t => { }, null, 2), }) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - flatOptions: { - '@npm:registry': registry, - }, - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { libnpmpublish: { publish: (manifest, tarData, opts) => { t.ok(opts, 'gets opts object') @@ -449,10 +447,23 @@ t.test('should use auth for scope specific registry', t => { }, }, }) - return publish([testDir], (er) => { + const publish = new Publish({ + flatOptions: { + '@npm:registry': registry, + }, + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) @@ -469,16 +480,7 @@ t.test('read registry only from publishConfig', t => { }, null, 2), }) - const publish = requireInject('../../lib/publish.js', { - '../../lib/npm.js': { - config: { - ...config, - getCredentialsByURI: (uri) => { - t.same(uri, registry, 'gets credentials for expected registry') - return { token: 'some.registry.token' } - }, - }, - }, + const Publish = requireInject('../../lib/publish.js', { libnpmpublish: { publish: (manifest, tarData, opts) => { t.match(manifest, { name: 'my-cool-pkg', version: '1.0.0' }, 'gets manifest') @@ -486,10 +488,20 @@ t.test('read registry only from publishConfig', t => { }, }, }) + const publish = new Publish({ + config: { + ...config, + getCredentialsByURI: (uri) => { + t.same(uri, registry, 'gets credentials for expected registry') + return { token: 'some.registry.token' } + }, + }, + }) - return publish([testDir], (er) => { + publish.exec([testDir], (er) => { if (er) throw er t.pass('got to callback') + t.end() }) }) diff --git a/test/lib/rebuild.js b/test/lib/rebuild.js index d9df048d9057e..ee081c087f07f 100644 --- a/test/lib/rebuild.js +++ b/test/lib/rebuild.js @@ -13,14 +13,14 @@ const npm = { prefix: '', } const mocks = { - '../../lib/npm.js': npm, '../../lib/utils/output.js': (...msg) => { result += msg.join('\n') }, '../../lib/utils/usage.js': () => 'usage instructions', } -const rebuild = requireInject('../../lib/rebuild.js', mocks) +const Rebuild = requireInject('../../lib/rebuild.js', mocks) +const rebuild = new Rebuild(npm) t.afterEach(cb => { npm.prefix = '' @@ -67,7 +67,7 @@ t.test('no args', t => { npm.prefix = path - rebuild([], err => { + rebuild.exec([], err => { if (err) throw err @@ -115,7 +115,7 @@ t.test('filter by pkg name', t => { t.throws(() => fs.statSync(aBinFile)) t.throws(() => fs.statSync(bBinFile)) - rebuild(['b'], err => { + rebuild.exec(['b'], err => { if (err) throw err @@ -163,7 +163,7 @@ t.test('filter by pkg@', t => { const bBinFile = resolve(path, 'node_modules/.bin/b') const nestedBinFile = resolve(path, 'node_modules/a/node_modules/.bin/b') - rebuild(['b@2'], err => { + rebuild.exec(['b@2'], err => { if (err) throw err @@ -203,7 +203,7 @@ t.test('filter by directory', t => { t.throws(() => fs.statSync(aBinFile)) t.throws(() => fs.statSync(bBinFile)) - rebuild(['file:node_modules/b'], err => { + rebuild.exec(['file:node_modules/b'], err => { if (err) throw err @@ -215,7 +215,7 @@ t.test('filter by directory', t => { }) t.test('filter must be a semver version/range, or directory', t => { - rebuild(['git+ssh://github.com/npm/arborist'], err => { + rebuild.exec(['git+ssh://github.com/npm/arborist'], err => { t.match( err, /Error: `npm rebuild` only supports SemVer version\/range specifiers/, @@ -245,7 +245,7 @@ t.test('global prefix', t => { npm.flatOptions.global = true npm.globalDir = resolve(globalPath, 'lib', 'node_modules') - rebuild([], err => { + rebuild.exec([], err => { if (err) throw err diff --git a/test/lib/restart.js b/test/lib/restart.js index a19bfd0d41a17..f29592d9bfcec 100644 --- a/test/lib/restart.js +++ b/test/lib/restart.js @@ -1,4 +1,17 @@ const t = require('tap') -const restart = require('../../lib/restart.js') -t.isa(restart, Function) +let runArgs +const npm = { + commands: { + 'run-script': (args, cb) => { + runArgs = args + cb() + }, + }, +} +const Restart = require('../../lib/restart.js') +const restart = new Restart(npm) t.equal(restart.usage, 'npm restart [-- ]') +restart.exec(['foo'], () => { + t.match(runArgs, ['restart', 'foo']) + t.end() +}) diff --git a/test/lib/root.js b/test/lib/root.js index 8c23152b3efca..e8ccc1106d772 100644 --- a/test/lib/root.js +++ b/test/lib/root.js @@ -5,14 +5,14 @@ test('root', (t) => { t.plan(3) const dir = '/root/dir' - const root = requireInject('../../lib/root.js', { - '../../lib/npm.js': { dir }, + const Root = requireInject('../../lib/root.js', { '../../lib/utils/output.js': (output) => { t.equal(output, dir, 'prints the correct directory') }, }) + const root = new Root({ dir }) - root([], (err) => { + root.exec([], (err) => { t.ifError(err, 'npm root') t.ok('should have printed directory') }) diff --git a/test/lib/run-script.js b/test/lib/run-script.js index 974202aa8c6f0..ea1cabb750741 100644 --- a/test/lib/run-script.js +++ b/test/lib/run-script.js @@ -22,19 +22,30 @@ const npm = { const output = [] -const npmlog = { level: 'warn' } -const getRS = windows => requireInject('../../lib/run-script.js', { - '@npmcli/run-script': Object.assign(async opts => { - RUN_SCRIPTS.push(opts) - }, { - isServerPackage: require('@npmcli/run-script').isServerPackage, - }), - npmlog, - '../../lib/npm.js': npm, - '../../lib/utils/is-windows-shell.js': windows, - '../../lib/utils/output.js': (...msg) => output.push(msg), +t.afterEach(cb => { + output.length = 0 + RUN_SCRIPTS.length = 0 + npm.flatOptions.json = false + npm.flatOptions.parseable = false + cb() }) +const npmlog = { level: 'warn' } +const getRS = windows => { + const RunScript = requireInject('../../lib/run-script.js', { + '@npmcli/run-script': Object.assign(async opts => { + RUN_SCRIPTS.push(opts) + }, { + isServerPackage: require('@npmcli/run-script').isServerPackage, + }), + npmlog, + '../../lib/npm.js': npm, + '../../lib/utils/is-windows-shell.js': windows, + '../../lib/utils/output.js': (...msg) => output.push(msg), + }) + return new RunScript(npm) +} + const runScript = getRS(false) const runScriptWin = getRS(true) @@ -69,109 +80,119 @@ t.test('completion', t => { t.end() }) -t.test('fail if no package.json', async t => { +t.test('fail if no package.json', t => { + t.plan(2) npm.localPrefix = t.testdir() - await runScript([], er => t.match(er, { code: 'ENOENT' })) - await runScript(['test'], er => t.match(er, { code: 'ENOENT' })) + runScript.exec([], er => t.match(er, { code: 'ENOENT' })) + runScript.exec(['test'], er => t.match(er, { code: 'ENOENT' })) }) -t.test('default env, start, and restart scripts', async t => { +t.test('default env, start, and restart scripts', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', version: '1.2.3' }), 'server.js': 'console.log("hello, world")', }) - await runScript(['start'], er => { - if (er) - throw er + t.test('start', t => { + runScript.exec(['start'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: {}}, - event: 'start', - }, - ]) + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', version: '1.2.3', _id: 'x@1.2.3', scripts: {}}, + event: 'start', + }, + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 - await runScript(['env'], er => { - if (er) - throw er + t.test('env', t => { + runScript.exec(['env'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { - name: 'x', - version: '1.2.3', - _id: 'x@1.2.3', - scripts: { - env: 'env', + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { + name: 'x', + version: '1.2.3', + _id: 'x@1.2.3', + scripts: { + env: 'env', + }, }, + event: 'env', }, - event: 'env', - }, - ]) + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 - await runScriptWin(['env'], er => { - if (er) - throw er + t.test('windows env', t => { + runScriptWin.exec(['env'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { name: 'x', - version: '1.2.3', - _id: 'x@1.2.3', - scripts: { - env: 'SET', - } }, - event: 'env', - }, - ]) + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', + version: '1.2.3', + _id: 'x@1.2.3', + scripts: { + env: 'SET', + } }, + event: 'env', + }, + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 - await runScript(['restart'], er => { - if (er) - throw er + t.test('restart', t => { + runScript.exec(['restart'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { name: 'x', - version: '1.2.3', - _id: 'x@1.2.3', - scripts: { - restart: 'npm stop --if-present && npm start', - } }, - event: 'restart', - }, - ]) + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', + version: '1.2.3', + _id: 'x@1.2.3', + scripts: { + restart: 'npm stop --if-present && npm start', + } }, + event: 'restart', + }, + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 + t.end() }) -t.test('non-default env script', async t => { +t.test('non-default env script', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', @@ -182,54 +203,59 @@ t.test('non-default env script', async t => { }), }) - await runScript(['env'], er => { - if (er) - throw er + t.test('env', t => { + runScript.exec(['env'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { - name: 'x', - version: '1.2.3', - _id: 'x@1.2.3', - scripts: { - env: 'hello', + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { + name: 'x', + version: '1.2.3', + _id: 'x@1.2.3', + scripts: { + env: 'hello', + }, }, + event: 'env', }, - event: 'env', - }, - ]) + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 - await runScriptWin(['env'], er => { - if (er) - throw er + t.test('env windows', t => { + runScriptWin.exec(['env'], er => { + if (er) + throw er - t.match(RUN_SCRIPTS, [ - { - path: npm.localPrefix, - args: [], - scriptShell: undefined, - stdio: 'inherit', - stdioString: true, - pkg: { name: 'x', - version: '1.2.3', - _id: 'x@1.2.3', - scripts: { - env: 'hello', + t.match(RUN_SCRIPTS, [ + { + path: npm.localPrefix, + args: [], + scriptShell: undefined, + stdio: 'inherit', + stdioString: true, + pkg: { name: 'x', + version: '1.2.3', + _id: 'x@1.2.3', + scripts: { + env: 'hello', + }, }, + event: 'env', }, - event: 'env', - }, - ]) + ]) + t.end() + }) }) - RUN_SCRIPTS.length = 0 + t.end() }) t.test('try to run missing script', t => { @@ -238,33 +264,36 @@ t.test('try to run missing script', t => { scripts: { hello: 'world' }, }), }) - t.test('no suggestions', async t => { - await runScript(['notevenclose'], er => { + t.test('no suggestions', t => { + runScript.exec(['notevenclose'], er => { t.match(er, { message: 'missing script: notevenclose', }) + t.end() }) }) - t.test('suggestions', async t => { - await runScript(['helo'], er => { + t.test('suggestions', t => { + runScript.exec(['helo'], er => { t.match(er, { message: 'missing script: helo\n\nDid you mean this?\n hello', }) + t.end() }) }) - t.test('with --if-present', async t => { + t.test('with --if-present', t => { npm.config.set('if-present', true) - await runScript(['goodbye'], er => { + runScript.exec(['goodbye'], er => { if (er) throw er t.strictSame(RUN_SCRIPTS, [], 'did not try to run anything') + t.end() }) }) t.end() }) -t.test('run pre/post hooks', async t => { +t.test('run pre/post hooks', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', @@ -276,7 +305,7 @@ t.test('run pre/post hooks', async t => { }), }) - await runScript(['env'], er => { + runScript.exec(['env'], er => { if (er) throw er @@ -298,11 +327,11 @@ t.test('run pre/post hooks', async t => { }, { event: 'postenv' }, ]) + t.end() }) - RUN_SCRIPTS.length = 0 }) -t.test('skip pre/post hooks when using ignoreScripts', async t => { +t.test('skip pre/post hooks when using ignoreScripts', t => { npm.flatOptions.ignoreScripts = true npm.localPrefix = t.testdir({ @@ -316,7 +345,7 @@ t.test('skip pre/post hooks when using ignoreScripts', async t => { }), }) - await runScript(['env'], er => { + runScript.exec(['env'], er => { if (er) throw er @@ -339,13 +368,12 @@ t.test('skip pre/post hooks when using ignoreScripts', async t => { event: 'env', }, ]) - + t.end() delete npm.flatOptions.ignoreScripts }) - RUN_SCRIPTS.length = 0 }) -t.test('run silent', async t => { +t.test('run silent', t => { npmlog.level = 'silent' t.teardown(() => { npmlog.level = 'warn' @@ -362,7 +390,7 @@ t.test('run silent', async t => { }), }) - await runScript(['env'], er => { + runScript.exec(['env'], er => { if (er) throw er @@ -391,11 +419,11 @@ t.test('run silent', async t => { stdio: 'inherit', }, ]) + t.end() }) - RUN_SCRIPTS.length = 0 }) -t.test('list scripts', async t => { +t.test('list scripts', t => { const scripts = { test: 'exit 2', start: 'node server.js', @@ -411,55 +439,62 @@ t.test('list scripts', async t => { }), }) - await runScript([], er => { - if (er) - throw er + t.test('no args', t => { + runScript.exec([], er => { + if (er) + throw er + t.strictSame(output, [ + ['Lifecycle scripts included in x:'], + [' test\n exit 2'], + [' start\n node server.js'], + [' stop\n node kill-server.js'], + ['\navailable via `npm run-script`:'], + [' preenv\n echo before the env'], + [' postenv\n echo after the env'], + ], 'basic report') + t.end() + }) }) - t.strictSame(output, [ - ['Lifecycle scripts included in x:'], - [' test\n exit 2'], - [' start\n node server.js'], - [' stop\n node kill-server.js'], - ['\navailable via `npm run-script`:'], - [' preenv\n echo before the env'], - [' postenv\n echo after the env'], - ], 'basic report') - output.length = 0 - npmlog.level = 'silent' - await runScript([], er => { - if (er) - throw er + t.test('silent', t => { + npmlog.level = 'silent' + runScript.exec([], er => { + if (er) + throw er + t.strictSame(output, []) + t.end() + }) }) - t.strictSame(output, []) - npmlog.level = 'warn' - - npm.flatOptions.json = true - await runScript([], er => { - if (er) - throw er + t.test('warn json', t => { + npmlog.level = 'warn' + npm.flatOptions.json = true + runScript.exec([], er => { + if (er) + throw er + t.strictSame(output, [[JSON.stringify(scripts, 0, 2)]], 'json report') + t.end() + }) }) - t.strictSame(output, [[JSON.stringify(scripts, 0, 2)]], 'json report') - output.length = 0 - npm.flatOptions.json = false - npm.flatOptions.parseable = true - await runScript([], er => { - if (er) - throw er + t.test('parseable', t => { + npm.flatOptions.parseable = true + runScript.exec([], er => { + if (er) + throw er + t.strictSame(output, [ + ['test:exit 2'], + ['start:node server.js'], + ['stop:node kill-server.js'], + ['preenv:echo before the env'], + ['postenv:echo after the env'], + ]) + t.end() + }) }) - t.strictSame(output, [ - ['test:exit 2'], - ['start:node server.js'], - ['stop:node kill-server.js'], - ['preenv:echo before the env'], - ['postenv:echo after the env'], - ]) - output.length = 0 - npm.flatOptions.parseable = false + t.end() }) -t.test('list scripts when no scripts', async t => { +t.test('list scripts when no scripts', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', @@ -467,15 +502,15 @@ t.test('list scripts when no scripts', async t => { }), }) - await runScript([], er => { + runScript.exec([], er => { if (er) throw er + t.strictSame(output, [], 'nothing to report') + t.end() }) - t.strictSame(output, [], 'nothing to report') - output.length = 0 }) -t.test('list scripts, only commands', async t => { +t.test('list scripts, only commands', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', @@ -484,18 +519,18 @@ t.test('list scripts, only commands', async t => { }), }) - await runScript([], er => { + runScript.exec([], er => { if (er) throw er + t.strictSame(output, [ + ['Lifecycle scripts included in x:'], + [' preversion\n echo doing the version dance'], + ]) + t.end() }) - t.strictSame(output, [ - ['Lifecycle scripts included in x:'], - [' preversion\n echo doing the version dance'], - ]) - output.length = 0 }) -t.test('list scripts, only non-commands', async t => { +t.test('list scripts, only non-commands', t => { npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'x', @@ -504,13 +539,13 @@ t.test('list scripts, only non-commands', async t => { }), }) - await runScript([], er => { + runScript.exec([], er => { if (er) throw er + t.strictSame(output, [ + ['Scripts available in x via `npm run-script`:'], + [' glorp\n echo doing the glerp glop'], + ]) + t.end() }) - t.strictSame(output, [ - ['Scripts available in x via `npm run-script`:'], - [' glorp\n echo doing the glerp glop'], - ]) - output.length = 0 }) diff --git a/test/lib/start.js b/test/lib/start.js index 4f599223d7aa7..9a3328309b84a 100644 --- a/test/lib/start.js +++ b/test/lib/start.js @@ -1,4 +1,17 @@ const t = require('tap') -const start = require('../../lib/start.js') -t.isa(start, Function) +let runArgs +const npm = { + commands: { + 'run-script': (args, cb) => { + runArgs = args + cb() + }, + }, +} +const Start = require('../../lib/start.js') +const start = new Start(npm) t.equal(start.usage, 'npm start [-- ]') +start.exec(['foo'], () => { + t.match(runArgs, ['start', 'foo']) + t.end() +}) diff --git a/test/lib/stop.js b/test/lib/stop.js index 4e26703c93c57..e6cb193b62973 100644 --- a/test/lib/stop.js +++ b/test/lib/stop.js @@ -1,4 +1,17 @@ const t = require('tap') -const stop = require('../../lib/stop.js') -t.isa(stop, Function) +let runArgs +const npm = { + commands: { + 'run-script': (args, cb) => { + runArgs = args + cb() + }, + }, +} +const Stop = require('../../lib/stop.js') +const stop = new Stop(npm) t.equal(stop.usage, 'npm stop [-- ]') +stop.exec(['foo'], () => { + t.match(runArgs, ['stop', 'foo']) + t.end() +}) diff --git a/test/lib/test.js b/test/lib/test.js index 6f4a7395d732a..f6f3d7afb858d 100644 --- a/test/lib/test.js +++ b/test/lib/test.js @@ -1,7 +1,7 @@ const t = require('tap') const requireInject = require('require-inject') let RUN_ARGS = null -const npmock = { +const npm = { commands: { 'run-script': (args, cb) => { RUN_ARGS = args @@ -9,15 +9,14 @@ const npmock = { }, }, } -const test = requireInject('../../lib/test.js', { - '../../lib/npm.js': npmock, -}) +const Test = requireInject('../../lib/test.js') +const test = new Test(npm) t.test('run a test', t => { - test([], (er) => { + test.exec([], (er) => { t.strictSame(RUN_ARGS, ['test'], 'added "test" to the args') }) - test(['hello', 'world'], (er) => { + test.exec(['hello', 'world'], (er) => { t.strictSame(RUN_ARGS, ['test', 'hello', 'world'], 'added positional args') }) @@ -26,13 +25,13 @@ t.test('run a test', t => { }) const otherErr = new Error('should see this') - npmock.commands['run-script'] = (args, cb) => cb(lcErr) - test([], (er) => { + npm.commands['run-script'] = (args, cb) => cb(lcErr) + test.exec([], (er) => { t.equal(er, 'Test failed. See above for more details.') }) - npmock.commands['run-script'] = (args, cb) => cb(otherErr) - test([], (er) => { + npm.commands['run-script'] = (args, cb) => cb(otherErr) + test.exec([], (er) => { t.match(er, { message: 'should see this' }) })