diff --git a/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js b/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js index eaf14ebd716..2493f16c5ca 100644 --- a/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js +++ b/internal/shrinkwrap-extractor/lib/convertPackageLockToShrinkwrap.js @@ -139,7 +139,8 @@ function collectDependencies(node, relevantPackageLocations) { node = node.target; } for (const edge of node.edgesOut.values()) { - if (edge.dev) { + if (edge.dev || !edge.to) { + // Skip dev dependencies and optional peer dependencies that are not installed continue; } collectDependencies(edge.to, relevantPackageLocations); diff --git a/internal/shrinkwrap-extractor/test/expected/package.a/npm-shrinkwrap.json b/internal/shrinkwrap-extractor/test/expected/package.a/npm-shrinkwrap.json index a5086d39e13..fa4e014d72f 100644 --- a/internal/shrinkwrap-extractor/test/expected/package.a/npm-shrinkwrap.json +++ b/internal/shrinkwrap-extractor/test/expected/package.a/npm-shrinkwrap.json @@ -867,7 +867,8 @@ "replacestream": "^4.0.3", "router": "^2.2.0", "spdy": "^4.0.2", - "yesno": "^0.4.0" + "yesno": "^0.4.0", + "ws": "^8.21.0" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -6108,6 +6109,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", diff --git a/internal/shrinkwrap-extractor/test/fixture/project.a/package-lock.fixture.json b/internal/shrinkwrap-extractor/test/fixture/project.a/package-lock.fixture.json index 48868cbf299..fc68b9e50ab 100644 --- a/internal/shrinkwrap-extractor/test/fixture/project.a/package-lock.fixture.json +++ b/internal/shrinkwrap-extractor/test/fixture/project.a/package-lock.fixture.json @@ -19247,7 +19247,8 @@ "replacestream": "^4.0.3", "router": "^2.2.0", "spdy": "^4.0.2", - "yesno": "^0.4.0" + "yesno": "^0.4.0", + "ws": "^8.21.0" }, "devDependencies": { "@eslint/js": "^9.8.0", @@ -19366,6 +19367,27 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } -} +} \ No newline at end of file diff --git a/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js b/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js index 307736a3af0..2f9ea90d476 100644 --- a/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js +++ b/internal/shrinkwrap-extractor/test/lib/convertToShrinkwrap.js @@ -204,6 +204,29 @@ test("Compare generated shrinkwrap with expected result", async (t) => { "Generated shrinkwrap packages should match expected"); }); +test("Optional peer dependencies with null edges should be excluded", async (t) => { + // Guards against: ws declares bufferutil and utf-8-validate as peerOptional, but they are not + // installed. Arborist represents these as edges with edge.to === null. The generator must skip + // them instead of throwing "Cannot read properties of null (reading 'location')". + const __dirname = import.meta.dirname; + + const cwd = path.join(__dirname, "..", "fixture", "project.a"); + const symlinkPath = await setupFixtureSymlink(cwd); + t.after(async () => await unlink(symlinkPath).catch(() => {})); + + const shrinkwrapJson = await convertPackageLockToShrinkwrap(cwd, "@ui5/cli"); + + // ws itself must be present (it is a real production dep of @ui5/server) + assert.ok(shrinkwrapJson.packages["node_modules/ws"], + "ws should be included in the shrinkwrap"); + + // Its optional peer deps are not installed and must NOT appear + assert.equal(shrinkwrapJson.packages["node_modules/bufferutil"], undefined, + "bufferutil (optional peerDep of ws) must not be included"); + assert.equal(shrinkwrapJson.packages["node_modules/utf-8-validate"], undefined, + "utf-8-validate (optional peerDep of ws) must not be included"); +}); + // Error handling tests test("Error handling - invalid target package name", async (t) => { const __dirname = import.meta.dirname;