-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
npm_lockfile_go.go
276 lines (238 loc) · 9.17 KB
/
npm_lockfile_go.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
//go:build go || !rust
// +build go !rust
package lockfile
import (
"encoding/json"
"fmt"
"io"
"strings"
"github.com/vercel/turbo/cli/internal/turbopath"
)
// NpmLockfile representation of package-lock.json
type NpmLockfile struct {
Name string `json:"name"`
Version string `json:"version"`
LockfileVersion int `json:"lockfileVersion,omitempty"`
Requires bool `json:"requires,omitempty"`
// Keys are paths to package.json, can be nested in node_modules
Packages map[string]NpmPackage `json:"packages,omitempty"`
// Legacy info for npm version 5 & 6
Dependencies map[string]NpmDependency `json:"dependencies,omitempty"`
}
// NpmPackage Representation of dependencies used in LockfileVersion 2+
type NpmPackage struct {
// Only used for root level package
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Resolved string `json:"resolved,omitempty"`
Integrity string `json:"integrity,omitempty"`
Link bool `json:"link,omitempty"`
Dev bool `json:"dev,omitempty"`
Optional bool `json:"optional,omitempty"`
// Only used if included as a dev dep of one package and an optional dep of another
// An optional dep of a dev dep will have dev and optional set to true
DevOptional bool `json:"devOptional,omitempty"`
InBundle bool `json:"inBundle,omitempty"`
HasInstallScript bool `json:"hasInstallScript,omitempty"`
HasShrinkwrap bool `json:"hasShrinkwrap,omitempty"`
Extraneous bool `json:"extraneous,omitempty"`
Dependencies map[string]string `json:"dependencies,omitempty"`
DevDependencies map[string]string `json:"devDependencies,omitempty"`
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
PeerDependenciesMeta map[string]struct {
Optional bool `json:"optional,omitempty"`
} `json:"peerDependenciesMeta,omitempty"`
OptionalDependencies map[string]string `json:"optionalDependencies,omitempty"`
Bin map[string]string `json:"bin,omitempty"`
// Engines has two valid formats historically, an array and an object.
// Since we don't use this property we just need to propagate it through.
//
// Go serializes correctly even with `interface{}`.
Engines interface{} `json:"engines,omitempty"`
CPU []string `json:"cpu,omitempty"`
OS []string `json:"os,omitempty"`
// Only used for root level package
Workspaces Workspaces `json:"workspaces,omitempty"`
}
// NpmDependency Legacy representation of dependencies
type NpmDependency struct {
Version string `json:"version"`
Resolved string `json:"resolved,omitempty"`
Integrity string `json:"integrity,omitempty"`
Bundled bool `json:"bundled,omitempty"`
Dev bool `json:"dev,omitempty"`
Optional bool `json:"optional,omitempty"`
Requires map[string]string `json:"requires,omitempty"`
Dependencies map[string]NpmDependency `json:"dependencies,omitempty"`
}
// Workspaces represents the standard workspaces field in package.json
type Workspaces []string
// WorkspacesAlt represents the alternate workspaces field (nested) package.json
type WorkspacesAlt struct {
Packages []string `json:"packages,omitempty"`
}
// UnmarshalJSON determines the correct format of the workspaces field to unmarshal
func (r *Workspaces) UnmarshalJSON(data []byte) error {
var tmp = &WorkspacesAlt{}
if err := json.Unmarshal(data, tmp); err == nil {
*r = Workspaces(tmp.Packages)
return nil
}
var tempstr = []string{}
if err := json.Unmarshal(data, &tempstr); err != nil {
return err
}
*r = tempstr
return nil
}
var _ Lockfile = (*NpmLockfile)(nil)
// ResolvePackage Given a workspace, a package it imports and version returns the key, resolved version, and if it was found
func (l *NpmLockfile) ResolvePackage(workspacePath turbopath.AnchoredUnixPath, name string, version string) (Package, error) {
_, ok := l.Packages[workspacePath.ToString()]
if !ok {
return Package{}, fmt.Errorf("No package found in lockfile for '%s'", workspacePath)
}
// AllDependencies will return a key to avoid choosing the incorrect transitive dep
if entry, ok := l.Packages[name]; ok {
return Package{
Key: name,
Version: entry.Version,
Found: true,
}, nil
}
// If we didn't find the entry just using name, then this is an initial call to ResolvePackage
// based on information coming from internal packages' package.json
// First we check if the workspace uses a nested version of the package
nestedPath := fmt.Sprintf("%s/node_modules/%s", workspacePath, name)
if entry, ok := l.Packages[nestedPath]; ok {
return Package{
Key: nestedPath,
Version: entry.Version,
Found: true,
}, nil
}
// Next we check for a top level version of the package
hoistedPath := fmt.Sprintf("node_modules/%s", name)
if entry, ok := l.Packages[hoistedPath]; ok {
return Package{
Key: hoistedPath,
Version: entry.Version,
Found: true,
}, nil
}
return Package{}, nil
}
// AllDependencies Given a lockfile key return all (dev/optional/peer) dependencies of that package
func (l *NpmLockfile) AllDependencies(key string) (map[string]string, bool) {
entry, ok := l.Packages[key]
if !ok {
return nil, false
}
deps := make(map[string]string, len(entry.Dependencies)+len(entry.DevDependencies)+len(entry.PeerDependencies)+len(entry.OptionalDependencies))
addDep := func(d map[string]string) {
for name := range d {
for _, possibleKey := range possibleNpmDeps(key, name) {
if entry, ok := l.Packages[possibleKey]; ok {
deps[possibleKey] = entry.Version
break
}
}
}
}
addDep(entry.Dependencies)
addDep(entry.DevDependencies)
addDep(entry.OptionalDependencies)
addDep(entry.PeerDependencies)
return deps, true
}
// Subgraph Given a list of lockfile keys returns a Lockfile based off the original one that only contains the packages given
func (l *NpmLockfile) Subgraph(workspacePackages []turbopath.AnchoredSystemPath, packages []string) (Lockfile, error) {
prunedPackages := make(map[string]NpmPackage, len(packages))
for _, pkg := range packages {
if entry, ok := l.Packages[pkg]; ok {
prunedPackages[pkg] = entry
} else {
return nil, fmt.Errorf("No lockfile entry found for %s", pkg)
}
}
if rootEntry, ok := l.Packages[""]; ok {
prunedPackages[""] = rootEntry
}
for _, workspacePackages := range workspacePackages {
workspacePkg := workspacePackages.ToUnixPath().ToString()
if workspaceEntry, ok := l.Packages[workspacePkg]; ok {
prunedPackages[workspacePkg] = workspaceEntry
} else {
return nil, fmt.Errorf("No lockfile entry found for %s", workspacePkg)
}
// Each workspace package has a fake version of the package that links back to the original
// but is needed for dependency resolution.
for key, entry := range l.Packages {
if entry.Resolved == workspacePkg {
prunedPackages[key] = entry
break
}
}
}
return &NpmLockfile{
Name: l.Name,
Version: l.Version,
// Forcible upgrade to version 3 since we aren't including backward compatibility capabilities
LockfileVersion: 3,
Requires: l.Requires,
Packages: prunedPackages,
// We omit dependencies since they are only used for supporting npm <=6
}, nil
}
// Encode the lockfile representation and write it to the given writer
func (l *NpmLockfile) Encode(w io.Writer) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
encoder.SetEscapeHTML(false)
return encoder.Encode(l)
}
// Patches return a list of patches used in the lockfile
func (l *NpmLockfile) Patches() []turbopath.AnchoredUnixPath {
return nil
}
// DecodeNpmLockfile Parse contents of package-lock.json into NpmLockfile
func DecodeNpmLockfile(content []byte) (*NpmLockfile, error) {
var lockfile NpmLockfile
if err := json.Unmarshal(content, &lockfile); err != nil {
return nil, err
}
// LockfileVersion <=1 is for npm <=6
ancientLockfile := lockfile.LockfileVersion <= 1 || (len(lockfile.Dependencies) > 0 && len(lockfile.Packages) == 0)
if ancientLockfile {
// TODO: Older versions of package-lock.json required crawling through node_modules and other
// various fixups to make it a deterministic lockfile.
// See https://github.com/npm/cli/blob/9609e9eed87c735f0319ac0af265f4d406cbf800/workspaces/arborist/lib/shrinkwrap.js#L674
return nil, fmt.Errorf("Support for lockfiles without a 'packages' field isn't implemented yet")
}
return &lockfile, nil
}
// GlobalChange checks if there are any differences between lockfiles that would completely invalidate
// the cache.
func (l *NpmLockfile) GlobalChange(other Lockfile) bool {
o, ok := other.(*NpmLockfile)
return !ok ||
l.LockfileVersion != o.LockfileVersion ||
l.Requires != o.Requires
}
// returns a list of possible keys for a dependency of package key
func possibleNpmDeps(key string, dep string) []string {
possibleDeps := []string{fmt.Sprintf("%s/node_modules/%s", key, dep)}
curr := key
for curr != "" {
next := npmPathParent(curr)
possibleDeps = append(possibleDeps, fmt.Sprintf("%snode_modules/%s", next, dep))
curr = next
}
return possibleDeps
}
func npmPathParent(key string) string {
if index := strings.LastIndex(key, "node_modules/"); index != -1 {
return key[0:index]
}
return ""
}