From 37ae5a53632704d92c28b38784d2daa9b793f6c0 Mon Sep 17 00:00:00 2001 From: Sanjays2402 <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:46:12 -0700 Subject: [PATCH] fix(arborist): honor omit flags for deps of linked-in packages (#9624) Node#shouldOmit bailed out whenever a node's `top` was not the project root or a workspace, returning false unconditionally. For nodes inside a `npm link`-ed package, `top` is the link target, which lives at its own filesystem root and is therefore neither isProjectRoot nor isWorkspace. The omit gate never ran, so `npm audit --production` (omit=dev) surfaced the linked dependency's own devDependencies even though they were correctly flagged as dev by calcDepFlags. Detect this case by checking that the node's `.root` still resolves to the consuming project (or workspace) and that `top.linksIn` is non-empty, then fall through to the usual dev/optional/peer flag check. --- workspaces/arborist/lib/node.js | 15 +++++- workspaces/arborist/test/audit-report.js | 69 ++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/workspaces/arborist/lib/node.js b/workspaces/arborist/lib/node.js index 13370a50ab475..012e929fd8446 100644 --- a/workspaces/arborist/lib/node.js +++ b/workspaces/arborist/lib/node.js @@ -510,8 +510,21 @@ class Node { const { top } = this // if the top is not the root or workspace then we do not want to omit it + // unless we landed in a linked-in package: the link target is its own + // fs root (so it's neither `isProjectRoot` nor `isWorkspace`), but the + // node's `.root` still resolves to the consuming project, and its + // `.dev`/`.optional`/`.peer` flags were already computed relative to + // that consuming project. Honor the omit flags in that case so that + // e.g. `npm audit --omit=dev` does not surface a linked dependency's + // own devDependencies. if (!top.isProjectRoot && !top.isWorkspace) { - return false + const { root } = this + if (!root || (!root.isProjectRoot && !root.isWorkspace)) { + return false + } + if (!top.linksIn || top.linksIn.size === 0) { + return false + } } // omit node if the dep type matches any omit flags that were set diff --git a/workspaces/arborist/test/audit-report.js b/workspaces/arborist/test/audit-report.js index ad22416a3e356..1068948702c99 100644 --- a/workspaces/arborist/test/audit-report.js +++ b/workspaces/arborist/test/audit-report.js @@ -2,6 +2,8 @@ const t = require('tap') const localeCompare = require('@isaacs/string-locale-compare')('en') const AuditReport = require('../lib/audit-report.js') const Node = require('../lib/node.js') +const Link = require('../lib/link.js') +const calcDepFlags = require('../lib/calc-dep-flags.js') const Arborist = require('../') const MockRegistry = require('@npmcli/mock-registry') @@ -636,3 +638,70 @@ t.test('determinism: multiple metavulns with identical range but different depen t.ok(BEffects.includes('A'), 'B effects includes A') t.ok(CEffects.includes('A'), 'C effects includes A') }) + +t.test('omit=dev skips devDependencies of a linked-in package (#9624)', async t => { + // Repro for npm/cli#9624: `npm audit --production` (== omit=dev) was + // surfacing the dev deps of `npm link`-ed packages because the omit gate + // in Node#shouldOmit bailed out when the node's `top` was not the + // project root or a workspace. For nodes that live inside a linked-in + // package, `top` is the link target (its own fs root) - but its `.root` + // still resolves to the consuming project, so the omit flags should apply. + const root = new Node({ + path: '/proj', + realpath: '/proj', + pkg: { + name: 'proj', + version: '1.0.0', + dependencies: { foo: 'file:../foo' }, + }, + }) + + const fooTarget = new Node({ + path: '/foo', + realpath: '/foo', + pkg: { + name: 'foo', + version: '1.0.0', + dependencies: { proddep: '1.0.0' }, + devDependencies: { devdep: '1.0.0' }, + }, + }) + + // eslint-disable-next-line no-new + new Link({ + name: 'foo', + realpath: '/foo', + parent: root, + target: fooTarget, + pkg: fooTarget.package, + }) + + const proddep = new Node({ + pkg: { name: 'proddep', version: '1.0.0' }, + parent: fooTarget, + }) + + const devdep = new Node({ + pkg: { name: 'devdep', version: '1.0.0' }, + parent: fooTarget, + }) + + calcDepFlags(root) + + // Sanity: calcDepFlags already classifies the linked package's deps from + // the consuming project's POV, so devdep is `dev` and proddep is not. + t.equal(proddep.dev, false, 'linked package\'s prod dep is not dev') + t.equal(devdep.dev, true, 'linked package\'s dev dep is dev') + + const noOmit = new AuditReport(root, {}) + const allPayload = noOmit.prepareBulkData() + t.ok(allPayload.proddep, 'prod dep is audited with no omit') + t.ok(allPayload.devdep, 'dev dep is audited with no omit') + + const omitDev = new AuditReport(root, { omit: ['dev'] }) + const prodPayload = omitDev.prepareBulkData() + t.ok(prodPayload.proddep, + 'linked package\'s prod dep is still audited under omit=dev') + t.notOk(prodPayload.devdep, + 'linked package\'s dev dep is omitted under omit=dev') +})