cobra_commander 0.6.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,12 +8,12 @@ module CobraCommander
8
8
  class Change
9
9
  InvalidSelectionError = Class.new(StandardError)
10
10
 
11
- def initialize(tree, results, branch)
12
- @root_dir = Dir.chdir(tree.path) { `git rev-parse --show-toplevel`.chomp }
11
+ def initialize(umbrella, results, branch)
12
+ @root_dir = Dir.chdir(umbrella.path) { `git rev-parse --show-toplevel`.chomp }
13
13
  @results = results
14
14
  @branch = branch
15
- @tree = tree.to_h
16
- @affected = Affected.new(@tree, changes, tree.path)
15
+ @umbrella = umbrella
16
+ @affected = Affected.new(@umbrella, changes)
17
17
  end
18
18
 
19
19
  def run!
@@ -42,16 +42,16 @@ module CobraCommander
42
42
  Open3.capture3("git", "diff", "--name-only", @branch)
43
43
  end
44
44
 
45
- if result.exitstatus == 128
46
- raise InvalidSelectionError, "Specified --branch could not be found"
47
- end
45
+ raise InvalidSelectionError, "Specified --branch could not be found" if result.exitstatus == 128
48
46
 
49
47
  diff.split("\n").map { |f| File.join(@root_dir, f) }
50
48
  end
51
49
  end
52
50
 
53
51
  def assert_valid_result_choice
54
- raise InvalidSelectionError, "--results must be 'test', 'full', 'name' or 'json'" unless %w[test full name json].include?(@results) # rubocop:disable Metrics/LineLength
52
+ return if %w[test full name json].include?(@results)
53
+
54
+ raise InvalidSelectionError, "--results must be 'test', 'full', 'name' or 'json'"
55
55
  end
56
56
 
57
57
  def selected_result?(result)
@@ -85,8 +85,8 @@ module CobraCommander
85
85
  end
86
86
  end
87
87
 
88
- def display(component)
89
- "#{component[:name]} - #{component[:type]}"
88
+ def display(name:, type:, **)
89
+ "#{name} - #{type}"
90
90
  end
91
91
 
92
92
  def blank_line
@@ -3,103 +3,93 @@
3
3
  require "thor"
4
4
  require "fileutils"
5
5
 
6
+ require "cobra_commander"
7
+ require "cobra_commander/affected"
8
+ require "cobra_commander/change"
9
+ require "cobra_commander/executor"
10
+ require "cobra_commander/output"
11
+
6
12
  module CobraCommander
7
13
  # Implements the tool's CLI
8
14
  class CLI < Thor
9
- CACHE_DESCRIPTION = "Path to a cache file to use (default: nil). If specified, this file will be used to store " \
10
- "the component tree for the app to speed up subsequent invocations. Must be rotated any time the component " \
11
- "dependency structure changes."
12
- COMMON_OPTIONS = "[--app=pwd] [--format=FORMAT] [--cache=nil]"
13
-
14
- desc "do [command] [--app=pwd] [--cache=nil]", "Executes the command in the context of each component in [app]"
15
- method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
16
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
17
- def do(command)
18
- tree = maybe_cached_tree(options.app, options.cache)
19
- executor = Executor.new(tree)
20
- executor.exec(command)
21
- end
15
+ class_option :app, default: Dir.pwd, aliases: "-a", type: :string
16
+ class_option :js, default: false, type: :boolean, desc: "Consider only the JS dependency graph"
17
+ class_option :ruby, default: false, type: :boolean, desc: "Consider only the Ruby dependency graph"
22
18
 
23
- desc "ls [app_path] #{COMMON_OPTIONS}", "Prints tree of components for an app"
24
- method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
25
- method_option :format, default: "tree", aliases: "-f", desc: "Format (list or tree, default: list)"
26
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
27
- def ls(app_path = nil)
28
- tree = maybe_cached_tree(app_path || options.app, options.cache)
29
- Output.print(
30
- tree,
31
- options.format
32
- )
19
+ desc "version", "Prints version"
20
+ def version
21
+ puts CobraCommander::VERSION
33
22
  end
34
23
 
