Skip to content

[BUG] linked strategy: calcDepFlags produces incorrect dev flags due to multiple Links overwriting target #9100

Closed

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

When using install-strategy=linked, npm query reports incorrect dev flags on packages. The bug has two layers:

1. calcDepFlags: unconditional flag assignment overwrites correct values

When a target Node is reachable through multiple Links (common in linked strategy where each workspace has its own node_modules/ with Links), calcDepFlags unconditionally assigns target.dev = link.dev for each Link visited. The last Link processed wins — if it came through a dev path, it overwrites a previously correct dev=false.

2. loadActual: workspace edges resolve to null when workspaces are not in root node_modules/

In the linked strategy, only workspaces explicitly declared in root's dependencies/devDependencies get symlinks in root's node_modules/. Undeclared workspaces (those discovered only via the workspaces field) only get self-links in their own node_modules/. This means root's workspace edges resolve to null, calcDepFlags can't reach them, and ALL packages end up extraneous=true and dev=true.

3. edgesIn is always empty on store target nodes

In the linked strategy, edges resolve to Link nodes, not their targets. So from is always [] in npm query output.

These break any tooling that relies on npm query with .prod or .dev selectors.

See: WordPress/gutenberg#74429, WordPress/gutenberg#75039

Expected Behavior

npm query with linked strategy should produce the same dev flag classification as hoisted strategy.

Root Cause (calcDepFlags)

In calcDepFlags (arborist lib/calc-dep-flags.js), the Link handling unconditionally assigns flags:

if (node.isLink) {
  node.target.dev = node.dev          // ← can overwrite false back to true
  node.target.optional = node.optional
  // ...
  queue.push(node.target)
}

In the linked strategy, the same target node can have multiple Links from different locations:

  • node_modules/@test/tools → packages/tools/ (root-level link, prod edge from root)
  • packages/app/node_modules/@test/tools → packages/tools/ (workspace-level link, dev edge from @test/app)

The second Link overwrites the correct dev=false with dev=true.

Fix: Only unset flags (true→false), never set them back — matching the pattern already used for edge processing:

if (node.target.dev && !node.dev) {
  node.target.dev = false
  changed = true
}

Steps To Reproduce

mkdir test-linked && cd test-linked
mkdir -p packages/app packages/tools

cat > package.json << 'EOF'
{
  "name": "test-linked",
  "version": "1.0.0",
  "workspaces": ["packages/*"]
}
EOF

cat > packages/app/package.json << 'EOF'
{
  "name": "@test/app",
  "version": "1.0.0",
  "dependencies": { "abbrev": "^2.0.0" },
  "devDependencies": { "@test/tools": "*" }
}
EOF

cat > packages/tools/package.json << 'EOF'
{
  "name": "@test/tools",
  "version": "1.0.0",
  "dependencies": { "nopt": "^7.0.0" }
}
EOF

echo "install-strategy=linked" > .npmrc
npm install
npm query ':is(.prod)' | node -e "
  const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
  d.forEach(p => console.log(p.name, 'dev='+p.dev))
"

Real-World Impact (Gutenberg)

Gutenberg uses install-strategy=linked in .npmrc. The license check script runs:

const query = `.workspace:attr([wpScript],[wpScriptModuleExports]) :is(.prod):not(...)`;
const child = spawn.sync("npm", ["query", query]);

This incorrectly includes packages like @bufbuild/protobuf (Apache-2.0) and rxjs (Apache-2.0) which are transitive dependencies of sass-embedded — a dev-only build tool. These packages are flagged as GPL2-incompatible license violations, even though they are never distributed with WordPress.

The workaround was to rewrite the license checker to use Node's native module resolution instead of npm query (see PR #75039).

Environment

  • npm: 11.11.0 and 10.9.5
  • Node.js: v22.20.0
  • OS Name: macOS (Darwin 25.3.0)
  • npm config:
install-strategy=linked

Activity

  1. manzoorwanijk commented on Mar 10, 2026

    @manzoorwanijk
    ContributorAuthor

    @wraithgar there seems to be the same issue in the default hoisted strategy 🤔

  2. wraithgar commented on Mar 10, 2026

    @wraithgar
    Contributor

    A clue here is that even without the app workspace created, the tools workspace and abbrev and nopt show up as .prod dependencies.

    Workspaces are added to the tree even if they're not declared in the root package.json. So when another workspace eventually does require one as adevDependency, it does so in an environment where that workspace has already been brought into the tree as a production dependency.

    One solution here is to specifically declare these workspaces as dev dependencies. Workspaces are always in your tree by design. Change their default to a devDependency would be a breaking change and not one to be taken lightly.

    Another approach is the one taken in this repo itself, and that is to declare all dependencies in these dev workspaces as dev dependencies. The smoke-tests workspace is a good example. That is not declared in the package.json but it does show up as a "production" dependency in npm query. However, because it is a workspace and not in dependencies it is not included or looked for when npm itself is being installed.

    The concept of "dependency" vs "dev dependency" is really only to distinguish between which packages will be required when consuming a package as a registry package. When it is a local workspace these distinctions aren't "real". It's perfectly fine to set all dependencies in them as "dev" since they are not published packages.

  3. wraithgar commented on Mar 10, 2026

    @wraithgar
    Contributor

    Example from npm itself:

    $ npm query '.prod'|npx json -a name|grep smoke
    @npmcli/smoke-tests
    $ npm pkg get dependencies|grep smoke
    $ npm query '.workspace:attr([name=@npmcli/smoke-tests])'|npx json -a name
    @npmcli/smoke-tests
  4. added a commit that references this issue on Jun 25, 2026
    f9e3a80
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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