cobra_commander 0.4.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/.editorconfig +11 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +25 -0
- data/README.md +10 -6
- data/cobra_commander.gemspec +4 -2
- data/lib/cobra_commander.rb +23 -2
- data/lib/cobra_commander/affected.rb +37 -30
- data/lib/cobra_commander/cached_component_tree.rb +24 -0
- data/lib/cobra_commander/calculated_component_tree.rb +141 -0
- data/lib/cobra_commander/change.rb +6 -7
- data/lib/cobra_commander/cli.rb +61 -11
- data/lib/cobra_commander/component.rb +52 -0
- data/lib/cobra_commander/component_tree.rb +46 -117
- data/lib/cobra_commander/dependencies.rb +4 -0
- data/lib/cobra_commander/dependencies/bundler.rb +38 -0
- data/lib/cobra_commander/dependencies/yarn/package.rb +37 -0
- data/lib/cobra_commander/dependencies/yarn/package_repo.rb +31 -0
- data/lib/cobra_commander/dependencies/yarn_workspace.rb +55 -0
- data/lib/cobra_commander/executor.rb +20 -0
- data/lib/cobra_commander/graph.rb +6 -15
- data/lib/cobra_commander/output.rb +74 -0
- data/lib/cobra_commander/umbrella.rb +51 -0
- data/lib/cobra_commander/version.rb +1 -1
- data/renovate.json +8 -0
- metadata +29 -9
- data/lib/cobra_commander/formatted_output.rb +0 -76
data/lib/cobra_commander/cli.rb
CHANGED
@@ -1,20 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "fileutils"
|
4
5
|
|
5
6
|
module CobraCommander
|
6
7
|
# Implements the tool's CLI
|
7
8
|
class CLI < Thor
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
CACHE_DESCRIPTION = "[DEPRECATED] Path to a cache file to use (default: nil). If specified, this file will " \
|
10
|
+
"be used to store the component tree for the app to speed up subsequent invocations. Must be rotated any time " \
|
11
|
+
"the component 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
|
+
executor = Executor.new(umbrella(options.app).components)
|
19
|
+
executor.exec(command)
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "ls [app_path] #{COMMON_OPTIONS}", "Prints tree of components for an app"
|
23
|
+
method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
|
24
|
+
method_option :format, default: "tree", aliases: "-f", desc: "Format (list or tree, default: list)"
|
25
|
+
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
26
|
+
def ls(app_path = Dir.pwd)
|
27
|
+
Output.print(
|
28
|
+
umbrella(app_path).root,
|
29
|
+
options.format
|
30
|
+
)
|
11
31
|
end
|
12
32
|
|
13
|
-
desc "
|
14
|
-
method_option :
|
33
|
+
desc "dependents_of [component] #{COMMON_OPTIONS}", "Outputs count of components in [app] dependent on [component]"
|
34
|
+
method_option :app, default: Dir.pwd, aliases: "-a", desc: "Path to the root app where the component is mounted"
|
15
35
|
method_option :format, default: "count", aliases: "-f", desc: "count or list"
|
16
|
-
|
17
|
-
|
36
|
+
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
37
|
+
def dependents_of(component)
|
38
|
+
dependents = umbrella(options.app).dependents_of(component)
|
39
|
+
return unless dependents
|
40
|
+
puts "list" == options.format ? dependents.map(&:name) : dependents.size
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "dependencies_of [component] #{COMMON_OPTIONS}", "Outputs a list of components that [component] depends on"
|
44
|
+
method_option :app, default: Dir.pwd, aliases: "-a", desc: "App path (default: CWD)"
|
45
|
+
method_option :format, default: "list", aliases: "-f", desc: "Format (list or tree, default: list)"
|
46
|
+
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
47
|
+
def dependencies_of(component)
|
48
|
+
Output.print(
|
49
|
+
umbrella(options.app).find(component),
|
50
|
+
options.format
|
51
|
+
)
|
18
52
|
end
|
19
53
|
|
20
54
|
desc "version", "Prints version"
|
@@ -22,17 +56,33 @@ module CobraCommander
|
|
22
56
|
puts CobraCommander::VERSION
|
23
57
|
end
|
24
58
|
|
25
|
-
desc "graph APP_PATH [--format=FORMAT]", "Outputs graph"
|
59
|
+
desc "graph APP_PATH [--format=FORMAT] [--cache=nil] [--component]", "Outputs graph"
|
26
60
|
method_option :format, default: "png", aliases: "-f", desc: "Accepts png or dot"
|
61
|
+
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
27
62
|
def graph(app_path)
|
28
|
-
Graph.new(app_path,
|
63
|
+
Graph.new(umbrella(app_path).root, options.format).generate!
|
29
64
|
end
|
30
65
|
|
31
|
-
desc "changes APP_PATH [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
|
66
|
+
desc "changes APP_PATH [--results=RESULTS] [--branch=BRANCH] [--cache=nil]", "Prints list of changed files"
|
32
67
|
method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
|
33
68
|
method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
|
69
|
+
method_option :cache, default: nil, aliases: "-c", desc: CACHE_DESCRIPTION
|
34
70
|
def changes(app_path)
|
35
|
-
Change.new(app_path,
|
71
|
+
Change.new(umbrella(app_path), options.results, options.branch).run!
|
72
|
+
end
|
73
|
+
|
74
|
+
desc "cache APP_PATH CACHE_PATH", "[DEPRECATED] Caches a representation of the component structure of the app"
|
75
|
+
def cache(app_path, cache_path)
|
76
|
+
tree = CobraCommander.umbrella_tree(app_path)
|
77
|
+
FileUtils.mkdir_p(File.dirname(cache_path))
|
78
|
+
File.write(cache_path, tree.to_json)
|
79
|
+
puts "Created cache of component tree at #{cache_path}"
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def umbrella(path)
|
85
|
+
CobraCommander.umbrella(path)
|
36
86
|
end
|
37
87
|
end
|
38
88
|
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, :dependencies, :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
|
@@ -1,140 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "bundler"
|
3
4
|
require "json"
|
4
5
|
|
5
6
|
module CobraCommander
|
6
|
-
#
|
7
|
+
# Represents a dependency tree in a given context
|
7
8
|
class ComponentTree
|
8
|
-
|
9
|
-
@root_path = path
|
10
|
-
end
|
9
|
+
attr_reader :name, :path
|
11
10
|
|
12
|
-
def
|
13
|
-
|
11
|
+
def initialize(name, path)
|
12
|
+
@name = name
|
13
|
+
@path = path
|
14
14
|
end
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@name = name
|
20
|
-
@root_path = path
|
21
|
-
@ancestry = ancestry
|
22
|
-
@ruby = Ruby.new(path)
|
23
|
-
@js = Js.new(path)
|
24
|
-
@type = type_of_component
|
25
|
-
end
|
26
|
-
|
27
|
-
def to_h
|
28
|
-
{
|
29
|
-
name: @name,
|
30
|
-
path: @root_path,
|
31
|
-
type: @type,
|
32
|
-
ancestry: @ancestry,
|
33
|
-
dependencies: dependencies.map(&method(:dep_representation)),
|
34
|
-
}
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def type_of_component
|
40
|
-
return "Ruby & JS" if @ruby.gem? && @js.node?
|
41
|
-
return "Ruby" if @ruby.gem?
|
42
|
-
return "JS" if @js.node?
|
43
|
-
end
|
16
|
+
def flatten
|
17
|
+
_flatten(self)
|
18
|
+
end
|
44
19
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
deps.sort_by { |dep| dep[:name] }
|
49
|
-
end
|
50
|
-
end
|
20
|
+
def subtree(name)
|
21
|
+
_subtree(name, self)
|
22
|
+
end
|
51
23
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
self.class.new(dep[:name], full_path, ancestry).to_h
|
24
|
+
def depends_on?(component_name)
|
25
|
+
dependencies.any? do |component|
|
26
|
+
component.name == component_name || component.depends_on?(component_name)
|
56
27
|
end
|
28
|
+
end
|
57
29
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
def dependencies
|
65
|
-
@deps ||= begin
|
66
|
-
return [] unless gem?
|
67
|
-
gems = bundler_definition.dependencies.select { |dep| path?(dep.source) }
|
68
|
-
format(gems)
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def path?(source)
|
73
|
-
return if source.nil?
|
74
|
-
source_has_path = source.respond_to?(:path?) ? source.path? : source.is_a_path?
|
75
|
-
source_has_path && source.path.to_s != "."
|
76
|
-
end
|
77
|
-
|
78
|
-
def format(deps)
|
79
|
-
deps.map do |dep|
|
80
|
-
path = File.join(dep.source.path, dep.name)
|
81
|
-
{ name: dep.name, path: path }
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def gem?
|
86
|
-
@gem ||= File.exist?(gemfile_path)
|
87
|
-
end
|
88
|
-
|
89
|
-
def bundler_definition
|
90
|
-
::Bundler::Definition.build(gemfile_path, gemfile_lock_path, nil)
|
91
|
-
end
|
92
|
-
|
93
|
-
def gemfile_path
|
94
|
-
File.join(@root_path, "Gemfile")
|
95
|
-
end
|
96
|
-
|
97
|
-
def gemfile_lock_path
|
98
|
-
File.join(@root_path, "Gemfile.lock")
|
99
|
-
end
|
30
|
+
def dependents_of(component_name)
|
31
|
+
depends = depends_on?(component_name) ? self : nil
|
32
|
+
dependents_below = dependencies.map do |component|
|
33
|
+
component.dependents_of(component_name)
|
100
34
|
end
|
35
|
+
[depends, dependents_below].flatten.compact.uniq(&:name)
|
36
|
+
end
|
101
37
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
json = JSON.parse(File.read(package_json_path))
|
112
|
-
format combined_deps(json)
|
113
|
-
end
|
114
|
-
end
|
38
|
+
def to_h(json_compatible: false)
|
39
|
+
{
|
40
|
+
name: @name,
|
41
|
+
path: path,
|
42
|
+
type: @type,
|
43
|
+
ancestry: json_compatible ? @ancestry.to_a : @ancestry,
|
44
|
+
dependencies: dependencies.map { |dep| dep.to_h(json_compatible: json_compatible) },
|
45
|
+
}
|
46
|
+
end
|
115
47
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
linked_deps.map do |_, v|
|
120
|
-
relational_path = v.split("link:")[1]
|
121
|
-
dep_name = relational_path.split("/")[-1]
|
122
|
-
{ name: dep_name, path: relational_path }
|
123
|
-
end
|
124
|
-
end
|
48
|
+
def to_json
|
49
|
+
JSON.dump(to_h(json_compatible: true))
|
50
|
+
end
|
125
51
|
|
126
|
-
|
127
|
-
@node ||= File.exist?(package_json_path)
|
128
|
-
end
|
52
|
+
private
|
129
53
|
|
130
|
-
|
131
|
-
|
132
|
-
|
54
|
+
def _flatten(component)
|
55
|
+
component.dependencies.map do |dep|
|
56
|
+
[dep] + _flatten(dep)
|
57
|
+
end.flatten.uniq(&:name)
|
58
|
+
end
|
133
59
|
|
134
|
-
|
135
|
-
|
136
|
-
|
60
|
+
def _subtree(name, tree)
|
61
|
+
return tree if tree.name == name
|
62
|
+
tree.dependencies.each do |component|
|
63
|
+
presence = _subtree(name, component)
|
64
|
+
return presence if presence
|
137
65
|
end
|
66
|
+
nil
|
138
67
|
end
|
139
68
|
end
|
140
69
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CobraCommander
|
4
|
+
module Dependencies
|
5
|
+
# Calculates ruby bundler dependencies
|
6
|
+
class Bundler
|
7
|
+
def initialize(root)
|
8
|
+
@definition = ::Bundler::Definition.build(
|
9
|
+
Pathname.new(File.join(root, "Gemfile")).realpath,
|
10
|
+
Pathname.new(File.join(root, "Gemfile.lock")).realpath,
|
11
|
+
false
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def path
|
16
|
+
@definition.lockfile
|
17
|
+
end
|
18
|
+
|
19
|
+
def dependencies
|
20
|
+
@definition.dependencies.map(&:name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def components
|
24
|
+
components_source.specs.map do |spec|
|
25
|
+
{ path: spec.loaded_from, name: spec.name, dependencies: spec.dependencies.map(&:name) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def components_source
|
32
|
+
@components_source ||= @definition.send(:sources).path_sources.find do |source|
|
33
|
+
source.path.to_s.eql?("components")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
module Dependencies
|
7
|
+
module Yarn
|
8
|
+
# Represents an Yarn package.json file
|
9
|
+
class Package
|
10
|
+
attr_reader :path
|
11
|
+
|
12
|
+
def initialize(path)
|
13
|
+
@path = Pathname.new(File.join(path, "package.json")).realpath
|
14
|
+
end
|
15
|
+
|
16
|
+
def project_tag
|
17
|
+
name.match(%r{^@[\w-]+\/}).to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
json["name"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def dependencies
|
25
|
+
json.fetch("dependencies", {})
|
26
|
+
.merge(json.fetch("devDependencies", {}))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def json
|
32
|
+
@json ||= JSON.parse(File.read(@path))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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.values.each do |spec|
|
18
|
+
next unless spec =~ /link:(.+)/
|
19
|
+
load_spec(File.join(package.path, "..", Regexp.last_match(1)))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_spec(path)
|
24
|
+
@specs[path] ||= Package.new(path).tap do |package|
|
25
|
+
load_linked_specs(package)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|