Skip to content

Commit a10291b

Browse files
authored
fix(nodejs): parse project dependencies from multi-document pnpm-lock.yaml (#10861)
1 parent a2777ae commit a10291b

5 files changed

Lines changed: 287 additions & 5 deletions

File tree

pkg/dependency/parser/nodejs/pnpm/parse.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package pnpm
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
7+
"io"
68
"sort"
79
"strconv"
810
"strings"
@@ -30,11 +32,28 @@ func NewParser() *Parser {
3032
}
3133

3234
func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
33-
var lockFile LockFile
34-
if err := yaml.NewDecoder(r).Decode(&lockFile); err != nil {
35-
return nil, nil, xerrors.Errorf("decode error: %w", err)
35+
// pnpm 11+ stores the env lockfile (config and package manager dependencies)
36+
// as a separate YAML document in pnpm-lock.yaml, separated by `---`.
37+
// Decode all documents and use the project lockfile, skipping the env one.
38+
dec := yaml.NewDecoder(r)
39+
for {
40+
var lockFile LockFile
41+
if err := dec.Decode(&lockFile); err != nil {
42+
if errors.Is(err, io.EOF) {
43+
// No project document (empty file or env-only lockfile).
44+
p.logger.Debug("No project lockfile document found")
45+
return nil, nil, nil
46+
}
47+
return nil, nil, xerrors.Errorf("decode error: %w", err)
48+
}
49+
if lockFile.isEnvLockfile() {
50+
continue
51+
}
52+
return p.parseLockFile(lockFile)
3653
}
54+
}
3755

56+
func (p *Parser) parseLockFile(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependency, error) {
3857
lockVer := p.parseLockfileVersion(lockFile)
3958
if lockVer < 0 {
4059
return nil, nil, nil

pkg/dependency/parser/nodejs/pnpm/parse_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ func TestParse(t *testing.T) {
7171
want: pnpmV9SameVersDiffPeers,
7272
wantDeps: pnpmV9SameVersDiffPeersDeps,
7373
},
74+
{
75+
name: "v9 with multiple documents",
76+
file: "testdata/pnpm-lock_v9_multiple_documents.yaml",
77+
want: pnpmV9MultipleDocuments,
78+
wantDeps: pnpmV9MultipleDocumentsDeps,
79+
},
7480
}
7581

7682
for _, tt := range tests {

pkg/dependency/parser/nodejs/pnpm/parse_testcase.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,4 +1069,25 @@ var (
10691069
},
10701070
},
10711071
}
1072+
1073+
// pnpm 11 stores the env lockfile (config and package manager dependencies)
1074+
// as the first YAML document, separated by `---`. Only the project
1075+
// dependencies from the second document must be parsed; the package manager
1076+
// environment (pnpm, @pnpm/exe, etc.) is skipped.
1077+
//
1078+
// docker run --rm node@sha256:2c87ef9bd3c6a3bd4b472b4bec2ce9d16354b0c574f736c476489d09f560a203 sh -c '
1079+
// npm install -g pnpm@11.7.0
1080+
// mkdir /app && cd /app
1081+
// pnpm init
1082+
// pnpm add is-number@7.0.0
1083+
// cat pnpm-lock.yaml'
1084+
pnpmV9MultipleDocuments = []ftypes.Package{
1085+
{
1086+
ID: "is-number@7.0.0",
1087+
Name: "is-number",
1088+
Version: "7.0.0",
1089+
Relationship: ftypes.RelationshipDirect,
1090+
},
1091+
}
1092+
pnpmV9MultipleDocumentsDeps = []ftypes.Dependency{}
10721093
)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
lockfileVersion: '9.0'
2+
3+
importers:
4+
5+
.:
6+
configDependencies: {}
7+
packageManagerDependencies:
8+
'@pnpm/exe':
9+
specifier: ^11.7.0
10+
version: 11.9.0
11+
pnpm:
12+
specifier: ^11.7.0
13+
version: 11.9.0
14+
15+
packages:
16+
17+
'@pnpm/exe@11.9.0':
18+
resolution: {integrity: sha512-pPPOpR79qW3nsNhlyDIdfstli4Bi78mk8r22ySxpFRwMbO8KXSjGrVzGmJBsVX39NnJTh7/WADj527nZhG9H9g==}
19+
hasBin: true
20+
21+
'@pnpm/linux-arm64@11.9.0':
22+
resolution: {integrity: sha512-XYmY2qadHauBA3QaHi2R7fI6kt5Flje0WHz9MVrbH0kVH/XLpfOLnwPeE1+EX6K/nDa2CBvzp35VjYCNGFJa9A==}
23+
cpu: [arm64]
24+
os: [linux]
25+
26+
'@pnpm/linux-x64@11.9.0':
27+
resolution: {integrity: sha512-fl7W5imnSmmgXIqMQFZ/rPaVvk9OkKF8/anqHZE3XEDfWcn3BlWGndyOEas/JN7u2BXWYjs63DJZ3rnG6WOhLA==}
28+
cpu: [x64]
29+
os: [linux]
30+
31+
'@pnpm/linuxstatic-arm64@11.9.0':
32+
resolution: {integrity: sha512-fif8xbnzVEAIlvaU4yIgWKXeXYb4Kj6WMEl/KvM2x1Rp3AKAjBW/53SGzxO4cZP9doAqUzOIpMRGFVbHGeMDRw==}
33+
cpu: [arm64]
34+
os: [linux]
35+
libc: [musl]
36+
37+
'@pnpm/linuxstatic-x64@11.9.0':
38+
resolution: {integrity: sha512-9dKu3QdShqOpnWrjW9owARpIJeP0ul8UgIIbBUv8VDGEYibt+g49zQsNaD7SdMD3WeRSjExPQ1zIhplzr5cwvQ==}
39+
cpu: [x64]
40+
os: [linux]
41+
libc: [musl]
42+
43+
'@pnpm/macos-arm64@11.9.0':
44+
resolution: {integrity: sha512-MWzBTgeI5p3odjdVltYvFXaSWAjF2Xk5YaxiP/u2RmW8N6PHsLyIyj37Ds992CrXgFM8fO1RpvsEirozhsD6KA==}
45+
cpu: [arm64]
46+
os: [darwin]
47+
48+
'@pnpm/win-arm64@11.9.0':
49+
resolution: {integrity: sha512-u/QxEcbKJZxC1t3zUYCZiHzu7TaZ/iXc6EGZoQjkeVT0LXmEVR6ypcK3ByjhIqbxQ3HGX75gnpIpR+VjRjubfw==}
50+
cpu: [arm64]
51+
os: [win32]
52+
53+
'@pnpm/win-x64@11.9.0':
54+
resolution: {integrity: sha512-HqJVHmZG5UKfLi38AjMl2azVmq87TlWRhwSW+f4q9LLaZeAkFyIZ7LY/pN6mh4VlH9yWj90C+tsb1yKiy7OKnw==}
55+
cpu: [x64]
56+
os: [win32]
57+
58+
'@reflink/reflink-darwin-arm64@0.1.19':
59+
resolution: {integrity: sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==}
60+
engines: {node: '>= 10'}
61+
cpu: [arm64]
62+
os: [darwin]
63+
64+
'@reflink/reflink-darwin-x64@0.1.19':
65+
resolution: {integrity: sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==}
66+
engines: {node: '>= 10'}
67+
cpu: [x64]
68+
os: [darwin]
69+
70+
'@reflink/reflink-linux-arm64-gnu@0.1.19':
71+
resolution: {integrity: sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==}
72+
engines: {node: '>= 10'}
73+
cpu: [arm64]
74+
os: [linux]
75+
libc: [glibc]
76+
77+
'@reflink/reflink-linux-arm64-musl@0.1.19':
78+
resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==}
79+
engines: {node: '>= 10'}
80+
cpu: [arm64]
81+
os: [linux]
82+
libc: [musl]
83+
84+
'@reflink/reflink-linux-x64-gnu@0.1.19':
85+
resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==}
86+
engines: {node: '>= 10'}
87+
cpu: [x64]
88+
os: [linux]
89+
libc: [glibc]
90+
91+
'@reflink/reflink-linux-x64-musl@0.1.19':
92+
resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==}
93+
engines: {node: '>= 10'}
94+
cpu: [x64]
95+
os: [linux]
96+
libc: [musl]
97+
98+
'@reflink/reflink-win32-arm64-msvc@0.1.19':
99+
resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==}
100+
engines: {node: '>= 10'}
101+
cpu: [arm64]
102+
os: [win32]
103+
104+
'@reflink/reflink-win32-x64-msvc@0.1.19':
105+
resolution: {integrity: sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==}
106+
engines: {node: '>= 10'}
107+
cpu: [x64]
108+
os: [win32]
109+
110+
'@reflink/reflink@0.1.19':
111+
resolution: {integrity: sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==}
112+
engines: {node: '>= 10'}
113+
114+
detect-libc@2.1.2:
115+
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
116+
engines: {node: '>=8'}
117+
118+
pnpm@11.9.0:
119+
resolution: {integrity: sha512-vWgtXQP+Ul73yf1ngMaITR51asTJyf4AxTh4KCQxDc+Q493E9Tg18G3669UIXkGFXgvLs7YN4qxburieUDbwOw==}
120+
engines: {node: '>=22.13'}
121+
hasBin: true
122+
123+
snapshots:
124+
125+
'@pnpm/exe@11.9.0':
126+
dependencies:
127+
'@reflink/reflink': 0.1.19
128+
detect-libc: 2.1.2
129+
optionalDependencies:
130+
'@pnpm/linux-arm64': 11.9.0
131+
'@pnpm/linux-x64': 11.9.0
132+
'@pnpm/linuxstatic-arm64': 11.9.0
133+
'@pnpm/linuxstatic-x64': 11.9.0
134+
'@pnpm/macos-arm64': 11.9.0
135+
'@pnpm/win-arm64': 11.9.0
136+
'@pnpm/win-x64': 11.9.0
137+
138+
'@pnpm/linux-arm64@11.9.0':
139+
optional: true
140+
141+
'@pnpm/linux-x64@11.9.0':
142+
optional: true
143+
144+
'@pnpm/linuxstatic-arm64@11.9.0':
145+
optional: true
146+
147+
'@pnpm/linuxstatic-x64@11.9.0':
148+
optional: true
149+
150+
'@pnpm/macos-arm64@11.9.0':
151+
optional: true
152+
153+
'@pnpm/win-arm64@11.9.0':
154+
optional: true
155+
156+
'@pnpm/win-x64@11.9.0':
157+
optional: true
158+
159+
'@reflink/reflink-darwin-arm64@0.1.19':
160+
optional: true
161+
162+
'@reflink/reflink-darwin-x64@0.1.19':
163+
optional: true
164+
165+
'@reflink/reflink-linux-arm64-gnu@0.1.19':
166+
optional: true
167+
168+
'@reflink/reflink-linux-arm64-musl@0.1.19':
169+
optional: true
170+
171+
'@reflink/reflink-linux-x64-gnu@0.1.19':
172+
optional: true
173+
174+
'@reflink/reflink-linux-x64-musl@0.1.19':
175+
optional: true
176+
177+
'@reflink/reflink-win32-arm64-msvc@0.1.19':
178+
optional: true
179+
180+
'@reflink/reflink-win32-x64-msvc@0.1.19':
181+
optional: true
182+
183+
'@reflink/reflink@0.1.19':
184+
optionalDependencies:
185+
'@reflink/reflink-darwin-arm64': 0.1.19
186+
'@reflink/reflink-darwin-x64': 0.1.19
187+
'@reflink/reflink-linux-arm64-gnu': 0.1.19
188+
'@reflink/reflink-linux-arm64-musl': 0.1.19
189+
'@reflink/reflink-linux-x64-gnu': 0.1.19
190+
'@reflink/reflink-linux-x64-musl': 0.1.19
191+
'@reflink/reflink-win32-arm64-msvc': 0.1.19
192+
'@reflink/reflink-win32-x64-msvc': 0.1.19
193+
194+
detect-libc@2.1.2: {}
195+
196+
pnpm@11.9.0: {}
197+
198+
---
199+
lockfileVersion: '9.0'
200+
201+
settings:
202+
autoInstallPeers: true
203+
excludeLinksFromLockfile: false
204+
205+
importers:
206+
207+
.:
208+
dependencies:
209+
is-number:
210+
specifier: 7.0.0
211+
version: 7.0.0
212+
213+
packages:
214+
215+
is-number@7.0.0:
216+
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
217+
engines: {node: '>=0.12.0'}
218+
219+
snapshots:
220+
221+
is-number@7.0.0: {}

