install.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /* eslint-disable camelcase */
  2. /* eslint-disable standard/no-callback-literal */
  3. const fs = require('fs')
  4. const util = require('util')
  5. const readdir = util.promisify(fs.readdir)
  6. const reifyFinish = require('./utils/reify-finish.js')
  7. const log = require('npmlog')
  8. const { resolve, join } = require('path')
  9. const Arborist = require('@npmcli/arborist')
  10. const runScript = require('@npmcli/run-script')
  11. const pacote = require('pacote')
  12. const checks = require('npm-install-checks')
  13. const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
  14. class Install extends ArboristWorkspaceCmd {
  15. /* istanbul ignore next - see test/lib/load-all-commands.js */
  16. static get description () {
  17. return 'Install a package'
  18. }
  19. /* istanbul ignore next - see test/lib/load-all-commands.js */
  20. static get name () {
  21. return 'install'
  22. }
  23. /* istanbul ignore next - see test/lib/load-all-commands.js */
  24. static get params () {
  25. return [
  26. 'save',
  27. 'save-exact',
  28. 'global',
  29. 'global-style',
  30. 'legacy-bundling',
  31. 'strict-peer-deps',
  32. 'package-lock',
  33. 'omit',
  34. 'ignore-scripts',
  35. 'audit',
  36. 'bin-links',
  37. 'fund',
  38. 'dry-run',
  39. ...super.params,
  40. ]
  41. }
  42. /* istanbul ignore next - see test/lib/load-all-commands.js */
  43. static get usage () {
  44. return [
  45. '[<@scope>/]<pkg>',
  46. '[<@scope>/]<pkg>@<tag>',
  47. '[<@scope>/]<pkg>@<version>',
  48. '[<@scope>/]<pkg>@<version range>',
  49. '<alias>@npm:<name>',
  50. '<folder>',
  51. '<tarball file>',
  52. '<tarball url>',
  53. '<git:// url>',
  54. '<github username>/<github project>',
  55. ]
  56. }
  57. async completion (opts) {
  58. const { partialWord } = opts
  59. // install can complete to a folder with a package.json, or any package.
  60. // if it has a slash, then it's gotta be a folder
  61. // if it starts with https?://, then just give up, because it's a url
  62. if (/^https?:\/\//.test(partialWord)) {
  63. // do not complete to URLs
  64. return []
  65. }
  66. if (/\//.test(partialWord)) {
  67. // Complete fully to folder if there is exactly one match and it
  68. // is a folder containing a package.json file. If that is not the
  69. // case we return 0 matches, which will trigger the default bash
  70. // complete.
  71. const lastSlashIdx = partialWord.lastIndexOf('/')
  72. const partialName = partialWord.slice(lastSlashIdx + 1)
  73. const partialPath = partialWord.slice(0, lastSlashIdx) || '/'
  74. const annotatePackageDirMatch = async (sibling) => {
  75. const fullPath = join(partialPath, sibling)
  76. if (sibling.slice(0, partialName.length) !== partialName)
  77. return null // not name match
  78. try {
  79. const contents = await readdir(fullPath)
  80. return {
  81. fullPath,
  82. isPackage: contents.indexOf('package.json') !== -1,
  83. }
  84. } catch (er) {
  85. return { isPackage: false }
  86. }
  87. }
  88. try {
  89. const siblings = await readdir(partialPath)
  90. const matches = await Promise.all(
  91. siblings.map(async sibling => {
  92. return await annotatePackageDirMatch(sibling)
  93. })
  94. )
  95. const match = matches.filter(el => !el || el.isPackage).pop()
  96. if (match) {
  97. // Success - only one match and it is a package dir
  98. return [match.fullPath]
  99. } else {
  100. // no matches
  101. return []
  102. }
  103. } catch (er) {
  104. return [] // invalid dir: no matching
  105. }
  106. }
  107. // Note: there used to be registry completion here,
  108. // but it stopped making sense somewhere around
  109. // 50,000 packages on the registry
  110. }
  111. exec (args, cb) {
  112. this.install(args).then(() => cb()).catch(cb)
  113. }
  114. async install (args) {
  115. // the /path/to/node_modules/..
  116. const globalTop = resolve(this.npm.globalDir, '..')
  117. const ignoreScripts = this.npm.config.get('ignore-scripts')
  118. const isGlobalInstall = this.npm.config.get('global')
  119. const where = isGlobalInstall ? globalTop : this.npm.prefix
  120. const forced = this.npm.config.get('force')
  121. const isDev = this.npm.config.get('dev')
  122. const scriptShell = this.npm.config.get('script-shell') || undefined
  123. // be very strict about engines when trying to update npm itself
  124. const npmInstall = args.find(arg => arg.startsWith('npm@') || arg === 'npm')
  125. if (isGlobalInstall && npmInstall) {
  126. const npmOptions = this.npm.flatOptions
  127. const npmManifest = await pacote.manifest(npmInstall, npmOptions)
  128. try {
  129. checks.checkEngine(npmManifest, npmManifest.version, process.version)
  130. } catch (e) {
  131. if (forced)
  132. this.npm.log.warn('install', `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}`)
  133. else
  134. throw e
  135. }
  136. }
  137. // don't try to install the prefix into itself
  138. args = args.filter(a => resolve(a) !== this.npm.prefix)
  139. // `npm i -g` => "install this package globally"
  140. if (where === globalTop && !args.length)
  141. args = ['.']
  142. // TODO: Add warnings for other deprecated flags? or remove this one?
  143. if (isDev)
  144. log.warn('install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.')
  145. const opts = {
  146. ...this.npm.flatOptions,
  147. log: this.npm.log,
  148. auditLevel: null,
  149. path: where,
  150. add: args,
  151. workspaces: this.workspaceNames,
  152. }
  153. const arb = new Arborist(opts)
  154. await arb.reify(opts)
  155. if (!args.length && !isGlobalInstall && !ignoreScripts) {
  156. const scripts = [
  157. 'preinstall',
  158. 'install',
  159. 'postinstall',
  160. 'prepublish', // XXX should we remove this finally??
  161. 'preprepare',
  162. 'prepare',
  163. 'postprepare',
  164. ]
  165. for (const event of scripts) {
  166. await runScript({
  167. path: where,
  168. args: [],
  169. scriptShell,
  170. stdio: 'inherit',
  171. stdioString: true,
  172. banner: log.level !== 'silent',
  173. event,
  174. })
  175. }
  176. }
  177. await reifyFinish(this.npm, arb)
  178. }
  179. }
  180. module.exports = Install