35
- desc "dependents_of [component] #{COMMON_OPTIONS}", "Outputs count of components in [app] dependent on [component]"
36
- method_option :app, default: Dir.pwd, aliases: "-a", desc: "Path to the root app where the component is mounted"
37
- method_option :format, default: "count", aliases: "-f", desc: "count or list"
38
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
39
- def dependents_of(component)
40
- tree = maybe_cached_tree(options.app, options.cache)
41
- dependents = tree.dependents_of(component)
42
- puts "list" == options.format ? dependents.map(&:name) : dependents.size
24
+ desc "ls [component]", "Lists the components in the context of a given component or umbrella"
25
+ method_option :dependencies, type: :boolean, aliases: "-d",
26
+ desc: "Run the command on each dependency of a given component"
27
+ method_option :dependents, type: :boolean, aliases: "-D",
28
+ desc: "Run the command on each dependency of a given component"
29
+ method_option :total, type: :boolean, aliases: "-t", desc: "Prints the total count of components"
30
+ def ls(component = nil)
31
+ components = components_filtered(component)
32
+ puts options.total ? components.size : CobraCommander::Output::FlatList.new(components).to_s
43
33
  end
44
34
 
45
- desc "dependencies_of [component] #{COMMON_OPTIONS}", "Outputs a list of components that [component] depends on"
46
- method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
47
- method_option :format, default: "list", aliases: "-f", desc: "Format (list or tree, default: list)"
48
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
49
- def dependencies_of(component)
50
- tree = maybe_cached_tree(options.app, options.cache)
51
- Output.print(
52
- tree.subtree(component),
53
- options.format
35
+ desc "exec [component] <command>", "Executes the command in the context of a given component or set thereof. " \
36
+ "Defaults to all components."
37
+ method_option :dependencies, type: :boolean, desc: "Run the command on each dependency of a given component"
38
+ method_option :dependents, type: :boolean, desc: "Run the command on each dependency of a given component"
39
+ def exec(command_or_component, command = nil)
40
+ CobraCommander::Executor.exec(
41
+ components_filtered(command && command_or_component),
42
+ command || command_or_component
54
43
  )
55
44
  end
56
45
 
57
- desc "version", "Prints version"
58
- def version
59
- puts CobraCommander::VERSION
46
+ desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
47
+ def tree(component = nil)
48
+ component = find_component(component)
49
+ puts CobraCommander::Output::AsciiTree.new(component).to_s
60
50
  end
61
51
 
62
- desc "graph APP_PATH [--format=FORMAT] [--cache=nil]", "Outputs graph"
63
- method_option :format, default: "png", aliases: "-f", desc: "Accepts png or dot"
64
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
65
- def graph(app_path)
66
- tree = maybe_cached_tree(app_path, options.cache)
67
- Graph.new(tree, options.format).generate!
52
+ desc "graph [component]", "Outputs a graph of a given component or umbrella"
53
+ method_option :output, default: File.join(Dir.pwd, "output.png"), aliases: "-o",
54
+ desc: "Output file, accepts .png or .dot"
55
+ def graph(component = nil)
56
+ CobraCommander::Output::GraphViz.generate(
57
+ find_component(component),
58
+ options.output
59
+ )
60
+ puts "Graph generated at #{options.output}"
61
+ rescue ArgumentError => e
62
+ error e.message
68
63
  end
69
64
 
70
- desc "changes APP_PATH [--results=RESULTS] [--branch=BRANCH] [--cache=nil]", "Prints list of changed files"
65
+ desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
71
66
  method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
72
67
  method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
73
- method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
74
- def changes(app_path)
75
- tree = maybe_cached_tree(app_path, options.cache)
76
- Change.new(tree, options.results, options.branch).run!
77
- end
78
-
79
- desc "cache APP_PATH CACHE_PATH", "Caches a representation of the component structure of the app"
80
- def cache(app_path, cache_path)
81
- tree = CobraCommander.umbrella_tree(app_path)
82
- write_tree_cache(tree, cache_path)
83
- puts "Created cache of component tree at #{cache_path}"
68
+ def changes
69
+ Change.new(umbrella, options.results, options.branch).run!
84
70
  end
85
71
 
86
72
  private
87
73
 
88
- def maybe_cached_tree(app_path, cache_path)
89
- return CobraCommander.umbrella_tree(app_path) unless cache_path
74
+ def umbrella
75
+ @umbrella ||= CobraCommander.umbrella(options.app, yarn: options.js, bundler: options.ruby)
76
+ end
77
+
78
+ def find_component(name)
79
+ return umbrella.root unless name
90
80
 
91
- if File.exist?(cache_path)
92
- CobraCommander.tree_from_cache(cache_path)
93
- else
94
- tree = CobraCommander.umbrella_tree(app_path)
95
- write_tree_cache(tree, cache_path)
96
- tree
97
- end
81
+ umbrella.find(name) || error("Component #{name} not found, try one of `cobra ls`") || exit(1)
98
82
  end
99
83
 
100
- def write_tree_cache(tree, cache_path)
101
- FileUtils.mkdir_p(File.dirname(cache_path))
102
- File.write(cache_path, tree.to_json)
84
+ def components_filtered(component_name)
85
+ return umbrella.components unless component_name
86
+
87
+ component = find_component(component_name)
88
+
89
+ return component.deep_dependencies if options.dependencies
90
+ return component.deep_dependents if options.dependents
91
+
92
+ [component]
103
93
  end
104
94
  end
105
95
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ # Represents a component withing an Umbrella
5
+ class Component
6
+ attr_reader :name, :sources
7
+
8
+ def initialize(umbrella, name)
9
+ @umbrella = umbrella
10
+ @name = name
11
+ @dependency_names = []
12
+ @sources = {}
13
+ end
14
+
15
+ def add_source(key, path, dependency_names)
16
+ @sources[key] = path
17
+ @dependency_names |= dependency_names
18
+ end
19
+
20
+ def root_paths
21
+ @sources.values.map(&File.method(:dirname)).uniq
22
+ end
23
+
24
+ def inspect
25
+ "#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{sources}>"
26
+ end
27
+
28
+ def deep_dependents
29
+ @deep_dependents ||= @umbrella.components.find_all do |dep|
30
+ dep.deep_dependencies.include?(self)
31
+ end
32
+ end
33
+
34
+ def deep_dependencies
35
+ @deep_dependencies ||= dependencies.reduce(dependencies) do |deps, dep|
36
+ deps | dep.deep_dependencies
37
+ end
38
+ end
39
+
40
+ def dependents
41
+ @dependents ||= @umbrella.components.find_all do |dep|
42
+ dep.dependencies.include?(self)
43
+ end
44
+ end
45
+
46
+ def dependencies
47
+ @dependencies ||= @dependency_names.sort
48
+ .map(&@umbrella.method(:find))
49
+ .compact
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dependencies/yarn_workspace"
4
+ require_relative "dependencies/bundler"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "bundler/lockfile_parser"
5
+ require "pathname"
6
+
7
+ module CobraCommander
8
+ module Dependencies
9
+ # Calculates ruby bundler dependencies
10
+ class Bundler
11
+ attr_reader :path
12
+
13
+ def initialize(root)
14
+ @root = root
15
+ @path = Pathname.new(File.join(root, "Gemfile.lock")).realpath
16
+ end
17
+
18
+ def dependencies
19
+ lockfile.dependencies.values.map(&:name)
20
+ end
21
+
22
+ def components
23
+ components_source.specs.map do |spec|
24
+ { path: spec.loaded_from, name: spec.name, dependencies: spec.dependencies.map(&:name) }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def lockfile
31
+ @lockfile ||= ::Bundler::LockfileParser.new(::Bundler.read_file(path))
32
+ end
33
+
34
+ def components_source
35
+ @components_source ||= begin
36
+ source = @lockfile.sources.find { |s| s.path.to_s.eql?("components") }
37
+ ::Bundler::Source::Path.new(source.options.merge("root_path" => @root))
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "pathname"
5
+
6
+ module CobraCommander
7
+ module Dependencies
8
+ module Yarn
9
+ # Represents an Yarn package.json file
10
+ class Package
11
+ attr_reader :path
12
+
13
+ def initialize(path)
14
+ @path = ::Pathname.new(File.join(path, "package.json")).realpath
15
+ end
16
+
17
+ def project_tag
18
+ name.match(%r{^@[\w-]+/}).to_s
19
+ end
20
+
21
+ def name
22
+ json["name"]
23
+ end
24
+
25
+ def dependencies
26
+ json.fetch("dependencies", {})
27
+ .merge(json.fetch("devDependencies", {}))
28
+ end
29
+
30
+ private
31
+
32
+ def json
33
+ @json ||= JSON.parse(File.read(@path))
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Dependencies
5
+ module Yarn
6
+ # Yarn package repository to load and cache package.json files
7
+ class PackageRepo
8
+ def initialize
9
+ @specs = {}
10
+ end
11
+
12
+ def specs
13
+ @specs.values
14
+ end
15
+
16
+ def load_linked_specs(package)
17
+ package.dependencies.each_value do |spec|
18
+ next unless spec =~ /link:(.+)/
19
+
20
+ load_spec(File.join(package.path, "..", Regexp.last_match(1)))
21
+ end
22
+ end
23
+
24
+ def load_spec(path)
25
+ @specs[path] ||= Package.new(path).tap do |package|
26
+ load_linked_specs(package)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ require_relative "yarn/package"
6
+ require_relative "yarn/package_repo"
7
+
8
+ module CobraCommander
9
+ module Dependencies
10
+ # Yarn workspace components source for an umbrella
11
+ class YarnWorkspace
12
+ attr_reader :packages
13
+
14
+ def initialize(root_path)
15
+ @repo = Yarn::PackageRepo.new
16
+ @root_package = Yarn::Package.new(root_path)
17
+ @repo.load_linked_specs(@root_package)
18
+ load_workspace_packages
19
+ end
20
+
21
+ def path
22
+ @root_package.path
23
+ end
24
+
25
+ def dependencies
26
+ (workspace_spec.keys | @root_package.dependencies.keys).map(&method(:untag))
27
+ end
28
+
29
+ def components
30
+ @repo.specs.map do |spec|
31
+ { path: spec.path, name: untag(spec.name), dependencies: spec.dependencies.keys.map(&method(:untag)) }
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def load_workspace_packages
38
+ workspace_spec.map do |_name, spec|
39
+ @repo.load_spec File.expand_path(File.join(@root_package.path, "..", spec["location"]))
40
+ end
41
+ end
42
+
43
+ def workspace_spec
44
+ @workspace_spec ||= begin
45
+ output, = Open3.capture2("yarn workspaces --json info", chdir: File.dirname(@root_package.path))
46
+ JSON.parse(JSON.parse(output)["data"])
47
+ end
48
+ end
49
+
50
+ def untag(name)
51
+ name.gsub(@root_package.project_tag, "")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,30 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "executor/component_exec"
4
+ require_relative "executor/multi_exec"
5
+
3
6
  module CobraCommander
4
7
  # Execute commands on all components of a ComponentTree
5
- class Executor
6
- def initialize(tree)
7
- @tree = tree
8
- end
9
-
10
- def exec(command, printer = $stdout)
11
- @tree.flatten.each do |component|
12
- printer.puts "===> #{component.name} (#{component.path})"
13
- output, = run_in_component(component, command)
14
- printer.puts output
15
- end
16
- end
17
-
18
- private
19
-
20
- def run_in_component(component, command)
21
- Dir.chdir(component.path) do
22
- Open3.capture2e(env_vars(component), command)
23
- end
24
- end
25
-
26
- def env_vars(component)
27
- { "CURRENT_COMPONENT" => component.name, "CURRENT_COMPONENT_PATH" => component.path }
8
+ module Executor
9
+ def self.exec(components, command, output = $stdout, status_output = $stderr)
10
+ components = Array(components)
11
+ exec = if components.size == 1
12
+ ComponentExec.new(components.first)
13
+ else
14
+ MultiExec.new(components)
15
+ end
16
+ exec.run(command, output: output, spin_output: status_output)
28
17
  end
29
18
  end
30
19
  end