From 8689cc1f2be363bd4ec481265fde67a1856eeef3 Mon Sep 17 00:00:00 2001 From: Zied Yousfi Date: Sat, 14 Mar 2026 11:19:13 +0100 Subject: [PATCH 1/3] feat(npm.go, semver.go): print dependency diffs after updates Make dependency changes visible without requiring verbose logging so package updates clearly show before-and-after versions. Add reusable semver diff formatting to keep dependency output consistent across update paths. --- ctx.go | 4 ++++ npm.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++---- npm_test.go | 28 +++++++++++++++++++--- semver.go | 4 ++++ semver_test.go | 3 +++ 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/ctx.go b/ctx.go index dca734d..2b85455 100644 --- a/ctx.go +++ b/ctx.go @@ -1,7 +1,9 @@ package main import ( + "io" "net/http" + "os" "time" ) @@ -17,6 +19,7 @@ type Context struct { ProjectType TypeOfProject ProjectFilePath string HTTPClient *http.Client + Output io.Writer // Flags DryRun bool @@ -27,6 +30,7 @@ var Ctx = Context{ ProjectType: NotSupported, ProjectFilePath: "", HTTPClient: &http.Client{Timeout: 5 * time.Second}, + Output: os.Stdout, DryRun: false, Verbose: false, } diff --git a/npm.go b/npm.go index 5503db0..82b6fd9 100644 --- a/npm.go +++ b/npm.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "io" "net/http" "net/url" "os" @@ -34,6 +35,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() @@ -116,7 +123,49 @@ func getNPMPackageLatestVersion(packageName string) (string, error) { return result.Version, nil } -func updateDependencies(deps []DependencyJSON) error { +func outputWriter() io.Writer { + if Ctx.Output != nil { + return Ctx.Output + } + + return os.Stdout +} + +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" + } + + _, _ = fmt.Fprintf(outputWriter(), "%s %s in %s:\n", action, section, packagePath) + for _, update := range updates { + _, _ = fmt.Fprintf(outputWriter(), "- %s\n", 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()) if dep.Name == "" { @@ -126,7 +175,7 @@ func updateDependencies(deps []DependencyJSON) error { 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) @@ -145,10 +194,11 @@ func updateDependencies(deps []DependencyJSON) error { 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) { @@ -214,12 +264,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) } @@ -245,5 +297,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 } diff --git a/npm_test.go b/npm_test.go index 2bb24ab..b2c96e2 100644 --- a/npm_test.go +++ b/npm_test.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "errors" "io" @@ -151,10 +152,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()) } @@ -203,9 +207,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()) @@ -361,7 +369,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, Output: &output}) if err := processNPMPackage(packagePath); err != nil { t.Fatalf("expected no error, got %v", err) @@ -402,6 +411,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) { diff --git a/semver.go b/semver.go index 1cef621..393e31b 100644 --- a/semver.go +++ b/semver.go @@ -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 { diff --git a/semver_test.go b/semver_test.go index 95d2cdd..026b2b5 100644 --- a/semver_test.go +++ b/semver_test.go @@ -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) { From 51784297ba49242dbbde3d5757e17ee805829cf8 Mon Sep 17 00:00:00 2001 From: Zied Yousfi Date: Sat, 14 Mar 2026 11:22:14 +0100 Subject: [PATCH 2/3] refactor(npm.go): use charm log for dependency diff output Route visible dependency diff messages through the existing log package so normal output follows the project's logging infrastructure. Keep tests deterministic by injecting a test logger instead of writing directly to stdout. --- ctx.go | 8 ++++---- npm.go | 14 +++++++------- npm_test.go | 12 +++++++++++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ctx.go b/ctx.go index 2b85455..15cb96c 100644 --- a/ctx.go +++ b/ctx.go @@ -1,10 +1,10 @@ package main import ( - "io" "net/http" - "os" "time" + + "github.com/charmbracelet/log" ) type TypeOfProject int @@ -19,7 +19,7 @@ type Context struct { ProjectType TypeOfProject ProjectFilePath string HTTPClient *http.Client - Output io.Writer + Logger *log.Logger // Flags DryRun bool @@ -30,7 +30,7 @@ var Ctx = Context{ ProjectType: NotSupported, ProjectFilePath: "", HTTPClient: &http.Client{Timeout: 5 * time.Second}, - Output: os.Stdout, + Logger: log.Default(), DryRun: false, Verbose: false, } diff --git a/npm.go b/npm.go index 82b6fd9..8f8cfb0 100644 --- a/npm.go +++ b/npm.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "io" "net/http" "net/url" "os" @@ -123,12 +122,12 @@ func getNPMPackageLatestVersion(packageName string) (string, error) { return result.Version, nil } -func outputWriter() io.Writer { - if Ctx.Output != nil { - return Ctx.Output +func outputLogger() *log.Logger { + if Ctx.Logger != nil { + return Ctx.Logger } - return os.Stdout + return log.Default() } func formatDependencyDiff(before DependencyVersion, after DependencyVersion) string { @@ -157,9 +156,10 @@ func printDependencyUpdates(packagePath string, section string, updates []Depend action = "Would update" } - _, _ = fmt.Fprintf(outputWriter(), "%s %s in %s:\n", action, section, packagePath) + logger := outputLogger() + logger.Print(action + " " + section + " in " + packagePath + ":") for _, update := range updates { - _, _ = fmt.Fprintf(outputWriter(), "- %s\n", update.String()) + logger.Print("- " + update.String()) } } diff --git a/npm_test.go b/npm_test.go index b2c96e2..1a20bf5 100644 --- a/npm_test.go +++ b/npm_test.go @@ -15,6 +15,8 @@ import ( "strings" "testing" "time" + + "github.com/charmbracelet/log" ) type roundTripFunc func(*http.Request) (*http.Response, error) @@ -54,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", @@ -370,7 +380,7 @@ func TestProcessNPMPackage(t *testing.T) { }) var output bytes.Buffer - withTestContext(t, Context{HTTPClient: client, Output: &output}) + withTestContext(t, Context{HTTPClient: client, Logger: newTestLogger(&output)}) if err := processNPMPackage(packagePath); err != nil { t.Fatalf("expected no error, got %v", err) From ed4a746f9146e678e0491e44e6c6046ce3d9175f Mon Sep 17 00:00:00 2001 From: Zied Yousfi Date: Sat, 14 Mar 2026 11:27:03 +0100 Subject: [PATCH 3/3] feat(npm.go): reduce log verbosity in updateDependencies - Change log output from Info to Debug level for dependency version checks and up-to-date reporting. - Remove redundant update action log, streamlining console output during dependency updates. - No changes to update logic; only logging/moderation of output for improved clarity. --- npm.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/npm.go b/npm.go index 8f8cfb0..538153e 100644 --- a/npm.go +++ b/npm.go @@ -167,7 +167,7 @@ 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 @@ -179,7 +179,7 @@ func updateDependencies(deps []DependencyJSON) ([]DependencyUpdate, error) { } 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 { @@ -188,12 +188,11 @@ func updateDependencies(deps []DependencyJSON) ([]DependencyUpdate, 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