mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* feat: Add separate package manager and resolver * fix: Npm dependency resolver * feat: Add analyzer for malysis query * feat: Add package manager guard as the orchestrator * feat: Add PMG to orchestrate installation * Add concurrent scan execution * Introduce package manager interaction abstraction * feat: Add UI port for guard * Remove refactored source files * Update README * fix: CI script for multi-arch build * ci: goreleaser CI fix * fix: npm command parser to extract package names * feat: Introduce global config primitive * fix: Close results channel for clean goroutine exit * ci: Add container image releaser * test: Improve test for npm resolver * refactor: Analyzer to generalise * Improve UI with additional info * fix: Goreleaser config * fix: npm resolver bug * fix: Fail when command exec workflow fails * fix: Bug with transitive dependency resolution * fix: Synchronize common data update in dependency resolver * chore: Improve log handling * docs: Update README * fix: UI text wrapping * fix: UI handling bugs * feat: Use concurrent dependency resolver
157 lines
3.6 KiB
Go
157 lines
3.6 KiB
Go
package packagemanager
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
|
|
)
|
|
|
|
type NpmPackageManagerConfig struct {
|
|
InstallCommands []string
|
|
CommandName string
|
|
}
|
|
|
|
func DefaultNpmPackageManagerConfig() NpmPackageManagerConfig {
|
|
return NpmPackageManagerConfig{
|
|
InstallCommands: []string{"install", "i", "add"},
|
|
CommandName: "npm",
|
|
}
|
|
}
|
|
|
|
func DefaultPnpmPackageManagerConfig() NpmPackageManagerConfig {
|
|
return NpmPackageManagerConfig{
|
|
InstallCommands: []string{"install", "i", "add"},
|
|
CommandName: "pnpm",
|
|
}
|
|
}
|
|
|
|
type npmPackageManager struct {
|
|
Config NpmPackageManagerConfig
|
|
}
|
|
|
|
func NewNpmPackageManager(config NpmPackageManagerConfig) (*npmPackageManager, error) {
|
|
return &npmPackageManager{
|
|
Config: config,
|
|
}, nil
|
|
}
|
|
|
|
func (npm *npmPackageManager) Name() string {
|
|
return "npm"
|
|
}
|
|
|
|
func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
|
|
if len(args) > 0 && (args[0] == "npm" || args[0] == "pnpm") {
|
|
args = args[1:]
|
|
}
|
|
|
|
command := Command{Exe: npm.Config.CommandName, Args: args}
|
|
|
|
// No command specified
|
|
if len(args) < 2 {
|
|
return &ParsedCommand{
|
|
Command: command,
|
|
}, nil
|
|
}
|
|
|
|
// Extract packages from args
|
|
var packages []string
|
|
for idx, arg := range args {
|
|
if slices.Contains(npm.Config.InstallCommands, arg) {
|
|
// All subsequent args are packages except for flags
|
|
for i := idx + 1; i < len(args); i++ {
|
|
if strings.HasPrefix(args[i], "-") {
|
|
continue
|
|
}
|
|
|
|
packages = append(packages, args[i])
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// No packages found
|
|
if len(packages) == 0 {
|
|
return &ParsedCommand{
|
|
Command: command,
|
|
}, nil
|
|
}
|
|
|
|
// Process all package arguments
|
|
var installTargets []*PackageInstallTarget
|
|
for _, pkg := range packages {
|
|
packageName, version, err := npmParsePackageInfo(pkg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse package info: %w", err)
|
|
}
|
|
|
|
// Clean version if specified
|
|
if version != "" {
|
|
version = npmCleanVersion(version)
|
|
}
|
|
|
|
installTargets = append(installTargets, &PackageInstallTarget{
|
|
PackageVersion: &packagev1.PackageVersion{
|
|
Package: &packagev1.Package{
|
|
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
|
|
Name: packageName,
|
|
},
|
|
Version: version,
|
|
},
|
|
})
|
|
}
|
|
|
|
return &ParsedCommand{
|
|
Command: command,
|
|
InstallTargets: installTargets,
|
|
}, nil
|
|
}
|
|
|
|
func npmParsePackageInfo(input string) (packageName, version string, err error) {
|
|
if input == "" {
|
|
return "", "", fmt.Errorf("package info cannot be empty")
|
|
}
|
|
|
|
input = strings.TrimSpace(input)
|
|
if strings.HasPrefix(input, "@") {
|
|
// Scoped package (e.g. @types/node or @types/node@1.0.0)
|
|
lastAtIndex := strings.LastIndex(input, "@")
|
|
if lastAtIndex > 0 {
|
|
packageName = strings.TrimSpace(input[:lastAtIndex])
|
|
version = strings.TrimSpace(input[lastAtIndex+1:])
|
|
return packageName, version, nil
|
|
}
|
|
|
|
// If no version specifier, return the whole input as package name
|
|
return strings.TrimSpace(input), "", nil
|
|
}
|
|
|
|
// Normal package (e.g. lodash or lodash@4.17.21)
|
|
parts := strings.Split(input, "@")
|
|
if len(parts) == 2 {
|
|
packageName = strings.TrimSpace(parts[0])
|
|
version = strings.TrimSpace(parts[1])
|
|
return packageName, version, nil
|
|
}
|
|
|
|
if len(parts) == 1 {
|
|
packageName = strings.TrimSpace(parts[0])
|
|
return packageName, "", nil
|
|
}
|
|
|
|
return "", "", fmt.Errorf("invalid format: expected 'package' OR 'package@version', got '%s'", input)
|
|
}
|
|
|
|
func npmCleanVersion(version string) string {
|
|
version = strings.TrimPrefix(version, "^")
|
|
version = strings.TrimPrefix(version, "~")
|
|
|
|
if version == "*" || version == "" {
|
|
return "latest"
|
|
}
|
|
|
|
return version
|
|
}
|