bundler-gem_bytes 0.1.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.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module GemBytes
5
+ module Actions
6
+ class Gemspec < Parser::TreeRewriter
7
+ # Holds information about the gemspec file
8
+ # @!attribute [r] gemspec_object
9
+ # @api private
10
+ class GemSpecification
11
+ # Create a new GemSpecification object
12
+ #
13
+ # @param source [String] The contents of the gemspec file
14
+ # @param source_path [String] The path to the gemspec file
15
+ # @api private
16
+ #
17
+ def initialize(source, source_path)
18
+ @source = source
19
+ @source_path = source_path
20
+
21
+ @dependencies = []
22
+ @attributes = []
23
+
24
+ @source_buffer, @source_ast = parse(source, source_path)
25
+ end
26
+
27
+ # The contents of the gemspec file
28
+ # @example
29
+ # data.source # => "Gem::Specification.new do |spec|\n spec.name = 'example'\nend"
30
+ # @return [String]
31
+ attr_reader :source
32
+
33
+ # The path to the gemspec file (used for error reporting)
34
+ # @example
35
+ # data.source_path # => "example.gemspec"
36
+ # @return [String]
37
+ attr_reader :soure_path
38
+
39
+ # The source buffer used when updating the gemspec file
40
+ # @return [Parser::Source::Buffer]
41
+ # @api private
42
+ attr_reader :source_buffer
43
+
44
+ # The parsed AST for the gemspec file source
45
+ # @example
46
+ # data.source_ast # => (send nil :puts)
47
+ # @return [Parser::AST::Node]
48
+ attr_reader :source_ast
49
+
50
+ # The Gem::Specification object
51
+ # @example
52
+ # data.gemspec_object # => #<Gem::Specification:0x00007f9b1b8b3f10>
53
+ # @return [Gem::Specification]
54
+ def gemspec_object
55
+ @gemspec_object ||= load_gem_specification
56
+ end
57
+
58
+ # The name of the Gem::Specification object within the gemspec block
59
+ # @example When the gemspec block starts `Gem::Specification.new do |spec|`
60
+ # data.gemspec_object_name # => :spec
61
+ # @return [Symbol]
62
+ attr_accessor :gemspec_object_name
63
+
64
+ # The AST node for the Gem::Specification block within the source
65
+ # @return [Parser::AST::Node]
66
+ attr_accessor :gemspec_ast
67
+
68
+ # The dependencies found in the gemspec file
69
+ # @return [Array<Dependency>]
70
+ attr_reader :dependencies
71
+
72
+ # The attributes found in the gemspec file
73
+ # @return [Array<Attribute>]
74
+ attr_reader :attributes
75
+
76
+ private
77
+
78
+ # Parses the given code into an AST
79
+ # @param source [String] The code to parse
80
+ # @param source_path [String] The path to the file being parsed (used for error messages only)
81
+ # @return [Array<Parser::AST::Node, Parser::Source::Buffer>] The AST and buffer
82
+ # @api private
83
+ def parse(source, source_path)
84
+ source_buffer = Parser::Source::Buffer.new(source_path, source: source)
85
+ processed_source = RuboCop::AST::ProcessedSource.new(source, ruby_version, source_path)
86
+ unless processed_source.valid_syntax?
87
+ raise "Invalid syntax in #{source_path}\n#{processed_source.diagnostics.map(&:render).join("\n")}"
88
+ end
89
+
90
+ source_ast = processed_source.ast
91
+ [source_buffer, source_ast]
92
+ end
93
+
94
+ # The currently running Ruby version as a float (MAJOR.MINOR only)
95
+ #
96
+ # @return [Float] The Ruby version number, e.g., 3.0
97
+ # @api private
98
+ def ruby_version = RUBY_VERSION.match(/^(?<version>\d+\.\d+)/)['version'].to_f
99
+
100
+ # Load the gemspec file into a Gem::Specification object
101
+ # @return [Gem::Specification] The Gem::Specification object
102
+ # @api private
103
+ def load_gem_specification
104
+ # Store the current $LOAD_PATH
105
+ original_load_path = $LOAD_PATH.dup
106
+
107
+ # Temporarily add 'lib' to the $LOAD_PATH
108
+ lib_path = File.expand_path('lib', Dir.pwd)
109
+ $LOAD_PATH.unshift(lib_path)
110
+
111
+ # Evaluate the gemspec file
112
+ eval(source, binding, '.').tap do # rubocop:disable Security/Eval
113
+ # Restore the original $LOAD_PATH
114
+ $LOAD_PATH.replace(original_load_path)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -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'
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_bytes'
4
+
5
+ module Bundler
6
+ module GemBytes
7
+ # The API for GemBytes templates
8
+ # @api public
9
+ module Actions
10
+ # The gemspec at `gemspec_path` is updated per instructions in `action_block`
11
+ #
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
17
+ #
18
+ # @param gemspec_path [String] the path to the gemspec file to process
19
+ #
20
+ # Defaults to the first gemspec file found in the current directory.
21
+ #
22
+ # @yield a block with instructions to modify the gemspec
23
+ #
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.)
27
+ #
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
31
+ #
32
+ # @return [void]
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)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ require_relative 'actions/gemspec'
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bundler'
4
+
3
5
  module Bundler
4
6
  module GemBytes
5
7
  # A bundler command that adds features to your existing Ruby Gems project
6
- #
7
8
  # @api public
8
- #
9
9
  class BundlerCommand < Bundler::Plugin::API
10
- # Called when the `gem-bytes` command is invoked
10
+ # Executes the `gem-bytes` command
11
11
  #
12
12
  # @example
13
- # BundlerCommand.new.exec('gem-bytes', ['path_or_uri_to_script'])
14
- # @param _command [String] the command that was invoked (in this case, 'gem-bytes')
15
- # @param args [Array<String>] any additional arguments passed to the command
16
- # @raise [SystemExit] if there was an error executing the command
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
17
17
  # @return [void]
18
18
  def exec(_command, args)
19
19
  uri_or_path = validate_args(args)