@@ -2,10 +2,12 @@ package pnpm
2
2
3
3
import (
4
4
"fmt"
5
+ "sort"
5
6
"strconv"
6
7
"strings"
7
8
8
9
"github.com/samber/lo"
10
+ "golang.org/x/exp/maps"
9
11
"golang.org/x/xerrors"
10
12
"gopkg.in/yaml.v3"
11
13
@@ -34,6 +36,28 @@ type LockFile struct {
34
36
Dependencies map [string ]any `yaml:"dependencies,omitempty"`
35
37
DevDependencies map [string ]any `yaml:"devDependencies,omitempty"`
36
38
Packages map [string ]PackageInfo `yaml:"packages,omitempty"`
39
+
40
+ // V9
41
+ Importers Importer `yaml:"importers,omitempty"`
42
+ Snapshots map [string ]Snapshot `yaml:"snapshots,omitempty"`
43
+ }
44
+
45
+ type Importer struct {
46
+ Root RootImporter `yaml:".,omitempty"`
47
+ }
48
+
49
+ type RootImporter struct {
50
+ Dependencies map [string ]ImporterDepVersion `yaml:"dependencies,omitempty"`
51
+ DevDependencies map [string ]ImporterDepVersion `yaml:"devDependencies,omitempty"`
52
+ }
53
+
54
+ type ImporterDepVersion struct {
55
+ Version string `yaml:"version,omitempty"`
56
+ }
57
+
58
+ type Snapshot struct {
59
+ Dependencies map [string ]string `yaml:"dependencies,omitempty"`
60
+ OptionalDependencies map [string ]string `yaml:"optionalDependencies,omitempty"`
37
61
}
38
62
39
63
type Parser struct {
@@ -57,8 +81,16 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
57
81
return nil , nil , nil
58
82
}
59
83
60
- pkgs , deps := p .parse (lockVer , lockFile )
84
+ var pkgs []ftypes.Package
85
+ var deps []ftypes.Dependency
86
+ if lockVer >= 9 {
87
+ pkgs , deps = p .parseV9 (lockFile )
88
+ } else {
89
+ pkgs , deps = p .parse (lockVer , lockFile )
90
+ }
61
91
92
+ sort .Sort (ftypes .Packages (pkgs ))
93
+ sort .Sort (ftypes .Dependencies (deps ))
62
94
return pkgs , deps , nil
63
95
}
64
96
@@ -78,9 +110,11 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
78
110
// cf. https://github.com/pnpm/spec/blob/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile/6.0.md#packagesdependencypathname
79
111
name := info .Name
80
112
version := info .Version
113
+ var ref string
81
114
82
115
if name == "" {
83
- name , version = p .parsePackage (depPath , lockVer )
116
+ name , version , ref = p .parseDepPath (depPath , lockVer )
117
+ version = p .parseVersion (depPath , version , lockVer )
84
118
}
85
119
pkgID := packageID (name , version )
86
120
@@ -90,13 +124,15 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
90
124
}
91
125
92
126
pkgs = append (pkgs , ftypes.Package {
93
- ID : pkgID ,
94
- Name : name ,
95
- Version : version ,
96
- Relationship : lo .Ternary (isDirectPkg (name , lockFile .Dependencies ), ftypes .RelationshipDirect , ftypes .RelationshipIndirect ),
127
+ ID : pkgID ,
128
+ Name : name ,
129
+ Version : version ,
130
+ Relationship : lo .Ternary (isDirectPkg (name , lockFile .Dependencies ), ftypes .RelationshipDirect , ftypes .RelationshipIndirect ),
131
+ ExternalReferences : toExternalRefs (ref ),
97
132
})
98
133
99
134
if len (dependencies ) > 0 {
135
+ sort .Strings (dependencies )
100
136
deps = append (deps , ftypes.Dependency {
101
137
ID : pkgID ,
102
138
DependsOn : dependencies ,
@@ -107,6 +143,98 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
107
143
return pkgs , deps
108
144
}
109
145
146
+ func (p * Parser ) parseV9 (lockFile LockFile ) ([]ftypes.Package , []ftypes.Dependency ) {
147
+ lockVer := 9.0
148
+ resolvedPkgs := make (map [string ]ftypes.Package )
149
+ resolvedDeps := make (map [string ]ftypes.Dependency )
150
+
151
+ // Check all snapshots and save with resolved versions
152
+ resolvedSnapshots := make (map [string ][]string )
153
+ for depPath , snapshot := range lockFile .Snapshots {
154
+ name , version , _ := p .parseDepPath (depPath , lockVer )
155
+
156
+ var dependsOn []string
157
+ for depName , depVer := range lo .Assign (snapshot .OptionalDependencies , snapshot .Dependencies ) {
158
+ depVer = p .trimPeerDeps (depVer , lockVer ) // pnpm has already separated dep name. therefore, we only need to separate peer deps.
159
+ depVer = p .parseVersion (depPath , depVer , lockVer )
160
+ id := packageID (depName , depVer )
161
+ if _ , ok := lockFile .Packages [id ]; ok {
162
+ dependsOn = append (dependsOn , id )
163
+ }
164
+ }
165
+ if len (dependsOn ) > 0 {
166
+ resolvedSnapshots [packageID (name , version )] = dependsOn
167
+ }
168
+
169
+ }
170
+
171
+ for depPath , pkgInfo := range lockFile .Packages {
172
+ name , ver , ref := p .parseDepPath (depPath , lockVer )
173
+ parsedVer := p .parseVersion (depPath , ver , lockVer )
174
+
175
+ if pkgInfo .Version != "" {
176
+ parsedVer = pkgInfo .Version
177
+ }
178
+
179
+ // By default, pkg is dev pkg.
180
+ // We will update `Dev` field later.
181
+ dev := true
182
+ relationship := ftypes .RelationshipIndirect
183
+ if dep , ok := lockFile .Importers .Root .DevDependencies [name ]; ok && dep .Version == ver {
184
+ relationship = ftypes .RelationshipDirect
185
+ }
186
+ if dep , ok := lockFile .Importers .Root .Dependencies [name ]; ok && dep .Version == ver {
187
+ relationship = ftypes .RelationshipDirect
188
+ dev = false // mark root direct deps to update `dev` field of their child deps.
189
+ }
190
+
191
+ id := packageID (name , parsedVer )
192
+ resolvedPkgs [id ] = ftypes.Package {
193
+ ID : id ,
194
+ Name : name ,
195
+ Version : parsedVer ,
196
+ Relationship : relationship ,
197
+ Dev : dev ,
198
+ ExternalReferences : toExternalRefs (ref ),
199
+ }
200
+
201
+ // Save child deps
202
+ if dependsOn , ok := resolvedSnapshots [depPath ]; ok {
203
+ sort .Strings (dependsOn )
204
+ resolvedDeps [id ] = ftypes.Dependency {
205
+ ID : id ,
206
+ DependsOn : dependsOn , // Deps from dependsOn has been resolved when parsing snapshots
207
+ }
208
+ }
209
+ }
210
+
211
+ // Overwrite the `Dev` field for dev deps and their child dependencies.
212
+ for _ , pkg := range resolvedPkgs {
213
+ if ! pkg .Dev {
214
+ p .markRootPkgs (pkg .ID , resolvedPkgs , resolvedDeps )
215
+ }
216
+ }
217
+
218
+ return maps .Values (resolvedPkgs ), maps .Values (resolvedDeps )
219
+ }
220
+
221
+ // markRootPkgs sets `Dev` to false for non dev dependency.
222
+ func (p * Parser ) markRootPkgs (id string , pkgs map [string ]ftypes.Package , deps map [string ]ftypes.Dependency ) {
223
+ pkg , ok := pkgs [id ]
224
+ if ! ok {
225
+ return
226
+ }
227
+
228
+ pkg .Dev = false
229
+ pkgs [id ] = pkg
230
+
231
+ // Update child deps
232
+ for _ , depID := range deps [id ].DependsOn {
233
+ p .markRootPkgs (depID , pkgs , deps )
234
+ }
235
+ return
236
+ }
237
+
110
238
func (p * Parser ) parseLockfileVersion (lockFile LockFile ) float64 {
111
239
switch v := lockFile .LockfileVersion .(type ) {
112
240
// v5
@@ -127,55 +255,109 @@ func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 {
127
255
}
128
256
}
129
257
130
- // cf. https://github.com/pnpm/pnpm/blob/ce61f8d3c29eee46cee38d56ced45aea8a439a53/packages/dependency-path/src/index.ts#L112-L163
131
- func (p * Parser ) parsePackage (depPath string , lockFileVersion float64 ) (string , string ) {
132
- // The version separator is different between v5 and v6+.
133
- versionSep := "@"
134
- if lockFileVersion < 6 {
135
- versionSep = "/"
258
+ func (p * Parser ) parseDepPath (depPath string , lockVer float64 ) (string , string , string ) {
259
+ dPath , nonDefaultRegistry := p .trimRegistry (depPath , lockVer )
260
+
261
+ var scope string
262
+ scope , dPath = p .separateScope (dPath )
263
+
264
+ var name string
265
+ name , dPath = p .separateName (dPath , lockVer )
266
+
267
+ // add scope to pkg name
268
+ if scope != "" {
269
+ name = fmt .Sprintf ("%s/%s" , scope , name )
136
270
}
137
- return p .parseDepPath (depPath , versionSep )
271
+
272
+ ver := p .trimPeerDeps (dPath , lockVer )
273
+
274
+ return name , ver , lo .Ternary (nonDefaultRegistry , depPath , "" )
138
275
}
139
276
140
- func (p * Parser ) parseDepPath (depPath , versionSep string ) (string , string ) {
141
- // Skip registry
142
- // e.g.
143
- // - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10"
144
- // - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9"
145
- // - "/lodash/4.17.10" => "lodash/4.17.10"
146
- _ , depPath , _ = strings .Cut (depPath , "/" )
277
+ // trimRegistry trims registry (or `/` prefix) for depPath.
278
+ // It returns true if non-default registry has been trimmed.
279
+ // e.g.
280
+ // - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10", false
281
+ // - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", false
282
+ // - "private.npm.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", true
283
+ // - "/lodash/4.17.10" => "lodash/4.17.10", false
284
+
285
+ func (p * Parser ) trimRegistry (depPath string , lockVer float64 ) (string , bool ) {
286
+ var nonDefaultRegistry bool
287
+ // lock file v9 doesn't use registry prefix
288
+ if lockVer < 9 {
289
+ var registry string
290
+ registry , depPath , _ = strings .Cut (depPath , "/" )
291
+ if registry != "" && registry != "registry.npmjs.org" {
292
+ nonDefaultRegistry = true
293
+ }
294
+ }
295
+ return depPath , nonDefaultRegistry
296
+ }
147
297
148
- // Parse scope
149
- // e.g.
150
- // - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"}
151
- // - v6+: "@babel/[email protected] " => "{"babel", "[email protected] "}
298
+ // separateScope separates the scope (if set) from the rest of the depPath.
299
+ // e.g.
300
+ // - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"}
301
+ // - v6+: "@babel/[email protected] " => "{"babel", "[email protected] "}
302
+ func (p * Parser ) separateScope (depPath string ) (string , string ) {
152
303
var scope string
153
304
if strings .HasPrefix (depPath , "@" ) {
154
305
scope , depPath , _ = strings .Cut (depPath , "/" )
155
306
}
307
+ return scope , depPath
308
+ }
156
309
157
- // Parse package name
158
- // e.g.
159
- // - v5: "generator/7.21.9" => {"generator", "7.21.9"}
160
- // - v6+: "[email protected] " => {"helper-annotate-as-pure", "7.18.6"}
161
- var name , version string
162
- name , version , _ = strings .Cut (depPath , versionSep )
163
- if scope != "" {
164
- name = fmt .Sprintf ("%s/%s" , scope , name )
310
+ // separateName separates pkg name and version.
311
+ // e.g.
312
+ // - v5: "generator/7.21.9" => {"generator", "7.21.9"}
313
+ // - v6+: "7.21.5(@babel/[email protected] )" => "7.21.5"
314
+ //
315
+ // for v9+ version can be filePath or link:
316
+ // - "package1@file:package1:"
317
+ // - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5"
318
+ //
319
+ // Also version can contain peer deps:
320
+
321
+ func (p * Parser ) separateName (depPath string , lockVer float64 ) (string , string ) {
322
+ sep := "@"
323
+ if lockVer < 6 {
324
+ sep = "/"
325
+ }
326
+ name , version , _ := strings .Cut (depPath , sep )
327
+ return name , version
328
+ }
329
+
330
+ // Trim peer deps
331
+ // e.g.
332
+ // - v5: "7.21.5_@[email protected] " => "7.21.5"
333
+ // - v6+: "7.21.5(@babel/[email protected] )" => "7.21.5"
334
+ func (p * Parser ) trimPeerDeps (depPath string , lockVer float64 ) string {
335
+ sep := "("
336
+ if lockVer < 6 {
337
+ sep = "_"
165
338
}
166
- // Trim peer deps
167
- // e.g.
168
- // - v5: "7.21.5_@[email protected] " => "7.21.5"
169
- // - v6+: "7.21.5(@babel/[email protected] )" => "7.21.5"
170
- if idx := strings .IndexAny (version , "_(" ); idx != - 1 {
171
- version = version [:idx ]
339
+ version , _ , _ := strings .Cut (depPath , sep )
340
+ return version
341
+ }
342
+
343
+ // parseVersion parses version.
344
+ // v9 can use filePath or link as version - we need to clear these versions.
345
+ // e.g.
346
+ // - "package1@file:package1:"
347
+ // - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5"
348
+ //
349
+ // Other versions should be semver valid.
350
+ func (p * Parser ) parseVersion (depPath , ver string , lockVer float64 ) string {
351
+ if lockVer < 9 && (strings .HasPrefix (ver , "file:" ) || strings .HasPrefix (ver , "http" )) {
352
+ return ""
172
353
}
173
- if _ , err := semver .Parse (version ); err != nil {
354
+ if _ , err := semver .Parse (ver ); err != nil {
174
355
p .logger .Debug ("Skip non-semver package" , log .String ("pkg_path" , depPath ),
175
- log .String ("version" , version ), log .Err (err ))
176
- return "" , ""
356
+ log .String ("version" , ver ), log .Err (err ))
357
+ return ""
177
358
}
178
- return name , version
359
+
360
+ return ver
179
361
}
180
362
181
363
func isDirectPkg (name string , directDeps map [string ]interface {}) bool {
@@ -186,3 +368,15 @@ func isDirectPkg(name string, directDeps map[string]interface{}) bool {
186
368
func packageID (name , version string ) string {
187
369
return dependency .ID (ftypes .Pnpm , name , version )
188
370
}
371
+
372
+ func toExternalRefs (ref string ) []ftypes.ExternalRef {
373
+ if ref == "" {
374
+ return nil
375
+ }
376
+ return []ftypes.ExternalRef {
377
+ {
378
+ Type : ftypes .RefVCS ,
379
+ URL : ref ,
380
+ },
381
+ }
382
+ }
0 commit comments