bundler-gem_bytes 0.2.0 → 0.2.2
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/.release-please-manifest.json +3 -0
- data/CHANGELOG.md +21 -0
- data/README.md +92 -10
- data/Rakefile +8 -0
- data/lib/bundler/gem_bytes/actions/gemspec/attribute.rb +51 -0
- data/lib/bundler/gem_bytes/actions/gemspec/attribute_node.rb +32 -0
- data/lib/bundler/gem_bytes/actions/gemspec/delete_dependency.rb +106 -0
- data/lib/bundler/gem_bytes/actions/gemspec/dependency.rb +73 -0
- data/lib/bundler/gem_bytes/actions/gemspec/dependency_node.rb +32 -0
- data/lib/bundler/gem_bytes/actions/gemspec/gem_specification.rb +121 -0
- data/lib/bundler/gem_bytes/actions/gemspec/upsert_dependency.rb +210 -0
- data/lib/bundler/gem_bytes/actions/gemspec.rb +281 -0
- data/lib/bundler/gem_bytes/actions.rb +22 -32
- data/lib/bundler/gem_bytes/bundler_command.rb +5 -7
- data/lib/bundler/gem_bytes/script_executor.rb +26 -6
- data/lib/bundler/gem_bytes/version.rb +1 -1
- data/lib/bundler/gem_bytes.rb +1 -1
- data/release-please-config.json +22 -0
- metadata +15 -11
- data/lib/bundler/gem_bytes/gemspec/delete_dependency.rb +0 -216
- data/lib/bundler/gem_bytes/gemspec/upsert_dependency.rb +0 -336
- data/lib/bundler/gem_bytes/gemspec.rb +0 -11
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parser/current'
|
4
|
+
require 'rubocop-ast'
|
5
|
+
require 'active_support/core_ext/object'
|
6
|
+
|
7
|
+
module Bundler
|
8
|
+
module GemBytes
|
9
|
+
module Actions
|
10
|
+
class Gemspec < Parser::TreeRewriter
|
11
|
+
# Add or update a dependency in a gemspec
|
12
|
+
#
|
13
|
+
# If a dependency on the given gem is not found, a new dependency is added to
|
14
|
+
# the end of the Gem::Specification block.
|
15
|
+
#
|
16
|
+
# If one or more dependencies are found on the same gem as new_dependency,
|
17
|
+
# the version constraint is updated to the new_dependency version constraint.
|
18
|
+
#
|
19
|
+
# The gemspec is updated via calls to the tree_rewriter object.
|
20
|
+
#
|
21
|
+
# @!attribute [r] tree_rewriter
|
22
|
+
# The object that updates the source
|
23
|
+
# @return [Parser::TreeRewriter]
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
# @!attribute [r] gemspec_block
|
27
|
+
# The root AST node of the Gem::Specification block from the gemspec
|
28
|
+
# @return [Parser::AST::Node]
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
# @!attribute [r] receiver_name
|
32
|
+
# The name of the receiver for the Gem::Specification block
|
33
|
+
# @return [Symbol]
|
34
|
+
# @api private
|
35
|
+
#
|
36
|
+
# @!attribute [r] dependencies
|
37
|
+
# The dependency declarations found in the gemspec file
|
38
|
+
# @return [Array<DependencyNode>]
|
39
|
+
# @api private
|
40
|
+
#
|
41
|
+
# @!attribute [r] new_dependency
|
42
|
+
# The dependency declaration to add or update
|
43
|
+
# @return [Dependency]
|
44
|
+
# @api private
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
class UpsertDependency
|
48
|
+
# Initializes the upsert dependency action
|
49
|
+
# @param tree_rewriter [Parser::TreeRewriter] The object that updates the source
|
50
|
+
# @param gemspec_block [Parser::AST::Node] The Gem::Specification block
|
51
|
+
# @param receiver_name [Symbol] The name of the receiver for the Gem::Specification block
|
52
|
+
# @param dependencies [Array<DependencyNode>] The dependency declarations found in the gemspec file
|
53
|
+
# @api private
|
54
|
+
def initialize(tree_rewriter, gemspec_block, receiver_name, dependencies)
|
55
|
+
@tree_rewriter = tree_rewriter
|
56
|
+
@gemspec_block = gemspec_block
|
57
|
+
@receiver_name = receiver_name
|
58
|
+
@dependencies = dependencies
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :tree_rewriter, :gemspec_block, :receiver_name, :dependencies, :new_dependency
|
62
|
+
|
63
|
+
# Adds or updates a dependency to the Gem::Specification block
|
64
|
+
#
|
65
|
+
# @example
|
66
|
+
# upsert_dependency = UpsertDependency.new(tree_rewriter, gemspec_block, receiver_name, dependencies)
|
67
|
+
# new_dependency = Dependency.new(:add_runtime_dependency, 'rubocop', '~> 1.68')
|
68
|
+
# upsert_dependency.call(new_dependency)
|
69
|
+
# @param new_dependency [Dependency] The dependency declaration to add or update
|
70
|
+
# @return [void]
|
71
|
+
# @api public
|
72
|
+
def call(new_dependency)
|
73
|
+
@new_dependency = new_dependency
|
74
|
+
matching_dependencies = dependencies.select { |d| d.dependency.gem_name == new_dependency.gem_name }
|
75
|
+
|
76
|
+
if matching_dependencies.any?
|
77
|
+
update_dependencies(matching_dependencies)
|
78
|
+
else
|
79
|
+
add_dependency
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Update the version constraint of the existing dependency(s)
|
84
|
+
# @param matching_dependencies [Array<DependencyNode>] The existing dependencies that match new_dependency
|
85
|
+
# @return [void]
|
86
|
+
# @api private
|
87
|
+
def update_dependencies(matching_dependencies)
|
88
|
+
matching_dependencies.each do |found_dependency|
|
89
|
+
raise(dependency_type_conflict_error(found_dependency)) unless dependency_type_match?(found_dependency)
|
90
|
+
|
91
|
+
tree_rewriter.replace(found_dependency.node.loc.expression, dependency_source_code(found_dependency))
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Add the new_dependency to the end of the Gem::Specification block
|
96
|
+
# @return [void]
|
97
|
+
# @api private
|
98
|
+
def add_dependency
|
99
|
+
# Add the dependency to the end of the Gem::Specification block
|
100
|
+
internal_block = gemspec_block.children[2]
|
101
|
+
if internal_block
|
102
|
+
tree_rewriter.insert_after(internal_block.children.last.loc.expression, "\n #{dependency_source_code}")
|
103
|
+
else
|
104
|
+
# When the Gem::Specification block is empty, it require special handling
|
105
|
+
add_dependency_to_empty_gemspec_block
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Error message for a dependency type conflict
|
110
|
+
# @param existing_dependency [DependencyNode] The existing dependency
|
111
|
+
# @return [String] The error message
|
112
|
+
# @api private
|
113
|
+
def dependency_type_conflict_error(existing_dependency)
|
114
|
+
# :nocov: JRuby give false positive for this line being uncovered by tests
|
115
|
+
<<~MESSAGE.chomp.gsub("\n", ' ')
|
116
|
+
Trying to add a
|
117
|
+
#{dependency_method_to_type(new_dependency.method_name).upcase}
|
118
|
+
dependency on "#{new_dependency.gem_name}" which conflicts with the existing
|
119
|
+
#{dependency_method_to_type(existing_dependency.dependency.method_name).upcase}
|
120
|
+
dependency.
|
121
|
+
MESSAGE
|
122
|
+
# :nocov:
|
123
|
+
end
|
124
|
+
|
125
|
+
# The dependency type (:runtime or :development) based on a given method name
|
126
|
+
# @param method [Symbol] The method name to convert to a dependency type
|
127
|
+
# @return [Symbol] The dependency type
|
128
|
+
# @api private
|
129
|
+
def dependency_method_to_type(method)
|
130
|
+
method == :add_development_dependency ? :development : :runtime
|
131
|
+
end
|
132
|
+
|
133
|
+
# Checks if the new dependency type is the same as the existing dependency type
|
134
|
+
#
|
135
|
+
# @param existing_dependency [DependencyNode] The existing dependency
|
136
|
+
# @return [Boolean] Whether the dependency type conflicts
|
137
|
+
# @api private
|
138
|
+
def dependency_type_match?(existing_dependency)
|
139
|
+
# Either both are :add_development_dependency or both are not
|
140
|
+
(existing_dependency.dependency.method_name == :add_development_dependency) ==
|
141
|
+
(new_dependency.method_name == :add_development_dependency)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Add new_dependency to an empty Gem::Specification block
|
145
|
+
# @return [void]
|
146
|
+
# @api private
|
147
|
+
def add_dependency_to_empty_gemspec_block
|
148
|
+
source = gemspec_block.loc.expression.source
|
149
|
+
# :nocov: supress false reporting of no coverage of multiline string literals on JRuby
|
150
|
+
tree_rewriter.replace(gemspec_block.loc.expression, <<~GEMSPEC_BLOCK.chomp)
|
151
|
+
#{source[0..-5]}
|
152
|
+
#{dependency_source_code}
|
153
|
+
#{source[-3..]}
|
154
|
+
GEMSPEC_BLOCK
|
155
|
+
# :nocov:
|
156
|
+
end
|
157
|
+
|
158
|
+
# The source code for the updated dependency declaration
|
159
|
+
# @param existing_dependency [DependencyNode] The existing dependency
|
160
|
+
# @return [String] The source code for the dependency declaration
|
161
|
+
# @api private
|
162
|
+
def dependency_source_code(existing_dependency = nil)
|
163
|
+
# Use existing quote character for string literals
|
164
|
+
q = new_quote_char(existing_dependency)
|
165
|
+
|
166
|
+
# :nocov: supress false reporting of no coverage of multiline string literals on JRuby
|
167
|
+
"#{receiver_name}.#{new_method_name(existing_dependency)} " \
|
168
|
+
"#{q}#{new_dependency.gem_name}#{q}, " \
|
169
|
+
"#{new_dependency.version_constraints.map { |vc| "#{q}#{vc}#{q}" }.join(', ')}"
|
170
|
+
# :nocov:
|
171
|
+
end
|
172
|
+
|
173
|
+
# Use the same quote char as the existing dependency or default to single quote
|
174
|
+
# @param existing_dependency [DependencyNode, nil] The existing dependency being updated
|
175
|
+
# @return [String] The quote character to use
|
176
|
+
# @api private
|
177
|
+
def new_quote_char(existing_dependency)
|
178
|
+
if existing_dependency
|
179
|
+
existing_dependency.node.children[3].loc.expression.source[0]
|
180
|
+
else
|
181
|
+
"'"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# The method to use for the new dependency
|
186
|
+
#
|
187
|
+
# If `existing_dependency` is given and the dependency type (runtime vs.
|
188
|
+
# development) matches, the existing dependency method is used. Otherwise,
|
189
|
+
# the new_dependency method is used.
|
190
|
+
#
|
191
|
+
# The purpose of this method is ensure that an #add_dependency call is not
|
192
|
+
# replaced with an #add_runtime_dependency call or vice versa. This
|
193
|
+
# maintains consistency within the user's gemspec even though these methods
|
194
|
+
# are functionally equivalent.
|
195
|
+
#
|
196
|
+
# @param existing_dependency [DependencyNode, nil] The existing dependency being updated
|
197
|
+
# @return [Symbol] The method to use for the new dependency
|
198
|
+
# @api private
|
199
|
+
def new_method_name(existing_dependency)
|
200
|
+
if existing_dependency && dependency_type_match?(existing_dependency)
|
201
|
+
existing_dependency.dependency.method_name
|
202
|
+
else
|
203
|
+
new_dependency.method_name
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require 'parser/current'
|
5
|
+
require 'rubocop-ast'
|
6
|
+
require 'active_support/core_ext/object'
|
7
|
+
|
8
|
+
module Bundler
|
9
|
+
module GemBytes
|
10
|
+
module Actions
|
11
|
+
# Updates a gemspec according to the given block
|
12
|
+
#
|
13
|
+
# This class enables you to programmatically update a gemspec file by adding or
|
14
|
+
# removing dependencies, updating configuration parameters, and manipulating
|
15
|
+
# metadata. It processes the gemspec file as an Abstract Syntax Tree (AST),
|
16
|
+
# which allows granular control over gemspec updates.
|
17
|
+
#
|
18
|
+
# Key terms:
|
19
|
+
# * `gemspec_block`: The AST node representing the Gem::Specification block in
|
20
|
+
# the gemspec file.
|
21
|
+
# * `receiver_name`: The receiver of the Gem::Specification block (e.g., 'spec'
|
22
|
+
# in `spec.add_dependency`).
|
23
|
+
# * `dependency declarations`: Calls to methods like `add_dependency` or
|
24
|
+
# `add_runtime_dependency` within the gemspec.
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# gemspec_path = Dir['*.gemspec'].first
|
28
|
+
# gemspec_content = File.read(gemspec_path)
|
29
|
+
# updated_gemspec_content = Gemspec.new.call(gemspec_content, path: gemspec_path) do
|
30
|
+
# add_dependency 'activesupport', '~> 7.0'
|
31
|
+
# add_runtime_dependency 'process_executer', '~> 1.1'
|
32
|
+
# add_development_dependency 'rubocop', '~> 1.68'
|
33
|
+
# remove_dependency 'byebug'
|
34
|
+
# config 'required_ruby_version', '>= 2.5.0'
|
35
|
+
# remove_config 'required_ruby_version'
|
36
|
+
# config_metadata 'homepage', 'https://example.com'
|
37
|
+
# remove_config_metadata 'homepage'
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @api public
|
41
|
+
#
|
42
|
+
class Gemspec < Parser::TreeRewriter
|
43
|
+
extend Forwardable
|
44
|
+
|
45
|
+
# Create a new Gemspec action
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Gemspec.new(context: self)
|
49
|
+
# @param context [Bundler::GemBytes::ScriptExecuter] The context in which the action is being run
|
50
|
+
#
|
51
|
+
def initialize(context:)
|
52
|
+
@context = context
|
53
|
+
super()
|
54
|
+
end
|
55
|
+
|
56
|
+
def_delegators :data, :attributes, :dependencies, :gemspec_ast, :gemspec_object,
|
57
|
+
:gemspec_object_name, :source, :source_path, :source_ast, :source_buffer
|
58
|
+
|
59
|
+
# Processes the given gemspec file and returns the updated content
|
60
|
+
#
|
61
|
+
# @example
|
62
|
+
# updated_gemspec_content = Gemspec.new.call(gemspec_content, source_path: gemspec_path) do
|
63
|
+
# add_dependency 'activesupport', '~> 7.0'
|
64
|
+
# end
|
65
|
+
# @param code [String] The content of the gemspec file
|
66
|
+
# @param path [String] The path to the gemspec file (used for error reporting)
|
67
|
+
# @return [String] The updated gemspec file content
|
68
|
+
# @raise [ArgumentError] if the Gem Specification block is not found in the given gemspec content
|
69
|
+
#
|
70
|
+
def call(source, source_path: '(string)', &action_block)
|
71
|
+
@data = GemSpecification.new(source, source_path)
|
72
|
+
@action_block = action_block
|
73
|
+
@processing_gemspec_block = false
|
74
|
+
rewrite(source_buffer, source_ast).tap do |_result|
|
75
|
+
raise ArgumentError, 'Gem::Specification block not found' unless gemspec_ast.present?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# The ScriptExecuter object that called this action (used for testing)
|
80
|
+
# @return [Bundler::GemBytes::ScriptExecuter]
|
81
|
+
# @api private
|
82
|
+
attr_reader :context
|
83
|
+
|
84
|
+
# Indicates that the gemspec block was found and is being processed
|
85
|
+
# @return [Boolean]
|
86
|
+
# @api private
|
87
|
+
attr_reader :processing_gemspec_block
|
88
|
+
|
89
|
+
# The block passed to #call containing the instructions to update the gemspec
|
90
|
+
# @return [Proc]
|
91
|
+
# @api private
|
92
|
+
attr_reader :action_block
|
93
|
+
|
94
|
+
# The GemSpecification object containing information about the gemspec file
|
95
|
+
# @return [GemSpecification]
|
96
|
+
# @api private
|
97
|
+
attr_reader :data
|
98
|
+
|
99
|
+
alias processing_gemspec_block? processing_gemspec_block
|
100
|
+
|
101
|
+
# Handles block nodes within the AST to locate the Gem Specification block
|
102
|
+
#
|
103
|
+
# @param node [Parser::AST::Node] The block node within the AST
|
104
|
+
# @return [void]
|
105
|
+
# @api private
|
106
|
+
def on_block(node)
|
107
|
+
# If already processing the Gem Specification block, this must be some other nested block
|
108
|
+
return if processing_gemspec_block?
|
109
|
+
|
110
|
+
data.gemspec_object_name = gem_specification_pattern.match(node)
|
111
|
+
|
112
|
+
return unless gemspec_object_name
|
113
|
+
|
114
|
+
@processing_gemspec_block = true
|
115
|
+
data.gemspec_ast = node
|
116
|
+
|
117
|
+
super # process the children of this node to find interesting parts of the Gem::Specification block
|
118
|
+
|
119
|
+
@processing_gemspec_block = false
|
120
|
+
|
121
|
+
# Call the action_block to do requested modifications the Gem::Specification block.
|
122
|
+
# The default receiver in the block is this object.
|
123
|
+
# receiver_name and gem_specification are passed as arguments.
|
124
|
+
return unless action_block
|
125
|
+
|
126
|
+
instance_exec(gemspec_object_name, gemspec_object, &action_block)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Handles `send` nodes within the AST to locate dependency calls
|
130
|
+
#
|
131
|
+
# Only processes `send` nodes within the Gem::Specification block.
|
132
|
+
#
|
133
|
+
# @param node [Parser::AST::Node] The `send` node to check for dependency patterns
|
134
|
+
# @return [void]
|
135
|
+
# @api private
|
136
|
+
def on_send(node)
|
137
|
+
return unless processing_gemspec_block?
|
138
|
+
|
139
|
+
handle_dependency(node) || handle_attribute(node)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Removes a dependency from the Gem::Specification block
|
143
|
+
#
|
144
|
+
# @example
|
145
|
+
# remove_dependency 'rubocop'
|
146
|
+
# # Removes the dependency on 'rubocop' from the Gem::Specification block:
|
147
|
+
# # spec.add_development_dependency 'rubocop', '~> 1.68'
|
148
|
+
# @param gem_name [String] the name of the gem to remove a dependency on
|
149
|
+
# @return [void]
|
150
|
+
#
|
151
|
+
# TODO: just pass data to DeleteDependency.new
|
152
|
+
def remove_dependency(gem_name)
|
153
|
+
DeleteDependency.new(self, gemspec_ast, gemspec_object_name, dependencies).call(gem_name)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Adds or updates a dependency to the Gem::Specification block
|
157
|
+
#
|
158
|
+
# @example
|
159
|
+
# add_dependency 'rails', '~> 7.0'
|
160
|
+
# # Adds (or updates) the following line to the Gem::Specification block:
|
161
|
+
# # spec.add_dependency 'rails', '~> 7.0'
|
162
|
+
# @param gem_name [String] the name of the gem to add a dependency on
|
163
|
+
# @param version_constraints [Array[String]] one or more version constraints on the gem
|
164
|
+
# @param method_name [String] the name of the method to use to add the dependency
|
165
|
+
# @return [void]
|
166
|
+
#
|
167
|
+
# TODO: just pass data to DeleteDependency.new
|
168
|
+
def add_dependency(gem_name, *version_constraints, method_name: :add_dependency)
|
169
|
+
new_dependency = Dependency.new(method_name, gem_name, version_constraints)
|
170
|
+
UpsertDependency.new(self, gemspec_ast, gemspec_object_name, dependencies).call(new_dependency)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Adds or updates a dependency to the Gem::Specification block
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# add_runtime_dependency 'rails', '~> 7.0'
|
177
|
+
# # Adds (or updates) the following line to the Gem::Specification block:
|
178
|
+
# # spec.add_runtime_dependency 'rails', '~> 7.0'
|
179
|
+
# @param gem_name [String] the name of the gem to add a dependency on
|
180
|
+
# @param version_constraint [String] the version constraint on the gem
|
181
|
+
# @return [void]
|
182
|
+
#
|
183
|
+
def add_runtime_dependency(gem_name, version_constraint)
|
184
|
+
add_dependency(gem_name, version_constraint, method_name: :add_runtime_dependency)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Adds or updates a development dependency to the Gem::Specification block
|
188
|
+
#
|
189
|
+
# @example
|
190
|
+
# add_runtime_development_dependency 'rubocop', '~> 1.68'
|
191
|
+
# # Adds (or updates) the following line to the Gem::Specification block:
|
192
|
+
# # spec.add_development_dependency 'rubocop', '~> 1.68'
|
193
|
+
# @param gem_name [String] the name of the gem to add a dependency on
|
194
|
+
# @param version_constraint [String] the version constraint on the gem
|
195
|
+
# @return [void]
|
196
|
+
#
|
197
|
+
def add_development_dependency(gem_name, version_constraint)
|
198
|
+
add_dependency(gem_name, version_constraint, method_name: :add_development_dependency)
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
|
203
|
+
# Save the dependency if the node is a dependency
|
204
|
+
# @param node [Parser::AST::Node] the node to check if it is a dependency
|
205
|
+
# @return [Boolean] true if the node is a dependency, false otherwise
|
206
|
+
# @api private
|
207
|
+
def handle_dependency(node)
|
208
|
+
return false unless (match = dependency_pattern.match(node))
|
209
|
+
|
210
|
+
dependencies << DependencyNode.new(node, Dependency.new(*match))
|
211
|
+
|
212
|
+
true
|
213
|
+
end
|
214
|
+
|
215
|
+
# Save the attribute if the node is an attribute
|
216
|
+
# @param node [Parser::AST::Node] the node to check if it is an attribute
|
217
|
+
# @return [Boolean] true if the node is an attribute, false otherwise
|
218
|
+
# @api private
|
219
|
+
def handle_attribute(node)
|
220
|
+
return false unless (match = attribute_pattern.match(node))
|
221
|
+
return false unless match[0].end_with?('=')
|
222
|
+
|
223
|
+
name = match[0][0..-2]
|
224
|
+
value = match[1]
|
225
|
+
attributes << AttributeNode.new(node, Attribute.new(name, value))
|
226
|
+
|
227
|
+
true
|
228
|
+
end
|
229
|
+
|
230
|
+
# The pattern to match the Gem::Specification block in the AST
|
231
|
+
# @return [RuboCop::AST::NodePattern] The Gem::Specification pattern
|
232
|
+
# @api private
|
233
|
+
def gem_specification_pattern
|
234
|
+
@gem_specification_pattern ||=
|
235
|
+
RuboCop::AST::NodePattern.new(<<~PATTERN)
|
236
|
+
(block (send (const (const nil? :Gem) :Specification) :new)(args (arg $_)) ...)
|
237
|
+
PATTERN
|
238
|
+
end
|
239
|
+
|
240
|
+
# The pattern to match a dependency declaration in the AST
|
241
|
+
# @return [RuboCop::AST::NodePattern] The dependency pattern
|
242
|
+
# @api private
|
243
|
+
def dependency_pattern
|
244
|
+
# :nocov: JRuby give false positive for this line being uncovered by tests
|
245
|
+
@dependency_pattern ||=
|
246
|
+
RuboCop::AST::NodePattern.new(<<~PATTERN)
|
247
|
+
(send
|
248
|
+
{ (send _ :#{gemspec_object_name}) | (lvar :#{gemspec_object_name}) }
|
249
|
+
${ :add_dependency :add_runtime_dependency :add_development_dependency }
|
250
|
+
(str $_gem_name)
|
251
|
+
<(str $_version_constraint) ...>
|
252
|
+
)
|
253
|
+
PATTERN
|
254
|
+
# :nocov:
|
255
|
+
end
|
256
|
+
|
257
|
+
# The pattern to match an attribute in the AST
|
258
|
+
# @return [RuboCop::AST::NodePattern] The attribute pattern
|
259
|
+
# @api private
|
260
|
+
def attribute_pattern
|
261
|
+
# :nocov: JRuby give false positive for this line being uncovered by tests
|
262
|
+
@attribute_pattern ||=
|
263
|
+
RuboCop::AST::NodePattern.new(<<~PATTERN)
|
264
|
+
(send
|
265
|
+
(lvar :#{gemspec_object_name}) $_name $_value
|
266
|
+
)
|
267
|
+
PATTERN
|
268
|
+
# :nocov:
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
require_relative 'gemspec/attribute'
|
276
|
+
require_relative 'gemspec/attribute_node'
|
277
|
+
require_relative 'gemspec/gem_specification'
|
278
|
+
require_relative 'gemspec/delete_dependency'
|
279
|
+
require_relative 'gemspec/dependency'
|
280
|
+
require_relative 'gemspec/dependency_node'
|
281
|
+
require_relative 'gemspec/upsert_dependency'
|
@@ -7,47 +7,37 @@ module Bundler
|
|
7
7
|
# The API for GemBytes templates
|
8
8
|
# @api public
|
9
9
|
module Actions
|
10
|
-
#
|
10
|
+
# The gemspec at `gemspec_path` is updated per instructions in `action_block`
|
11
11
|
#
|
12
|
-
# @example
|
13
|
-
#
|
14
|
-
#
|
12
|
+
# @example Adding a runtime dependency
|
13
|
+
# actions = Actions.new
|
14
|
+
# actions.gemspec(gemspec_path: 'test.gemspec') do
|
15
|
+
# add_runtime_dependency 'rubocop', '~> 1.68'
|
16
|
+
# end
|
15
17
|
#
|
16
|
-
# @param
|
17
|
-
# @param gem_name [String] the name of the gem to add
|
18
|
-
# @param version_constraint [String] the version constraint for the gem
|
19
|
-
# @param force [Boolean] whether to overwrite the existing dependency
|
20
|
-
# @param gemspec [String] the path to the gemspec file
|
18
|
+
# @param gemspec_path [String] the path to the gemspec file to process
|
21
19
|
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# @api public
|
20
|
+
# Defaults to the first gemspec file found in the current directory.
|
25
21
|
#
|
26
|
-
|
27
|
-
source = File.read(gemspec)
|
28
|
-
updated_source = Bundler::GemBytes::Gemspec::UpsertDependency.new(
|
29
|
-
dependency_type, gem_name, version_constraint, force: force
|
30
|
-
).call(source, path: gemspec)
|
31
|
-
File.write(gemspec, updated_source)
|
32
|
-
end
|
33
|
-
|
34
|
-
# Removes a dependency from the project's gemspec file
|
22
|
+
# @yield a block with instructions to modify the gemspec
|
35
23
|
#
|
36
|
-
#
|
37
|
-
#
|
24
|
+
# This block is run in the context of a {GemBytes::Actions::Gemspec} instance.
|
25
|
+
# The instructions are methods defined by this instance (e.g.
|
26
|
+
# `add_dependency`, `remove_dependency`, etc.)
|
38
27
|
#
|
39
|
-
# @
|
40
|
-
# @
|
28
|
+
# @yieldparam gemspec_name [Symbol] the name of the Gem::Specification varaible used in the gemspec
|
29
|
+
# @yieldparam gemspec [Gem::Specification] the evaluated gemspec
|
30
|
+
# @yieldreturn [String] the updated gemspec
|
41
31
|
#
|
42
32
|
# @return [void]
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
updated_source = Bundler::GemBytes::Gemspec::DeleteDependency.new(gem_name).call(source, path: gemspec)
|
49
|
-
File.write(gemspec, updated_source)
|
33
|
+
def gemspec(gemspec_path: Dir['*.gemspec'].first, &action_block)
|
34
|
+
source = File.read(gemspec_path)
|
35
|
+
action = Bundler::GemBytes::Actions::Gemspec.new(context: self)
|
36
|
+
updated_source = action.call(source, source_path: gemspec_path, &action_block)
|
37
|
+
File.write(gemspec_path, updated_source)
|
50
38
|
end
|
51
39
|
end
|
52
40
|
end
|
53
41
|
end
|
42
|
+
|
43
|
+
require_relative 'actions/gemspec'
|
@@ -5,17 +5,15 @@ require 'bundler'
|
|
5
5
|
module Bundler
|
6
6
|
module GemBytes
|
7
7
|
# A bundler command that adds features to your existing Ruby Gems project
|
8
|
-
#
|
9
8
|
# @api public
|
10
|
-
#
|
11
9
|
class BundlerCommand < Bundler::Plugin::API
|
12
|
-
#
|
10
|
+
# Executes the `gem-bytes` command
|
13
11
|
#
|
14
12
|
# @example
|
15
|
-
# BundlerCommand.new.exec('gem-bytes', ['
|
16
|
-
# @param _command [String] the
|
17
|
-
# @param args [Array<String>]
|
18
|
-
# @raise [SystemExit] if
|
13
|
+
# BundlerCommand.new.exec('gem-bytes', ['uri_or_path', *extra_args])
|
14
|
+
# @param _command [String] the invoked bundler command (in this case, 'gem-bytes')
|
15
|
+
# @param args [Array<String>] command arguments
|
16
|
+
# @raise [SystemExit] if an error occurs
|
19
17
|
# @return [void]
|
20
18
|
def exec(_command, args)
|
21
19
|
uri_or_path = validate_args(args)
|
@@ -5,32 +5,52 @@ require_relative 'actions'
|
|
5
5
|
|
6
6
|
module Bundler
|
7
7
|
module GemBytes
|
8
|
-
#
|
8
|
+
# Responsible for executing scripts using Thor and GemBytes actions
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# This class enables the execution of scripts from a file or URI, integrating
|
11
|
+
# with `Thor::Actions` to allow advanced file manipulation and action chaining.
|
12
|
+
# This can be particularly useful for tasks like creating or modifying files,
|
13
|
+
# managing dependencies, and implementing workflows for gem development.
|
11
14
|
#
|
12
15
|
# @example Executing a script from a file or URI
|
13
16
|
# executor = Bundler::GemBytes::ScriptExecutor.new
|
14
17
|
# executor.execute('path_or_uri_to_script')
|
18
|
+
# @api public
|
15
19
|
class ScriptExecutor < ::Thor::Group
|
16
20
|
include ::Thor::Actions
|
17
21
|
include Bundler::GemBytes::Actions
|
18
22
|
|
19
|
-
#
|
20
|
-
#
|
23
|
+
# Sets the source paths for `Thor::Actions`
|
24
|
+
#
|
25
|
+
# This determines where action scripts will be sourced from.
|
26
|
+
#
|
27
|
+
# By default, the source path is set to the current working directory, which
|
28
|
+
# allows scripts to access files from the local file system during execution.
|
29
|
+
#
|
30
|
+
# @return [Array<String>] the list of source paths
|
31
|
+
#
|
21
32
|
# @api private
|
22
33
|
def self.source_paths
|
23
|
-
# Add the current working directory or the directory of the script
|
24
34
|
[Dir.pwd]
|
25
35
|
end
|
26
36
|
|
27
|
-
# Executes
|
37
|
+
# Executes a script from a given URI or file path
|
38
|
+
#
|
39
|
+
# This method loads the script located at the specified `path_or_uri` and
|
40
|
+
# executes it within the context of this class which includes `Thor::Actions`
|
41
|
+
# and `Bundler::GemBytes::ScriptExecutor`. This allows the script to perform
|
42
|
+
# tasks such as modifying files, creating directories, and other common file
|
43
|
+
# system operations.
|
28
44
|
#
|
29
45
|
# @param path_or_uri [String] the URI or file path to the script
|
46
|
+
#
|
30
47
|
# @return [void]
|
48
|
+
#
|
31
49
|
# @raise [RuntimeError] if the script cannot be loaded
|
50
|
+
#
|
32
51
|
# @example Execute a script from a path
|
33
52
|
# execute('path/to/script.rb')
|
53
|
+
#
|
34
54
|
def execute(path_or_uri)
|
35
55
|
apply(path_or_uri)
|
36
56
|
rescue StandardError => e
|
data/lib/bundler/gem_bytes.rb
CHANGED
@@ -16,7 +16,7 @@ end
|
|
16
16
|
require 'active_support'
|
17
17
|
require 'thor'
|
18
18
|
|
19
|
+
require_relative 'gem_bytes/actions'
|
19
20
|
require_relative 'gem_bytes/bundler_command'
|
20
|
-
require_relative 'gem_bytes/gemspec'
|
21
21
|
require_relative 'gem_bytes/script_executor'
|
22
22
|
require_relative 'gem_bytes/version'
|