-
Notifications
You must be signed in to change notification settings - Fork 337
/
parse-pnpm-lock.go
141 lines (107 loc) · 3.35 KB
/
parse-pnpm-lock.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package lockfile
import (
"fmt"
"os"
"regexp"
"strings"
"gopkg.in/yaml.v3"
)
type PnpmLockPackageResolution struct {
Tarball string `yaml:"tarball"`
Commit string `yaml:"commit"`
Repo string `yaml:"repo"`
Type string `yaml:"type"`
}
type PnpmLockPackage struct {
Resolution PnpmLockPackageResolution `yaml:"resolution"`
Name string `yaml:"name"`
Version string `yaml:"version"`
}
type PnpmLockfile struct {
Version float64 `yaml:"lockfileVersion"`
Packages map[string]PnpmLockPackage `yaml:"packages,omitempty"`
}
const PnpmEcosystem = NpmEcosystem
func startsWithNumber(str string) bool {
matcher := regexp.MustCompile(`^\d`)
return matcher.MatchString(str)
}
// extractPnpmPackageNameAndVersion parses a dependency path, attempting to
// extract the name and version of the package it represents
func extractPnpmPackageNameAndVersion(dependencyPath string) (string, string) {
// file dependencies must always have a name property to be installed,
// and their dependency path never has the version encoded, so we can
// skip trying to extract either from their dependency path
if strings.HasPrefix(dependencyPath, "file:") {
return "", ""
}
parts := strings.Split(dependencyPath, "/")
var name string
parts = parts[1:]
if strings.HasPrefix(parts[0], "@") {
name = strings.Join(parts[:2], "/")
parts = parts[2:]
} else {
name = parts[0]
parts = parts[1:]
}
version := ""
if len(parts) != 0 {
version = parts[0]
}
if version == "" || !startsWithNumber(version) {
return "", ""
}
underscoreIndex := strings.Index(version, "_")
if underscoreIndex != -1 {
version = strings.Split(version, "_")[0]
}
return name, version
}
func parsePnpmLock(lockfile PnpmLockfile) []PackageDetails {
packages := make([]PackageDetails, 0, len(lockfile.Packages))
for s, pkg := range lockfile.Packages {
name, version := extractPnpmPackageNameAndVersion(s)
// "name" is only present if it's not in the dependency path and takes
// priority over whatever name we think we've extracted (if any)
if pkg.Name != "" {
name = pkg.Name
}
// "version" is only present if it's not in the dependency path and takes
// priority over whatever version we think we've extracted (if any)
if pkg.Version != "" {
version = pkg.Version
}
if name == "" || version == "" {
continue
}
commit := pkg.Resolution.Commit
if strings.HasPrefix(pkg.Resolution.Tarball, "https://codeload.github.com") {
re := regexp.MustCompile(`https://codeload\.github\.com(?:/[\w-.]+){2}/tar\.gz/(\w+)$`)
matched := re.FindStringSubmatch(pkg.Resolution.Tarball)
if matched != nil {
commit = matched[1]
}
}
packages = append(packages, PackageDetails{
Name: name,
Version: version,
Ecosystem: PnpmEcosystem,
CompareAs: PnpmEcosystem,
Commit: commit,
})
}
return packages
}
func ParsePnpmLock(pathToLockfile string) ([]PackageDetails, error) {
var parsedLockfile *PnpmLockfile
lockfileContents, err := os.ReadFile(pathToLockfile)
if err != nil {
return []PackageDetails{}, fmt.Errorf("could not read %s: %w", pathToLockfile, err)
}
err = yaml.Unmarshal(lockfileContents, &parsedLockfile)
if err != nil {
return []PackageDetails{}, fmt.Errorf("could not parse %s: %w", pathToLockfile, err)
}
return parsePnpmLock(*parsedLockfile), nil
}