@@ -2,10 +2,12 @@ package pnpm
22
33import (
44 "fmt"
5+ "sort"
56 "strconv"
67 "strings"
78
89 "github.com/samber/lo"
10+ "golang.org/x/exp/maps"
911 "golang.org/x/xerrors"
1012 "gopkg.in/yaml.v3"
1113
@@ -34,6 +36,28 @@ type LockFile struct {
3436 Dependencies map [string ]any `yaml:"dependencies,omitempty"`
3537 DevDependencies map [string ]any `yaml:"devDependencies,omitempty"`
3638 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"`
3761}
3862
3963type Parser struct {
@@ -57,8 +81,16 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
5781 return nil , nil , nil
5882 }
5983
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+ }
6191
92+ sort .Sort (ftypes .Packages (pkgs ))
93+ sort .Sort (ftypes .Dependencies (deps ))
6294 return pkgs , deps , nil
6395}
6496
@@ -78,9 +110,11 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
78110 // cf. https://github.com/pnpm/spec/blob/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile/6.0.md#packagesdependencypathname
79111 name := info .Name
80112 version := info .Version
113+ var ref string
81114
82115 if name == "" {
83- name , version = p .parsePackage (depPath , lockVer )
116+ name , version , ref = p .parseDepPath (depPath , lockVer )
117+ version = p .parseVersion (depPath , version , lockVer )
84118 }
85119 pkgID := packageID (name , version )
86120
@@ -90,13 +124,15 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
90124 }
91125
92126 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 ),
97132 })
98133
99134 if len (dependencies ) > 0 {
135+ sort .Strings (dependencies )
100136 deps = append (deps , ftypes.Dependency {
101137 ID : pkgID ,
102138 DependsOn : dependencies ,
@@ -107,6 +143,98 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []
107143 return pkgs , deps
108144}
109145
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+
110238func (p * Parser ) parseLockfileVersion (lockFile LockFile ) float64 {
111239 switch v := lockFile .LockfileVersion .(type ) {
112240 // v5
@@ -127,55 +255,109 @@ func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 {
127255 }
128256}
129257
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 )
136270 }
137- return p .parseDepPath (depPath , versionSep )
271+
272+ ver := p .trimPeerDeps (dPath , lockVer )
273+
274+ return name , ver , lo .Ternary (nonDefaultRegistry , depPath , "" )
138275}
139276
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+ // - "/asap@2.0.6" => "asap@2.0.6", false
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+ }
147297
148- // Parse scope
149- // e.g.
150- // - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"}
151- // - v6+: "@babel/helper-annotate-as-pure@7.18.6" => "{"babel", "helper-annotate-as-pure@7.18.6"}
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/helper-annotate-as-pure@7.18.6" => "{"babel", "helper-annotate-as-pure@7.18.6"}
302+ func (p * Parser ) separateScope (depPath string ) (string , string ) {
152303 var scope string
153304 if strings .HasPrefix (depPath , "@" ) {
154305 scope , depPath , _ = strings .Cut (depPath , "/" )
155306 }
307+ return scope , depPath
308+ }
156309
157- // Parse package name
158- // e.g.
159- // - v5: "generator/7.21.9" => {"generator", "7.21.9"}
160- // - v6+: "helper-annotate-as-pure@7.18.6" => {"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/core@7.20.7)" => "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+ // - "debug@4.3.4(supports-color@8.1.1)"
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_@babel+core@7.21.8" => "7.21.5"
333+ // - v6+: "7.21.5(@babel/core@7.20.7)" => "7.21.5"
334+ func (p * Parser ) trimPeerDeps (depPath string , lockVer float64 ) string {
335+ sep := "("
336+ if lockVer < 6 {
337+ sep = "_"
165338 }
166- // Trim peer deps
167- // e.g.
168- // - v5: "7.21.5_@babel+core@7.21.8" => "7.21.5"
169- // - v6+: "7.21.5(@babel/core@7.20.7)" => "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 ""
172353 }
173- if _ , err := semver .Parse (version ); err != nil {
354+ if _ , err := semver .Parse (ver ); err != nil {
174355 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 ""
177358 }
178- return name , version
359+
360+ return ver
179361}
180362
181363func isDirectPkg (name string , directDeps map [string ]interface {}) bool {
@@ -186,3 +368,15 @@ func isDirectPkg(name string, directDeps map[string]interface{}) bool {
186368func packageID (name , version string ) string {
187369 return dependency .ID (ftypes .Pnpm , name , version )
188370}
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