这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
4 changes: 2 additions & 2 deletions cli/internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func (c *Context) resolveWorkspaceRootDeps(rootPackageJSON *fs.PackageJSON) erro
for dep, version := range pkg.Dependencies {
pkg.UnresolvedExternalDeps[dep] = version
}
if util.IsYarn(c.PackageManager.Name) {
if c.Lockfile != nil {
pkg.TransitiveDeps = []string{}
c.resolveDepGraph(&lockfileWg, pkg.UnresolvedExternalDeps, depSet, seen, pkg)
lockfileWg.Wait()
Expand Down Expand Up @@ -326,7 +326,7 @@ func (c *Context) parsePackageJSON(repoRoot turbopath.AbsolutePath, pkgJSONPath
}

func (c *Context) resolveDepGraph(wg *sync.WaitGroup, unresolvedDirectDeps map[string]string, resolvedDepsSet mapset.Set, seen mapset.Set, pkg *fs.PackageJSON) {
if !util.IsYarn(c.PackageManager.Name) {
if c.Lockfile == nil {
return
}
for directDepName, unresolvedVersion := range unresolvedDirectDeps {
Expand Down
236 changes: 236 additions & 0 deletions cli/internal/lockfile/pnpm_lockfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package lockfile

import (
"fmt"
"io"

"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)

// PnpmLockfile Go representation of the contents of 'pnpm-lock.yaml'
// Reference https://github.com/pnpm/pnpm/blob/main/packages/lockfile-types/src/index.ts
type PnpmLockfile struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upstream now has Time. 😅

pnpm/pnpm@0373af2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's verbose but I'd prefer to match the upstream key names on all of these so that we don't have any confusion.

Version float32 `yaml:"lockfileVersion"`
NeverBuiltDependencies []string `yaml:"neverBuiltDependencies,omitempty"`
OnlyBuiltDependencies []string `yaml:"onlyBuiltDependencies,omitempty"`
Overrides map[string]string `yaml:"overrides,omitempty"`
PackageExtensionsChecksum string `yaml:"packageExtensionsChecksum,omitempty"`
PatchedDependencies map[string]PatchFile `yaml:"patchedDependencies,omitempty"`
Importers map[string]ProjectSnapshot `yaml:"importers"`
Packages map[string]PackageSnapshot `yaml:"packages,omitempty"`
Time map[string]string `yaml:"time,omitempty"`
}

var _ Lockfile = (*PnpmLockfile)(nil)

// ProjectSnapshot Snapshot used to represent projects in the importers section
type ProjectSnapshot struct {
Specifiers map[string]string `yaml:"specifiers"`
Dependencies map[string]string `yaml:"dependencies,omitempty"`
OptionalDependencies map[string]string `yaml:"optionalDependencies,omitempty"`
DevDependencies map[string]string `yaml:"devDependencies,omitempty"`
DependenciesMeta map[string]DependenciesMeta `yaml:"dependenciesMeta,omitempty"`
PublishDirectory string `yaml:"publishDirectory,omitempty"`
}

// PackageSnapshot Snapshot used to represent a package in the packages setion
type PackageSnapshot struct {
Resolution PackageResolution `yaml:"resolution,flow"`
ID string `yaml:"id,omitempty"`

// only needed for packages that aren't in npm
Name string `yaml:"name,omitempty"`
Version string `yaml:"version,omitempty"`

Engines struct {
Node string `yaml:"node"`
NPM string `yaml:"npm,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need npm here? It's not in the upstream; did it cause failures? (Is it a 6/7 thing?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was supported in pnpm6 🙃

} `yaml:"engines,omitempty,flow"`
CPU []string `yaml:"cpu,omitempty,flow"`
Os []string `yaml:"os,omitempty,flow"`
LibC []string `yaml:"libc,omitempty"`

Deprecated string `yaml:"deprecated,omitempty"`
HasBin bool `yaml:"hasBin,omitempty"`
Prepare bool `yaml:"prepare,omitempty"`
RequiresBuild bool `yaml:"requiresBuild,omitempty"`

BundledDependencies []string `yaml:"bundledDependencies,omitempty"`
PeerDependencies map[string]string `yaml:"peerDependencies,omitempty"`
PeerDependenciesMeta map[string]struct {
Optional bool `yaml:"optional"`
} `yaml:"peerDependenciesMeta,omitempty"`

Dependencies map[string]string `yaml:"dependencies,omitempty"`
OptionalDependencies map[string]string `yaml:"optionalDependencies,omitempty"`

TransitivePeerDependencies []string `yaml:"transitivePeerDependencies,omitempty"`
Dev bool `yaml:"dev"`
Optional bool `yaml:"optional,omitempty"`
Patched bool `yaml:"patched,omitempty"`
}

// PackageResolution Various resolution strategies for packages
type PackageResolution struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type string `yaml:"type,omitempty"`
// For npm or tarball
Integrity string `yaml:"integrity,omitempty"`

// For tarball
Tarball string `yaml:"tarball,omitempty"`

// For local directory
Dir string `yaml:"directory,omitempty"`

// For git repo
Repo string `yaml:"repo,omitempty"`
Commit string `yaml:"commit,omitempty"`
}

// PatchFile represent a patch applied to a package
type PatchFile struct {
Path string `yaml:"path"`
Hash string `yaml:"hash"`
}

func isSupportedVersion(version float32) error {
supportedVersions := []float32{5.3, 5.4}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make these strings.

for _, supportedVersion := range supportedVersions {
if version == supportedVersion {
return nil
}
}
Comment on lines +99 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still can't believe that this is the recommended method for testing existence in a slice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As someone who is new to Go, this was a shock.

return errors.Errorf("Unable to generate pnpm-lock.yaml with lockfileVersion: %f. Supported lockfile versions are %v", version, supportedVersions)
}

// DependenciesMeta metadata for dependencies
type DependenciesMeta struct {
Injected bool `yaml:"injected,omitempty"`
Node string `yaml:"node,omitempty"`
Patch string `yaml:"patch,omitempty"`
}

// DecodePnpmLockfile parse a pnpm lockfile
func DecodePnpmLockfile(contents []byte) (*PnpmLockfile, error) {
var lockfile PnpmLockfile
if err := yaml.Unmarshal(contents, &lockfile); err != nil {
return nil, errors.Wrap(err, "could not unmarshal lockfile: ")
}

if err := isSupportedVersion(lockfile.Version); err != nil {
return nil, err
}

return &lockfile, nil
}

// ResolvePackage Given a package and version returns the key, resolved version, and if it was found
func (p *PnpmLockfile) ResolvePackage(name string, version string) (string, string, bool) {
resolvedVersion, ok := p.resolveSpecifier(name, version)
if !ok {
return "", "", false
}
key := fmt.Sprintf("/%s/%s", name, resolvedVersion)
if entry, ok := (p.Packages)[key]; ok {
var version string
if entry.Version != "" {
version = entry.Version
} else {
version = resolvedVersion
}
return key, version, true
}

return "", "", false
}

// AllDependencies Given a lockfile key return all (dev/optional/peer) dependencies of that package
func (p *PnpmLockfile) AllDependencies(key string) (map[string]string, bool) {
deps := map[string]string{}
entry, ok := (p.Packages)[key]
if !ok {
return deps, false
}

for name, version := range entry.Dependencies {
deps[name] = version
}

for name, version := range entry.OptionalDependencies {
deps[name] = version
}

for name, version := range entry.PeerDependencies {
deps[name] = version
}
Comment on lines +156 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these collide? Do we care if they do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading through pnpm I don't think these can collide. If a dep is marked optional then it won't appear in dependencies, peer deps are handled in the same manner. In the case that there's an optional peer it appears under peer with the optional attribute set true in peerDependenciesMeta

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation!


return deps, true
}

// Subgraph Given a list of lockfile keys returns a Lockfile based off the original one that only contains the packages given
func (p *PnpmLockfile) Subgraph(packages []string) (Lockfile, error) {
lockfilePackages := make(map[string]PackageSnapshot, len(packages))
for _, key := range packages {
entry, ok := p.Packages[key]
if ok {
lockfilePackages[key] = entry
} else {
return nil, fmt.Errorf("Unable to find lockfile entry for %s", key)
}
}

lockfile := PnpmLockfile{
Version: p.Version,
Importers: p.Importers,
Packages: lockfilePackages,
NeverBuiltDependencies: p.NeverBuiltDependencies,
OnlyBuiltDependencies: p.OnlyBuiltDependencies,
Overrides: p.Overrides,
PackageExtensionsChecksum: p.PackageExtensionsChecksum,
PatchedDependencies: p.PatchedDependencies,
}

return &lockfile, nil
}

// Encode encode the lockfile representation and write it to the given writer
func (p *PnpmLockfile) Encode(w io.Writer) error {
if err := isSupportedVersion(p.Version); err != nil {
return err
}

encoder := yaml.NewEncoder(w)
encoder.SetIndent(2)

if err := encoder.Encode(p); err != nil {
return errors.Wrap(err, "unable to encode pnpm lockfile")
}
return nil
}

func (p *PnpmLockfile) resolveSpecifier(name string, specifier string) (string, bool) {
// Check if the specifier is already a resolved version
_, ok := p.Packages[fmt.Sprintf("/%s/%s", name, specifier)]
if ok {
return specifier, true
}
for workspacePkg, importer := range p.Importers {
for pkgName, pkgSpecifier := range importer.Specifiers {
if name == pkgName && specifier == pkgSpecifier {
if resolvedVersion, ok := importer.Dependencies[name]; ok {
return resolvedVersion, true
}
if resolvedVersion, ok := importer.DevDependencies[name]; ok {
return resolvedVersion, true
}
if resolvedVersion, ok := importer.OptionalDependencies[name]; ok {
return resolvedVersion, true
}

panic(fmt.Sprintf("Unable to find resolved version for %s@%s in %s", name, specifier, workspacePkg))
}
}
}
return "", false
}
82 changes: 82 additions & 0 deletions cli/internal/lockfile/pnpm_lockfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package lockfile

import (
"bytes"
"os"
"testing"

"github.com/pkg/errors"
"github.com/vercel/turborepo/cli/internal/fs"
"gotest.tools/v3/assert"
)

func getFixture(t *testing.T, name string) ([]byte, error) {
defaultCwd, err := os.Getwd()
if err != nil {
t.Errorf("failed to get cwd: %v", err)
}
cwd, err := fs.CheckedToAbsolutePath(defaultCwd)
if err != nil {
t.Fatalf("cwd is not an absolute directory %v: %v", defaultCwd, err)
}
lockfilePath := cwd.Join("testdata", "pnpm-lockfiles", name)
if !lockfilePath.FileExists() {
return nil, errors.Errorf("unable to find 'testdata/%s'", name)
}
return os.ReadFile(lockfilePath.ToStringDuringMigration())
}

func Test_Roundtrip(t *testing.T) {
lockfiles := []string{"pnpm6-workspace.yaml", "pnpm7-workspace.yaml"}

for _, lockfilePath := range lockfiles {
lockfileContent, err := getFixture(t, lockfilePath)
if err != nil {
t.Errorf("failure getting fixture: %s", err)
}
lockfile, err := DecodePnpmLockfile(lockfileContent)
if err != nil {
t.Errorf("decoding failed %s", err)
}
var b bytes.Buffer
if err := lockfile.Encode(&b); err != nil {
t.Errorf("encoding failed %s", err)
}
newLockfile, err := DecodePnpmLockfile(b.Bytes())
if err != nil {
t.Errorf("decoding failed %s", err)
}

assert.DeepEqual(t, lockfile, newLockfile)
}
}

func Test_SpecifierResolution(t *testing.T) {
contents, err := getFixture(t, "pnpm7-workspace.yaml")
if err != nil {
t.Error(err)
}
lockfile, err := DecodePnpmLockfile(contents)
if err != nil {
t.Errorf("failure decoding lockfile: %v", err)
}

type Case struct {
pkg string
specifier string
version string
found bool
}

cases := []Case{
{pkg: "lodash", specifier: "latest", version: "4.17.21", found: true},
{pkg: "express", specifier: "^4.18.1", version: "4.18.1", found: true},
{pkg: "lodash", specifier: "other-tag", version: "", found: false},
}

for _, testCase := range cases {
actualVersion, actualFound := lockfile.resolveSpecifier(testCase.pkg, testCase.specifier)
assert.Equal(t, actualFound, testCase.found, "%s@%s", testCase.pkg, testCase.version)
assert.Equal(t, actualVersion, testCase.version, "%s@%s", testCase.pkg, testCase.version)
}
}
Loading