Skip to content

[BUG] npx always re-reifies directory/file: specs even when already cached #9251

@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

Every invocation of npx <cmd> that resolves to a local directory (file: spec) triggers a full reify() cycle, even when the package is already installed in the npx cache. Registry packages correctly skip the reify on subsequent runs.

This happens regardless of install-strategy — both hoisted and linked are affected.

Expected Behavior

npx should detect that a directory spec is already installed in the npx cache and skip the reify step when the package is unchanged, just as it does for registry packages.

Steps To Reproduce

rm -rf /tmp/host-app ~/.npm/_npx
mkdir -p /tmp/host-app/dist

cat > /tmp/host-app/package.json << 'EOF'
{
  "name": "host-app",
  "version": "1.0.0",
  "bin": { "my-tool": "dist/main.js" }
}
EOF

cat > /tmp/host-app/dist/main.js << 'EOF'
#!/usr/bin/env node
console.log('my-tool ran')
EOF
chmod +x /tmp/host-app/dist/main.js

cd /tmp/host-app
npm install
npx my-tool        # run 1 — reifies (expected, cold cache)
npx my-tool        # run 2 — reifies again (unexpected)
npx my-tool        # run 3 — reifies again (unexpected)

Contrast with a registry package:

rm -rf ~/.npm/_npx
npx cowsay@1.6.0 hello   # run 1 — reifies (expected)
npx cowsay@1.6.0 hello   # run 2 — skips reify (cache hit)

Confirmed with debug logging

Adding console.error('DEBUG add:', JSON.stringify(add)) before the if (add.length) check in libnpmexec/lib/index.js:

# directory spec — add is always non-empty
Run 1: DEBUG add: ["file:/private/tmp/host-app"]
Run 2: DEBUG add: ["file:/private/tmp/host-app"]  ← should be []

# registry spec — add is empty on cache hit
Run 1: DEBUG add: ["cowsay@1.6.0"]
Run 2: DEBUG add: []                              ← correctly cached

Environment

  • npm: 11.12.1
  • Node.js: v24.14.0
  • OS Name: macOS (Darwin 25.4.0)

Root Cause Analysis

In libnpmexec/lib/index.js, missingFromTree() has an early return for directory specs that bypasses the cache check:

if (spec.type === 'directory') {
  return { manifest }
}

Returning { manifest } (without a node) signals "not in tree — needs install." This causes the caller to unconditionally push the package onto the add list and call npxArb.reify().

For registry specs, the function instead walks the tree looking for a matching resolved:

for (const node of nodesByManifest) {
  if (node.package.resolved === manifest._resolved) {
    return { node }  // found in cache — no install needed
  }
}

Directory specs skip this check entirely, so the npx cache is never consulted.

Surfaced while investigating #9210.

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