1// Base class for npm commands 2 3const { relative } = require('path') 4 5const { definitions } = require('@npmcli/config/lib/definitions') 6const getWorkspaces = require('./workspaces/get-workspaces.js') 7const { aliases: cmdAliases } = require('./utils/cmd-list') 8const log = require('./utils/log-shim.js') 9 10class BaseCommand { 11 static workspaces = false 12 static ignoreImplicitWorkspace = true 13 14 // these are all overridden by individual commands 15 static name = null 16 static description = null 17 static params = null 18 19 // this is a static so that we can read from it without instantiating a command 20 // which would require loading the config 21 static get describeUsage () { 22 const seenExclusive = new Set() 23 const wrapWidth = 80 24 const { description, usage = [''], name, params } = this 25 26 const fullUsage = [ 27 `${description}`, 28 '', 29 'Usage:', 30 ...usage.map(u => `npm ${name} ${u}`.trim()), 31 ] 32 33 if (params) { 34 let results = '' 35 let line = '' 36 for (const param of params) { 37 /* istanbul ignore next */ 38 if (seenExclusive.has(param)) { 39 continue 40 } 41 const { exclusive } = definitions[param] 42 let paramUsage = `${definitions[param].usage}` 43 if (exclusive) { 44 const exclusiveParams = [paramUsage] 45 seenExclusive.add(param) 46 for (const e of exclusive) { 47 seenExclusive.add(e) 48 exclusiveParams.push(definitions[e].usage) 49 } 50 paramUsage = `${exclusiveParams.join('|')}` 51 } 52 paramUsage = `[${paramUsage}]` 53 if (line.length + paramUsage.length > wrapWidth) { 54 results = [results, line].filter(Boolean).join('\n') 55 line = '' 56 } 57 line = [line, paramUsage].filter(Boolean).join(' ') 58 } 59 fullUsage.push('') 60 fullUsage.push('Options:') 61 fullUsage.push([results, line].filter(Boolean).join('\n')) 62 } 63 64 const aliases = Object.entries(cmdAliases).reduce((p, [k, v]) => { 65 return p.concat(v === name ? k : []) 66 }, []) 67 68 if (aliases.length) { 69 const plural = aliases.length === 1 ? '' : 'es' 70 fullUsage.push('') 71 fullUsage.push(`alias${plural}: ${aliases.join(', ')}`) 72 } 73 74 fullUsage.push('') 75 fullUsage.push(`Run "npm help ${name}" for more info`) 76 77 return fullUsage.join('\n') 78 } 79 80 constructor (npm) { 81 this.npm = npm 82 83 const { config } = this.npm 84 85 if (!this.constructor.skipConfigValidation) { 86 config.validate() 87 } 88 89 if (config.get('workspaces') === false && config.get('workspace').length) { 90 throw new Error('Can not use --no-workspaces and --workspace at the same time') 91 } 92 } 93 94 get name () { 95 return this.constructor.name 96 } 97 98 get description () { 99 return this.constructor.description 100 } 101 102 get params () { 103 return this.constructor.params 104 } 105 106 get usage () { 107 return this.constructor.describeUsage 108 } 109 110 usageError (prefix = '') { 111 if (prefix) { 112 prefix += '\n\n' 113 } 114 return Object.assign(new Error(`\n${prefix}${this.usage}`), { 115 code: 'EUSAGE', 116 }) 117 } 118 119 async cmdExec (args) { 120 const { config } = this.npm 121 122 if (config.get('usage')) { 123 return this.npm.output(this.usage) 124 } 125 126 const hasWsConfig = config.get('workspaces') || config.get('workspace').length 127 // if cwd is a workspace, the default is set to [that workspace] 128 const implicitWs = config.get('workspace', 'default').length 129 130 // (-ws || -w foo) && (cwd is not a workspace || command is not ignoring implicit workspaces) 131 if (hasWsConfig && (!implicitWs || !this.constructor.ignoreImplicitWorkspace)) { 132 if (this.npm.global) { 133 throw new Error('Workspaces not supported for global packages') 134 } 135 if (!this.constructor.workspaces) { 136 throw Object.assign(new Error('This command does not support workspaces.'), { 137 code: 'ENOWORKSPACES', 138 }) 139 } 140 return this.execWorkspaces(args) 141 } 142 143 return this.exec(args) 144 } 145 146 // Compare the number of entries with what was expected 147 checkExpected (entries) { 148 if (!this.npm.config.isDefault('expect-results')) { 149 const expected = this.npm.config.get('expect-results') 150 if (!!entries !== !!expected) { 151 log.warn(this.name, `Expected ${expected ? '' : 'no '}results, got ${entries}`) 152 process.exitCode = 1 153 } 154 } else if (!this.npm.config.isDefault('expect-result-count')) { 155 const expected = this.npm.config.get('expect-result-count') 156 if (expected !== entries) { 157 /* eslint-disable-next-line max-len */ 158 log.warn(this.name, `Expected ${expected} result${expected === 1 ? '' : 's'}, got ${entries}`) 159 process.exitCode = 1 160 } 161 } 162 } 163 164 async setWorkspaces () { 165 const includeWorkspaceRoot = this.isArboristCmd 166 ? false 167 : this.npm.config.get('include-workspace-root') 168 169 const prefixInsideCwd = relative(this.npm.localPrefix, process.cwd()).startsWith('..') 170 const relativeFrom = prefixInsideCwd ? this.npm.localPrefix : process.cwd() 171 172 const filters = this.npm.config.get('workspace') 173 const ws = await getWorkspaces(filters, { 174 path: this.npm.localPrefix, 175 includeWorkspaceRoot, 176 relativeFrom, 177 }) 178 179 this.workspaces = ws 180 this.workspaceNames = [...ws.keys()] 181 this.workspacePaths = [...ws.values()] 182 } 183} 184module.exports = BaseCommand 185