dependabot-npm_and_yarn 0.294.0 → 0.296.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72e338772b3c3aac3cf86538fc2d70dbbc45f5f7cb854cd7fd74913b140fe056
4
- data.tar.gz: 1856c138b871ebe80e5cc6faa5984ba80c10b342aa8bf0822e325e5a31a3f815
3
+ metadata.gz: 40c0445c84d264374459bc6e6d333d051a116ec598ae0e208536123242a2a56b
4
+ data.tar.gz: 208a49c91628dda0ada48108de1ef09b07b8ca7b4f12df008906689e5ad173f9
5
5
  SHA512:
6
- metadata.gz: 0e3267d0aafcf35e345505c87a23b2b783cfc36377e46f5f10875a4bd8b0c4fe201b6dea0dbed75463b75dccba175014af850451cfc52b39c0a2a410a5c5ab34
7
- data.tar.gz: a1c6be5ccbebcf43a76a51d7871a37743428f052c734a985a48fc90d4b1e2944679a8b6eb17910a8ef7e14c69dbe46d9761319b28d6a57d7dcbac7e94fbb9c09
6
+ metadata.gz: c1a2bc9cfa98144e171f653efedd490ccf0eb02176c29496a5ee288d83a54b83f8390e3a7d67b98fa544ef3f5e4074080101d8115414fdc34cb4c3ec075a2f25
7
+ data.tar.gz: da594bed80a0ad0c2e0fef360bb026f176020c0edc1c665e4e4823f882edd72e85e07a52b5a2036c78620f0889a0e050d0342462b21f6671a8d6daaa5976cbb3
@@ -0,0 +1,359 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module NpmAndYarn
8
+ module ConstraintHelper
9
+ extend T::Sig
10
+
11
+ # Regex Components for Semantic Versioning
12
+ DIGIT = "\\d+" # Matches a single number (e.g., "1")
13
+ PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
14
+ BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
15
+
16
+ # Matches semantic versions:
17
+ VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
18
+
19
+ VERSION_REGEX = T.let(/^#{VERSION}$/, Regexp)
20
+
21
+ # Base regex for SemVer (major.minor.patch[-prerelease][+build])
22
+ # This pattern extracts valid semantic versioning strings based on the SemVer 2.0 specification.
23
+ SEMVER_REGEX = T.let(/
24
+ (?<version>\d+\.\d+\.\d+) # Match major.minor.patch (e.g., 1.2.3)
25
+ (?:-(?<prerelease>[a-zA-Z0-9.-]+))? # Optional prerelease (e.g., -alpha.1, -rc.1, -beta.5)
26
+ (?:\+(?<build>[a-zA-Z0-9.-]+))? # Optional build metadata (e.g., +build.20231101, +exp.sha.5114f85)
27
+ /x, Regexp)
28
+
29
+ # Full SemVer validation regex (ensures the entire string is a valid SemVer)
30
+ # This ensures the entire input strictly follows SemVer, without extra characters before/after.
31
+ SEMVER_VALIDATION_REGEX = T.let(/^#{SEMVER_REGEX}$/, Regexp)
32
+
33
+ # SemVer constraint regex (supports package.json version constraints)
34
+ # This pattern ensures proper parsing of SemVer versions with optional operators.
35
+ SEMVER_CONSTRAINT_REGEX = T.let(/
36
+ (?: (>=|<=|>|<|=|~|\^)\s*)? # Make operators optional (e.g., >=, ^, ~)
37
+ (\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?) # Match full SemVer versions
38
+ | (\*|latest) # Match wildcard (*) or 'latest'
39
+ /x, Regexp)
40
+
41
+ # /(>=|<=|>|<|=|~|\^)\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)|(\*|latest)/
42
+
43
+ SEMVER_OPERATOR_REGEX = /^(>=|<=|>|<|~|\^|=)$/
44
+
45
+ # Constraint Types as Constants
46
+ CARET_CONSTRAINT_REGEX = T.let(/^\^\s*(#{VERSION})$/, Regexp)
47
+ TILDE_CONSTRAINT_REGEX = T.let(/^~\s*(#{VERSION})$/, Regexp)
48
+ EXACT_CONSTRAINT_REGEX = T.let(/^\s*(#{VERSION})$/, Regexp)
49
+ GREATER_THAN_EQUAL_REGEX = T.let(/^>=\s*(#{VERSION})$/, Regexp)
50
+ LESS_THAN_EQUAL_REGEX = T.let(/^<=\s*(#{VERSION})$/, Regexp)
51
+ GREATER_THAN_REGEX = T.let(/^>\s*(#{VERSION})$/, Regexp)
52
+ LESS_THAN_REGEX = T.let(/^<\s*(#{VERSION})$/, Regexp)
53
+ WILDCARD_REGEX = T.let(/^\*$/, Regexp)
54
+ LATEST_REGEX = T.let(/^latest$/, Regexp)
55
+ SEMVER_CONSTANTS = ["*", "latest"].freeze
56
+
57
+ # Unified Regex for Valid Constraints
58
+ VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
59
+ CARET_CONSTRAINT_REGEX,
60
+ TILDE_CONSTRAINT_REGEX,
61
+ EXACT_CONSTRAINT_REGEX,
62
+ GREATER_THAN_EQUAL_REGEX,
63
+ LESS_THAN_EQUAL_REGEX,
64
+ GREATER_THAN_REGEX,
65
+ LESS_THAN_REGEX,
66
+ WILDCARD_REGEX,
67
+ LATEST_REGEX
68
+ ).freeze, Regexp)
69
+
70
+ # Extract unique constraints from the given constraint expression.
71
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
72
+ # @return [T::Array[String]] The list of unique Ruby-compatible constraints.
73
+ sig do
74
+ params(
75
+ constraint_expression: T.nilable(String),
76
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
77
+ )
78
+ .returns(T.nilable(T::Array[String]))
79
+ end
80
+ def self.extract_ruby_constraints(constraint_expression, dependabot_versions = nil)
81
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
82
+
83
+ return nil unless parsed_constraints
84
+
85
+ parsed_constraints.filter_map { |parsed| parsed[:constraint] }
86
+ end
87
+
88
+ # rubocop:disable Metrics/AbcSize
89
+ # rubocop:disable Metrics/CyclomaticComplexity
90
+ # rubocop:disable Metrics/MethodLength
91
+ # rubocop:disable Metrics/PerceivedComplexity
92
+ sig do
93
+ params(constraint_expression: T.nilable(String))
94
+ .returns(T.nilable(T::Array[String]))
95
+ end
96
+ def self.split_constraints(constraint_expression)
97
+ normalized_constraint = constraint_expression&.strip
98
+ return [] if normalized_constraint.nil? || normalized_constraint.empty?
99
+
100
+ # Split constraints by logical OR (`||`)
101
+ constraint_groups = normalized_constraint.split("||")
102
+
103
+ # Split constraints by logical AND (`,`)
104
+ constraint_groups = constraint_groups.map do |or_constraint|
105
+ or_constraint.split(",").map(&:strip)
106
+ end.flatten
107
+
108
+ constraint_groups = constraint_groups.map do |constraint|
109
+ tokens = constraint.split(/\s+/).map(&:strip)
110
+
111
+ and_constraints = []
112
+
113
+ previous = T.let(nil, T.nilable(String))
114
+ operator = T.let(false, T.nilable(T::Boolean))
115
+ wildcard = T.let(false, T::Boolean)
116
+
117
+ tokens.each do |token|
118
+ token = token.strip
119
+ next if token.empty?
120
+
121
+ # Invalid constraint if wildcard and anything else
122
+ return nil if wildcard
123
+
124
+ # If token is one of the operators (>=, <=, >, <, ~, ^, =)
125
+ if token.match?(SEMVER_OPERATOR_REGEX)
126
+ wildcard = false
127
+ operator = true
128
+ # If token is wildcard or latest
129
+ elsif token.match?(/(\*|latest)/)
130
+ and_constraints << token
131
+ wildcard = true
132
+ operator = false
133
+ # If token is exact version (e.g., "1.2.3")
134
+ elsif token.match(VERSION_REGEX)
135
+ and_constraints << if operator
136
+ "#{previous}#{token}"
137
+ else
138
+ token
139
+ end
140
+ wildcard = false
141
+ operator = false
142
+ # If token is a valid constraint (e.g., ">=1.2.3", "<=2.0.0")
143
+ elsif token.match(VALID_CONSTRAINT_REGEX)
144
+ return nil if operator
145
+
146
+ and_constraints << token
147
+
148
+ wildcard = false
149
+ operator = false
150
+ else
151
+ # invalid constraint
152
+ return nil
153
+ end
154
+ previous = token
155
+ end
156
+ and_constraints.uniq
157
+ end.flatten
158
+ constraint_groups if constraint_groups.any?
159
+ end
160
+
161
+ # rubocop:enable Metrics/AbcSize
162
+ # rubocop:enable Metrics/CyclomaticComplexity
163
+ # rubocop:enable Metrics/MethodLength
164
+ # rubocop:enable Metrics/PerceivedComplexity
165
+
166
+ # Find the highest version from the given constraint expression.
167
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
168
+ # @return [T.nilable(String)] The highest version, or nil if no versions are available.
169
+ sig do
170
+ params(
171
+ constraint_expression: T.nilable(String),
172
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
173
+ )
174
+ .returns(T.nilable(String))
175
+ end
176
+ def self.find_highest_version_from_constraint_expression(constraint_expression, dependabot_versions = nil)
177
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
178
+
179
+ return nil unless parsed_constraints
180
+
181
+ parsed_constraints
182
+ .filter_map { |parsed| parsed[:version] } # Extract all versions
183
+ .max_by { |version| Version.new(version) }
184
+ end
185
+
186
+ # Parse all constraints (split by logical OR `||`) and convert to Ruby-compatible constraints.
187
+ # Return:
188
+ # - `nil` if the constraint expression is invalid
189
+ # - `[]` if the constraint expression is valid but represents "no constraints"
190
+ # - An array of hashes for valid constraints with details about the constraint and version
191
+ sig do
192
+ params(
193
+ constraint_expression: T.nilable(String),
194
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
195
+ )
196
+ .returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
197
+ end
198
+ def self.parse_constraints(constraint_expression, dependabot_versions = nil)
199
+ splitted_constraints = split_constraints(constraint_expression)
200
+
201
+ return unless splitted_constraints
202
+
203
+ constraints = to_ruby_constraints_with_versions(splitted_constraints, dependabot_versions)
204
+ constraints
205
+ end
206
+
207
+ sig do
208
+ params(
209
+ constraints: T::Array[String],
210
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
211
+ ).returns(T::Array[T::Hash[Symbol, T.nilable(String)]])
212
+ end
213
+ def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
214
+ constraints.filter_map do |constraint|
215
+ parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
216
+ parsed if parsed
217
+ end.uniq
218
+ end
219
+
220
+ # rubocop:disable Metrics/MethodLength
221
+ # rubocop:disable Metrics/PerceivedComplexity
222
+ # rubocop:disable Metrics/AbcSize
223
+ # rubocop:disable Metrics/CyclomaticComplexity
224
+ # Converts a semver constraint to a Ruby-compatible constraint and extracts the version, if available.
225
+ # @param constraint [String] The semver constraint to parse.
226
+ # @return [T.nilable(T::Hash[Symbol, T.nilable(String)])] Returns the Ruby-compatible constraint and the version,
227
+ # if available, or nil if the constraint is invalid.
228
+ #
229
+ # @example
230
+ # to_ruby_constraint_with_version("=1.2.3") # => { constraint: "=1.2.3", version: "1.2.3" }
231
+ # to_ruby_constraint_with_version("^1.2.3") # => { constraint: ">=1.2.3 <2.0.0", version: "1.2.3" }
232
+ # to_ruby_constraint_with_version("*") # => { constraint: nil, version: nil }
233
+ # to_ruby_constraint_with_version("invalid") # => nil
234
+ sig do
235
+ params(
236
+ constraint: String,
237
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
238
+ )
239
+ .returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
240
+ end
241
+ def self.to_ruby_constraint_with_version(constraint, dependabot_versions = [])
242
+ return nil if constraint.empty?
243
+
244
+ case constraint
245
+ when EXACT_CONSTRAINT_REGEX # Exact version, e.g., "1.2.3-alpha"
246
+ return unless Regexp.last_match
247
+
248
+ full_version = Regexp.last_match(1)
249
+ { constraint: "=#{full_version}", version: full_version }
250
+ when CARET_CONSTRAINT_REGEX # Caret constraint, e.g., "^1.2.3"
251
+ return unless Regexp.last_match
252
+
253
+ full_version = Regexp.last_match(1)
254
+ _, major, minor = version_components(full_version)
255
+ return nil if major.nil?
256
+
257
+ ruby_constraint =
258
+ if major.to_i.zero?
259
+ minor.nil? ? ">=#{full_version} <1.0.0" : ">=#{full_version} <0.#{minor.to_i + 1}.0"
260
+ else
261
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
262
+ end
263
+ { constraint: ruby_constraint, version: full_version }
264
+ when TILDE_CONSTRAINT_REGEX # Tilde constraint, e.g., "~1.2.3"
265
+ return unless Regexp.last_match
266
+
267
+ full_version = Regexp.last_match(1)
268
+ _, major, minor = version_components(full_version)
269
+ ruby_constraint =
270
+ if minor.nil?
271
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
272
+ else
273
+ ">=#{full_version} <#{major}.#{minor.to_i + 1}.0"
274
+ end
275
+ { constraint: ruby_constraint, version: full_version }
276
+ when GREATER_THAN_EQUAL_REGEX # Greater than or equal, e.g., ">=1.2.3"
277
+
278
+ return unless Regexp.last_match && Regexp.last_match(1)
279
+
280
+ found_version = highest_matching_version(
281
+ dependabot_versions,
282
+ T.must(Regexp.last_match(1))
283
+ ) do |version, constraint_version|
284
+ version >= Version.new(constraint_version)
285
+ end
286
+ { constraint: ">=#{Regexp.last_match(1)}", version: found_version&.to_s }
287
+ when LESS_THAN_EQUAL_REGEX # Less than or equal, e.g., "<=1.2.3"
288
+ return unless Regexp.last_match
289
+
290
+ full_version = Regexp.last_match(1)
291
+ { constraint: "<=#{full_version}", version: full_version }
292
+ when GREATER_THAN_REGEX # Greater than, e.g., ">1.2.3"
293
+ return unless Regexp.last_match && Regexp.last_match(1)
294
+
295
+ found_version = highest_matching_version(
296
+ dependabot_versions,
297
+ T.must(Regexp.last_match(1))
298
+ ) do |version, constraint_version|
299
+ version > Version.new(constraint_version)
300
+ end
301
+ { constraint: ">#{Regexp.last_match(1)}", version: found_version&.to_s }
302
+ when LESS_THAN_REGEX # Less than, e.g., "<1.2.3"
303
+ return unless Regexp.last_match && Regexp.last_match(1)
304
+
305
+ found_version = highest_matching_version(
306
+ dependabot_versions,
307
+ T.must(Regexp.last_match(1))
308
+ ) do |version, constraint_version|
309
+ version < Version.new(constraint_version)
310
+ end
311
+ { constraint: "<#{Regexp.last_match(1)}", version: found_version&.to_s }
312
+ when WILDCARD_REGEX # No specific constraint, resolves to the highest available version
313
+ { constraint: nil, version: dependabot_versions&.max&.to_s }
314
+ when LATEST_REGEX
315
+ { constraint: nil, version: dependabot_versions&.max&.to_s } # Resolves to the latest available version
316
+ end
317
+ end
318
+
319
+ sig do
320
+ params(
321
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version]),
322
+ constraint_version: String,
323
+ condition: T.proc.params(version: Dependabot::Version, constraint: Dependabot::Version).returns(T::Boolean)
324
+ )
325
+ .returns(T.nilable(Dependabot::Version))
326
+ end
327
+ def self.highest_matching_version(dependabot_versions, constraint_version, &condition)
328
+ return unless dependabot_versions&.any?
329
+
330
+ # Returns the highest version that satisfies the condition, or nil if none.
331
+ dependabot_versions
332
+ .sort
333
+ .reverse
334
+ .find { |version| condition.call(version, Version.new(constraint_version)) } # rubocop:disable Performance/RedundantBlockCall
335
+ end
336
+
337
+ # rubocop:enable Metrics/MethodLength
338
+ # rubocop:enable Metrics/PerceivedComplexity
339
+ # rubocop:enable Metrics/AbcSize
340
+ # rubocop:enable Metrics/CyclomaticComplexity
341
+
342
+ # Parses a semantic version string into its components as per the SemVer spec
343
+ # Example: "1.2.3-alpha+001" → ["1.2.3", "1", "2", "3", "alpha", "001"]
344
+ sig { params(full_version: T.nilable(String)).returns(T.nilable(T::Array[String])) }
345
+ def self.version_components(full_version)
346
+ return [] if full_version.nil?
347
+
348
+ match = full_version.match(SEMVER_VALIDATION_REGEX)
349
+ return [] unless match
350
+
351
+ version = match[:version]
352
+ return [] unless version
353
+
354
+ major, minor, patch = version.split(".")
355
+ [version, major, minor, patch, match[:prerelease], match[:build]].compact
356
+ end
357
+ end
358
+ end
359
+ end
@@ -342,6 +342,11 @@ module Dependabot
342
342
  fetch_support_file(PNPMPackageManager::PNPM_WS_YML_FILENAME),
343
343
  T.nilable(DependencyFile)
344
344
  )
345
+
346
+ # Only fetch from parent directories if the file wasn't found initially
347
+ @pnpm_workspace_yaml ||= fetch_file_from_parent_directories(PNPMPackageManager::PNPM_WS_YML_FILENAME)
348
+
349
+ @pnpm_workspace_yaml
345
350
  end
346
351
 
347
352
  sig { returns(T.nilable(DependencyFile)) }
@@ -31,7 +31,6 @@ module Dependabot
31
31
  version = content["lockfileVersion"]
32
32
  raise_invalid!("expected 'lockfileVersion' to be an integer") unless version.is_a?(Integer)
33
33
  raise_invalid!("expected 'lockfileVersion' to be >= 0") unless version >= 0
34
- raise_invalid!("unsupported 'lockfileVersion' = #{version}") unless version.zero?
35
34
 
36
35
  T.let(content, T.untyped)
37
36
  end
@@ -55,10 +55,11 @@ module Dependabot
55
55
  end
56
56
 
57
57
  sig { override.returns(T::Array[Dependency]) }
58
- def parse
58
+ def parse # rubocop:disable Metrics/PerceivedComplexity
59
59
  dependency_set = DependencySet.new
60
60
  dependency_set += manifest_dependencies
61
61
  dependency_set += lockfile_dependencies
62
+ dependency_set += workspace_catalog_dependencies if pnpm_workspace_yml
62
63
 
63
64
  dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
64
65
 
@@ -168,6 +169,13 @@ module Dependabot
168
169
  end, T.nilable(Dependabot::DependencyFile))
169
170
  end
170
171
 
172
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
173
+ def pnpm_workspace_yml
174
+ @pnpm_workspace_yml ||= T.let(dependency_files.find do |f|
175
+ f.name.end_with?(PNPMPackageManager::PNPM_WS_YML_FILENAME)
176
+ end, T.nilable(Dependabot::DependencyFile))
177
+ end
178
+
171
179
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
172
180
  def bun_lock
173
181
  @bun_lock ||= T.let(dependency_files.find do |f|
@@ -225,6 +233,30 @@ module Dependabot
225
233
  dependency_set
226
234
  end
227
235
 
236
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
237
+ def workspace_catalog_dependencies
238
+ dependency_set = DependencySet.new
239
+ workspace_config = YAML.safe_load(T.must(pnpm_workspace_yml&.content), aliases: true)
240
+
241
+ workspace_config["catalog"]&.each do |name, version|
242
+ dep = build_dependency(
243
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
244
+ )
245
+ dependency_set << dep if dep
246
+ end
247
+
248
+ workspace_config["catalogs"]&.each do |_, group_depenencies|
249
+ group_depenencies.each do |name, version|
250
+ dep = build_dependency(
251
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
252
+ )
253
+ dependency_set << dep if dep
254
+ end
255
+ end
256
+
257
+ dependency_set
258
+ end
259
+
228
260
  sig { returns(LockfileParser) }
229
261
  def lockfile_parser
230
262
  @lockfile_parser ||= T.let(LockfileParser.new(
@@ -11,11 +11,15 @@ module Dependabot
11
11
  class PackageJsonUpdater
12
12
  extend T::Sig
13
13
 
14
+ LOCAL_PACKAGE = T.let([/portal:/, /file:/].freeze, T::Array[Regexp])
15
+
16
+ PATCH_PACKAGE = T.let([/patch:/].freeze, T::Array[Regexp])
17
+
14
18
  sig do
15
19
  params(
16
20
  package_json: Dependabot::DependencyFile,
17
21
  dependencies: T::Array[Dependabot::Dependency]
18
- ) .void
22
+ ).void
19
23
  end
20
24
  def initialize(package_json:, dependencies:)
21
25
  @package_json = package_json
@@ -37,8 +41,13 @@ module Dependabot
37
41
  sig { returns(T::Array[Dependabot::Dependency]) }
38
42
  attr_reader :dependencies
39
43
 
44
+ # rubocop:disable Metrics/PerceivedComplexity
45
+
40
46
  sig { returns(T.nilable(String)) }
41
47
  def updated_package_json_content
48
+ # checks if we are updating single dependency in package.json
49
+ unique_deps_count = dependencies.map(&:name).to_a.uniq.compact.length
50
+
42
51
  dependencies.reduce(package_json.content.dup) do |content, dep|
43
52
  updated_requirements(dep)&.each do |new_req|
44
53
  old_req = old_requirement(dep, new_req)
@@ -50,7 +59,25 @@ module Dependabot
50
59
  new_req: new_req
51
60
  )
52
61
 
53
- raise "Expected content to change!" if content == new_content
62
+ if Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) &&
63
+ (content == new_content && unique_deps_count > 1)
64
+
65
+ # (we observed that) package.json does not always contains the same dependencies compared to
66
+ # "dependencies" list, for example, dependencies object can contain same name dependency "dep"=> "1.0.0"
67
+ # and "dev" => "1.0.1" while package.json can only contain "dep" => "1.0.0",the other dependency is
68
+ # not present in package.json so we don't have to update it, this is most likely (as observed)
69
+ # a transitive dependency which only needs update in lockfile, So we avoid throwing exception and let
70
+ # the update continue.
71
+
72
+ Dependabot.logger.info("experiment: avoid_duplicate_updates_package_json.
73
+ Updating package.json for #{dep.name} ")
74
+
75
+ raise "Expected content to change!"
76
+ end
77
+
78
+ if !Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) && (content == new_content)
79
+ raise "Expected content to change!"
80
+ end
54
81
 
55
82
  content = new_content
56
83
  end
@@ -69,7 +96,7 @@ module Dependabot
69
96
  content
70
97
  end
71
98
  end
72
-
99
+ # rubocop:enable Metrics/PerceivedComplexity
73
100
  sig do
74
101
  params(
75
102
  dependency: Dependabot::Dependency,
@@ -92,6 +119,8 @@ module Dependabot
92
119
  def updated_requirements(dependency)
93
120
  return unless dependency.previous_requirements
94
121
 
122
+ preliminary_check_for_update(dependency)
123
+
95
124
  updated_requirement_pairs =
96
125
  dependency.requirements.zip(T.must(dependency.previous_requirements))
97
126
  .reject do |new_req, old_req|
@@ -318,6 +347,31 @@ module Dependabot
318
347
 
319
348
  0
320
349
  end
350
+
351
+ sig { params(dependency: Dependabot::Dependency).void }
352
+ def preliminary_check_for_update(dependency)
353
+ T.must(dependency.previous_requirements).each do |req, _dep|
354
+ next if req.fetch(:requirement).nil?
355
+
356
+ # some deps are patched with local patches, we don't need to update them
357
+ if req.fetch(:requirement).match?(Regexp.union(PATCH_PACKAGE))
358
+ Dependabot.logger.info("Func: updated_requirements. dependency patched #{dependency.name}," \
359
+ " Requirement: '#{req.fetch(:requirement)}'")
360
+
361
+ raise DependencyFileNotResolvable,
362
+ "Dependency is patched locally, Update not required."
363
+ end
364
+
365
+ # some deps are added as local packages, we don't need to update them as they are referred to a local path
366
+ next unless req.fetch(:requirement).match?(Regexp.union(LOCAL_PACKAGE))
367
+
368
+ Dependabot.logger.info("Func: updated_requirements. local package #{dependency.name}," \
369
+ " Requirement: '#{req.fetch(:requirement)}'")
370
+
371
+ raise DependencyFileNotResolvable,
372
+ "Local package, Update not required."
373
+ end
374
+ end
321
375
  end
322
376
  end
323
377
  end
@@ -24,11 +24,14 @@ module Dependabot
24
24
  )
25
25
  end
26
26
 
27
- def updated_pnpm_lock_content(pnpm_lock)
27
+ def updated_pnpm_lock_content(pnpm_lock, updated_pnpm_workspace_content: nil)
28
28
  @updated_pnpm_lock_content ||= {}
29
29
  return @updated_pnpm_lock_content[pnpm_lock.name] if @updated_pnpm_lock_content[pnpm_lock.name]
30
30
 
31
- new_content = run_pnpm_update(pnpm_lock: pnpm_lock)
31
+ new_content = run_pnpm_update(
32
+ pnpm_lock: pnpm_lock,
33
+ updated_pnpm_workspace_content: updated_pnpm_workspace_content
34
+ )
32
35
  @updated_pnpm_lock_content[pnpm_lock.name] = new_content
33
36
  rescue SharedHelpers::HelperSubprocessFailed => e
34
37
  handle_pnpm_lock_updater_error(e, pnpm_lock)
@@ -100,14 +103,17 @@ module Dependabot
100
103
  # Peer dependencies configuration error
101
104
  ERR_PNPM_PEER_DEP_ISSUES = /ERR_PNPM_PEER_DEP_ISSUES/
102
105
 
103
- def run_pnpm_update(pnpm_lock:)
106
+ def run_pnpm_update(pnpm_lock:, updated_pnpm_workspace_content: nil)
104
107
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
105
108
  File.write(".npmrc", npmrc_content(pnpm_lock))
106
109
 
107
110
  SharedHelpers.with_git_configured(credentials: credentials) do
108
- run_pnpm_update_packages
109
-
110
- write_final_package_json_files
111
+ if updated_pnpm_workspace_content
112
+ File.write("pnpm-workspace.yaml", updated_pnpm_workspace_content["pnpm-workspace.yaml"])
113
+ else
114
+ run_pnpm_update_packages
115
+ write_final_package_json_files
116
+ end
111
117
 
112
118
  run_pnpm_install
113
119
 
@@ -140,11 +146,15 @@ module Dependabot
140
146
  )
141
147
  end
142
148
 
149
+ def workspace_files
150
+ @workspace_files ||= dependency_files.select { |f| f.name.end_with?("pnpm-workspace.yaml") }
151
+ end
152
+
143
153
  def lockfile_dependencies(lockfile)
144
154
  @lockfile_dependencies ||= {}
145
155
  @lockfile_dependencies[lockfile.name] ||=
146
156
  NpmAndYarn::FileParser.new(
147
- dependency_files: [lockfile, *package_files],
157
+ dependency_files: [lockfile, *package_files, *workspace_files],
148
158
  source: nil,
149
159
  credentials: credentials
150
160
  ).parse