-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Add pnpm support for turbo prune #1819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2587038
49aa0c3
5180af6
f787c24
1978cb7
b55d94b
5288cb9
d1f7137
677bcb6
489fa24
1385938
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
||
| 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"` | ||
|
||
| } `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 { | ||
|
||
| 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} | ||
|
||
| for _, supportedVersion := range supportedVersions { | ||
| if version == supportedVersion { | ||
| return nil | ||
| } | ||
| } | ||
|
||
| 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 | ||
| } | ||
|
||
|
|
||
| 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 | ||
| } | ||
| 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) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.