mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
Add support for defaulting to latest version and improve code structure
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safedep/dry/log"
|
||||
"github.com/safedep/pmg/pkg/common/utils"
|
||||
"github.com/safedep/pmg/pkg/models"
|
||||
)
|
||||
|
||||
// NpmFetcher fetches dependencies from NPM registry
|
||||
type NpmFetcher struct {
|
||||
*BaseFetcher
|
||||
}
|
||||
|
||||
// NewNpmFetcher creates a new NPM registry fetcher
|
||||
func NewNpmFetcher(timeout time.Duration) *NpmFetcher {
|
||||
client := NewHttpRegistryClient(
|
||||
timeout,
|
||||
"https://registry.npmjs.org/%s/%s",
|
||||
parseNpmPackageInfo,
|
||||
)
|
||||
return &NpmFetcher{
|
||||
BaseFetcher: NewBaseFetcher(client),
|
||||
}
|
||||
}
|
||||
|
||||
// parseNpmPackageInfo parses NPM package information from JSON
|
||||
func parseNpmPackageInfo(data []byte) (*models.PackageInfo, error) {
|
||||
var packageInfo models.PackageInfo
|
||||
if err := json.Unmarshal(data, &packageInfo); err != nil {
|
||||
return nil, fmt.Errorf("parsing package info: %w", err)
|
||||
}
|
||||
return &packageInfo, nil
|
||||
}
|
||||
|
||||
// GetDependencyTree fetches the complete dependency tree for an NPM package
|
||||
func (nf *NpmFetcher) GetDependencyTree(ctx context.Context, pkg models.Package) (*models.DependencyNode, error) {
|
||||
return nf.fetchDependenciesConcurrent(ctx, pkg)
|
||||
}
|
||||
|
||||
// GetFlattenedDependencies returns a flat list of all dependencies as strings
|
||||
func (nf *NpmFetcher) GetFlattenedDependencies(ctx context.Context, packageName, version string) ([]string, error) {
|
||||
// Reset the visited map to ensure we get a complete tree
|
||||
nf.resetVisited()
|
||||
|
||||
// Get the complete dependency tree
|
||||
tree, err := nf.GetDependencyTree(ctx, models.Package{
|
||||
Name: packageName,
|
||||
Version: version,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch dependency tree: %w", err)
|
||||
}
|
||||
|
||||
// Convert tree to flat list
|
||||
var dependencies []string
|
||||
flattenDependencyTree(tree, &dependencies)
|
||||
|
||||
// Remove duplicates if needed
|
||||
uniqueDeps := make(map[string]bool)
|
||||
var result []string
|
||||
|
||||
for _, dep := range dependencies {
|
||||
if !uniqueDeps[dep] {
|
||||
uniqueDeps[dep] = true
|
||||
result = append(result, dep)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fetchDependenciesConcurrent recursively fetches package dependencies concurrently
|
||||
func (nf *NpmFetcher) fetchDependenciesConcurrent(ctx context.Context, pkg models.Package) (*models.DependencyNode, error) {
|
||||
key := cacheKey(pkg)
|
||||
if nf.isVisited(key) {
|
||||
return &models.DependencyNode{
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
}, nil
|
||||
}
|
||||
nf.markVisited(key)
|
||||
|
||||
packageInfo, err := nf.client.FetchPackageInfo(ctx, pkg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch package info for %s: %w", pkg.Name, err)
|
||||
}
|
||||
|
||||
dependencies := packageInfo.Dependencies
|
||||
node := &models.DependencyNode{
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
Dependencies: make(map[string]*models.DependencyNode),
|
||||
}
|
||||
|
||||
if len(dependencies) == 0 {
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Process dependencies concurrently
|
||||
type result struct {
|
||||
name string
|
||||
node *models.DependencyNode
|
||||
err error
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan result, len(dependencies))
|
||||
|
||||
for depName, depVersion := range dependencies {
|
||||
// Check if context is canceled
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(name, version string) {
|
||||
defer wg.Done()
|
||||
version = utils.CleanVersion(version)
|
||||
depNode, err := nf.fetchDependenciesConcurrent(ctx, models.Package{Name: name, Version: version})
|
||||
resultChan <- result{name, depNode, err}
|
||||
}(depName, depVersion)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete in a separate goroutine
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
for res := range resultChan {
|
||||
if res.err != nil {
|
||||
log.Warnf("Failed to fetch dependency %s: %v", res.name, res.err)
|
||||
continue
|
||||
}
|
||||
node.Dependencies[res.name] = res.node
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (nf *NpmFetcher) ResolveVersion(ctx context.Context, packageName, version string) (string, error) {
|
||||
if version != "" {
|
||||
return version, nil
|
||||
}
|
||||
|
||||
latestVersion, err := nf.client.GetLatestVersion(ctx, packageName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get latest version for %s: %w", packageName, err)
|
||||
}
|
||||
|
||||
return latestVersion, nil
|
||||
}
|
||||
Reference in New Issue
Block a user