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