base-command.js 4.5 KB

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