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