internal/deps/npm.go
67 lines · 1779 bytes
1package deps
2
3import (
4 "encoding/json"
5 "strings"
6)
7
8// parseNPM takes the direct dependency names from package.json and their
9// installed versions from package-lock.json where one exists. The lockfile
10// is the honest source: package.json records ranges, and a range that
11// already admits the newest release is not something to report.
12func parseNPM(read ReadFile) []Dep {
13 raw, err := read("package.json")
14 if err != nil {
15 return nil
16 }
17 var pkg struct {
18 Dependencies map[string]string `json:"dependencies"`
19 DevDependencies map[string]string `json:"devDependencies"`
20 }
21 if json.Unmarshal(raw, &pkg) != nil {
22 return nil
23 }
24 locked := npmLock(read)
25 var out []Dep
26 for _, set := range []map[string]string{pkg.Dependencies, pkg.DevDependencies} {
27 for name, spec := range set {
28 current := locked[name]
29 if current == "" {
30 current = pin(spec)
31 }
32 if current != "" {
33 out = append(out, Dep{Ecosystem: EcoNPM, Name: name, Current: current})
34 }
35 }
36 }
37 return out
38}
39
40// npmLock reads installed versions out of a v2 or v3 lockfile, keyed by
41// package name. Nested entries (a transitive copy under another package's
42// node_modules) are ignored: only the top-level install is the direct one.
43func npmLock(read ReadFile) map[string]string {
44 raw, err := read("package-lock.json")
45 if err != nil {
46 return nil
47 }
48 var lock struct {
49 Packages map[string]struct {
50 Version string `json:"version"`
51 } `json:"packages"`
52 }
53 if json.Unmarshal(raw, &lock) != nil {
54 return nil
55 }
56 out := map[string]string{}
57 for path, entry := range lock.Packages {
58 name, ok := strings.CutPrefix(path, "node_modules/")
59 if !ok || strings.Contains(name, "node_modules/") {
60 continue
61 }
62 if v := pin(entry.Version); v != "" {
63 out[name] = v
64 }
65 }
66 return out
67}