diff --git a/cli/internal/context/context.go b/cli/internal/context/context.go index 23b504f917805..d0acc652b2423 100644 --- a/cli/internal/context/context.go +++ b/cli/internal/context/context.go @@ -419,3 +419,32 @@ func (c *Context) resolveDepGraph(wg *errgroup.Group, workspace *fs.PackageJSON, }) } } + +// InternalDependencies finds all dependencies required by the slice of starting +// packages, as well as the starting packages themselves. +func (c *Context) InternalDependencies(start []string) ([]string, error) { + vertices := make(dag.Set) + for _, v := range start { + vertices.Add(v) + } + s := make(dag.Set) + memoFunc := func(v dag.Vertex, d int) error { + s.Add(v) + return nil + } + + if err := c.WorkspaceGraph.DepthFirstWalk(vertices, memoFunc); err != nil { + return nil, err + } + + // Use for loop so we can coerce to string + // .List() returns a list of interface{} types, but + // we know they are strings. + targets := make([]string, 0, s.Len()) + for _, dep := range s.List() { + targets = append(targets, dep.(string)) + } + sort.Strings(targets) + + return targets, nil +} diff --git a/cli/internal/prune/prune.go b/cli/internal/prune/prune.go index 1e34a4c5f00bf..7c2770896e162 100644 --- a/cli/internal/prune/prune.go +++ b/cli/internal/prune/prune.go @@ -3,6 +3,7 @@ package prune import ( "bufio" "fmt" + "strings" "github.com/vercel/turbo/cli/internal/config" @@ -21,13 +22,13 @@ import ( ) type opts struct { - scope string + scope []string docker bool outputDir string } func addPruneFlags(opts *opts, flags *pflag.FlagSet) { - flags.StringVar(&opts.scope, "scope", "", "Specify package to act as entry point for pruned monorepo (required).") + flags.StringArrayVar(&opts.scope, "scope", nil, "Specify package(s) to act as entry points for pruned monorepo (required).") flags.BoolVar(&opts.docker, "docker", false, "Output pruned workspace into 'full' and 'json' directories optimized for Docker layer caching.") flags.StringVar(&opts.outputDir, "out-dir", "out", "Set the root directory for files output by this command") // No-op the cwd flag while the root level command is not yet cobra @@ -53,7 +54,7 @@ func GetCmd(helper *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if opts.scope == "" { + if len(opts.scope) == 0 { err := errors.New("at least one target must be specified") base.LogError(err.Error()) return err @@ -93,24 +94,28 @@ func (p *prune) prune(opts *opts) error { if err != nil { return errors.Wrap(err, "could not construct graph") } - p.base.Logger.Trace("scope", "value", opts.scope) - target, scopeIsValid := ctx.WorkspaceInfos[opts.scope] - if !scopeIsValid { - return errors.Errorf("invalid scope: package %v not found", opts.scope) - } outDir := p.base.RepoRoot.UntypedJoin(opts.outputDir) fullDir := outDir if opts.docker { fullDir = fullDir.UntypedJoin("full") } - p.base.Logger.Trace("target", "value", target.Name) - p.base.Logger.Trace("directory", "value", target.Dir) - p.base.Logger.Trace("external deps", "value", target.UnresolvedExternalDeps) - p.base.Logger.Trace("internal deps", "value", target.InternalDeps) + p.base.Logger.Trace("scope", "value", strings.Join(opts.scope, ", ")) p.base.Logger.Trace("docker", "value", opts.docker) p.base.Logger.Trace("out dir", "value", outDir.ToString()) + for _, scope := range opts.scope { + p.base.Logger.Trace("scope", "value", scope) + target, scopeIsValid := ctx.WorkspaceInfos[scope] + if !scopeIsValid { + return errors.Errorf("invalid scope: package %v not found", scope) + } + p.base.Logger.Trace("target", "value", target.Name) + p.base.Logger.Trace("directory", "value", target.Dir) + p.base.Logger.Trace("external deps", "value", target.UnresolvedExternalDeps) + p.base.Logger.Trace("internal deps", "value", target.InternalDeps) + } + canPrune, err := ctx.PackageManager.CanPrune(p.base.RepoRoot) if err != nil { return err @@ -122,7 +127,7 @@ func (p *prune) prune(opts *opts) error { return errors.New("Cannot prune without parsed lockfile") } - p.base.UI.Output(fmt.Sprintf("Generating pruned monorepo for %v in %v", ui.Bold(opts.scope), ui.Bold(outDir.ToString()))) + p.base.UI.Output(fmt.Sprintf("Generating pruned monorepo for %v in %v", ui.Bold(strings.Join(opts.scope, ", ")), ui.Bold(outDir.ToString()))) packageJSONPath := outDir.UntypedJoin("package.json") if err := packageJSONPath.EnsureDir(); err != nil { @@ -143,18 +148,11 @@ func (p *prune) prune(opts *opts) error { } } workspaces := []turbopath.AnchoredSystemPath{} - targets := []string{opts.scope} - internalDeps, err := ctx.WorkspaceGraph.Ancestors(opts.scope) + targets, err := ctx.InternalDependencies(opts.scope) if err != nil { - return errors.Wrap(err, "could find traverse the dependency graph to find topological dependencies") - } - - // Use for loop so we can coerce to string - // .List() returns a list of interface{} types, but - // we know they are strings. - for _, dep := range internalDeps.List() { - targets = append(targets, dep.(string)) + return errors.Wrap(err, "could not traverse the dependency graph to find topological dependencies") } + p.base.Logger.Trace("targets", "value", targets) lockfileKeys := make([]string, 0, len(rootPackageJSON.TransitiveDeps)) lockfileKeys = append(lockfileKeys, rootPackageJSON.TransitiveDeps...)