npm prune --production (after npm ci only) erroneously uninstalls regular dependency

What I Wanted to Do

Produce a production-only build with only dev dependencies pruned.

What Happened Instead

One of the dependencies is erroneously removed despite it not being a dev dependency.

Reproduction Steps

Our CICD pipeline runs the following commands:

npm ci
npm prune --production

For some reason, one of the packages which is not flagged as a dev dependency in package-lock.json is erroneously removed. It’s not a devDependency in package.json either, and nothing has it as a dependency other than the root package.json. Strangely, if you do this sequence of events:

npm install
npm prune --production

…the package is not erroneously removed. This leads me to speculate that the version of the package.json for the dependency has an impact on things, since as far as I can tell that’s the only file difference produced between npm install and npm ci.

Details

The entry in package-lock.json looks like this:

"@gasket/mocha-plugin": {
  "version": "1.1.1",
  "resolved": "<url elided>/@gasket/mocha-plugin/-/@gasket/mocha-plugin-1.1.1.tgz",
  "integrity": "sha1-p4xEaJEKxREiBT5wp+UZEMkRjRc="
}

…with a package.json entry like:

"dependencies": {
  "@gasket/mocha-plugin": "latest"
}

Platform Info

$ npm --versions

    { 'seechange-pwa': '0.0.0',
      npm: '6.7.0',
      ares: '1.15.0',
      cldr: '33.1',
      http_parser: '2.8.0',
      icu: '62.1',
      modules: '64',
      napi: '3',
      nghttp2: '1.34.0',
      node: '10.15.0',
      openssl: '1.1.0j',
      tz: '2018e',
      unicode: '11.0',
      uv: '1.23.2',
      v8: '6.8.275.32-node.45',
      zlib: '1.2.11' }

$ node -p process.platform

    darwin

Also strange; a very similar package named @gasket/lint-plugin which is also a regular dependency does not get installed with npm prune --production. The _ contents tacked on in the dependency’s package.json look the same, so maybe it’s something more than just package.json breaking things :confused:

Is there something caused by the name of the package itself, maybe? I do know that it’s npm prune that’s removing the file because doing npm prune --production --dry-run --json shows that it intends to remove the package. This is the only one that’s being erroneously removed.

What criteria are used when selecting packages for removal? Anything undocumented that could be accounting for this?

Another clue. Doing an npm ls after an npm ci shows:

npm ERR! invalid: @gasket/mocha-plugin@1.1.1 /Users/jpage/Code/seechange-pwa/node_modules/@gasket/mocha-plugin

Is there any way to get details on why a package is flagged as invalid?

Because you’re using latest. Try putting * in there instead.

OK, I think I found the difference. If package.json has:

"dependencies": {
  "@gasket/mocha-plugin": "latest"
}

…then the package is flagged as invalid and gets removed. If it contains a version instead:

"dependencies": {
  "@gasket/mocha-plugin": "^1.1.1"
}

…then it is not. It appears that the node_modules/@gasket/mocha-plugin/package.json is generated differently when doing an npm ci:

"_from": "@gasket/mocha-plugin@1.1.1"

…versus npm i:

"_from": "@gasket/mocha-plugin@latest"

The mismatch between _from and the root package.json seems to be the cause for the package being flagged as invalid and its removal.

I’m guessing this should count as a bug in npm ci.

yup. Might be pretty straightforward to fix, too.

I actually just ran into this issue as well when using dev tags in my dependencies. It seems like this may be an issue with npm ci, npm prune --production and tags. My team is currently developing several libraries and services with node and the result is sometimes we are using: "somePackage": "dev" to keep up to date with the frequent changes until we can get to a point where we have a stable release.

Working:

// package.json - "somePackage": "dev"
npm i
npm prune --production

Working:

// package.json - "somePackage": "dev"
npm ci

Working:

// package.json - "somePackage": "0.1.1-dev.0"
npm ci
npm prune --production

Not working:

// package.json - "somePackage": "dev"
npm ci
npm prune --production

I do not know if this is the right way to go (that condition must have been there for a reason), and if it is it should probably include pinned (version) deps:

1 Like

Probably doesn’t mean much coming from someone who has never contributed to the npm repository but the code looks good to me!

1 Like

I think a teammate is hitting a similar issue with npm update after an npm ci causing packages with a “latest” in the version number to be removed. Would this same bug be the root cause of that?

Seeing the same issue with packages tagged as beta.
//package.json
“somePackage”: “beta”
npm ci
npm prune --production

The beta packages are getting removed even though they are a regular dependency