diff --git a/lib/access.js b/lib/access.js index 3c4ed590683c0..e11934af43ebc 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( - '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( + '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(this.npm, 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..dac0f5a46840d 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,76 @@ 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) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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, { + ...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..dfa01cb2709fa 100644 --- a/lib/audit.js +++ b/lib/audit.js @@ -1,55 +1,65 @@ 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.js') -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) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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)]' -) + async completion (opts) { + const argv = opts.conf.argv.remain -const completion = async (opts) => { - const argv = opts.conf.argv.remain + if (argv.length === 2) + return ['fix'] - if (argv.length === 2) - return ['fix'] + switch (argv[2]) { + case 'fix': + return [] + default: + throw new Error(argv[2] + ' not recognized') + } + } - 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/auth/legacy.js b/lib/auth/legacy.js index f291ca794e7f1..8659446dc4c02 100644 --- a/lib/auth/legacy.js +++ b/lib/auth/legacy.js @@ -4,11 +4,6 @@ const profile = require('npm-profile') const openUrl = require('../utils/open-url.js') const read = require('../utils/read-user-info.js') -// TODO: refactor lib/utils/open-url and its usages -const openerPromise = (url) => new Promise((resolve, reject) => { - openUrl(url, 'to complete your login please visit', (er) => er ? reject(er) : resolve()) -}) - const loginPrompter = async (creds) => { const opts = { log: log } @@ -19,7 +14,7 @@ const loginPrompter = async (creds) => { return creds } -const login = async (opts) => { +const login = async (npm, opts) => { let res const requestOTP = async () => { @@ -54,6 +49,7 @@ const login = async (opts) => { return newUser } + const openerPromise = (url) => openUrl(npm, url, 'to complete your login please visit') try { res = await profile.login(openerPromise, loginPrompter, opts) } catch (err) { diff --git a/lib/auth/oauth.js b/lib/auth/oauth.js index ee45317113421..99c2ca0ca04b7 100644 --- a/lib/auth/oauth.js +++ b/lib/auth/oauth.js @@ -1,9 +1,8 @@ const sso = require('./sso.js') -const npm = require('../npm.js') -const login = (opts) => { +const login = (npm, opts) => { npm.config.set('sso-type', 'oauth') - return sso(opts) + return sso(npm, opts) } module.exports = login diff --git a/lib/auth/saml.js b/lib/auth/saml.js index f30d82849dbf9..3dd31ca013f52 100644 --- a/lib/auth/saml.js +++ b/lib/auth/saml.js @@ -1,9 +1,8 @@ const sso = require('./sso.js') -const npm = require('../npm.js') -const login = (opts) => { +const login = (npm, opts) => { npm.config.set('sso-type', 'saml') - return sso(opts) + return sso(npm, opts) } module.exports = login diff --git a/lib/auth/sso.js b/lib/auth/sso.js index 378295f5f606f..ca8c501684c29 100644 --- a/lib/auth/sso.js +++ b/lib/auth/sso.js @@ -13,7 +13,6 @@ const log = require('npmlog') const profile = require('npm-profile') const npmFetch = require('npm-registry-fetch') -const npm = require('../npm.js') const openUrl = promisify(require('../utils/open-url.js')) const otplease = require('../utils/otplease.js') @@ -38,7 +37,7 @@ function sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)) } -const login = async ({ creds, registry, scope }) => { +const login = async (npm, { creds, registry, scope }) => { log.warn('deprecated', 'SSO --auth-type is deprecated') const opts = { ...npm.flatOptions, creds, registry, scope } @@ -65,7 +64,7 @@ const login = async ({ creds, registry, scope }) => { if (!sso) throw new Error('no SSO URL returned by services') - await openUrl(sso, 'to complete your login please visit') + await openUrl(npm, sso, 'to complete your login please visit') const username = await pollForSession({ registry, token, opts }) diff --git a/lib/bin.js b/lib/bin.js index e627ce22f13a6..11490c41cbcc5 100644 --- a/lib/bin.js +++ b/lib/bin.js @@ -1,13 +1,26 @@ -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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..fb0d7c92770c7 100644 --- a/lib/bugs.js +++ b/lib/bugs.js @@ -1,46 +1,55 @@ 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) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..03a91a60463f2 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,70 @@ 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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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(this.npm, 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..4c37e6ef354ef 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,127 @@ 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 + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 +277,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..2805db9b80ec7 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,173 @@ 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 +// 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() } + return kv +} - const action = argv[2] - switch (action) { - case 'set': - // todo: complete with valid values, if possible. - if (argv.length > 3) - return [] +const publicVar = k => !/^(\/\/[^:]+:)?_/.test(k) - // fallthrough - /* eslint no-fallthrough:0 */ - case 'get': - case 'delete': - case 'rm': - return Object.keys(types) - case 'edit': - case 'list': - case 'ls': - default: - return [] +class Config { + constructor (npm) { + this.npm = npm } -} -const UsageError = () => - Object.assign(new Error(usage), { code: 'EUSAGE' }) + 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 [ [ ...]]' + ) + } + + async completion (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') -const config = async ([action, ...args]) => { - npm.log.disableProgress() - try { + 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 +197,67 @@ ${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('') - } - 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.' - ) + output(msg.join('\n').trim()) } - output(msg.join('\n').trim()) -} + async listJson () { + const publicConf = {} + for (const key in this.npm.config.list[0]) { + if (!publicVar(key)) + continue -const listJson = async () => { - const publicConf = {} - for (const key in npm.config.list[0]) { - if (!publicVar(key)) - continue + publicConf[key] = this.npm.config.get(key) + } + output(JSON.stringify(publicConf, null, 2)) + } - publicConf[key] = npm.config.get(key) + usageError () { + return Object.assign(new Error(this.usage), { code: 'EUSAGE' }) } - output(JSON.stringify(publicConf, null, 2)) } -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = Config diff --git a/lib/dedupe.js b/lib/dedupe.js index 2211fcac8b481..59978895effb2 100644 --- a/lib/dedupe.js +++ b/lib/dedupe.js @@ -1,29 +1,39 @@ // 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) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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.config.get('global')) { + const er = new Error('`npm dedupe` does not work in global mode.') + er.code = 'EDEDUPEGLOBAL' + throw er + } + + const dryRun = this.npm.config.get('dry-run') + const where = this.npm.prefix + const arb = new Arborist({ + ...this.npm.flatOptions, + path: where, + dryRun, + }) + await arb.dedupe(this.npm.flatOptions) + await reifyFinish(this.npm, arb) + } } -module.exports = Object.assign(cmd, { usage }) +module.exports = Dedupe diff --git a/lib/deprecate.js b/lib/deprecate.js index 42d099b544e31..48f27ab6c35e8 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, 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..2dad7a26db4e7 100644 --- a/lib/docs.js +++ b/lib/docs.js @@ -1,39 +1,47 @@ 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 [ [ ...]]') - -const cmd = (args, cb) => docs(args).then(() => cb()).catch(cb) - -const docs = async args => { - if (!args || !args.length) - args = ['.'] - - await Promise.all(args.map(pkg => getDocs(pkg))) -} - -const getDocsUrl = mani => { - if (mani.homepage) - return mani.homepage - - const info = hostedFromMani(mani) - if (info) - return info.docs() - - return 'https://www.npmjs.com/package/' + mani.name +class Docs { + constructor (npm) { + this.npm = npm + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('docs', 'npm docs [ [ ...]]') + } + + exec (args, cb) { + this.docs(args).then(() => cb()).catch(cb) + } + + async docs (args) { + if (!args || !args.length) + args = ['.'] + + await Promise.all(args.map(pkg => this.getDocs(pkg))) + } + + 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`) + } + + getDocsUrl (mani) { + if (mani.homepage) + return mani.homepage + + const info = hostedFromMani(mani) + if (info) + return info.docs() + + return 'https://www.npmjs.com/package/' + mani.name + } } - -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`) -} - -module.exports = Object.assign(cmd, { usage }) +module.exports = Docs diff --git a/lib/doctor.js b/lib/doctor.js index e149aec1286d5..81860004e344e 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,268 @@ 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 - } + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..a7dbb38205b02 100644 --- a/lib/edit.js +++ b/lib/edit.js @@ -4,33 +4,55 @@ 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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('edit', 'npm edit [/...]') + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..01541040ef649 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,101 @@ 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 + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('explain', 'npm explain ') + } - const arb = new Arborist({ path: npm.prefix, ...npm.flatOptions }) - const tree = await arb.loadActual() + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..fdfe6e1bcf7c8 100644 --- a/lib/explore.js +++ b/lib/explore.js @@ -1,69 +1,82 @@ // 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() + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..5061be9cc381a 100644 --- a/lib/find-dupes.js +++ b/lib/find-dupes.js @@ -1,8 +1,19 @@ // dedupe duplicated packages, or find them in the tree -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 { + constructor (npm) { + this.npm = npm + } -module.exports = Object.assign(cmd, { usage }) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('find-dupes', 'npm find-dupes') + } + + exec (args, cb) { + this.npm.config.set('dry-run', true) + this.npm.commands.dedupe([], cb) + } +} +module.exports = FindDupes diff --git a/lib/fund.js b/lib/fund.js index 41dd48c465342..1e9724266401f 100644 --- a/lib/fund.js +++ b/lib/fund.js @@ -11,200 +11,210 @@ 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 - } + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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' + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 openUrl(this.npm, url, msg) + } 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..a5b2f5514473d 100644 --- a/lib/get.js +++ b/lib/get.js @@ -1,15 +1,25 @@ -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 + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil( + 'get', + 'npm get [ ...] (See `npm config`)' + ) + } -const cmd = (args, cb) => - npm.commands.config(['get'].concat(args), cb) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + async completion (opts) { + return this.npm.commands.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..d7897326f3118 100644 --- a/lib/help.js +++ b/lib/help.js @@ -1,191 +1,224 @@ - -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 openUrl = require('./utils/open-url.js') const glob = require('glob') 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 - - 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 npm.commands['help-search'](args, cb) +class Help { + constructor (npm) { + this.npm = npm + } - const affordances = { - 'find-dupes': 'dedupe', + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usage('help', 'npm help []') } - let section = affordances[args[0]] || npm.deref(args[0]) || args[0] - // npm help : show basic usage - if (!section) { - npmUsage(argv[0] === 'help') - return 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) + }) + }) + + 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 })) } - // 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() + exec (args, cb) { + this.help(args).then(() => cb()).catch(cb) } - 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 + 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) { + this.helpSearch(args).then(resolve).catch(reject) + 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() + }) + }) }) + } - 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] -} + helpSearch (args) { + return new Promise((resolve, reject) => { + this.npm.commands['help-search'](args, (err) => { + // This would only error if args was empty, which it never is + /* istanbul ignore next */ + if (err) + return reject(err) -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', + resolve() + }) + }) } - 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}`)) + viewMan (man, cb) { + const nre = /([0-9]+)$/ + const num = man.match(nre)[1] + const section = path.basename(man, '.' + num) - return cb() + // 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(this.npm, url, 'help available at the following URL').then( + () => cb() + ).catch(cb) + } catch (err) { + 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..af97a9614e368 100644 --- a/lib/init.js +++ b/lib/init.js @@ -1,88 +1,97 @@ 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()) - }) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..d7fd384d5bd6f 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,143 @@ 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', + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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(this.npm, 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..6d5e207105825 100644 --- a/lib/link.js +++ b/lib/link.js @@ -8,145 +8,154 @@ 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' } + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..7915f5d27c011 100644 --- a/lib/ll.js +++ b/lib/ll.js @@ -1,9 +1,19 @@ -const { usage, completion } = require('./ls.js') -const npm = require('./npm.js') +const LS = require('./ls.js') +const usageUtil = require('./utils/usage.js') -const cmd = (args, cb) => { - npm.config.set('long', true) - return npm.commands.ls(args, cb) +class LL extends LS { + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil( + 'll', + 'npm ll [[<@scope>/] ...]' + ) + } + + exec (args, cb) { + this.npm.config.set('long', true) + super.exec(args, cb) + } } -module.exports = Object.assign(cmd, { usage, completion }) +module.exports = LL diff --git a/lib/logout.js b/lib/logout.js index d2762c1ba3e5f..9fb1eab21a152 100644 --- a/lib/logout.js +++ b/lib/logout.js @@ -1,44 +1,52 @@ -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') + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 d9c06de875df7..359fe21e6f8cc 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,166 @@ 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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil( + 'ls', + 'npm ls [[<@scope>/] ...]' + ) + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 }) + + 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, + 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 }) => { - 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 }) { + const tree = await arb.loadActual() + tree[_include] = args.length === 0 + tree[_depth] = 0 + + return tree + } } +module.exports = LS const isGitNode = (node) => { if (!node.resolved) @@ -358,137 +503,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, - }) - - 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, - 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 949e6309e7bba..1f8c785e755c4 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -13,40 +13,28 @@ 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 && !Reflect.has(target, actual)) { + const Impl = require(`./${actual}.js`) + const impl = new Impl(npm) + // Our existing npm.commands[x] act like a function with attributes, but + // our modules have non-inumerable attributes so we can't just assign + // them to an anonymous function like we used to. This acts like that + // old way of doing things, until we can make breaking changes to the + // npm.commands[x] api + target[actual] = new Proxy( + (args, cb) => npm[_runCmd](cmd, impl, args, cb), + { + get: (target, attr, receiver) => { + return Reflect.get(impl, attr, receiver) + }, + }) + } + return target[actual] + }, +}) const { types, defaults, shorthands } = require('./utils/config.js') const { shellouts } = require('./utils/cmd-list.js') @@ -68,7 +56,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 +109,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..fc6967faf60fe 100644 --- a/lib/outdated.js +++ b/lib/outdated.js @@ -9,112 +9,135 @@ 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) -} + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 +148,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 +159,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 +168,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 +190,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 +200,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..cf1e77f48ee69 100644 --- a/lib/pack.js +++ b/lib/pack.js @@ -4,46 +4,53 @@ 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 = ['.'] + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..e43f0640f212b 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -1,27 +1,36 @@ 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') + /* istanbul ignore next - see test/lib/load-all-commands.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..e46f9c4cdd94a 100644 --- a/lib/prefix.js +++ b/lib/prefix.js @@ -1,7 +1,22 @@ -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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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 f5db75eb4cce6..dab99092b0a0f 100644 --- a/lib/profile.js +++ b/lib/profile.js @@ -6,72 +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( - 'profile', - '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', @@ -86,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', @@ -154,242 +38,364 @@ 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( + 'profile', + '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..b839301d5194c 100644 --- a/lib/prune.js +++ b/lib/prune.js @@ -1,24 +1,32 @@ // prune extraneous packages -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) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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(this.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..1091b01589389 100644 --- a/lib/rebuild.js +++ b/lib/rebuild.js @@ -2,64 +2,74 @@ 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>/][@] ...]') + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('rebuild', 'npm rebuild [[<@scope>/][@] ...]') + } -const completion = require('./utils/completion/installed-deep.js') + /* istanbul ignore next - see test/lib/load-all-commands.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/repo.js b/lib/repo.js index e9074dca68d7c..60fe6dbaf90b9 100644 --- a/lib/repo.js +++ b/lib/repo.js @@ -1,52 +1,64 @@ const log = require('npmlog') const pacote = require('pacote') +const { URL } = require('url') const { promisify } = require('util') + +const hostedFromMani = require('./utils/hosted-git-info-from-manifest.js') const openUrl = promisify(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 { URL } = require('url') -const usage = usageUtil('repo', 'npm repo [ [ ...]]') +class Repo { + constructor (npm) { + this.npm = npm + } -const cmd = (args, cb) => repo(args).then(() => cb()).catch(cb) + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil('repo', 'npm repo [ [ ...]]') + } -const repo = async args => { - if (!args || !args.length) - args = ['.'] + exec (args, cb) { + this.repo(args).then(() => cb()).catch(cb) + } - await Promise.all(args.map(pkg => getRepo(pkg))) -} + async repo (args) { + if (!args || !args.length) + args = ['.'] -const getRepo = async pkg => { - const opts = { ...npm.flatOptions, fullMetadata: true } - const mani = await pacote.manifest(pkg, opts) + await Promise.all(args.map(pkg => this.get(pkg))) + } - const r = mani.repository - const rurl = !r ? null - : typeof r === 'string' ? r - : typeof r === 'object' && typeof r.url === 'string' ? r.url - : null + async get (pkg) { + const opts = { ...this.npm.flatOptions, fullMetadata: true } + const mani = await pacote.manifest(pkg, opts) - if (!rurl) { - throw Object.assign(new Error('no repository'), { - pkgid: pkg, - }) - } + const r = mani.repository + const rurl = !r ? null + : typeof r === 'string' ? r + : typeof r === 'object' && typeof r.url === 'string' ? r.url + : null - const info = hostedFromMani(mani) - const url = info ? - info.browse(mani.repository.directory) : unknownHostedUrl(rurl) + if (!rurl) { + throw Object.assign(new Error('no repository'), { + pkgid: pkg, + }) + } - if (!url) { - throw Object.assign(new Error('no repository: could not get url'), { - pkgid: pkg, - }) - } + const info = hostedFromMani(mani) + const url = info ? + info.browse(mani.repository.directory) : unknownHostedUrl(rurl) - log.silly('docs', 'url', url) - await openUrl(url, `${mani.name} repo available at the following URL`) + if (!url) { + throw Object.assign(new Error('no repository: could not get url'), { + pkgid: pkg, + }) + } + + log.silly('docs', 'url', url) + await openUrl(this.npm, url, `${mani.name} repo available at the following URL`) + } } +module.exports = Repo const unknownHostedUrl = url => { try { @@ -67,5 +79,3 @@ const unknownHostedUrl = url => { return null } } - -module.exports = Object.assign(cmd, { usage }) diff --git a/lib/restart.js b/lib/restart.js index 1462cf6051d0f..d5a7789ca92c0 100644 --- a/lib/restart.js +++ b/lib/restart.js @@ -1,2 +1,9 @@ -const npm = require('./npm.js') -module.exports = require('./utils/lifecycle-cmd.js')(npm, 'restart') +const LifecycleCmd = require('./utils/lifecycle-cmd.js') + +// This ends up calling run-script(['restart', ...args]) +class Restart extends LifecycleCmd { + constructor (npm) { + super(npm, 'restart') + } +} +module.exports = Restart diff --git a/lib/root.js b/lib/root.js index 631aef83867d1..8e5ac63d7b9b8 100644 --- a/lib/root.js +++ b/lib/root.js @@ -1,7 +1,22 @@ -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 + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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..cdfd88f10f7b8 100644 --- a/lib/run-script.js +++ b/lib/run-script.js @@ -1,144 +1,157 @@ const runScript = require('@npmcli/run-script') const { isServerPackage } = runScript -const npm = require('./npm.js') const readJson = require('read-package-json-fast') 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 usageUtil = require('./utils/usage.js') +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) -} + /* istanbul ignore next - see test/lib/load-all-commands.js */ + 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(this.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/search.js b/lib/search.js index 3f8fd99fb8ad8..ba6c28163bd97 100644 --- a/lib/search.js +++ b/lib/search.js @@ -5,68 +5,9 @@ const log = require('npmlog') const formatPackageStream = require('./search/format-package-stream.js') const packageFilter = require('./search/package-filter.js') -const npm = require('./npm.js') const output = require('./utils/output.js') const usageUtil = require('./utils/usage.js') -const usage = usageUtil( - 'search', - 'npm search [-l|--long] [--json] [--parseable] [--no-description] [search terms ...]' -) - -const cmd = (args, cb) => search(args).then(() => cb()).catch(cb) - -const search = async (args) => { - const opts = { - ...npm.flatOptions, - ...npm.flatOptions.search, - include: prepareIncludes(args, npm.flatOptions.search.opts), - exclude: prepareExcludes(npm.flatOptions.search.exclude), - } - - if (opts.include.length === 0) - throw new Error('search must be called with arguments') - - // Used later to figure out whether we had any packages go out - let anyOutput = false - - class FilterStream extends Minipass { - write (pkg) { - if (packageFilter(pkg, opts.include, opts.exclude)) - super.write(pkg) - } - } - - const filterStream = new FilterStream() - - // Grab a configured output stream that will spit out packages in the - // desired format. - const outputStream = formatPackageStream({ - args, // --searchinclude options are not highlighted - ...opts, - }) - - log.silly('search', 'searching packages') - const p = new Pipeline( - libSearch.stream(opts.include, opts), - filterStream, - outputStream - ) - - p.on('data', chunk => { - if (!anyOutput) - anyOutput = true - output(chunk.toString('utf8')) - }) - - await p.promise() - if (!anyOutput && !opts.json && !opts.parseable) - output('No matches found for ' + (args.map(JSON.stringify).join(' '))) - - log.silly('search', 'search completed') - log.clearProgress() -} - function prepareIncludes (args, searchopts) { return args .map(s => s.toLowerCase()) @@ -85,4 +26,72 @@ function prepareExcludes (searchexclude) { .filter(s => s) } -module.exports = Object.assign(cmd, { usage }) +class Search { + constructor (npm) { + this.npm = npm + } + + /* istanbul ignore next - see test/lib/load-all-commands.js */ + get usage () { + return usageUtil( + 'search', + 'npm search [-l|--long] [--json] [--parseable] [--no-description] [search terms ...]' + ) + } + + exec (args, cb) { + this.search(args).then(() => cb()).catch(cb) + } + + async search (args) { + const opts = { + ...this.npm.flatOptions, + ...this.npm.flatOptions.search, + include: prepareIncludes(args, this.npm.flatOptions.search.opts), + exclude: prepareExcludes(this.npm.flatOptions.search.exclude), + } + + if (opts.include.length === 0) + throw new Error('search must be called with arguments') + + // Used later to figure out whether we had any packages go out + let anyOutput = false + + class FilterStream extends Minipass { + write (pkg) { + if (packageFilter(pkg, opts.include, opts.exclude)) + super.write(pkg) + } + } + + const filterStream = new FilterStream() + + // Grab a configured output stream that will spit out packages in the + // desired format. + const outputStream = formatPackageStream({ + args, // --searchinclude options are not highlighted + ...opts, + }) + + log.silly('search', 'searching packages') + const p = new Pipeline( + libSearch.stream(opts.include, opts), + filterStream, + outputStream + ) + + p.on('data', chunk => { + if (!anyOutput) + anyOutput = true + output(chunk.toString('utf8')) + }) + + await p.promise() + if (!anyOutput && !opts.json && !opts.parseable) + output('No matches found for ' + (args.map(JSON.stringify).join(' '))) + + log.silly('search', 'search completed') + log.clearProgress() + } +} +module.exports = Search diff --git a/lib/set-script.js b/lib/set-script.js index 7bac6eca50604..25545898e1640 100644 --- a/lib/set-script.js +++ b/lib/set-script.js @@ -1,52 +1,62 @@ const log = require('npmlog') const usageUtil = require('./utils/usage.js') -const { localPrefix } = require('./npm.js') const fs = require('fs') -const usage = usageUtil('set-script', 'npm set-script [