• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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