Skip to content

Commit b146b38

Browse files
committed
fix(pnpm): select correct workspace member from pnpm ls array
When pnpm ls --json runs from a workspace root, it returns an array with one entry per workspace package. Previously, _buildDependencyTree always selected tree[0], which is typically the workspace root — not the member being analyzed. This caused both component and stack analysis to return 0 dependencies for workspace members. Now finds the array element matching the manifest name before falling back to tree[0]. Implements TC-4180 Assisted-by: Claude Code
1 parent 1684e79 commit b146b38

10 files changed

Lines changed: 187 additions & 1 deletion

File tree

src/providers/javascript_pnpm.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export default class Javascript_pnpm extends Base_javascript {
2121
_buildDependencyTree(includeTransitive, opts = {}) {
2222
const tree = super._buildDependencyTree(includeTransitive, opts);
2323
if (Array.isArray(tree) && tree.length > 0) {
24-
return tree[0];
24+
const memberName = this._getManifest().name;
25+
return tree.find(pkg => pkg.name === memberName) || tree[0];
2526
}
2627
return {};
2728
}

test/providers/javascript.test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,40 @@ suite('testing the javascript-npm data provider', async () => {
106106

107107
});
108108

109+
[
110+
{ providerName: 'pnpm', testCase: 'workspace_member' },
111+
].forEach(({ providerName, testCase }) => {
112+
/// Verifies that stack analysis resolves transitive dependencies for a workspace member.
113+
test(`verify workspace member data provided for ${providerName} - stack analysis`, async () => {
114+
// Given a workspace member manifest and mock listing from the workspace root
115+
const listing = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/listing_stack.json`).toString();
116+
const expectedSbom = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/stack_expected_sbom.json`).toString();
117+
const provider = await createMockProvider(providerName, listing);
118+
const manifestPath = `test/providers/tst_manifests/${providerName}/${testCase}/packages/member-a/package.json`;
119+
120+
// When running stack analysis on the workspace member
121+
const result = provider.provideStack(manifestPath);
122+
123+
// Then the SBOM should contain the member's transitive dependencies
124+
compareSboms(result.content, expectedSbom);
125+
}).timeout(process.env.GITHUB_ACTIONS ? 30000 : 10000);
126+
127+
/// Verifies that component analysis resolves direct dependencies for a workspace member.
128+
test(`verify workspace member data provided for ${providerName} - component analysis`, async () => {
129+
// Given a workspace member manifest and mock listing from the workspace root
130+
const listing = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/listing_component.json`).toString();
131+
const expectedSbom = fs.readFileSync(`test/providers/tst_manifests/${providerName}/${testCase}/component_expected_sbom.json`).toString();
132+
const provider = await createMockProvider(providerName, listing);
133+
const manifestPath = `test/providers/tst_manifests/${providerName}/${testCase}/packages/member-a/package.json`;
134+
135+
// When running component analysis on the workspace member
136+
const result = provider.provideComponent(manifestPath);
137+
138+
// Then the SBOM should contain only the member's direct dependencies
139+
compareSboms(result.content, expectedSbom);
140+
}).timeout(process.env.GITHUB_ACTIONS ? 15000 : 10000);
141+
});
142+
109143
test('loads a valid manifest with ignored dependencies', () => {
110144
const testCase = 'package_json_deps_with_exhortignore_object';
111145
const manifestPath = `test/providers/tst_manifests/npm/${testCase}/package.json`;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"timestamp": "2023-08-07T00:00:00.000Z",
7+
"component": {
8+
"name": "member-a",
9+
"version": "1.0.0",
10+
"purl": "pkg:npm/member-a@1.0.0",
11+
"type": "application",
12+
"bom-ref": "pkg:npm/member-a@1.0.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "axios",
18+
"version": "0.21.1",
19+
"purl": "pkg:npm/axios@0.21.1",
20+
"type": "library",
21+
"bom-ref": "pkg:npm/axios@0.21.1"
22+
}
23+
],
24+
"dependencies": [
25+
{
26+
"ref": "pkg:npm/member-a@1.0.0",
27+
"dependsOn": [
28+
"pkg:npm/axios@0.21.1"
29+
]
30+
},
31+
{
32+
"ref": "pkg:npm/axios@0.21.1",
33+
"dependsOn": []
34+
}
35+
]
36+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[
2+
{
3+
"name": "test-workspace-root",
4+
"version": "1.0.0",
5+
"path": "/workspace-root",
6+
"private": true,
7+
"dependencies": {}
8+
},
9+
{
10+
"name": "member-a",
11+
"version": "1.0.0",
12+
"path": "/workspace-root/packages/member-a",
13+
"private": false,
14+
"dependencies": {
15+
"axios": {
16+
"version": "0.21.1",
17+
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
18+
"overridden": false
19+
}
20+
}
21+
}
22+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[
2+
{
3+
"name": "test-workspace-root",
4+
"version": "1.0.0",
5+
"path": "/workspace-root",
6+
"private": true,
7+
"dependencies": {}
8+
},
9+
{
10+
"name": "member-a",
11+
"version": "1.0.0",
12+
"path": "/workspace-root/packages/member-a",
13+
"private": false,
14+
"dependencies": {
15+
"axios": {
16+
"version": "0.21.1",
17+
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
18+
"overridden": false,
19+
"dependencies": {
20+
"follow-redirects": {
21+
"version": "1.15.6",
22+
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
23+
"overridden": false
24+
}
25+
}
26+
}
27+
}
28+
}
29+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "test-workspace-root",
3+
"private": true,
4+
"version": "1.0.0"
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "member-a",
3+
"version": "1.0.0",
4+
"dependencies": {
5+
"axios": "0.21.1"
6+
}
7+
}

test/providers/tst_manifests/pnpm/workspace_member/pnpm-lock.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- "packages/*"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"timestamp": "2023-08-07T00:00:00.000Z",
7+
"component": {
8+
"name": "member-a",
9+
"version": "1.0.0",
10+
"purl": "pkg:npm/member-a@1.0.0",
11+
"type": "application",
12+
"bom-ref": "pkg:npm/member-a@1.0.0"
13+
}
14+
},
15+
"components": [
16+
{
17+
"name": "axios",
18+
"version": "0.21.1",
19+
"purl": "pkg:npm/axios@0.21.1",
20+
"type": "library",
21+
"bom-ref": "pkg:npm/axios@0.21.1"
22+
},
23+
{
24+
"name": "follow-redirects",
25+
"version": "1.15.6",
26+
"purl": "pkg:npm/follow-redirects@1.15.6",
27+
"type": "library",
28+
"bom-ref": "pkg:npm/follow-redirects@1.15.6"
29+
}
30+
],
31+
"dependencies": [
32+
{
33+
"ref": "pkg:npm/member-a@1.0.0",
34+
"dependsOn": [
35+
"pkg:npm/axios@0.21.1"
36+
]
37+
},
38+
{
39+
"ref": "pkg:npm/axios@0.21.1",
40+
"dependsOn": [
41+
"pkg:npm/follow-redirects@1.15.6"
42+
]
43+
},
44+
{
45+
"ref": "pkg:npm/follow-redirects@1.15.6",
46+
"dependsOn": []
47+
}
48+
]
49+
}

0 commit comments

Comments
 (0)