npm recursively deletes *every* file in project root under some conditions

What I Wanted to Do

If you create a symlink within node_modules then the npm tree diff/clean algorithm will follow the symlink and nuke everything past the symlink target.

This behavior changed sometime between nodejs version 12.4.0 and 12.11.1. Creating a symlink here is a common way to avoid require('../../../../') hell, as documented here: https://gist.github.com/branneman/8048520

This is also quite a destructive bug leading to rimraf running rampant on your filesystem deleting everything in its path including all source control data. Luckily I have backups but others certainly won’t be so fortunate.

What Happened Instead

npm follows the symlink and deletes the entire project root. Silly log reveals the following:

npm sill currentTree tmp@1.0.0
npm sill currentTree β”œβ”€β”€ @/.DS_Store
npm sill currentTree β”œβ”€β”€ @/index.js
npm sill currentTree β”œβ”€β”€ @/node_modules
npm sill currentTree β”œβ”€β”€ @/package-lock.json
npm sill currentTree β”œβ”€β”€ @/package.json
npm sill currentTree β”œβ”€β”€ js-tokens@4.0.0
npm sill currentTree β”œβ”€β”€ loose-envify@1.4.0
npm sill currentTree β”œβ”€β”€ object-assign@4.1.1
npm sill currentTree β”œβ”€β”€ prop-types@15.7.2
npm sill currentTree β”œβ”€β”€ react-is@16.10.2
npm sill currentTree └── react@16.10.2
npm sill idealTree tmp@1.0.0
npm sill idealTree β”œβ”€β”€ js-tokens@4.0.0
npm sill idealTree β”œβ”€β”€ loose-envify@1.4.0
npm sill idealTree β”œβ”€β”€ object-assign@4.1.1
npm sill idealTree β”œβ”€β”€ prop-types@15.7.2
npm sill idealTree β”œβ”€β”€ react-is@16.10.2
npm sill idealTree β”œβ”€β”€ react@16.10.2
npm sill idealTree β”œβ”€β”€ redux@4.0.4
npm sill idealTree └── symbol-observable@1.2.0
npm sill install generateActionsToTake
npm timing stage:generateActionsToTake Completed in 4ms
npm sill diffTrees action count 7
npm sill diffTrees remove @/.DS_Store
npm sill diffTrees remove @/index.js
npm sill diffTrees remove @/node_modules
npm sill diffTrees remove @/package-lock.json
npm sill diffTrees remove @/package.json
npm sill diffTrees add symbol-observable@1.2.0
npm sill diffTrees add redux@4.0.4
npm sill decomposeActions action count 26
npm sill decomposeActions unbuild @/.DS_Store
npm sill decomposeActions remove @/.DS_Store
npm sill decomposeActions unbuild @/index.js
npm sill decomposeActions remove @/index.js
npm sill decomposeActions unbuild @/node_modules
npm sill decomposeActions remove @/node_modules
npm sill decomposeActions unbuild @/package-lock.json
npm sill decomposeActions remove @/package-lock.json
npm sill decomposeActions unbuild @/package.json
npm sill decomposeActions remove @/package.json

Ok fine, this isn’t supported anymore. Please just delete the symlink instead of my entire project. This is bonkers.

Reproduction Steps

This will only work on Linux or OS X. Create a package.json file with the following content:

{
  "name": "tmp",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "postinstall": "ln -nsf .. node_modules/\\@"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.10.2"
  }
}

Also create an index.js file with any content. Now run npm install. Notice that the postinstall script creates a symlink in node_modules pointing to the project root.

Now install something else, for instance npm install redux. npm at this point permanently deletes everything in the project root.

Details

Platform Info

$ npm --versions
{
  tmp: '1.0.0',
  npm: '6.11.3',
  ares: '1.15.0',
  brotli: '1.0.7',
  cldr: '35.1',
  http_parser: '2.8.0',
  icu: '64.2',
  llhttp: '1.1.4',
  modules: '72',
  napi: '5',
  nghttp2: '1.39.2',
  node: '12.11.1',
  openssl: '1.1.1c',
  tz: '2019a',
  unicode: '12.1',
  uv: '1.32.0',
  v8: '7.7.299.11-node.12',
  zlib: '1.2.11'
}

$ node -p process.platform
darwin

(Arg! Good details thanks.)

Also reported here: npm install deletes almost all my code