diff options
Diffstat (limited to 'cli/internal/packagemanager/packagemanager.go')
| -rw-r--r-- | cli/internal/packagemanager/packagemanager.go | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/cli/internal/packagemanager/packagemanager.go b/cli/internal/packagemanager/packagemanager.go new file mode 100644 index 0000000..dc5b966 --- /dev/null +++ b/cli/internal/packagemanager/packagemanager.go @@ -0,0 +1,197 @@ +// Adapted from https://github.com/replit/upm +// Copyright (c) 2019 Neoreason d/b/a Repl.it. All rights reserved. +// SPDX-License-Identifier: MIT + +package packagemanager + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/vercel/turbo/cli/internal/fs" + "github.com/vercel/turbo/cli/internal/globby" + "github.com/vercel/turbo/cli/internal/lockfile" + "github.com/vercel/turbo/cli/internal/turbopath" + "github.com/vercel/turbo/cli/internal/util" +) + +// PackageManager is an abstraction across package managers +type PackageManager struct { + // The descriptive name of the Package Manager. + Name string + + // The unique identifier of the Package Manager. + Slug string + + // The command used to invoke the Package Manager. + Command string + + // The location of the package spec file used by the Package Manager. + Specfile string + + // The location of the package lock file used by the Package Manager. + Lockfile string + + // The directory in which package assets are stored by the Package Manager. + PackageDir string + + // The location of the file that defines the workspace. Empty if workspaces defined in package.json + WorkspaceConfigurationPath string + + // The separator that the Package Manger uses to identify arguments that + // should be passed through to the underlying script. + ArgSeparator []string + + // Return the list of workspace glob + getWorkspaceGlobs func(rootpath turbopath.AbsoluteSystemPath) ([]string, error) + + // Return the list of workspace ignore globs + getWorkspaceIgnores func(pm PackageManager, rootpath turbopath.AbsoluteSystemPath) ([]string, error) + + // Detect if Turbo knows how to produce a pruned workspace for the project + canPrune func(cwd turbopath.AbsoluteSystemPath) (bool, error) + + // Test a manager and version tuple to see if it is the Package Manager. + Matches func(manager string, version string) (bool, error) + + // Detect if the project is using the Package Manager by inspecting the system. + detect func(projectDirectory turbopath.AbsoluteSystemPath, packageManager *PackageManager) (bool, error) + + // Read a lockfile for a given package manager + UnmarshalLockfile func(rootPackageJSON *fs.PackageJSON, contents []byte) (lockfile.Lockfile, error) + + // Prune the given pkgJSON to only include references to the given patches + prunePatches func(pkgJSON *fs.PackageJSON, patches []turbopath.AnchoredUnixPath) error +} + +var packageManagers = []PackageManager{ + nodejsYarn, + nodejsBerry, + nodejsNpm, + nodejsPnpm, + nodejsPnpm6, +} + +var ( + packageManagerPattern = `(npm|pnpm|yarn)@(\d+)\.\d+\.\d+(-.+)?` + packageManagerRegex = regexp.MustCompile(packageManagerPattern) +) + +// ParsePackageManagerString takes a package manager version string parses it into consituent components +func ParsePackageManagerString(packageManager string) (manager string, version string, err error) { + match := packageManagerRegex.FindString(packageManager) + if len(match) == 0 { + return "", "", fmt.Errorf("We could not parse packageManager field in package.json, expected: %s, received: %s", packageManagerPattern, packageManager) + } + + return strings.Split(match, "@")[0], strings.Split(match, "@")[1], nil +} + +// GetPackageManager attempts all methods for identifying the package manager in use. +func GetPackageManager(projectDirectory turbopath.AbsoluteSystemPath, pkg *fs.PackageJSON) (packageManager *PackageManager, err error) { + result, _ := readPackageManager(pkg) + if result != nil { + return result, nil + } + + return detectPackageManager(projectDirectory) +} + +// readPackageManager attempts to read the package manager from the package.json. +func readPackageManager(pkg *fs.PackageJSON) (packageManager *PackageManager, err error) { + if pkg.PackageManager != "" { + manager, version, err := ParsePackageManagerString(pkg.PackageManager) + if err != nil { + return nil, err + } + + for _, packageManager := range packageManagers { + isResponsible, err := packageManager.Matches(manager, version) + if isResponsible && (err == nil) { + return &packageManager, nil + } + } + } + + return nil, errors.New(util.Sprintf("We did not find a package manager specified in your root package.json. Please set the \"packageManager\" property in your root package.json (${UNDERLINE}https://nodejs.org/api/packages.html#packagemanager)${RESET} or run `npx @turbo/codemod add-package-manager` in the root of your monorepo.")) +} + +// detectPackageManager attempts to detect the package manager by inspecting the project directory state. +func detectPackageManager(projectDirectory turbopath.AbsoluteSystemPath) (packageManager *PackageManager, err error) { + for _, packageManager := range packageManagers { + isResponsible, err := packageManager.detect(projectDirectory, &packageManager) + if err != nil { + return nil, err + } + if isResponsible { + return &packageManager, nil + } + } + + return nil, errors.New(util.Sprintf("We did not detect an in-use package manager for your project. Please set the \"packageManager\" property in your root package.json (${UNDERLINE}https://nodejs.org/api/packages.html#packagemanager)${RESET} or run `npx @turbo/codemod add-package-manager` in the root of your monorepo.")) +} + +// GetWorkspaces returns the list of package.json files for the current repository. +func (pm PackageManager) GetWorkspaces(rootpath turbopath.AbsoluteSystemPath) ([]string, error) { + globs, err := pm.getWorkspaceGlobs(rootpath) + if err != nil { + return nil, err + } + + justJsons := make([]string, len(globs)) + for i, space := range globs { + justJsons[i] = filepath.Join(space, "package.json") + } + + ignores, err := pm.getWorkspaceIgnores(pm, rootpath) + if err != nil { + return nil, err + } + + f, err := globby.GlobFiles(rootpath.ToStringDuringMigration(), justJsons, ignores) + if err != nil { + return nil, err + } + + return f, nil +} + +// GetWorkspaceIgnores returns an array of globs not to search for workspaces. +func (pm PackageManager) GetWorkspaceIgnores(rootpath turbopath.AbsoluteSystemPath) ([]string, error) { + return pm.getWorkspaceIgnores(pm, rootpath) +} + +// CanPrune returns if turbo can produce a pruned workspace. Can error if fs issues occur +func (pm PackageManager) CanPrune(projectDirectory turbopath.AbsoluteSystemPath) (bool, error) { + if pm.canPrune != nil { + return pm.canPrune(projectDirectory) + } + return false, nil +} + +// ReadLockfile will read the applicable lockfile into memory +func (pm PackageManager) ReadLockfile(projectDirectory turbopath.AbsoluteSystemPath, rootPackageJSON *fs.PackageJSON) (lockfile.Lockfile, error) { + if pm.UnmarshalLockfile == nil { + return nil, nil + } + contents, err := projectDirectory.UntypedJoin(pm.Lockfile).ReadFile() + if err != nil { + return nil, fmt.Errorf("reading %s: %w", pm.Lockfile, err) + } + lf, err := pm.UnmarshalLockfile(rootPackageJSON, contents) + if err != nil { + return nil, errors.Wrapf(err, "error in %v", pm.Lockfile) + } + return lf, nil +} + +// PrunePatchedPackages will alter the provided pkgJSON to only reference the provided patches +func (pm PackageManager) PrunePatchedPackages(pkgJSON *fs.PackageJSON, patches []turbopath.AnchoredUnixPath) error { + if pm.prunePatches != nil { + return pm.prunePatches(pkgJSON, patches) + } + return nil +} |
