diff --git a/workspaces/arborist/lib/unreviewed-scripts.js b/workspaces/arborist/lib/unreviewed-scripts.js index f7bed387845ec..1513a7fe347aa 100644 --- a/workspaces/arborist/lib/unreviewed-scripts.js +++ b/workspaces/arborist/lib/unreviewed-scripts.js @@ -57,6 +57,11 @@ const collectUnreviewedScripts = async ({ // must not be flagged (npm/cli#9562). continue } + if (node.extraneous) { + // Extraneous nodes are orphans pruned before reify runs any install script, so their scripts never execute. + // buildIdealTree drops top-level orphans but can retain one nested in a workspace's node_modules, so this gate must skip them (npm/cli#9680). + continue + } const verdict = isScriptAllowed(node, resolvedPolicy) if (verdict === true || verdict === false) { diff --git a/workspaces/arborist/test/unreviewed-scripts.js b/workspaces/arborist/test/unreviewed-scripts.js index 34e4878b4625a..db11680ec0688 100644 --- a/workspaces/arborist/test/unreviewed-scripts.js +++ b/workspaces/arborist/test/unreviewed-scripts.js @@ -29,6 +29,7 @@ const node = ({ isLink = false, inBundle = false, inert = false, + extraneous = false, resolved, } = {}) => ({ name, @@ -41,6 +42,7 @@ const node = ({ isLink, inBundle, inert, + extraneous, isRegistryDependency: true, package: { name, version, scripts }, }) @@ -92,6 +94,30 @@ t.test('collectUnreviewedScripts', async t => { t.strictSame(result, []) }) + t.test('skips extraneous (orphan) registry nodes', async t => { + // An extraneous orphan is pruned before reify runs any install script, so it must not gate the install even when the policy neither allows nor denies it (npm/cli#9680). + const result = await collectUnreviewedScripts({ + tree: tree([ + node({ name: 'orphan', scripts: { install: 'x' }, extraneous: true }), + ]), + policy: null, + }) + t.strictSame(result, []) + }) + + t.test('skips extraneous nodes even when policy denies by name', async t => { + // Same orphan, but with a name-only deny entry. + // An extraneous registry node usually has no resolved URL for the matcher to verify, so the deny misses and it falls through to "unreviewed". + // It still must not gate the install (npm/cli#9680). + const orphan = node({ name: 'esbuild', scripts: { install: 'x' }, extraneous: true }) + orphan.isRegistryDependency = false + const result = await collectUnreviewedScripts({ + tree: tree([orphan]), + policy: { esbuild: false }, + }) + t.strictSame(result, []) + }) + t.test('skips nodes with no install-relevant scripts', async t => { const result = await collectUnreviewedScripts({ tree: tree([node({ scripts: { test: 'jest' } })]),