diff --git a/go.mod b/go.mod index 9bc0287..a29f2a7 100644 --- a/go.mod +++ b/go.mod @@ -18,5 +18,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index 322043b..184d10c 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/npm.go b/npm.go index 86ab0c4..d8a9642 100644 --- a/npm.go +++ b/npm.go @@ -7,9 +7,11 @@ import ( "net/url" "os" "strings" + "sync" "time" "github.com/charmbracelet/log" + "golang.org/x/sync/errgroup" ) type PackageJSONRaw struct { @@ -156,52 +158,87 @@ func getOtherNPMPackageVersions(packageName string) ([]string, error) { } func updateDependencies(deps []DependencyJSON) ([]DependencyUpdate, error) { - updates := make([]DependencyUpdate, 0) + var ( + mu sync.Mutex + updates = make([]DependencyUpdate, 0) + ) - for i, dep := range deps { - log.Debugf("Dependency : %s, version : %s", dep.Name, dep.Version.String()) - if dep.Name == "" { - log.Warnf("Dependency name is empty, skipping...") - continue - } + var g errgroup.Group - latestVersionString, err := getNPMPackageLatestVersion(dep.Name) - if err != nil { - return nil, fmt.Errorf("failed to fetch latest version for %s: %w", dep.Name, err) - } + // Cap concurrent npm registry requests to avoid overwhelming the host. + g.SetLimit(8) - latestVersion := parseDependencyVersion(latestVersionString) - log.Debugf("Latest version of %s : %s", dep.Name, latestVersion.String()) + for i := range deps { + g.Go(func() error { + dep := deps[i] - changeType, shouldUpdate := classifyDependencyUpdate(dep.Version, latestVersion) - if changeType == SemverChangeDowngrade { - log.Warnf("Dependency %s current version %s is newer than registry latest %s, keeping current version", dep.Name, dep.Version.String(), latestVersion.String()) - continue - } + log.Debugf("Dependency : %s, version : %s", dep.Name, dep.Version.String()) - if !shouldUpdate { - if !Ctx.PatchOnly || !dep.Version.HasSemver { - log.Debugf("Dependency %s won't update (%s)", dep.Name, changeType) - continue + if dep.Name == "" { + log.Warnf("Dependency name is empty, skipping...") + return nil } - patchVersion, ok, err := getLatestPatchNPMPackageVersion(dep.Name, dep.Version) + latestVersionString, err := getNPMPackageLatestVersion(dep.Name) if err != nil { - return nil, fmt.Errorf("failed to fetch other versions for %s: %w", dep.Name, err) + return fmt.Errorf("failed to fetch latest version for %s: %w", dep.Name, err) } - if !ok { - log.Debugf("Dependency %s has no available patch update", dep.Name) - continue + + latestVersion := parseDependencyVersion(latestVersionString) + log.Debugf("Latest version of %s : %s", dep.Name, latestVersion.String()) + + changeType, shouldUpdate := classifyDependencyUpdate(dep.Version, latestVersion) + if changeType == SemverChangeDowngrade { + log.Warnf( + "Dependency %s current version %s is newer than registry latest %s, keeping current version", + dep.Name, + dep.Version.String(), + latestVersion.String(), + ) + return nil } - latestVersion = patchVersion - } + if !shouldUpdate { + if !Ctx.PatchOnly || !dep.Version.HasSemver { + log.Debugf("Dependency %s won't update (%s)", dep.Name, changeType) + return nil + } + + patchVersion, ok, err := getLatestPatchNPMPackageVersion(dep.Name, dep.Version) + if err != nil { + return fmt.Errorf("failed to fetch other versions for %s: %w", dep.Name, err) + } + if !ok { + log.Debugf("Dependency %s has no available patch update", dep.Name) + return nil + } + + latestVersion = patchVersion + } - updatedVersion := mergeDependencyVersion(dep.Version, latestVersion) - updates = append(updates, DependencyUpdate{Name: dep.Name, Before: dep.Version, After: updatedVersion}) - dep.Version = updatedVersion - deps[i] = dep + updatedVersion := mergeDependencyVersion(dep.Version, latestVersion) + + update := DependencyUpdate{ + Name: dep.Name, + Before: dep.Version, + After: updatedVersion, + } + + dep.Version = updatedVersion + + mu.Lock() + deps[i] = dep + updates = append(updates, update) + mu.Unlock() + + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err } + return updates, nil }