diff --git a/workspaces/arborist/lib/arborist/build-ideal-tree.js b/workspaces/arborist/lib/arborist/build-ideal-tree.js index 54f86dea0f65c..61b32b06f5ab0 100644 --- a/workspaces/arborist/lib/arborist/build-ideal-tree.js +++ b/workspaces/arborist/lib/arborist/build-ideal-tree.js @@ -1465,11 +1465,6 @@ This is a one-time fix-up, please be patient... const needPrune = metaFromDisk && (mutateTree || flagsSuspect) if (this.#prune && needPrune) { this.#idealTreePrune() - for (const node of this.idealTree.inventory.values()) { - if (node.extraneous) { - node.parent = null - } - } } timeEnd() @@ -1477,7 +1472,12 @@ This is a one-time fix-up, please be patient... #idealTreePrune () { for (const node of this.idealTree.inventory.values()) { - if (node.extraneous) { + // optional peer dependencies are meant to be added to the tree + // through an explicit required dependency (most commonly in the + // root package.json), at which point they won't be optional so + // any dependencies still marked as both optional and peer at + // this point can be pruned as a special kind of extraneous + if (node.extraneous || (node.peer && node.optional)) { node.parent = null } } diff --git a/workspaces/arborist/test/arborist/pruner.js b/workspaces/arborist/test/arborist/pruner.js index 7c4bec0c5e2ed..520560fe0f42b 100644 --- a/workspaces/arborist/test/arborist/pruner.js +++ b/workspaces/arborist/test/arborist/pruner.js @@ -1,4 +1,5 @@ const { resolve } = require('node:path') +const fs = require('node:fs') const t = require('tap') const Arborist = require('../../lib/arborist/index.js') @@ -39,6 +40,23 @@ t.test('prune with lockfile', async t => { t.matchSnapshot(printTree(tree)) }) +t.test('prune with lockfile with implicit optional peer dependencies', async t => { + const path = fixture(t, 'prune-lockfile-optional-peer') + const tree = await pruneTree(path, { audit: false }) + + const dep = tree.children.get('dedent') + t.ok(dep, 'required prod dep was not pruned from tree') + + const optionalPeerDep = tree.children.get('babel-plugin-macros') + t.notOk(optionalPeerDep, 'optional peer dep was pruned from tree') + + t.notMatch( + fs.readFileSync(path + '/package-lock.json'), + 'node_modules/babel-plugin-macros', + 'should remove optional peer dep from package-lock.json' + ) +}) + t.test('prune with actual tree omit dev', async t => { const path = fixture(t, 'prune-actual-omit-dev') const tree = await pruneTree(path, { omit: ['dev'] }) diff --git a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json new file mode 100644 index 0000000000000..50a7c71cc90d2 --- /dev/null +++ b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/node_modules/dedent/package.json @@ -0,0 +1,12 @@ +{ + "name": "dedent", + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json new file mode 100644 index 0000000000000..fb4f1a7d4a39c --- /dev/null +++ b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "dependencies": { + "dedent": "^1.6.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "optional": true, + "peer": true + }, + "node_modules/dedent": { + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + } + } +} \ No newline at end of file diff --git a/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package.json b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package.json new file mode 100644 index 0000000000000..e2dd1a11a8614 --- /dev/null +++ b/workspaces/arborist/test/fixtures/prune-lockfile-optional-peer/package.json @@ -0,0 +1,7 @@ +{ + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "dependencies": { + "dedent": "^1.6.0" + } +} diff --git a/workspaces/arborist/test/fixtures/registry-mocks/content/dedent/dedent-1.6.0.tgz b/workspaces/arborist/test/fixtures/registry-mocks/content/dedent/dedent-1.6.0.tgz new file mode 100644 index 0000000000000..2ea7701b64861 Binary files /dev/null and b/workspaces/arborist/test/fixtures/registry-mocks/content/dedent/dedent-1.6.0.tgz differ diff --git a/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js b/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js new file mode 100644 index 0000000000000..f39f2bf5fda83 --- /dev/null +++ b/workspaces/arborist/test/fixtures/reify-cases/prune-lockfile-optional-peer.js @@ -0,0 +1,60 @@ +// generated from test/fixtures/prune-lockfile-optional-peer +module.exports = t => { + const path = t.testdir({ + "node_modules": { + "dedent": { + "package.json": JSON.stringify({ + "name": "dedent", + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }) + } + }, + "package-lock.json": JSON.stringify({ + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "dependencies": { + "dedent": "^1.6.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "optional": true, + "peer": true + }, + "node_modules/dedent": { + "version": "1.6.0", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + } + } + }), + "package.json": JSON.stringify({ + "name": "prune-lockfile-optional-peer", + "version": "1.0.0", + "dependencies": { + "dedent": "^1.6.0" + } + }) + }) + return path +} \ No newline at end of file