Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"net/http"
"time"

"github.com/charmbracelet/log"
)

type TypeOfProject int
Expand All @@ -17,6 +19,7 @@ type Context struct {
ProjectType TypeOfProject
ProjectFilePath string
HTTPClient *http.Client
Logger *log.Logger

// Flags
DryRun bool
Expand All @@ -27,6 +30,7 @@ var Ctx = Context{
ProjectType: NotSupported,
ProjectFilePath: "",
HTTPClient: &http.Client{Timeout: 5 * time.Second},
Logger: log.Default(),
DryRun: false,
Verbose: false,
}
72 changes: 63 additions & 9 deletions npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ type PackageJSONDependencies struct {
DevDependencies []DependencyJSON
}

type DependencyUpdate struct {
Name string
Before DependencyVersion
After DependencyVersion
}

func (v DependencyVersion) String() string {
if v.HasSemver {
return v.Prefix + v.Semver.String()
Expand Down Expand Up @@ -116,21 +122,64 @@ func getNPMPackageLatestVersion(packageName string) (string, error) {
return result.Version, nil
}

func updateDependencies(deps []DependencyJSON) error {
func outputLogger() *log.Logger {
if Ctx.Logger != nil {
return Ctx.Logger
}

return log.Default()
}

func formatDependencyDiff(before DependencyVersion, after DependencyVersion) string {
if before.HasSemver && after.HasSemver {
if before.Prefix == "" && after.Prefix == "" {
return before.Semver.Diff(after.Semver)
}

return fmt.Sprintf("%s -> %s (%s)", before.String(), after.String(), before.Semver.ChangeType(after.Semver))
}

return fmt.Sprintf("%s -> %s", before.String(), after.String())
}

func (update DependencyUpdate) String() string {
return fmt.Sprintf("%s: %s", update.Name, formatDependencyDiff(update.Before, update.After))
}

func printDependencyUpdates(packagePath string, section string, updates []DependencyUpdate) {
if len(updates) == 0 {
return
}

action := "Updated"
if Ctx.DryRun {
action = "Would update"
}

logger := outputLogger()
logger.Print(action + " " + section + " in " + packagePath + ":")
for _, update := range updates {
logger.Print("- " + update.String())
}
}

func updateDependencies(deps []DependencyJSON) ([]DependencyUpdate, error) {
updates := make([]DependencyUpdate, 0)

for i, dep := range deps {
log.Infof("Dependency : %s, version : %s", dep.Name, dep.Version.String())
log.Debugf("Dependency : %s, version : %s", dep.Name, dep.Version.String())
if dep.Name == "" {
log.Warnf("Dependency name is empty, skipping...")
continue
}

latestVersionString, err := getNPMPackageLatestVersion(dep.Name)
if err != nil {
return fmt.Errorf("failed to fetch latest version for %s: %w", dep.Name, err)
return nil, fmt.Errorf("failed to fetch latest version for %s: %w", dep.Name, err)
}

latestVersion := parseDependencyVersion(latestVersionString)
log.Infof("Latest version of %s : %s", dep.Name, latestVersion.String())
log.Debugf("Latest version of %s : %s", dep.Name, latestVersion.String())

changeType, shouldUpdate := classifyDependencyUpdate(dep.Version, latestVersion)
if changeType == SemverChangeDowngrade {
Expand All @@ -139,16 +188,16 @@ func updateDependencies(deps []DependencyJSON) error {
}

if !shouldUpdate {
log.Infof("Dependency %s already up to date (%s)", dep.Name, changeType)
log.Debugf("Dependency %s already up to date (%s)", dep.Name, changeType)
continue
}

updatedVersion := mergeDependencyVersion(dep.Version, latestVersion)
log.Infof("Updating %s with %s change: %s -> %s", dep.Name, changeType, dep.Version.String(), updatedVersion.String())
updates = append(updates, DependencyUpdate{Name: dep.Name, Before: dep.Version, After: updatedVersion})
dep.Version = updatedVersion
deps[i] = dep
}
return nil
return updates, nil
}

func classifyDependencyUpdate(currentVersion DependencyVersion, latestVersion DependencyVersion) (SemverChange, bool) {
Expand Down Expand Up @@ -214,12 +263,14 @@ func processNPMPackage(packagePath string) error {
}

log.Infof("Dependencies number : %v", len(packageJSON.Dependencies))
if err := updateDependencies(packageJSON.Dependencies); err != nil {
dependencyUpdates, err := updateDependencies(packageJSON.Dependencies)
if err != nil {
return fmt.Errorf("failed to update dependencies: %w", err)
}

log.Infof("DevDependencies number : %v", len(packageJSON.DevDependencies))
if err := updateDependencies(packageJSON.DevDependencies); err != nil {
devDependencyUpdates, err := updateDependencies(packageJSON.DevDependencies)
if err != nil {
return fmt.Errorf("failed to update devDependencies: %w", err)
}

Expand All @@ -245,5 +296,8 @@ func processNPMPackage(packagePath string) error {
log.Infof("Dry run enabled, not writing changes to %v", packagePath)
}

printDependencyUpdates(packagePath, "dependencies", dependencyUpdates)
printDependencyUpdates(packagePath, "devDependencies", devDependencyUpdates)

return nil
}
38 changes: 35 additions & 3 deletions npm_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"io"
Expand All @@ -14,6 +15,8 @@ import (
"strings"
"testing"
"time"

"github.com/charmbracelet/log"
)

type roundTripFunc func(*http.Request) (*http.Response, error)
Expand Down Expand Up @@ -53,6 +56,14 @@ func newRegistryClient(t *testing.T, handler http.HandlerFunc) *http.Client {
}
}

func newTestLogger(output io.Writer) *log.Logger {
return log.NewWithOptions(output, log.Options{
Level: log.DebugLevel,
ReportTimestamp: false,
ReportCaller: false,
})
}

func TestMapToDepsAndDepsToMapRoundTrip(t *testing.T) {
depsMap := map[string]string{
"react": "^18.3.1",
Expand Down Expand Up @@ -151,10 +162,13 @@ func TestUpdateDependencies(t *testing.T) {
{Name: "broken", Version: parseDependencyVersion("3.0.0")},
}

err := updateDependencies(deps)
updates, err := updateDependencies(deps)
if err == nil {
t.Fatalf("expected error on broken package fetch, got nil")
}
if len(updates) != 0 {
t.Fatalf("expected no updates to be returned on error, got %v", updates)
}
if deps[0].Version.String() != "1.3.0" {
t.Fatalf("expected successful dependency to update, got %q", deps[0].Version.String())
}
Expand Down Expand Up @@ -203,9 +217,13 @@ func TestUpdateDependenciesSkipsNoopAndDowngrade(t *testing.T) {
{Name: "prefixed", Version: parseDependencyVersion("^1.2.0")},
}

if err := updateDependencies(deps); err != nil {
updates, err := updateDependencies(deps)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(updates) != 2 {
t.Fatalf("expected 2 updates, got %d", len(updates))
}

if deps[0].Version.String() != "1.0.0" {
t.Fatalf("expected same-version dependency to stay unchanged, got %q", deps[0].Version.String())
Expand Down Expand Up @@ -361,7 +379,8 @@ func TestProcessNPMPackage(t *testing.T) {
_, _ = io.WriteString(w, `{"version":"`+version+`"}`)
})

withTestContext(t, Context{HTTPClient: client})
var output bytes.Buffer
withTestContext(t, Context{HTTPClient: client, Logger: newTestLogger(&output)})

if err := processNPMPackage(packagePath); err != nil {
t.Fatalf("expected no error, got %v", err)
Expand Down Expand Up @@ -402,6 +421,19 @@ func TestProcessNPMPackage(t *testing.T) {
if document["private"] != true {
t.Fatalf("expected private field to be preserved, got %v", document["private"])
}

printed := output.String()
for _, expected := range []string{
"Updated dependencies in " + packagePath + ":",
"- react: ^18.2.0 -> ^18.3.1 (minor)",
"- @scope/pkg: ~1.0.0 -> ~1.2.3 (minor)",
"Updated devDependencies in " + packagePath + ":",
"- vitest: >=1.5.0 -> >=1.6.0 (minor)",
} {
if !strings.Contains(printed, expected) {
t.Fatalf("expected output to contain %q, got %q", expected, printed)
}
}
})

t.Run("does not add missing dependency sections", func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions semver.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ func (s Semver) String() string {
return builder.String()
}

func (s Semver) Diff(other Semver) string {
return fmt.Sprintf("%s -> %s (%s)", s.String(), other.String(), s.ChangeType(other))
}

func CompareSemver(left string, right string) (int, error) {
leftSemver, err := ParseSemver(left)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions semver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ func TestSemverHelpers(t *testing.T) {
if !(Semver{Major: 1, Minor: 2, Patch: 3}).IsRevisionUpdate(Semver{Major: 1, Minor: 2, Patch: 3, Revision: 1, HasRevision: true}) {
t.Fatal("expected IsRevisionUpdate to report true")
}
if diff := left.Diff(right); diff != "1.2.3 -> 1.2.4 (patch)" {
t.Fatalf("Diff() = %q, want %q", diff, "1.2.3 -> 1.2.4 (patch)")
}
}

func TestCompareSemver(t *testing.T) {
Expand Down
Loading