• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const reifyFinish = require('../utils/reify-finish.js')
2const runScript = require('@npmcli/run-script')
3const fs = require('fs/promises')
4const log = require('../utils/log-shim.js')
5const validateLockfile = require('../utils/validate-lockfile.js')
6
7const ArboristWorkspaceCmd = require('../arborist-cmd.js')
8
9class CI extends ArboristWorkspaceCmd {
10  static description = 'Clean install a project'
11  static name = 'ci'
12
13  // These are in the order they will show up in when running "-h"
14  static params = [
15    'install-strategy',
16    'legacy-bundling',
17    'global-style',
18    'omit',
19    'include',
20    'strict-peer-deps',
21    'foreground-scripts',
22    'ignore-scripts',
23    'audit',
24    'bin-links',
25    'fund',
26    'dry-run',
27    ...super.params,
28  ]
29
30  async exec () {
31    if (this.npm.global) {
32      throw Object.assign(new Error('`npm ci` does not work for global packages'), {
33        code: 'ECIGLOBAL',
34      })
35    }
36
37    const where = this.npm.prefix
38    const Arborist = require('@npmcli/arborist')
39    const opts = {
40      ...this.npm.flatOptions,
41      packageLock: true, // npm ci should never skip lock files
42      path: where,
43      save: false, // npm ci should never modify the lockfile or package.json
44      workspaces: this.workspaceNames,
45    }
46
47    const arb = new Arborist(opts)
48    await arb.loadVirtual().catch(er => {
49      log.verbose('loadVirtual', er.stack)
50      const msg =
51        'The `npm ci` command can only install with an existing package-lock.json or\n' +
52        'npm-shrinkwrap.json with lockfileVersion >= 1. Run an install with npm@5 or\n' +
53        'later to generate a package-lock.json file, then try again.'
54      throw this.usageError(msg)
55    })
56
57    // retrieves inventory of packages from loaded virtual tree (lock file)
58    const virtualInventory = new Map(arb.virtualTree.inventory)
59
60    // build ideal tree step needs to come right after retrieving the virtual
61    // inventory since it's going to erase the previous ref to virtualTree
62    await arb.buildIdealTree()
63
64    // verifies that the packages from the ideal tree will match
65    // the same versions that are present in the virtual tree (lock file)
66    // throws a validation error in case of mismatches
67    const errors = validateLockfile(virtualInventory, arb.idealTree.inventory)
68    if (errors.length) {
69      throw this.usageError(
70        '`npm ci` can only install packages when your package.json and ' +
71        'package-lock.json or npm-shrinkwrap.json are in sync. Please ' +
72        'update your lock file with `npm install` ' +
73        'before continuing.\n\n' +
74        errors.join('\n')
75      )
76    }
77
78    // Only remove node_modules after we've successfully loaded the virtual
79    // tree and validated the lockfile
80    await this.npm.time('npm-ci:rm', async () => {
81      const path = `${where}/node_modules`
82      // get the list of entries so we can skip the glob for performance
83      const entries = await fs.readdir(path, null).catch(er => [])
84      return Promise.all(entries.map(f => fs.rm(`${path}/${f}`, { force: true, recursive: true })))
85    })
86
87    await arb.reify(opts)
88
89    const ignoreScripts = this.npm.config.get('ignore-scripts')
90    // run the same set of scripts that `npm install` runs.
91    if (!ignoreScripts) {
92      const scripts = [
93        'preinstall',
94        'install',
95        'postinstall',
96        'prepublish', // XXX should we remove this finally??
97        'preprepare',
98        'prepare',
99        'postprepare',
100      ]
101      const scriptShell = this.npm.config.get('script-shell') || undefined
102      for (const event of scripts) {
103        await runScript({
104          path: where,
105          args: [],
106          scriptShell,
107          stdio: 'inherit',
108          banner: !this.npm.silent,
109          event,
110        })
111      }
112    }
113    await reifyFinish(this.npm, arb)
114  }
115}
116
117module.exports = CI
118