Skip to content

[BUG] install-strategy=linked runs postinstall scripts twice for store packages #9012

@manzoorwanijk

Description

@manzoorwanijk

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

Current Behavior

With install-strategy=linked, postinstall scripts run twice for every package in the store — once for the store entry and once for the symlink pointing to it. This causes race conditions in packages like esbuild whose postinstall modifies files in-place.

Specifically, esbuild's install.js has a maybeOptimizePackage() step that hard-links the native binary over its JS wrapper (bin/esbuild). When two instances run in parallel against the same directory:

  1. First run: fs.linkSync(binPath, tempPath) succeeds, fs.renameSync(tempPath, toPath) replaces the JS wrapper with the native binary
  2. Second run: fs.linkSync(binPath, tempPath) fails with EEXIST (caught silently), falls back to node bin/esbuild --version
  3. But bin/esbuild is now a Mach-O binary (from step 1), not a JS file → SyntaxError: Invalid or unexpected token

The install fails with:

npm error code 1
npm error path .../node_modules/.store/esbuild@0.27.2-.../node_modules/esbuild
npm error esbuild@0.27.2 postinstall: `node install.js`

Debug log confirms two runs at the same path:

info run esbuild@0.27.2 postinstall node_modules/.store/esbuild@0.27.2-xxx/node_modules/esbuild node install.js
info run esbuild@0.27.2 postinstall node_modules/.store/esbuild@0.27.2-xxx/node_modules/esbuild node install.js

Expected Behavior

Postinstall scripts should run once per package — on the store entry only. The store link (symlink) should be skipped since it points to the same directory.

Steps To Reproduce

rm -rf /tmp/esbuild-test
mkdir /tmp/esbuild-test && cd /tmp/esbuild-test

cat > package.json << 'EOF'
{
  "name": "esbuild-test",
  "devDependencies": {
    "esbuild": "0.27.2"
  }
}
EOF

npm install --install-strategy=linked
# Fails with SyntaxError: Invalid or unexpected token

Root Cause

In rebuild.js, #runScripts has a guard intended to skip store links:

const { ..., isStoreLink } = node.target
if (this[_trashList].has(path) || isStoreLink) { return }

The problem is that isStoreLink is destructured from node.target (the store entry), but isStoreLink is only set on node itself (the link). Store entries don't have isStoreLink, so it's always undefined, and the guard never triggers.

Both the store entry and its symlink end up in the build queue (via #buildQueues#retrieveNodesByType), and both run the same postinstall script against the same directory in parallel.

Environment

  • npm: latest (main branch)
  • Node.js: 22.x
  • OS Name: macOS (Apple Silicon)
  • npm config:
    install-strategy=linked

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions