cobra_commander 0.5.0 → 0.8.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.
@@ -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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dependencies/yarn_workspace"
4
+ require_relative "dependencies/bundler"
@@ -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
@@ -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
@@ -2,29 +2,15 @@
2
2
 
3
3
  module CobraCommander
4
4
  # 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)
5
+ module Executor
6
+ def self.exec(components, command, printer = $stdout)
7
+ components.each do |component|
8
+ component.root_paths.each do |path|
9
+ printer.puts "===> #{component.name} (#{path})"
10
+ output, = Open3.capture2e(command, chdir: path, unsetenv_others: true)
11
+ printer.puts output
12
+ end
23
13
  end
24
14
  end
25
-
26
- def env_vars(component)
27
- { "CURRENT_COMPONENT" => component.name, "CURRENT_COMPONENT_PATH" => component.path }
28
- end
29
15
  end
30
16
  end
@@ -1,74 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module CobraCommander
4
- # Module for pretty printing dependency trees
5
- module Output
6
- def self.print(tree, format)
7
- output = format == "list" ? Output::FlatList.new(tree) : Output::Tree.new(tree)
8
- puts output.to_s
9
- end
10
-
11
- # Flattens a tree and prints unique items
12
- class FlatList
13
- def initialize(tree)
14
- @tree = tree
15
- end
16
-
17
- def to_s
18
- @tree.flatten.map(&:name)
19
- end
20
- end
21
-
22
- # Prints the tree in a nice tree form
23
- class Tree
24
- attr_accessor :tree
25
-
26
- SPACE = " "
27
- BAR = "│   "
28
- TEE = "├── "
29
- CORNER = "└── "
30
-
31
- def initialize(tree)
32
- @tree = tree
33
- end
34
-
35
- def to_s
36
- StringIO.new.tap do |io|
37
- io.puts @tree.name
38
- list_dependencies(io, @tree)
39
- end.string
40
- end
41
-
42
- private
43
-
44
- def list_dependencies(io, deps, outdents = [])
45
- deps.dependencies.each do |dep|
46
- decide_on_line(io, deps, dep, outdents)
47
- end
48
- nil
49
- end
50
-
51
- def decide_on_line(io, parent, dep, outdents)
52
- if parent.dependencies.last != dep
53
- add_tee(io, outdents, dep)
54
- else
55
- add_corner(io, outdents, dep)
56
- end
57
- end
58
-
59
- def add_tee(io, outdents, dep)
60
- io.puts line(outdents, TEE, dep.name)
61
- list_dependencies(io, dep, (outdents + [BAR]))
62
- end
63
-
64
- def add_corner(io, outdents, dep)
65
- io.puts line(outdents, CORNER, dep.name)
66
- list_dependencies(io, dep, (outdents + [SPACE]))
67
- end
68
-
69
- def line(outdents, sym, name)
70
- (outdents + [sym] + [name]).join
71
- end
72
- end
73
- end
74
- end
3
+ require_relative "output/flat_list"
4
+ require_relative "output/ascii_tree"
5
+ require_relative "output/graph_viz"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Output
5
+ # Prints the tree in a nice tree form
6
+ class AsciiTree
7
+ SPACE = " "
8
+ BAR = "│   "
9
+ TEE = "├── "
10
+ CORNER = "└── "
11
+
12
+ def initialize(component)
13
+ @component = component
14
+ end
15
+
16
+ def to_s
17
+ StringIO.new.tap do |io|
18
+ io.puts @component.name
19
+ list_dependencies(io, @component)
20
+ end.string
21
+ end
22
+
23
+ private
24
+
25
+ def list_dependencies(io, component, outdents = [])
26
+ component.dependencies.each do |dep|
27
+ decide_on_line(io, component, dep, outdents)
28
+ end
29
+ nil
30
+ end
31
+
32
+ def decide_on_line(io, parent, dep, outdents)
33
+ if parent.dependencies.last != dep
34
+ add_tee(io, outdents, dep)
35
+ else
36
+ add_corner(io, outdents, dep)
37
+ end
38
+ end
39
+
40
+ def add_tee(io, outdents, dep)
41
+ io.puts line(outdents, TEE, dep.name)
42
+ list_dependencies(io, dep, (outdents + [BAR]))
43
+ end
44
+
45
+ def add_corner(io, outdents, dep)
46
+ io.puts line(outdents, CORNER, dep.name)
47
+ list_dependencies(io, dep, (outdents + [SPACE]))
48
+ end
49
+
50
+ def line(outdents, sym, name)
51
+ (outdents + [sym] + [name]).join
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Output
5
+ # Prints a list of components' names sorted alphabetically
6
+ class FlatList
7
+ def initialize(components)
8
+ @components = components
9
+ end
10
+
11
+ def to_s
12
+ @components.map(&:name).sort
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphviz"
4
+
5
+ module CobraCommander
6
+ module Output
7
+ # Generates graphs of components
8
+ module GraphViz
9
+ def self.generate(component, output)
10
+ g = ::GraphViz.new(:G, type: :digraph, concentrate: true)
11
+ ([component] + component.deep_dependencies).each do |comp|
12
+ g.add_nodes comp.name
13
+ g.add_edges comp.name, comp.dependencies.map(&:name)
14
+ end
15
+
16
+ g.output(extract_format(output) => output)
17
+ end
18
+
19
+ private_class_method def self.extract_format(output)
20
+ format = output[-3..-1]
21
+ return format if format == "png" || format == "dot"
22
+
23
+ raise ArgumentError, "output format must be 'png' or 'dot'"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ # An umbrella application
5
+ class Umbrella
6
+ attr_reader :name, :path
7
+
8
+ def initialize(name, path)
9
+ @root_component = Component.new(self, name)
10
+ @path = path
11
+ @components = {}
12
+ end
13
+
14
+ def find(name)
15
+ @components[name]
16
+ end
17
+
18
+ def root
19
+ @root_component
20
+ end
21
+
22
+ def resolve(component_root_path)
23
+ return root if root.root_paths.include?(component_root_path)
24
+ components.find do |component|
25
+ component.root_paths.include?(component_root_path)
26
+ end
27
+ end
28
+
29
+ def add_source(key, source)
30
+ @root_component.add_source key, source.path, source.dependencies
31
+ source.components.each do |path:, name:, dependencies:|
32
+ @components[name] ||= Component.new(self, name)
33
+ @components[name].add_source key, path, dependencies
34
+ end
35
+ end
36
+
37
+ def components
38
+ @components.values
39
+ end
40
+
41
+ def dependents_of(component)
42
+ find(component)&.deep_dependents
43
+ &.sort_by(&:name)
44
+ end
45
+
46
+ def dependencies_of(name)
47
+ find(name)&.deep_dependencies
48
+ &.sort_by(&:name)
49
+ end
50
+ end
51
+ end