Files
pack/internal/utils/utils.go
T
Michael BiancoandGitHub 0bc3bce09a feat: support package.json engines for package manager and node versions (#251)
* refactor(node): check engines after file-based detection for package manager

Engines are now considered only if file-based detection keeps default npm,
so lockfiles/configs take precedence over engines as a hint.

* feat: extract yarn version from package.json > engines > yarn

* test: add bun version to bun test assertion

* test: update snapshots

* test: add engines version extraction test

* test: assert against two distinct versions

we couldn't do this before the recent testing harness improvement

* test: snapshot update

* fix: properly handle corepack version definitions

* docs: update docs on node provider detection

* docs: add docs command to claude
2025-10-22 13:12:55 -06:00

179 lines
4.4 KiB
Go

package utils
import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"github.com/tailscale/hujson"
)
func RemoveDuplicates[T comparable](sliceList []T) []T {
allKeys := make(map[T]bool)
list := []T{}
for _, item := range sliceList {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
// MergeStringSlicePointers combines multiple string slice pointers, deduplicates values, and sorts them
func MergeStringSlicePointers(slices ...*[]string) *[]string {
if len(slices) == 0 {
return nil
}
var allStrings []string
for _, slice := range slices {
if slice != nil {
allStrings = append(allStrings, *slice...)
}
}
if len(allStrings) == 0 {
return nil
}
// Deduplicate and sort
seen := make(map[string]bool)
var uniqueStrings []string
for _, s := range allStrings {
if !seen[s] {
seen[s] = true
uniqueStrings = append(uniqueStrings, s)
}
}
sort.Strings(uniqueStrings)
return &uniqueStrings
}
// CapitalizeFirst converts the first character of a string to uppercase.
// The rest of the string remains unchanged.
// Examples:
// - "hello" -> "Hello"
// - "world" -> "World"
// - "" -> ""
// - "already Capitalized" -> "Already Capitalized"
func CapitalizeFirst(s string) string {
if s == "" {
return ""
}
runes := []rune(s)
runes[0] = []rune(strings.ToUpper(string(runes[0])))[0]
return string(runes)
}
// ParsePackageWithVersion parses a slice of package specifications in the format "name@version"
// and returns a map of package names to their versions.
// If a package has no version specified (no @ symbol), it defaults to "latest".
// Examples:
// - ["[email protected]"] -> {"node": "14.2"}
// - ["python"] -> {"python": "latest"}
// - ["[email protected]", "go"] -> {"ruby": "3.0.0", "go": "latest"}
// - ["node@^14.3", "python@>=3.9"] -> {"node": "^14.3", "python": ">=3.9"}
func ParsePackageWithVersion(versions []string) map[string]string {
parsedVersions := make(map[string]string)
for _, version := range versions {
parts := strings.Split(version, "@")
if len(parts) == 1 {
parsedVersions[parts[0]] = "latest"
} else {
parsedVersions[parts[0]] = parts[1]
}
}
return parsedVersions
}
// ExtractSemverVersion extracts the first version number found in a string.
// It supports full semver (major.minor.patch) as well as partial versions (major or major.minor).
// The version can appear anywhere in the string and can have prefixes or suffixes.
// Examples:
// - "1.2.3" -> "1.2.3"
// - "v1.2.3" -> "1.2.3"
// - "python-3.10.7" -> "3.10.7"
// - "version1.2.3-beta" -> "1.2.3"
// - "requires node 14.2" -> "14.2"
// - "python 3" -> "3"
//
// Returns an empty string if no version number is found.
func ExtractSemverVersion(version string) string {
semverRe := regexp.MustCompile(`(\d+(?:\.\d+)?(?:\.\d+)?)`)
matches := semverRe.FindStringSubmatch(strings.TrimSpace(version))
if len(matches) > 1 {
return matches[1]
}
return ""
}
// ParseSemver parses a semantic version string and returns a Semver struct.
func ParseSemver(version string) (*Semver, error) {
// Handle corepack version format like "[email protected]"
if idx := strings.Index(version, "@"); idx != -1 {
version = version[idx+1:]
}
// Remove potential prefixes like "v"
version = strings.TrimPrefix(version, "v")
// Split by dots
parts := strings.Split(version, ".")
// Parse major version
major, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid major version: %s", parts[0])
}
minor := 0
patch := 0
if len(parts) > 1 {
// Parse minor version
minorVer, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid minor version: %s", parts[1])
}
minor = minorVer
}
if len(parts) > 2 {
// Parse patch version - take care of potential suffixes like "-alpha", "-beta", etc.
patchStr := parts[2]
patchParts := strings.Split(patchStr, "-")
patchVer, err := strconv.Atoi(patchParts[0])
if err != nil {
return nil, fmt.Errorf("invalid patch version: %s", patchParts[0])
}
patch = patchVer
}
return &Semver{
Major: major,
Minor: minor,
Patch: patch,
}, nil
}
type Semver struct {
Major int
Minor int
Patch int
}
// convert hujson/extended JSON into standardized json
func StandardizeJSON(b []byte) ([]byte, error) {
ast, err := hujson.Parse(b)
if err != nil {
return b, err
}
ast.Standardize()
return ast.Pack(), nil
}