From cd9ee157536b0988ff4c4385dc6d48a980507225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=92=D0=B5=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B2?= Date: Fri, 7 Aug 2026 08:41:55 +0300 Subject: [PATCH] fix(ci): ignore inert nodes during lockfile validation --- lib/utils/validate-lockfile.js | 4 +++ test/lib/commands/ci.js | 39 +++++++++++++++++++++++++++++ test/lib/utils/validate-lockfile.js | 18 +++++++++++++ 3 files changed, 61 insertions(+) diff --git a/lib/utils/validate-lockfile.js b/lib/utils/validate-lockfile.js index 9039a797c03dd..714bcc69a09b1 100644 --- a/lib/utils/validate-lockfile.js +++ b/lib/utils/validate-lockfile.js @@ -11,6 +11,10 @@ function validateLockfile (virtualTree, idealTree) { // for each package compares the versions with the version stored in the // package-lock and adds an error to the list in case of mismatches for (const [key, entry] of idealTree.entries()) { + if (entry.inert) { + continue + } + const lock = virtualTree.get(key) if (!lock) { diff --git a/test/lib/commands/ci.js b/test/lib/commands/ci.js index a90ca4b0cffe6..77e9295eda4b5 100644 --- a/test/lib/commands/ci.js +++ b/test/lib/commands/ci.js @@ -413,6 +413,45 @@ t.test('should throw error when ideal inventory mismatches virtual', async t => t.equal(fs.existsSync(nmTestFile), true, 'does not remove node_modules') }) +t.test('accepts a lock file with an unavailable optional dependency', async t => { + const missingOptional = 'missing-optional' + const { npm, registry } = await loadMockNpm(t, { + config: { + audit: false, + 'dry-run': true, + 'ignore-scripts': true, + }, + prefixDir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + dependencies: { 'optional-parent': '1.0.0' }, + }), + 'package-lock.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + lockfileVersion: 3, + requires: true, + packages: { + '': { + name: 'test-package', + version: '1.0.0', + dependencies: { 'optional-parent': '1.0.0' }, + }, + 'node_modules/optional-parent': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/optional-parent/-/optional-parent-1.0.0.tgz', + optionalDependencies: { [missingOptional]: '1.0.0' }, + }, + }, + }), + }, + }) + registry.nock.get(`/${missingOptional}`).reply(404, { error: 'Not found' }) + + await npm.exec('ci', []) +}) + t.test('should remove dirty node_modules with unhoisted workspace module', async t => { const { npm, registry, assert } = await loadMockNpm(t, { prefixDir: workspaceMock(t, { diff --git a/test/lib/utils/validate-lockfile.js b/test/lib/utils/validate-lockfile.js index cee1891f49183..16cb50a79b44e 100644 --- a/test/lib/utils/validate-lockfile.js +++ b/test/lib/utils/validate-lockfile.js @@ -131,6 +131,24 @@ t.test('extra inventory items on idealTree', async t => { ) }) +t.test('inert inventory items on idealTree', async t => { + t.strictSame( + validateLockfile( + new Map(), + new Map([ + ['missing-optional', { + name: 'missing-optional', + version: '', + optional: true, + inert: true, + }], + ]) + ), + [], + 'does not require lock file entries for inert optional dependencies' + ) +}) + t.test('extra inventory items on virtualTree', async t => { t.matchSnapshot( validateLockfile(