mesh/merge-gate head 52e275ad2f3a merged into trunk 954e1a53eef9 — pass, with a note: builds mesh-tools (+39 dependent(s)) → ace, g14, novox, shanks; no bu…
mesh/repo-check head 52e275ad2f3a merged into trunk 954e1a53eef9 — pass: its merge-check.sh passed
mesh/delivery delivered
The lab installs node-tools with npm ci, which refuses a lock that no longer describes package.json; the SDK's range and the toolchain's development dependencies had moved in package.json alone, so every lab run stopped at setup. The image's build runs npm install and never read the lock, so nothing noticed.
103 lines
3.6 KiB
Go
103 lines
3.6 KiB
Go
// Package npmlock says whether a package's lock file still describes its package.json (novox/hq issue 624).
|
|
//
|
|
// `npm ci` installs only from a lock file in step with package.json, and refuses the whole install when it
|
|
// is not. node-tools' lock fell behind twice — the SDK's range moved and the toolchain's development
|
|
// dependencies were added to package.json alone — and nothing here noticed, because the image's build runs
|
|
// `npm install` and never reads the lock; the lab's runs, which run `npm ci`, stopped at setup instead.
|
|
// The check is offline and needs no node: it reads the two files the way `npm ci` compares them first.
|
|
package npmlock
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
)
|
|
|
|
type manifest struct {
|
|
Dependencies map[string]string `json:"dependencies"`
|
|
DevDependencies map[string]string `json:"devDependencies"`
|
|
OptionalDependencies map[string]string `json:"optionalDependencies"`
|
|
PeerDependencies map[string]string `json:"peerDependencies"`
|
|
}
|
|
|
|
type lock struct {
|
|
LockfileVersion int `json:"lockfileVersion"`
|
|
Packages map[string]manifest `json:"packages"`
|
|
}
|
|
|
|
// Check reads dir's package.json and package-lock.json and names every way the lock is out of step: a
|
|
// declared range the lock's root entry records differently or not at all, a range the lock records that
|
|
// package.json no longer declares, and a declared dependency the lock resolved no package for. None means
|
|
// in step.
|
|
func Check(dir string) ([]string, error) {
|
|
var pkg manifest
|
|
if err := readJSON(filepath.Join(dir, "package.json"), &pkg); err != nil {
|
|
return nil, err
|
|
}
|
|
var lk lock
|
|
if err := readJSON(filepath.Join(dir, "package-lock.json"), &lk); err != nil {
|
|
return nil, err
|
|
}
|
|
if lk.LockfileVersion < 2 {
|
|
return []string{fmt.Sprintf("package-lock.json is lockfileVersion %d; npm ci needs 2 or later here", lk.LockfileVersion)}, nil
|
|
}
|
|
root, ok := lk.Packages[""]
|
|
if !ok {
|
|
return []string{`package-lock.json has no root entry (packages[""])`}, nil
|
|
}
|
|
var out []string
|
|
for _, kind := range []struct {
|
|
name string
|
|
want, have map[string]string
|
|
}{
|
|
{"dependencies", pkg.Dependencies, root.Dependencies},
|
|
{"devDependencies", pkg.DevDependencies, root.DevDependencies},
|
|
{"optionalDependencies", pkg.OptionalDependencies, root.OptionalDependencies},
|
|
{"peerDependencies", pkg.PeerDependencies, root.PeerDependencies},
|
|
} {
|
|
for _, name := range sortedKeys(kind.want) {
|
|
have, ok := kind.have[name]
|
|
switch {
|
|
case !ok:
|
|
out = append(out, fmt.Sprintf("%s %s %q is in package.json and not in the lock", kind.name, name, kind.want[name]))
|
|
case have != kind.want[name]:
|
|
out = append(out, fmt.Sprintf("%s %s is %q in package.json and %q in the lock", kind.name, name, kind.want[name], have))
|
|
}
|
|
if kind.name == "peerDependencies" {
|
|
continue
|
|
}
|
|
if _, ok := lk.Packages["node_modules/"+name]; !ok {
|
|
out = append(out, fmt.Sprintf("%s %s has no resolved package in the lock", kind.name, name))
|
|
}
|
|
}
|
|
for _, name := range sortedKeys(kind.have) {
|
|
if _, ok := kind.want[name]; !ok {
|
|
out = append(out, fmt.Sprintf("%s %s %q is in the lock and no longer in package.json", kind.name, name, kind.have[name]))
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func readJSON(path string, v any) error {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := json.Unmarshal(b, v); err != nil {
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sortedKeys(m map[string]string) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|