• 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')
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