-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(nodejs): add v9 pnpm lock file support (#6617)
- Loading branch information
1 parent
9515695
commit 1e08648
Showing
5 changed files
with
698 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,10 +2,12 @@ package pnpm | |
|
||
import ( | ||
"fmt" | ||
"sort" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/samber/lo" | ||
"golang.org/x/exp/maps" | ||
"golang.org/x/xerrors" | ||
"gopkg.in/yaml.v3" | ||
|
||
|
@@ -34,6 +36,28 @@ type LockFile struct { | |
Dependencies map[string]any `yaml:"dependencies,omitempty"` | ||
DevDependencies map[string]any `yaml:"devDependencies,omitempty"` | ||
Packages map[string]PackageInfo `yaml:"packages,omitempty"` | ||
|
||
// V9 | ||
Importers Importer `yaml:"importers,omitempty"` | ||
Snapshots map[string]Snapshot `yaml:"snapshots,omitempty"` | ||
} | ||
|
||
type Importer struct { | ||
Root RootImporter `yaml:".,omitempty"` | ||
} | ||
|
||
type RootImporter struct { | ||
Dependencies map[string]ImporterDepVersion `yaml:"dependencies,omitempty"` | ||
DevDependencies map[string]ImporterDepVersion `yaml:"devDependencies,omitempty"` | ||
} | ||
|
||
type ImporterDepVersion struct { | ||
Version string `yaml:"version,omitempty"` | ||
} | ||
|
||
type Snapshot struct { | ||
Dependencies map[string]string `yaml:"dependencies,omitempty"` | ||
OptionalDependencies map[string]string `yaml:"optionalDependencies,omitempty"` | ||
} | ||
|
||
type Parser struct { | ||
|
@@ -57,8 +81,16 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc | |
return nil, nil, nil | ||
} | ||
|
||
pkgs, deps := p.parse(lockVer, lockFile) | ||
var pkgs []ftypes.Package | ||
var deps []ftypes.Dependency | ||
if lockVer >= 9 { | ||
pkgs, deps = p.parseV9(lockFile) | ||
} else { | ||
pkgs, deps = p.parse(lockVer, lockFile) | ||
} | ||
|
||
sort.Sort(ftypes.Packages(pkgs)) | ||
sort.Sort(ftypes.Dependencies(deps)) | ||
return pkgs, deps, nil | ||
} | ||
|
||
|
@@ -78,9 +110,11 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, [] | |
// cf. https://github.com/pnpm/spec/blob/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile/6.0.md#packagesdependencypathname | ||
name := info.Name | ||
version := info.Version | ||
var ref string | ||
|
||
if name == "" { | ||
name, version = p.parsePackage(depPath, lockVer) | ||
name, version, ref = p.parseDepPath(depPath, lockVer) | ||
version = p.parseVersion(depPath, version, lockVer) | ||
} | ||
pkgID := packageID(name, version) | ||
|
||
|
@@ -90,13 +124,15 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, [] | |
} | ||
|
||
pkgs = append(pkgs, ftypes.Package{ | ||
ID: pkgID, | ||
Name: name, | ||
Version: version, | ||
Relationship: lo.Ternary(isDirectPkg(name, lockFile.Dependencies), ftypes.RelationshipDirect, ftypes.RelationshipIndirect), | ||
ID: pkgID, | ||
Name: name, | ||
Version: version, | ||
Relationship: lo.Ternary(isDirectPkg(name, lockFile.Dependencies), ftypes.RelationshipDirect, ftypes.RelationshipIndirect), | ||
ExternalReferences: toExternalRefs(ref), | ||
}) | ||
|
||
if len(dependencies) > 0 { | ||
sort.Strings(dependencies) | ||
deps = append(deps, ftypes.Dependency{ | ||
ID: pkgID, | ||
DependsOn: dependencies, | ||
|
@@ -107,6 +143,98 @@ func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, [] | |
return pkgs, deps | ||
} | ||
|
||
func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependency) { | ||
lockVer := 9.0 | ||
resolvedPkgs := make(map[string]ftypes.Package) | ||
resolvedDeps := make(map[string]ftypes.Dependency) | ||
|
||
// Check all snapshots and save with resolved versions | ||
resolvedSnapshots := make(map[string][]string) | ||
for depPath, snapshot := range lockFile.Snapshots { | ||
name, version, _ := p.parseDepPath(depPath, lockVer) | ||
|
||
var dependsOn []string | ||
for depName, depVer := range lo.Assign(snapshot.OptionalDependencies, snapshot.Dependencies) { | ||
depVer = p.trimPeerDeps(depVer, lockVer) // pnpm has already separated dep name. therefore, we only need to separate peer deps. | ||
depVer = p.parseVersion(depPath, depVer, lockVer) | ||
id := packageID(depName, depVer) | ||
if _, ok := lockFile.Packages[id]; ok { | ||
dependsOn = append(dependsOn, id) | ||
} | ||
} | ||
if len(dependsOn) > 0 { | ||
resolvedSnapshots[packageID(name, version)] = dependsOn | ||
} | ||
|
||
} | ||
|
||
for depPath, pkgInfo := range lockFile.Packages { | ||
name, ver, ref := p.parseDepPath(depPath, lockVer) | ||
parsedVer := p.parseVersion(depPath, ver, lockVer) | ||
|
||
if pkgInfo.Version != "" { | ||
parsedVer = pkgInfo.Version | ||
} | ||
|
||
// By default, pkg is dev pkg. | ||
// We will update `Dev` field later. | ||
dev := true | ||
relationship := ftypes.RelationshipIndirect | ||
if dep, ok := lockFile.Importers.Root.DevDependencies[name]; ok && dep.Version == ver { | ||
relationship = ftypes.RelationshipDirect | ||
} | ||
if dep, ok := lockFile.Importers.Root.Dependencies[name]; ok && dep.Version == ver { | ||
relationship = ftypes.RelationshipDirect | ||
dev = false // mark root direct deps to update `dev` field of their child deps. | ||
} | ||
|
||
id := packageID(name, parsedVer) | ||
resolvedPkgs[id] = ftypes.Package{ | ||
ID: id, | ||
Name: name, | ||
Version: parsedVer, | ||
Relationship: relationship, | ||
Dev: dev, | ||
ExternalReferences: toExternalRefs(ref), | ||
} | ||
|
||
// Save child deps | ||
if dependsOn, ok := resolvedSnapshots[depPath]; ok { | ||
sort.Strings(dependsOn) | ||
resolvedDeps[id] = ftypes.Dependency{ | ||
ID: id, | ||
DependsOn: dependsOn, // Deps from dependsOn has been resolved when parsing snapshots | ||
} | ||
} | ||
} | ||
|
||
// Overwrite the `Dev` field for dev deps and their child dependencies. | ||
for _, pkg := range resolvedPkgs { | ||
if !pkg.Dev { | ||
p.markRootPkgs(pkg.ID, resolvedPkgs, resolvedDeps) | ||
} | ||
} | ||
|
||
return maps.Values(resolvedPkgs), maps.Values(resolvedDeps) | ||
} | ||
|
||
// markRootPkgs sets `Dev` to false for non dev dependency. | ||
func (p *Parser) markRootPkgs(id string, pkgs map[string]ftypes.Package, deps map[string]ftypes.Dependency) { | ||
pkg, ok := pkgs[id] | ||
if !ok { | ||
return | ||
} | ||
|
||
pkg.Dev = false | ||
pkgs[id] = pkg | ||
|
||
// Update child deps | ||
for _, depID := range deps[id].DependsOn { | ||
p.markRootPkgs(depID, pkgs, deps) | ||
} | ||
return | ||
} | ||
|
||
func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 { | ||
switch v := lockFile.LockfileVersion.(type) { | ||
// v5 | ||
|
@@ -127,55 +255,109 @@ func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 { | |
} | ||
} | ||
|
||
// cf. https://github.com/pnpm/pnpm/blob/ce61f8d3c29eee46cee38d56ced45aea8a439a53/packages/dependency-path/src/index.ts#L112-L163 | ||
func (p *Parser) parsePackage(depPath string, lockFileVersion float64) (string, string) { | ||
// The version separator is different between v5 and v6+. | ||
versionSep := "@" | ||
if lockFileVersion < 6 { | ||
versionSep = "/" | ||
func (p *Parser) parseDepPath(depPath string, lockVer float64) (string, string, string) { | ||
dPath, nonDefaultRegistry := p.trimRegistry(depPath, lockVer) | ||
|
||
var scope string | ||
scope, dPath = p.separateScope(dPath) | ||
|
||
var name string | ||
name, dPath = p.separateName(dPath, lockVer) | ||
|
||
// add scope to pkg name | ||
if scope != "" { | ||
name = fmt.Sprintf("%s/%s", scope, name) | ||
} | ||
return p.parseDepPath(depPath, versionSep) | ||
|
||
ver := p.trimPeerDeps(dPath, lockVer) | ||
|
||
return name, ver, lo.Ternary(nonDefaultRegistry, depPath, "") | ||
} | ||
|
||
func (p *Parser) parseDepPath(depPath, versionSep string) (string, string) { | ||
// Skip registry | ||
// e.g. | ||
// - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10" | ||
// - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9" | ||
// - "/lodash/4.17.10" => "lodash/4.17.10" | ||
_, depPath, _ = strings.Cut(depPath, "/") | ||
// trimRegistry trims registry (or `/` prefix) for depPath. | ||
// It returns true if non-default registry has been trimmed. | ||
// e.g. | ||
// - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10", false | ||
// - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", false | ||
// - "private.npm.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", true | ||
// - "/lodash/4.17.10" => "lodash/4.17.10", false | ||
// - "/[email protected]" => "[email protected]", false | ||
func (p *Parser) trimRegistry(depPath string, lockVer float64) (string, bool) { | ||
var nonDefaultRegistry bool | ||
// lock file v9 doesn't use registry prefix | ||
if lockVer < 9 { | ||
var registry string | ||
registry, depPath, _ = strings.Cut(depPath, "/") | ||
if registry != "" && registry != "registry.npmjs.org" { | ||
nonDefaultRegistry = true | ||
} | ||
} | ||
return depPath, nonDefaultRegistry | ||
} | ||
|
||
// Parse scope | ||
// e.g. | ||
// - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"} | ||
// - v6+: "@babel/[email protected]" => "{"babel", "[email protected]"} | ||
// separateScope separates the scope (if set) from the rest of the depPath. | ||
// e.g. | ||
// - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"} | ||
// - v6+: "@babel/[email protected]" => "{"babel", "[email protected]"} | ||
func (p *Parser) separateScope(depPath string) (string, string) { | ||
var scope string | ||
if strings.HasPrefix(depPath, "@") { | ||
scope, depPath, _ = strings.Cut(depPath, "/") | ||
} | ||
return scope, depPath | ||
} | ||
|
||
// Parse package name | ||
// e.g. | ||
// - v5: "generator/7.21.9" => {"generator", "7.21.9"} | ||
// - v6+: "[email protected]" => {"helper-annotate-as-pure", "7.18.6"} | ||
var name, version string | ||
name, version, _ = strings.Cut(depPath, versionSep) | ||
if scope != "" { | ||
name = fmt.Sprintf("%s/%s", scope, name) | ||
// separateName separates pkg name and version. | ||
// e.g. | ||
// - v5: "generator/7.21.9" => {"generator", "7.21.9"} | ||
// - v6+: "7.21.5(@babel/[email protected])" => "7.21.5" | ||
// | ||
// for v9+ version can be filePath or link: | ||
// - "package1@file:package1:" | ||
// - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5" | ||
// | ||
// Also version can contain peer deps: | ||
// - "[email protected]([email protected])" | ||
func (p *Parser) separateName(depPath string, lockVer float64) (string, string) { | ||
sep := "@" | ||
if lockVer < 6 { | ||
sep = "/" | ||
} | ||
name, version, _ := strings.Cut(depPath, sep) | ||
return name, version | ||
} | ||
|
||
// Trim peer deps | ||
// e.g. | ||
// - v5: "7.21.5_@[email protected]" => "7.21.5" | ||
// - v6+: "7.21.5(@babel/[email protected])" => "7.21.5" | ||
func (p *Parser) trimPeerDeps(depPath string, lockVer float64) string { | ||
sep := "(" | ||
if lockVer < 6 { | ||
sep = "_" | ||
} | ||
// Trim peer deps | ||
// e.g. | ||
// - v5: "7.21.5_@[email protected]" => "7.21.5" | ||
// - v6+: "7.21.5(@babel/[email protected])" => "7.21.5" | ||
if idx := strings.IndexAny(version, "_("); idx != -1 { | ||
version = version[:idx] | ||
version, _, _ := strings.Cut(depPath, sep) | ||
return version | ||
} | ||
|
||
// parseVersion parses version. | ||
// v9 can use filePath or link as version - we need to clear these versions. | ||
// e.g. | ||
// - "package1@file:package1:" | ||
// - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5" | ||
// | ||
// Other versions should be semver valid. | ||
func (p *Parser) parseVersion(depPath, ver string, lockVer float64) string { | ||
if lockVer < 9 && (strings.HasPrefix(ver, "file:") || strings.HasPrefix(ver, "http")) { | ||
return "" | ||
} | ||
if _, err := semver.Parse(version); err != nil { | ||
if _, err := semver.Parse(ver); err != nil { | ||
p.logger.Debug("Skip non-semver package", log.String("pkg_path", depPath), | ||
log.String("version", version), log.Err(err)) | ||
return "", "" | ||
log.String("version", ver), log.Err(err)) | ||
return "" | ||
} | ||
return name, version | ||
|
||
return ver | ||
} | ||
|
||
func isDirectPkg(name string, directDeps map[string]interface{}) bool { | ||
|
@@ -186,3 +368,15 @@ func isDirectPkg(name string, directDeps map[string]interface{}) bool { | |
func packageID(name, version string) string { | ||
return dependency.ID(ftypes.Pnpm, name, version) | ||
} | ||
|
||
func toExternalRefs(ref string) []ftypes.ExternalRef { | ||
if ref == "" { | ||
return nil | ||
} | ||
return []ftypes.ExternalRef{ | ||
{ | ||
Type: ftypes.RefVCS, | ||
URL: ref, | ||
}, | ||
} | ||
} |
Oops, something went wrong.