From 6a28ef33b7e5a6f18d7ab274f77bd2dff7a6f3b0 Mon Sep 17 00:00:00 2001 From: DevMiner Date: Wed, 10 Jul 2024 22:25:37 +0200 Subject: [PATCH] feat: ability to see the size difference between two package versions --- dependency_change.go | 241 +++++++++++++++++++++++++++++++++++++++++ main.go | 253 ++++--------------------------------------- report.go | 105 +++++++++++------- version_size_diff.go | 91 ++++++++++++++++ 4 files changed, 419 insertions(+), 271 deletions(-) create mode 100644 dependency_change.go create mode 100644 version_size_diff.go diff --git a/dependency_change.go b/dependency_change.go new file mode 100644 index 0000000..2161d23 --- /dev/null +++ b/dependency_change.go @@ -0,0 +1,241 @@ +package main + +import ( + "math" + "package_size_calculator/pkg/npm" + "package_size_calculator/pkg/ui_components" + "path/filepath" + "slices" + "sort" + "strings" + + npm_version "github.com/aquasecurity/go-npm-version/pkg" + docker_client "github.com/docker/docker/client" + "github.com/dustin/go-humanize" + "github.com/manifoldco/promptui" + "github.com/rs/zerolog/log" +) + +func replaceDeps() { + modifiedPackage := promptPackage(npmClient, dockerC) + removedDependencies := promptRemovedDependencies(modifiedPackage.Package.JSON, modifiedPackage.Lockfile) + + addedDependencies, err := ui_components.NewEditableList("Added dependencies", resolveNPMPackage(npmClient)).Run() + if err != nil { + log.Fatal().Err(err).Msg("Failed to run editable list") + } + + deps := combineDependencies(removedDependencies, addedDependencies) + for depName, dep := range deps { + // TODO: Parallelize + l := log.With().Str("package", depName).Logger() + + downloads, err := npmClient.GetPackageDownloadsLastWeek(dep.Name) + if err != nil { + l.Error().Err(err).Msg("Failed to fetch package downloads") + } else { + dep.DownloadsLastWeek = downloads[dep.Version] + l.Info().Msgf("Downloads last week: %v", dep.DownloadsLastWeek) + } + + dep.Size, _, err = measurePackageSize(dockerC, dep.DependencyInfo) + if err != nil { + l.Fatal().Err(err).Msg("Failed to measure package size") + } + + l.Info().Msgf("Package size: %s", humanize.Bytes(dep.Size)) + } + + printReport(modifiedPackage, removedDependencies, addedDependencies, deps) +} + +func resolveNPMPackage(client *npm.Client) ui_components.StringToItemConvertFunc[*npm.PackageJSON] { + return func(s string) (*npm.PackageJSON, error) { + l := log.With().Str("package", s).Logger() + + split := strings.Split(s, " ") + if len(split) > 2 { + log.Error().Msg("Invalid package format") + return nil, ui_components.ErrRetry + } + + log.Info().Msgf("Resolving package \"%s\"...", s) + + info, err := client.GetPackageInfo(split[0]) + if err != nil { + return nil, err + } + + if len(split) == 1 { + latest := info.LatestVersion.JSON + l.Info().Str("version", latest.Version).Msg("Found latest version") + + return &latest, nil + } + + l = log.With().Str("constraint", split[1]).Logger() + + c, err := npm_version.NewConstraints(split[1]) + if err != nil { + log.Error().Err(err).Msg("Failed to create constraints") + return nil, ui_components.ErrRetry + } + + for _, v := range info.Versions { + if c.Check(v.Version) { + l.Info().Str("version", v.JSON.Version).Msg("Found version") + + return &v.JSON, nil + } + } + + log.Error().Msg("No matching version could be found") + + return nil, ui_components.ErrRetry + } +} + +type dependencyPackageInfoType uint8 + +const ( + DependencyRemoved dependencyPackageInfoType = iota + DependencyAdded +) + +type dependencyPackageInfo struct { + npm.DependencyInfo + Size uint64 + DownloadsLastWeek uint64 + Type dependencyPackageInfoType +} + +func combineDependencies(removedDependencies []npm.DependencyInfo, addedDependencies []*npm.PackageJSON) map[string]*dependencyPackageInfo { + deps := map[string]*dependencyPackageInfo{} + + for _, d := range removedDependencies { + deps[d.String()] = &dependencyPackageInfo{ + DependencyInfo: d, + Type: DependencyRemoved, + Size: 0, + DownloadsLastWeek: 0, + } + } + + for _, d := range addedDependencies { + deps[d.String()] = &dependencyPackageInfo{ + DependencyInfo: d.AsDependency(), + Type: DependencyAdded, + Size: 0, + DownloadsLastWeek: 0, + } + } + + return deps +} + +func promptRemovedDependencies(packageJson npm.PackageJSON, pkgLock *npm.PackageLockJSON) []npm.DependencyInfo { + dependencies := make([]npm.DependencyInfo, 0, len(packageJson.Dependencies)) + for _, k := range packageJson.Dependencies { + dep, ok := pkgLock.Packages[k.Name] + if !ok { + log.Warn().Str("dependency", k.Name).Msg("Dependency not found") + continue + } + + dependencies = append(dependencies, dep.AsDependency()) + } + + removedDependencies, err := ui_components.NewMultiSelect("Removed dependencies", dependencies).Run() + if err != nil { + log.Fatal().Err(err).Msg("Failed to run multi select") + } + + return removedDependencies +} + +func promptPackageVersion(packageInfo *npm.PackageInfo, label string) string { + versions := make([]npm_version.Version, 0) + for _, v := range packageInfo.Versions { + versions = append(versions, v.Version) + } + + sort.Sort(npm_version.Collection(versions)) + slices.Reverse(versions) + + _, packageVersion, err := runSelect(&promptui.Select{ + Label: label, + Items: versions, + Size: int(math.Min(float64(len(versions)), 16)), + Searcher: func(input string, index int) bool { + return strings.Contains(versions[index].String(), input) + }, + }) + if err != nil { + log.Fatal().Err(err).Msg("Failed to select version") + } + + return packageVersion +} + +func promptPackage(npmClient *npm.Client, dockerC *docker_client.Client) *packageInfo { + packageName, err := runPrompt(&promptui.Prompt{Label: "Package"}) + if err != nil { + log.Fatal().Err(err).Msg("Prompt failed") + } + + log.Info().Str("package", packageName).Msg("Fetching package info") + + b := &packageInfo{} + + packageInfo, err := npmClient.GetPackageInfo(packageName) + if err != nil { + log.Fatal().Err(err).Msg("Failed to fetch package info") + } + + b.Info = packageInfo + + log.Debug().Msgf("Fetched package info for %s", packageInfo.Name) + + packageVersion := promptPackageVersion(packageInfo, "Select version") + b.Package = packageInfo.Versions[packageVersion] + log.Info().Str("version", packageVersion).Msg("Selected version") + + downloads, err := npmClient.GetPackageDownloadsLastWeek(packageInfo.Name) + if err != nil { + log.Fatal().Err(err).Msg("Failed to fetch package downloads") + } + + b.DownloadsLastWeek = downloads[packageVersion] + + // TODO: Parallelize + b.Size, b.TmpDir, err = measurePackageSize(dockerC, b.AsDependency()) + if err != nil { + log.Fatal().Err(err).Msg("Failed to measure package size") + } + + log.Info().Str("package", b.String()).Str("size", humanize.Bytes(b.Size)).Msg("Package size") + + b.Lockfile, err = npm.ParsePackageLockJSON(filepath.Join(b.TmpDir, "package-lock.json")) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse package-lock.json") + } + + return b +} + +type packageInfo struct { + Info *npm.PackageInfo + Package npm.PackageVersion + Lockfile *npm.PackageLockJSON + DownloadsLastWeek uint64 + Size uint64 + TmpDir string +} + +func (b *packageInfo) String() string { + return b.Package.JSON.String() +} + +func (b *packageInfo) AsDependency() npm.DependencyInfo { + return b.Package.JSON.AsDependency() +} diff --git a/main.go b/main.go index e931fed..f43664d 100644 --- a/main.go +++ b/main.go @@ -1,19 +1,11 @@ package main import ( - "math" "os" "package_size_calculator/internal/build" "package_size_calculator/pkg/npm" - "package_size_calculator/pkg/ui_components" - "path/filepath" - "slices" - "sort" - "strings" - npm_version "github.com/aquasecurity/go-npm-version/pkg" docker_client "github.com/docker/docker/client" - "github.com/dustin/go-humanize" "github.com/manifoldco/promptui" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -24,6 +16,11 @@ const ( MountNPMCache = "" ) +var ( + npmClient *npm.Client + dockerC *docker_client.Client +) + func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.SetGlobalLevel(zerolog.DebugLevel) @@ -32,46 +29,32 @@ func main() { log.Info().Msgf("Package size calculator %s (%s, built on %s)", build.Version, build.Commit, build.BuildTime) - npmClient := npm.New() + npmClient = npm.New() - dockerC, err := docker_client.NewClientWithOpts(docker_client.FromEnv, docker_client.WithAPIVersionNegotiation()) + var err error + dockerC, err = docker_client.NewClientWithOpts(docker_client.FromEnv, docker_client.WithAPIVersionNegotiation()) if err != nil { log.Fatal().Err(err).Msg("Failed to create Docker client") } + log.Info().Msgf("Pulling %s image for measuring package sizes", BaseImage) if err := downloadBaseImage(dockerC); err != nil { log.Fatal().Err(err).Msg("Failed to download Node 20 image") } - modifiedPackage := promptPackage(npmClient, dockerC) - removedDependencies := promptRemovedDependencies(modifiedPackage.Package.JSON, modifiedPackage.Lockfile) - - addedDependencies, err := ui_components.NewEditableList("Added dependencies", resolveNPMPackage(npmClient)).Run() + variant, _, err := runSelect(&promptui.Select{ + Label: "Select variant", + Items: []string{"Calculate size differences for replacing/removing dependencies", "Calculate size difference between package versions"}, + }) if err != nil { - log.Fatal().Err(err).Msg("Failed to run editable list") + log.Fatal().Err(err).Msg("Failed to select variant") } - deps := combineDependencies(removedDependencies, addedDependencies) - for depName, dep := range deps { - // TODO: Parallelize - l := log.With().Str("package", depName).Logger() - - downloads, err := npmClient.GetPackageDownloadsLastWeek(dep.Name) - if err != nil { - l.Error().Err(err).Msg("Failed to fetch package downloads") - } else { - dep.DownloadsLastWeek = downloads[dep.Version] - l.Info().Msgf("Downloads last week: %v", dep.DownloadsLastWeek) - } - - dep.Size, _, err = measurePackageSize(dockerC, dep.DependencyInfo) - if err != nil { - l.Fatal().Err(err).Msg("Failed to measure package size") - } - - l.Info().Msgf("Package size: %s", humanize.Bytes(dep.Size)) + switch variant { + case 0: + replaceDeps() + case 1: + calculateVersionSizeChange() } - - printReport(modifiedPackage, removedDependencies, addedDependencies, deps) } func runSelect(s *promptui.Select) (int, string, error) { @@ -81,201 +64,3 @@ func runSelect(s *promptui.Select) (int, string, error) { func runPrompt(p *promptui.Prompt) (string, error) { return p.Run() } - -func resolveNPMPackage(client *npm.Client) ui_components.StringToItemConvertFunc[*npm.PackageJSON] { - return func(s string) (*npm.PackageJSON, error) { - l := log.With().Str("package", s).Logger() - - split := strings.Split(s, " ") - if len(split) > 2 { - log.Error().Msg("Invalid package format") - return nil, ui_components.ErrRetry - } - - log.Info().Msgf("Resolving package \"%s\"...", s) - - info, err := client.GetPackageInfo(split[0]) - if err != nil { - return nil, err - } - - if len(split) == 1 { - latest := info.LatestVersion.JSON - l.Info().Str("version", latest.Version).Msg("Found latest version") - - return &latest, nil - } - - l = log.With().Str("constraint", split[1]).Logger() - - c, err := npm_version.NewConstraints(split[1]) - if err != nil { - log.Error().Err(err).Msg("Failed to create constraints") - return nil, ui_components.ErrRetry - } - - for _, v := range info.Versions { - if c.Check(v.Version) { - l.Info().Str("version", v.JSON.Version).Msg("Found version") - - return &v.JSON, nil - } - } - - log.Error().Msg("No matching version could be found") - - return nil, ui_components.ErrRetry - } -} - -func fmtPercent(v float64) string { - return humanize.FormatFloat("#,###.##", v) -} - -func fmtInt(v int) string { - return humanize.FormatInteger("#,###.", v) -} - -type dependencyPackageInfoType uint8 - -const ( - DependencyRemoved dependencyPackageInfoType = iota - DependencyAdded -) - -type dependencyPackageInfo struct { - npm.DependencyInfo - Size uint64 - DownloadsLastWeek uint64 - Type dependencyPackageInfoType -} - -func combineDependencies(removedDependencies []npm.DependencyInfo, addedDependencies []*npm.PackageJSON) map[string]*dependencyPackageInfo { - deps := map[string]*dependencyPackageInfo{} - - for _, d := range removedDependencies { - deps[d.String()] = &dependencyPackageInfo{ - DependencyInfo: d, - Type: DependencyRemoved, - Size: 0, - DownloadsLastWeek: 0, - } - } - - for _, d := range addedDependencies { - deps[d.String()] = &dependencyPackageInfo{ - DependencyInfo: d.AsDependency(), - Type: DependencyAdded, - Size: 0, - DownloadsLastWeek: 0, - } - } - - return deps -} - -func promptRemovedDependencies(packageJson npm.PackageJSON, pkgLock *npm.PackageLockJSON) []npm.DependencyInfo { - dependencies := make([]npm.DependencyInfo, 0, len(packageJson.Dependencies)) - for _, k := range packageJson.Dependencies { - dep, ok := pkgLock.Packages[k.Name] - if !ok { - log.Warn().Str("dependency", k.Name).Msg("Dependency not found") - continue - } - - dependencies = append(dependencies, dep.AsDependency()) - } - - removedDependencies, err := ui_components.NewMultiSelect("Removed dependencies", dependencies).Run() - if err != nil { - log.Fatal().Err(err).Msg("Failed to run multi select") - } - - return removedDependencies -} - -func promptPackageVersion(packageInfo *npm.PackageInfo) string { - versions := make([]npm_version.Version, 0) - for _, v := range packageInfo.Versions { - versions = append(versions, v.Version) - } - - sort.Sort(npm_version.Collection(versions)) - slices.Reverse(versions) - - _, packageVersion, err := runSelect(&promptui.Select{ - Label: "Select version", - Items: versions, - Size: int(math.Min(float64(len(versions)), 16)), - Searcher: func(input string, index int) bool { - return strings.Contains(versions[index].String(), input) - }, - }) - if err != nil { - log.Fatal().Err(err).Msg("Failed to select version") - } - - return packageVersion -} - -func promptPackage(npmClient *npm.Client, dockerC *docker_client.Client) *packageInfo { - packageName, err := runPrompt(&promptui.Prompt{Label: "Package"}) - if err != nil { - log.Fatal().Err(err).Msg("Prompt failed") - } - - log.Info().Str("package", packageName).Msg("Fetching package info") - - b := &packageInfo{} - - packageInfo, err := npmClient.GetPackageInfo(packageName) - if err != nil { - log.Fatal().Err(err).Msg("Failed to fetch package info") - } - - b.Info = packageInfo - - log.Debug().Msgf("Fetched package info for %s", packageInfo.Name) - - packageVersion := promptPackageVersion(packageInfo) - b.Package = packageInfo.Versions[packageVersion] - log.Info().Str("version", packageVersion).Msg("Selected version") - - downloads, err := npmClient.GetPackageDownloadsLastWeek(packageInfo.Name) - if err != nil { - log.Fatal().Err(err).Msg("Failed to fetch package downloads") - } - - b.DownloadsLastWeek = downloads[packageVersion] - - b.Size, b.TmpDir, err = measurePackageSize(dockerC, b.AsDependency()) - if err != nil { - log.Fatal().Err(err).Msg("Failed to measure package size") - } - - log.Info().Str("package", b.String()).Str("size", humanize.Bytes(b.Size)).Msg("Package size") - - b.Lockfile, err = npm.ParsePackageLockJSON(filepath.Join(b.TmpDir, "package-lock.json")) - if err != nil { - log.Fatal().Err(err).Msg("Failed to parse package-lock.json") - } - - return b -} - -type packageInfo struct { - Info *npm.PackageInfo - Package npm.PackageVersion - Lockfile *npm.PackageLockJSON - DownloadsLastWeek uint64 - Size uint64 - TmpDir string -} - -func (b *packageInfo) String() string { - return b.Package.JSON.String() -} - -func (b *packageInfo) AsDependency() npm.DependencyInfo { - return b.Package.JSON.AsDependency() -} diff --git a/report.go b/report.go index 13b8462..86feeef 100644 --- a/report.go +++ b/report.go @@ -5,6 +5,7 @@ import ( "math/big" "package_size_calculator/pkg/npm" "package_size_calculator/pkg/time_helpers" + "strings" "time" "github.com/dustin/go-humanize" @@ -28,46 +29,30 @@ func printReport( addedDependencies []*npm.PackageJSON, deps map[string]*dependencyPackageInfo, ) { - packageInfo := modifiedPackage.Info package_ := modifiedPackage.Package packageJson := package_.JSON - pkgDownloads := modifiedPackage.DownloadsLastWeek - installedPackageSize := modifiedPackage.Size + downloadsLastWeek := modifiedPackage.DownloadsLastWeek + oldPackageSize := modifiedPackage.Size modifiedPackageName := boldYellow.Sprint(packageJson.String()) - estPackageSize := installedPackageSize - packageSizeWithoutRemovedDeps := installedPackageSize + newPackageSize := oldPackageSize + packageSizeWithoutRemovedDeps := oldPackageSize for _, p := range deps { if p.Type == DependencyRemoved { - estPackageSize -= p.Size + newPackageSize -= p.Size packageSizeWithoutRemovedDeps -= p.Size } else { - estPackageSize += p.Size + newPackageSize += p.Size } } fmt.Println() boldGreen.Println("Package size report") boldGreen.Println("===================") + fmt.Println() - fmt.Printf("%s: %s\n", bold.Sprintf("Package info for \"%s\"", modifiedPackageName), humanize.Bytes(installedPackageSize)) - fmt.Printf( - " %s: %s %s\n", - bold.Sprint("Released"), - package_.ReleaseTime, - grayParens("%s ago", time_helpers.FormatDuration(time.Since(package_.ReleaseTime))), - ) - fmt.Printf(" %s: %s\n", bold.Sprint("Downloads last week"), fmtInt(int(pkgDownloads))) - fmt.Printf(" %s: %s\n", bold.Sprint("Estimated traffic last week"), humanize.Bytes(pkgDownloads*installedPackageSize)) - - if packageJson.Version != packageInfo.LatestVersion.JSON.Version { - fmt.Printf(" %s: %s %s\n", - bold.Sprint("Latest version"), - packageInfo.LatestVersion.Version, - grayParens("%s ago", time_helpers.FormatDuration(time.Since(packageInfo.LatestVersion.ReleaseTime))), - ) - } + reportPackageInfo(modifiedPackage, true, 0) if len(removedDependencies) > 0 { fmt.Println() @@ -76,22 +61,22 @@ func printReport( for _, p := range removedDependencies { info := deps[p.String()] - pcSize := float64(info.Size) * 100 / float64(installedPackageSize) + pcSize := float64(info.Size) * 100 / float64(oldPackageSize) traffic := info.DownloadsLastWeek * info.Size - pcTraffic := float64(pkgDownloads) * 100 / float64(info.DownloadsLastWeek) + pcTraffic := float64(downloadsLastWeek) * 100 / float64(info.DownloadsLastWeek) fmt.Printf(" %s %s: %s %s\n", color.RedString("-"), boldYellow.Sprint(p.String()), humanize.Bytes(info.Size), grayParens("%s%%", fmtPercent(pcSize))) fmt.Printf(" %s: %s\n", bold.Sprint("Downloads last week"), fmtInt(int(info.DownloadsLastWeek))) fmt.Printf( " %s: %s %s\n", bold.Sprintf("Downloads last week from \"%s\"", modifiedPackageName), - fmtInt(int(pkgDownloads)), + fmtInt(int(downloadsLastWeek)), grayParens("%s%%", fmtPercent(pcTraffic)), ) fmt.Printf(" %s: %s\n", bold.Sprint("Estimated traffic last week"), humanize.Bytes(traffic)) fmt.Printf(" %s: %s %s\n", bold.Sprintf("Estimated traffic from \"%s\"", modifiedPackageName), - humanize.Bytes(pkgDownloads*info.Size), + humanize.Bytes(downloadsLastWeek*info.Size), grayParens("%s%%", fmtPercent(pcTraffic)), ) } @@ -118,19 +103,59 @@ func printReport( } } + fmt.Println() + reportSizeDifference(oldPackageSize, newPackageSize, downloadsLastWeek) +} + +func reportPackageInfo(modifiedPackage *packageInfo, showLatestVersionHint bool, indentation int) { + indent := strings.Repeat(" ", indentation) + + packageInfo := modifiedPackage.Info + package_ := modifiedPackage.Package + packageJson := package_.JSON + downloadsLastWeek := modifiedPackage.DownloadsLastWeek + oldPackageSize := modifiedPackage.Size + + modifiedPackageName := boldYellow.Sprint(packageJson.String()) + + fmt.Printf("%s%s: %s\n", indent, bold.Sprintf("Package info for \"%s\"", modifiedPackageName), humanize.Bytes(oldPackageSize)) + fmt.Printf( + "%s %s: %s %s\n", + indent, + bold.Sprint("Released"), + package_.ReleaseTime, + grayParens("%s ago", time_helpers.FormatDuration(time.Since(package_.ReleaseTime))), + ) + fmt.Printf("%s %s: %s\n", indent, bold.Sprint("Downloads last week"), fmtInt(int(downloadsLastWeek))) + fmt.Printf("%s %s: %s\n", indent, bold.Sprint("Estimated traffic last week"), humanize.Bytes(downloadsLastWeek*oldPackageSize)) + + if showLatestVersionHint { + latestVersion := packageInfo.LatestVersion + if packageJson.Version != latestVersion.JSON.Version { + fmt.Printf("%s %s: %s %s\n", + indent, + bold.Sprint("Latest version"), + latestVersion.Version, + grayParens("%s ago", time_helpers.FormatDuration(time.Since(latestVersion.ReleaseTime))), + ) + } + } +} + +func reportSizeDifference(oldSize, newSize, downloads uint64) { indicatorColor := boldGreen - if estPackageSize > installedPackageSize { + if newSize > oldSize { indicatorColor = boldRed - } else if estPackageSize == installedPackageSize { + } else if newSize == oldSize { indicatorColor = boldGray } - pcSize := 100 * float64(estPackageSize) / float64(installedPackageSize) + pcSize := 100 * float64(newSize) / float64(oldSize) pcSizeFmt := indicatorColor.Sprintf("%s%%", fmtPercent(pcSize)) - oldTrafficLastWeek := big.NewInt(int64(pkgDownloads * installedPackageSize)) + oldTrafficLastWeek := big.NewInt(int64(downloads * oldSize)) oldTrafficLastWeekFmt := humanize.BigBytes(oldTrafficLastWeek) - estTrafficNextWeek := big.NewInt(int64(pkgDownloads * estPackageSize)) + estTrafficNextWeek := big.NewInt(int64(downloads * newSize)) estTrafficNextWeekFmt := humanize.BigBytes(estTrafficNextWeek) estTrafficChange := big.NewInt(0).Sub(oldTrafficLastWeek, estTrafficNextWeek) @@ -145,13 +170,12 @@ func printReport( } estTrafficChangeFmt = indicatorColor.Sprintf(estTrafficChangeFmt, humanize.BigBytes(estTrafficChange)) - fmt.Println() fmt.Printf( "%s: %s %s %s %s\n", bold.Sprint("Estimated package size"), - humanize.Bytes(installedPackageSize), + humanize.Bytes(oldSize), arrow, - indicatorColor.Sprintf(humanize.Bytes(estPackageSize)), + indicatorColor.Sprintf(humanize.Bytes(newSize)), grayParens("%s", pcSizeFmt), ) fmt.Printf( @@ -162,7 +186,6 @@ func printReport( indicatorColor.Sprint(estTrafficNextWeekFmt), grayParens("%s", estTrafficChangeFmt), ) - fmt.Println() } func grayParens(s string, args ...any) string { @@ -171,3 +194,11 @@ func grayParens(s string, args ...any) string { return fmt.Sprintf("%s%s%s", a, fmt.Sprintf(s, args...), b) } + +func fmtPercent(v float64) string { + return humanize.FormatFloat("#,###.##", v) +} + +func fmtInt(v int) string { + return humanize.FormatInteger("#,###.", v) +} diff --git a/version_size_diff.go b/version_size_diff.go new file mode 100644 index 0000000..67030db --- /dev/null +++ b/version_size_diff.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "package_size_calculator/pkg/npm" + + docker_client "github.com/docker/docker/client" + "github.com/dustin/go-humanize" + "github.com/manifoldco/promptui" + "github.com/rs/zerolog/log" +) + +func calculateVersionSizeChange() { + pkg := promptPackageVersions(npmClient, dockerC) + + fmt.Println() + reportPackageInfo(&pkg.Old, false, 0) + fmt.Println() + reportPackageInfo(&pkg.New, false, 0) + fmt.Println() + reportSizeDifference(pkg.Old.Size, pkg.New.Size, pkg.Old.DownloadsLastWeek) +} + +func promptPackageVersions(npmClient *npm.Client, dockerC *docker_client.Client) *packageVersionsInfo { + packageName, err := runPrompt(&promptui.Prompt{Label: "Package"}) + if err != nil { + log.Fatal().Err(err).Msg("Prompt failed") + } + + log.Info().Str("package", packageName).Msg("Fetching package info") + + b := &packageVersionsInfo{ + Old: packageInfo{}, + New: packageInfo{}, + } + + info, err := npmClient.GetPackageInfo(packageName) + if err != nil { + log.Fatal().Err(err).Msg("Failed to fetch package info") + } + + b.Old.Info = info + b.New.Info = info + + log.Debug().Msgf("Fetched package info for %s", info.Name) + + oldPackageVersion := promptPackageVersion(info, "Select the old version") + b.Old.Package = info.Versions[oldPackageVersion] + log.Info().Str("version", oldPackageVersion).Msg("Selected old version") + + newPackageVersion := promptPackageVersion(info, "Select the new version") + b.New.Package = info.Versions[newPackageVersion] + log.Info().Str("version", newPackageVersion).Msg("Selected new version") + + downloads, err := npmClient.GetPackageDownloadsLastWeek(info.Name) + if err != nil { + log.Fatal().Err(err).Msg("Failed to fetch package downloads") + } + + b.Old.DownloadsLastWeek = downloads[oldPackageVersion] + + // TODO: Figure out a way to get the download count of the old version when the new version was published. + // This would make the comparison more accurate, essentially answering "what would've happened + // if got published instead of ". + // NPM's API can't do this, they're missing downloads for a version at a specific timestamp + // https://github.com/npm/registry/blob/main/docs/download-counts.md#per-version-download-counts + b.New.DownloadsLastWeek = downloads[newPackageVersion] + + b.Old.Size, b.Old.TmpDir, err = measurePackageSize(dockerC, b.Old.AsDependency()) + if err != nil { + log.Fatal().Err(err).Msg("Failed to measure old package size") + } + + b.New.Size, b.New.TmpDir, err = measurePackageSize(dockerC, b.New.AsDependency()) + if err != nil { + log.Fatal().Err(err).Msg("Failed to measure new package size") + } + + log.Info().Str("package", b.String()).Str("size", humanize.Bytes(b.Old.Size)).Msg("Package size") + + return b +} + +type packageVersionsInfo struct { + Old packageInfo + New packageInfo +} + +func (b *packageVersionsInfo) String() string { + return b.Old.String() +}