pkg/dependency/parser/nodejs/pnpm/types.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,23 @@ type LockFile struct {
4343
}
4444

4545
type Importer struct {
46-
Dependencies map[string]ImporterDepVersion `yaml:"dependencies,omitempty"`
47-
DevDependencies map[string]ImporterDepVersion `yaml:"devDependencies,omitempty"`
46+
Dependencies map[string]ImporterDepVersion `yaml:"dependencies,omitempty"`
47+
DevDependencies map[string]ImporterDepVersion `yaml:"devDependencies,omitempty"`
48+
ConfigDependencies map[string]any `yaml:"configDependencies,omitempty"`
49+
PackageManagerDependencies map[string]any `yaml:"packageManagerDependencies,omitempty"`
50+
}
51+
52+
// isEnvLockfile reports whether this YAML document is the pnpm env lockfile
53+
// (config and package manager dependencies), which pnpm 11 stores as a
54+
// separate document in pnpm-lock.yaml. It holds the package manager
55+
// environment, not the project dependencies, so it must be skipped.
56+
func (l LockFile) isEnvLockfile() bool {
57+
for _, importer := range l.Importers {
58+
if importer.ConfigDependencies != nil || importer.PackageManagerDependencies != nil {
59+
return true
60+
}
61+
}
62+
return false
4863
}
4964

5065
type ImporterDepVersion struct {

0 commit comments

Comments
 (0)