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.
@@ -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
- desc "ls APP_PATH", "Prints tree of components for an app"
9
- def ls(app_path)
10
- puts FormattedOutput.new(app_path).run!
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 "dependencies_of APP_PATH", "Outputs count of components in APP_PATH dependent on COMPONENT"
14
- method_option :component, required: true, aliases: "-c", desc: "Name of component. Ex: my_component"
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
- def dependencies_of(app_path)
17
- puts FormattedOutput.new(app_path).dependencies_of!(@options[:component], @options[:format])
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, @options[:format]).generate!
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, @options[:results], @options[:branch]).run!
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
- # Representation of the tree of components and their dependencies
7
+ # Represents a dependency tree in a given context
7
8
  class ComponentTree
8
- def initialize(path)
9
- @root_path = path
10
- end
9
+ attr_reader :name, :path
11
10
 
12
- def to_h
13
- Tree.new(UMBRELLA_APP_NAME, @root_path).to_h
11
+ def initialize(name, path)
12
+ @name = name
13
+ @path = path
14
14
  end
15
15
 
16
- # Generates component tree
17
- class Tree
18
- def initialize(name, path, ancestry = Set.new)
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
- def dependencies
46
- @deps ||= begin
47
- deps = @ruby.dependencies + @js.dependencies
48
- deps.sort_by { |dep| dep[:name] }
49
- end
50
- end
20
+ def subtree(name)
21
+ _subtree(name, self)
22
+ end
51
23
 
52
- def dep_representation(dep)
53
- full_path = File.expand_path(File.join(@root_path, dep[:path]))
54
- ancestry = @ancestry + [{ name: @name, path: @root_path, type: @type }]
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
- # Calculates ruby dependencies
59
- class Ruby
60
- def initialize(root_path)
61
- @root_path = root_path
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
- # Calculates js dependencies
103
- class Js
104
- def initialize(root_path)
105
- @root_path = root_path
106
- end
107
-
108
- def dependencies
109
- @deps ||= begin
110
- return [] unless node?
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
- def format(deps)
117
- return [] if deps.nil?
118
- linked_deps = deps.select { |_, v| v.start_with? "link:" }
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
- def node?
127
- @node ||= File.exist?(package_json_path)
128
- end
52
+ private
129
53
 
130
- def package_json_path
131
- File.join(@root_path, "package.json")
132
- end
54
+ def _flatten(component)
55
+ component.dependencies.map do |dep|
56
+ [dep] + _flatten(dep)
57
+ end.flatten.uniq(&:name)
58
+ end
133
59
 
134
- def combined_deps(json)
135
- Hash(json["dependencies"]).merge(Hash(json["devDependencies"]))
136
- end
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,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