/
plugin_manager.rb
123 lines (113 loc) · 4.21 KB
/
plugin_manager.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# encoding: utf-8
module CLAide
class Command
# Handles plugin related logic logic for the `Command` class.
#
# Plugins are loaded the first time a command run and are identified by the
# prefix specified in the command class. Plugins must adopt the following
# conventions:
#
# - Support being loaded by a file located under the
# `lib/#{plugin_prefix}_plugin` relative path.
# - Be stored in a folder named after the plugin.
#
class PluginManager
# @return [Hash<String,Gem::Specification>] The loaded plugins,
# grouped by plugin prefix.
#
def self.loaded_plugins
@loaded_plugins ||= {}
end
# @return [Array<Gem::Specification>] Loads plugins via RubyGems looking
# for files named after the `PLUGIN_PREFIX_plugin` and returns the
# specifications of the gems loaded successfully.
# Plugins are required safely.
#
def self.load_plugins(plugin_prefix)
loaded_plugins[plugin_prefix] ||=
plugin_gems_for_prefix(plugin_prefix).map do |spec, paths|
spec if safe_activate_and_require(spec, paths)
end.compact
end
# @return [Array<Specification>] The RubyGems specifications for the
# loaded plugins.
#
def self.specifications
loaded_plugins.values.flatten.uniq
end
# @return [Array<Specification>] The RubyGems specifications for the
# installed plugins that match the given `plugin_prefix`.
#
def self.installed_specifications_for_prefix(plugin_prefix)
loaded_plugins[plugin_prefix] ||
plugin_gems_for_prefix(plugin_prefix).map(&:first)
end
# @return [Array<String>] The list of the plugins whose root path appears
# in the backtrace of an exception.
#
# @param [Exception] exception
# The exception to analyze.
#
def self.plugins_involved_in_exception(exception)
specifications.select do |gemspec|
exception.backtrace.any? do |line|
full_require_paths_for(gemspec).any? do |plugin_path|
line.include?(plugin_path)
end
end
end.map(&:name)
end
# @group Helper Methods
# @return [Array<[Gem::Specification, Array<String>]>]
# Returns an array of tuples containing the specifications and
# plugin files to require for a given plugin prefix.
#
def self.plugin_gems_for_prefix(prefix)
glob = "#{prefix}_plugin#{Gem.suffix_pattern}"
Gem::Specification.latest_specs(true).map do |spec|
matches = spec.matches_for_glob(glob)
[spec, matches] unless matches.empty?
end.compact
end
# Activates the given spec and requires the given paths.
# If any exception occurs it is caught and an
# informative message is printed.
#
# @param [Gem::Specification] spec
# The spec to be activated.
#
# @param [String] paths
# The paths to require.
#
# @return [Bool] Whether activation and requiring succeeded.
#
def self.safe_activate_and_require(spec, paths)
spec.activate
paths.each { |path| require(path) }
true
rescue Exception => exception # rubocop:disable RescueException
message = "\n---------------------------------------------"
message << "\nError loading the plugin `#{spec.full_name}`.\n"
message << "\n#{exception.class} - #{exception.message}"
message << "\n#{exception.backtrace.join("\n")}"
message << "\n---------------------------------------------\n"
warn message.ansi.yellow
false
end
def self.full_require_paths_for(gemspec)
if gemspec.respond_to?(:full_require_paths)
return gemspec.full_require_paths
end
# RubyGems < 2.2
gemspec.require_paths.map do |require_path|
if require_path.include?(gemspec.full_gem_path)
require_path
else
File.join(gemspec.full_gem_path, require_path)
end
end
end
private_class_method :full_require_paths_for
end
end
end