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