mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
164 lines
4.0 KiB
Go
164 lines
4.0 KiB
Go
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
|
|
}
|