install.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. const { readdir } = require('node:fs/promises')
  2. const { resolve, join } = require('node:path')
  3. const { log } = require('proc-log')
  4. const runScript = require('@npmcli/run-script')
  5. const pacote = require('pacote')
  6. const checks = require('npm-install-checks')
  7. const reifyFinish = require('../utils/reify-finish.js')
  8. const ArboristWorkspaceCmd = require('../arborist-cmd.js')
  9. class Install extends ArboristWorkspaceCmd {
  10. static description = 'Install a package'
  11. static name = 'install'
  12. // These are in the order they will show up in when running "-h" If adding to this list, consider adding also to ci.js
  13. static params = [
  14. 'save',
  15. 'save-exact',
  16. 'global',
  17. 'install-strategy',
  18. 'legacy-bundling',
  19. 'global-style',
  20. 'omit',
  21. 'include',
  22. 'strict-peer-deps',
  23. 'prefer-dedupe',
  24. 'package-lock',
  25. 'package-lock-only',
  26. 'foreground-scripts',
  27. 'ignore-scripts',
  28. 'allow-git',
  29. 'audit',
  30. 'before',
  31. 'min-release-age',
  32. 'bin-links',
  33. 'fund',
  34. 'dry-run',
  35. 'cpu',
  36. 'os',
  37. 'libc',
  38. ...super.params,
  39. ]
  40. static usage = ['[<package-spec> ...]']
  41. static async completion (opts) {
  42. const { partialWord } = opts
  43. // install can complete to a folder with a package.json, or any package.
  44. // if it has a slash, then it's gotta be a folder
  45. // if it starts with https?://, then just give up, because it's a url
  46. if (/^https?:\/\//.test(partialWord)) {
  47. // do not complete to URLs
  48. return []
  49. }
  50. if (/\//.test(partialWord)) {
  51. // Complete fully to folder if there is exactly one match and it is a folder containing a package.json file.
  52. // If that is not the case we return 0 matches, which will trigger the default bash complete.
  53. const lastSlashIdx = partialWord.lastIndexOf('/')
  54. const partialName = partialWord.slice(lastSlashIdx + 1)
  55. const partialPath = partialWord.slice(0, lastSlashIdx) || '/'
  56. const isDirMatch = async sibling => {
  57. if (sibling.slice(0, partialName.length) !== partialName) {
  58. return false
  59. }
  60. try {
  61. const contents = await readdir(join(partialPath, sibling))
  62. const result = (contents.indexOf('package.json') !== -1)
  63. return result
  64. } catch {
  65. return false
  66. }
  67. }
  68. try {
  69. const siblings = await readdir(partialPath)
  70. const matches = []
  71. for (const sibling of siblings) {
  72. if (await isDirMatch(sibling)) {
  73. matches.push(sibling)
  74. }
  75. }
  76. if (matches.length === 1) {
  77. return [join(partialPath, matches[0])]
  78. }
  79. // no matches
  80. return []
  81. } catch {
  82. return [] // invalid dir: no matching
  83. }
  84. }
  85. // Note: there used to be registry completion here, but it stopped making sense somewhere around 50,000 packages on the registry
  86. }
  87. async exec (args) {
  88. // the /path/to/node_modules/..
  89. const globalTop = resolve(this.npm.globalDir, '..')
  90. const ignoreScripts = this.npm.config.get('ignore-scripts')
  91. const isGlobalInstall = this.npm.global
  92. const where = isGlobalInstall ? globalTop : this.npm.prefix
  93. const forced = this.npm.config.get('force')
  94. const scriptShell = this.npm.config.get('script-shell') || undefined
  95. // be very strict about engines when trying to update npm itself
  96. const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm')
  97. if (isGlobalInstall && npmInstall) {
  98. const npmOptions = this.npm.flatOptions
  99. const npmManifest = await pacote.manifest(npmInstall, npmOptions)
  100. try {
  101. checks.checkEngine(npmManifest, npmManifest.version, process.version)
  102. } catch (e) {
  103. if (forced) {
  104. log.warn(
  105. 'install',
  106. `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`
  107. )
  108. } else {
  109. throw e
  110. }
  111. }
  112. }
  113. // don't try to install the prefix into itself
  114. args = args.filter(a => resolve(a) !== this.npm.prefix)
  115. // `npm i -g` => "install this package globally"
  116. if (isGlobalInstall && !args.length) {
  117. args = ['.']
  118. }
  119. // throw usage error if trying to install empty package name to global space, e.g: `npm i -g ""`
  120. if (where === globalTop && !args.every(Boolean)) {
  121. throw this.usageError()
  122. }
  123. const Arborist = require('@npmcli/arborist')
  124. const opts = {
  125. ...this.npm.flatOptions,
  126. auditLevel: null,
  127. path: where,
  128. add: args,
  129. workspaces: this.workspaceNames,
  130. }
  131. const arb = new Arborist(opts)
  132. await arb.reify(opts)
  133. if (!args.length && !isGlobalInstall && !ignoreScripts) {
  134. const scripts = [
  135. 'preinstall',
  136. 'install',
  137. 'postinstall',
  138. 'prepublish', // XXX(npm9) should we remove this finally??
  139. 'preprepare',
  140. 'prepare',
  141. 'postprepare',
  142. ]
  143. for (const event of scripts) {
  144. await runScript({
  145. path: where,
  146. args: [],
  147. scriptShell,
  148. stdio: 'inherit',
  149. event,
  150. })
  151. }
  152. }
  153. await reifyFinish(this.npm, arb)
  154. }
  155. }
  156. module.exports = Install