@@ -6,6 +6,7 @@ const runScript = require('@npmcli/run-script')
66const fs = require ( 'fs' )
77const readdir = util . promisify ( fs . readdir )
88const log = require ( '../utils/log-shim.js' )
9+ const validateLockfile = require ( '../utils/validate-lockfile.js' )
910
1011const removeNodeModules = async where => {
1112 const rimrafOpts = { glob : false }
@@ -37,6 +38,7 @@ class CI extends ArboristWorkspaceCmd {
3738 const where = this . npm . prefix
3839 const opts = {
3940 ...this . npm . flatOptions ,
41+ packageLock : true , // npm ci should never skip lock files
4042 path : where ,
4143 log,
4244 save : false , // npm ci should never modify the lockfile or package.json
@@ -55,6 +57,28 @@ class CI extends ArboristWorkspaceCmd {
5557 } ) ,
5658 removeNodeModules ( where ) ,
5759 ] )
60+
61+ // retrieves inventory of packages from loaded virtual tree (lock file)
62+ const virtualInventory = new Map ( arb . virtualTree . inventory )
63+
64+ // build ideal tree step needs to come right after retrieving the virtual
65+ // inventory since it's going to erase the previous ref to virtualTree
66+ await arb . buildIdealTree ( )
67+
68+ // verifies that the packages from the ideal tree will match
69+ // the same versions that are present in the virtual tree (lock file)
70+ // throws a validation error in case of mismatches
71+ const errors = validateLockfile ( virtualInventory , arb . idealTree . inventory )
72+ if ( errors . length ) {
73+ throw new Error (
74+ '`npm ci` can only install packages when your package.json and ' +
75+ 'package-lock.json or npm-shrinkwrap.json are in sync. Please ' +
76+ 'update your lock file with `npm install` ' +
77+ 'before continuing.\n\n' +
78+ errors . join ( '\n' ) + '\n'
79+ )
80+ }
81+
5882 await arb . reify ( opts )
5983
6084 const ignoreScripts = this . npm . config . get ( 'ignore-scripts' )
0 commit comments