dependabot-npm_and_yarn 0.293.0 → 0.295.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d548c0891264c8b407f2b673e39266b3494683e431ac1e8b9ebc899319208f5
4
- data.tar.gz: b43aeb89fb1a1c1e32a9d86eee5ccc188d8fc3da548ee4cbbcfbc4c76cfd59c7
3
+ metadata.gz: 14edb941111a95dc07d83e6449c01bd3b0f4b92c4e3168acd19d31ce1fa96183
4
+ data.tar.gz: 0ec9a05bb2ebda169ad58028bfc2c0465ef0bbf192265f317570842f5aaa81b7
5
5
  SHA512:
6
- metadata.gz: 95eef6390619686a8017fc949e5a5609e34f3675920fea726975fb42c55904e9901ef55b80df005ffa43bf74e87623ed00a9a287005af9446c93b78fcb0c010d
7
- data.tar.gz: 9a104350702acef102f005bdc0c4649b7713288de3685992538f9e58384487802f48a1f1c6e57dd6ac244fb11e849fd89b72f2f3648355eb3115d1174846e21f
6
+ metadata.gz: 5822f02ef7a153c079c70f91724efe53250ea696af5c0a995cfb86537be568e155344eb9cbb1481c2f2be01c080d486a0b98300dd318aa72c6271b36bd0d906e
7
+ data.tar.gz: 85bf7be8bb171f92b38a67d7c0b12b6c0e68056a6cdf09a0b20d8c2054517276e614e1bc7fd01bc366a53e917ba54c032ee816c977d47ca4eb11c22afda6c2ee
@@ -97,9 +97,9 @@ async function findVulnerableDependencies(directory, advisories) {
97
97
 
98
98
  for (const group of groupedFixUpdateChains.values()) {
99
99
  const fixUpdateNode = group[0].nodes[0]
100
- const groupTopLevelAncestors = group.reduce((anc, chain) => {
100
+ const groupTopLevelAncestors = group.reduce((ancestor, chain) => {
101
101
  const topLevelNode = chain.nodes[chain.nodes.length - 1]
102
- return anc.add(topLevelNode.name)
102
+ return ancestor.add(topLevelNode.name)
103
103
  }, new Set())
104
104
 
105
105
  // Add group's top-level ancestors to the set of all top-level ancestors of
@@ -269,23 +269,23 @@ const maybeReadFile = file => {
269
269
  }
270
270
 
271
271
  function loadCACerts(npmConfig) {
272
- if (npmConfig.ca) {
273
- return npmConfig.ca
274
- }
272
+ if (npmConfig.ca) {
273
+ return npmConfig.ca
274
+ }
275
275
 
276
- if (!npmConfig.cafile) {
277
- return
278
- }
276
+ if (!npmConfig.cafile) {
277
+ return
278
+ }
279
279
 
280
- const raw = maybeReadFile(npmConfig.cafile)
281
- if (!raw) {
282
- return
283
- }
280
+ const raw = maybeReadFile(npmConfig.cafile)
281
+ if (!raw) {
282
+ return
283
+ }
284
284
 
285
- const delim = '-----END CERTIFICATE-----'
286
- return raw.replace(/\r\n/g, '\n').split(delim)
287
- .filter(section => section.trim())
288
- .map(section => section.trimStart() + delim)
285
+ const delim = '-----END CERTIFICATE-----'
286
+ return raw.replace(/\r\n/g, '\n').split(delim)
287
+ .filter(section => section.trim())
288
+ .map(section => section.trimStart() + delim)
289
289
  }
290
290
 
291
291
  module.exports = { findVulnerableDependencies }
@@ -113,7 +113,7 @@ function flattenAllDependencies(manifest) {
113
113
  );
114
114
  }
115
115
 
116
- // NOTE: Re-used in npm 7 updater
116
+ // NOTE: Reused in npm 7 updater
117
117
  function installArgs(
118
118
  depName,
119
119
  desiredVersion,
@@ -39,7 +39,7 @@ module Dependabot
39
39
 
40
40
  sig { override.returns(T::Boolean) }
41
41
  def unsupported?
42
- supported_versions.all? { |supported| supported > version }
42
+ false
43
43
  end
44
44
  end
45
45
  end
@@ -0,0 +1,306 @@
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
+ INVALID = "invalid" # Invalid constraint
12
+ # Regex Components for Semantic Versioning
13
+ DIGIT = "\\d+" # Matches a single number (e.g., "1")
14
+ PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
15
+ BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
16
+ DOT = "\\." # Matches a literal dot "."
17
+
18
+ # Matches semantic versions:
19
+ VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
20
+
21
+ VERSION_REGEX = T.let(/\A#{VERSION}\z/o, Regexp)
22
+
23
+ # SemVer regex: major.minor.patch[-prerelease][+build]
24
+ SEMVER_REGEX = /^(?<version>\d+\.\d+\.\d+)(?:-(?<prerelease>[a-zA-Z0-9.-]+))?(?:\+(?<build>[a-zA-Z0-9.-]+))?$/
25
+
26
+ # Constraint Types as Constants
27
+ CARET_CONSTRAINT_REGEX = T.let(/^\^(#{VERSION})$/, Regexp)
28
+ TILDE_CONSTRAINT_REGEX = T.let(/^~(#{VERSION})$/, Regexp)
29
+ EXACT_CONSTRAINT_REGEX = T.let(/^(#{VERSION})$/, Regexp)
30
+ GREATER_THAN_EQUAL_REGEX = T.let(/^>=(#{VERSION})$/, Regexp)
31
+ LESS_THAN_EQUAL_REGEX = T.let(/^<=(#{VERSION})$/, Regexp)
32
+ GREATER_THAN_REGEX = T.let(/^>(#{VERSION})$/, Regexp)
33
+ LESS_THAN_REGEX = T.let(/^<(#{VERSION})$/, Regexp)
34
+ WILDCARD_REGEX = T.let(/^\*$/, Regexp)
35
+
36
+ # Unified Regex for Valid Constraints
37
+ VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
38
+ CARET_CONSTRAINT_REGEX,
39
+ TILDE_CONSTRAINT_REGEX,
40
+ EXACT_CONSTRAINT_REGEX,
41
+ GREATER_THAN_EQUAL_REGEX,
42
+ LESS_THAN_EQUAL_REGEX,
43
+ GREATER_THAN_REGEX,
44
+ LESS_THAN_REGEX,
45
+ WILDCARD_REGEX
46
+ ).freeze, Regexp)
47
+
48
+ # Validates if the provided semver constraint expression from a `package.json` is valid.
49
+ # A valid semver constraint expression in `package.json` can consist of multiple groups
50
+ # separated by logical OR (`||`). Within each group, space-separated constraints are treated
51
+ # as logical AND. Each individual constraint must conform to the semver rules defined in
52
+ # `VALID_CONSTRAINT_REGEX`.
53
+ #
54
+ # Example (valid `package.json` semver constraints):
55
+ # ">=1.2.3 <2.0.0 || ~3.4.5" → Valid (space-separated constraints are AND, `||` is OR)
56
+ # "^1.0.0 || >=2.0.0 <3.0.0" → Valid (caret and range constraints combined)
57
+ # "1.2.3" → Valid (exact version)
58
+ # "*" → Valid (wildcard allows any version)
59
+ #
60
+ # Example (invalid `package.json` semver constraints):
61
+ # ">=1.2.3 && <2.0.0" → Invalid (`&&` is not valid in semver)
62
+ # ">=x.y.z" → Invalid (non-numeric version parts are not valid)
63
+ # "1.2.3 ||" → Invalid (trailing OR operator)
64
+ #
65
+ # @param constraint_expression [String] The semver constraint expression from `package.json` to validate.
66
+ # @return [T::Boolean] Returns true if the constraint expression is valid semver, false otherwise.
67
+ sig { params(constraint_expression: T.nilable(String)).returns(T::Boolean) }
68
+ def self.valid_constraint_expression?(constraint_expression)
69
+ normalized_constraint = constraint_expression&.strip
70
+
71
+ # Treat nil or empty input as valid (no constraints)
72
+ return true if normalized_constraint.nil? || normalized_constraint.empty?
73
+
74
+ # Split the expression by logical OR (`||`) into groups
75
+ normalized_constraint.split("||").reject(&:empty?).all? do |or_group|
76
+ or_group.split(/\s+/).reject(&:empty?).all? do |and_constraint|
77
+ and_constraint.match?(VALID_CONSTRAINT_REGEX)
78
+ end
79
+ end
80
+ end
81
+
82
+ # Extract unique constraints from the given constraint expression.
83
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
84
+ # @return [T::Array[String]] The list of unique Ruby-compatible constraints.
85
+ sig do
86
+ params(
87
+ constraint_expression: T.nilable(String),
88
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
89
+ )
90
+ .returns(T.nilable(T::Array[String]))
91
+ end
92
+ def self.extract_constraints(constraint_expression, dependabot_versions = nil)
93
+ normalized_constraint = constraint_expression&.strip
94
+ return [] if normalized_constraint.nil? || normalized_constraint.empty?
95
+
96
+ parsed_constraints = parse_constraints(normalized_constraint, dependabot_versions)
97
+
98
+ return nil unless parsed_constraints
99
+
100
+ parsed_constraints.filter_map { |parsed| parsed[:constraint] }
101
+ end
102
+
103
+ # Find the highest version from the given constraint expression.
104
+ # @param constraint_expression [T.nilable(String)] The semver constraint expression.
105
+ # @return [T.nilable(String)] The highest version, or nil if no versions are available.
106
+ sig do
107
+ params(
108
+ constraint_expression: T.nilable(String),
109
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
110
+ )
111
+ .returns(T.nilable(String))
112
+ end
113
+ def self.find_highest_version_from_constraint_expression(constraint_expression, dependabot_versions = nil)
114
+ normalized_constraint = constraint_expression&.strip
115
+ return nil if normalized_constraint.nil? || normalized_constraint.empty?
116
+
117
+ parsed_constraints = parse_constraints(normalized_constraint, dependabot_versions)
118
+
119
+ return nil unless parsed_constraints
120
+
121
+ parsed_constraints
122
+ .filter_map { |parsed| parsed[:version] } # Extract all versions
123
+ .max_by { |version| Version.new(version) }
124
+ end
125
+
126
+ # Parse all constraints (split by logical OR `||`) and convert to Ruby-compatible constraints.
127
+ # Return:
128
+ # - `nil` if the constraint expression is invalid
129
+ # - `[]` if the constraint expression is valid but represents "no constraints"
130
+ # - An array of hashes for valid constraints with details about the constraint and version
131
+ sig do
132
+ params(
133
+ constraint_expression: T.nilable(String),
134
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
135
+ )
136
+ .returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
137
+ end
138
+ def self.parse_constraints(constraint_expression, dependabot_versions = nil)
139
+ normalized_constraint = constraint_expression&.strip
140
+
141
+ # Return an empty array for valid "no constraints" (nil or empty input)
142
+ return [] if normalized_constraint.nil? || normalized_constraint.empty?
143
+
144
+ # Return nil for invalid constraints
145
+ return nil unless valid_constraint_expression?(normalized_constraint)
146
+
147
+ # Parse valid constraints
148
+ constraints = normalized_constraint.split("||").flat_map do |or_group|
149
+ or_group.strip.split(/\s+/).map(&:strip)
150
+ end.then do |normalized_constraints| # rubocop:disable Style/MultilineBlockChain
151
+ to_ruby_constraints_with_versions(normalized_constraints, dependabot_versions)
152
+ end.uniq { |parsed| parsed[:constraint] } # Ensure uniqueness based on `:constraint` # rubocop:disable Style/MultilineBlockChain
153
+ constraints
154
+ end
155
+
156
+ sig do
157
+ params(
158
+ constraints: T::Array[String],
159
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
160
+ ).returns(T::Array[T::Hash[Symbol, T.nilable(String)]])
161
+ end
162
+ def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
163
+ constraints.filter_map do |constraint|
164
+ parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
165
+ parsed if parsed && parsed[:constraint] # Only include valid constraints
166
+ end.uniq
167
+ end
168
+
169
+ # rubocop:disable Metrics/MethodLength
170
+ # rubocop:disable Metrics/PerceivedComplexity
171
+ # rubocop:disable Metrics/AbcSize
172
+ # rubocop:disable Metrics/CyclomaticComplexity
173
+ # Converts a semver constraint to a Ruby-compatible constraint and extracts the version, if available.
174
+ # @param constraint [String] The semver constraint to parse.
175
+ # @return [T.nilable(T::Hash[Symbol, T.nilable(String)])] Returns the Ruby-compatible constraint and the version,
176
+ # if available, or nil if the constraint is invalid.
177
+ #
178
+ # @example
179
+ # to_ruby_constraint_with_version("=1.2.3") # => { constraint: "=1.2.3", version: "1.2.3" }
180
+ # to_ruby_constraint_with_version("^1.2.3") # => { constraint: ">=1.2.3 <2.0.0", version: "1.2.3" }
181
+ # to_ruby_constraint_with_version("*") # => { constraint: nil, version: nil }
182
+ # to_ruby_constraint_with_version("invalid") # => nil
183
+ sig do
184
+ params(
185
+ constraint: String,
186
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version])
187
+ )
188
+ .returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
189
+ end
190
+ def self.to_ruby_constraint_with_version(constraint, dependabot_versions = [])
191
+ return nil if constraint.empty?
192
+
193
+ case constraint
194
+ when EXACT_CONSTRAINT_REGEX # Exact version, e.g., "1.2.3-alpha"
195
+ return unless Regexp.last_match
196
+
197
+ full_version = Regexp.last_match(1)
198
+ { constraint: "=#{full_version}", version: full_version }
199
+ when CARET_CONSTRAINT_REGEX # Caret constraint, e.g., "^1.2.3"
200
+ return unless Regexp.last_match
201
+
202
+ full_version = Regexp.last_match(1)
203
+ _, major, minor = version_components(full_version)
204
+ return nil if major.nil?
205
+
206
+ ruby_constraint =
207
+ if major.to_i.zero?
208
+ minor.nil? ? ">=#{full_version} <1.0.0" : ">=#{full_version} <0.#{minor.to_i + 1}.0"
209
+ else
210
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
211
+ end
212
+ { constraint: ruby_constraint, version: full_version }
213
+ when TILDE_CONSTRAINT_REGEX # Tilde constraint, e.g., "~1.2.3"
214
+ return unless Regexp.last_match
215
+
216
+ full_version = Regexp.last_match(1)
217
+ _, major, minor = version_components(full_version)
218
+ ruby_constraint =
219
+ if minor.nil?
220
+ ">=#{full_version} <#{major.to_i + 1}.0.0"
221
+ else
222
+ ">=#{full_version} <#{major}.#{minor.to_i + 1}.0"
223
+ end
224
+ { constraint: ruby_constraint, version: full_version }
225
+ when GREATER_THAN_EQUAL_REGEX # Greater than or equal, e.g., ">=1.2.3"
226
+
227
+ return unless Regexp.last_match && Regexp.last_match(1)
228
+
229
+ found_version = highest_matching_version(
230
+ dependabot_versions,
231
+ T.must(Regexp.last_match(1))
232
+ ) do |version, constraint_version|
233
+ version >= Version.new(constraint_version)
234
+ end
235
+ { constraint: ">=#{Regexp.last_match(1)}", version: found_version&.to_s }
236
+ when LESS_THAN_EQUAL_REGEX # Less than or equal, e.g., "<=1.2.3"
237
+ return unless Regexp.last_match
238
+
239
+ full_version = Regexp.last_match(1)
240
+ { constraint: "<=#{full_version}", version: full_version }
241
+ when GREATER_THAN_REGEX # Greater than, e.g., ">1.2.3"
242
+ return unless Regexp.last_match && Regexp.last_match(1)
243
+
244
+ found_version = highest_matching_version(
245
+ dependabot_versions,
246
+ T.must(Regexp.last_match(1))
247
+ ) do |version, constraint_version|
248
+ version > Version.new(constraint_version)
249
+ end
250
+ { constraint: ">#{Regexp.last_match(1)}", version: found_version&.to_s }
251
+ when LESS_THAN_REGEX # Less than, e.g., "<1.2.3"
252
+ return unless Regexp.last_match && Regexp.last_match(1)
253
+
254
+ found_version = highest_matching_version(
255
+ dependabot_versions,
256
+ T.must(Regexp.last_match(1))
257
+ ) do |version, constraint_version|
258
+ version < Version.new(constraint_version)
259
+ end
260
+ { constraint: "<#{Regexp.last_match(1)}", version: found_version&.to_s }
261
+ when WILDCARD_REGEX # Wildcard
262
+ { constraint: nil, version: dependabot_versions&.max&.to_s } # Explicitly valid but no specific constraint
263
+ end
264
+ end
265
+
266
+ sig do
267
+ params(
268
+ dependabot_versions: T.nilable(T::Array[Dependabot::Version]),
269
+ constraint_version: String,
270
+ condition: T.proc.params(version: Dependabot::Version, constraint: Dependabot::Version).returns(T::Boolean)
271
+ )
272
+ .returns(T.nilable(Dependabot::Version))
273
+ end
274
+ def self.highest_matching_version(dependabot_versions, constraint_version, &condition)
275
+ return unless dependabot_versions&.any?
276
+
277
+ # Returns the highest version that satisfies the condition, or nil if none.
278
+ dependabot_versions
279
+ .sort
280
+ .reverse
281
+ .find { |version| condition.call(version, Version.new(constraint_version)) } # rubocop:disable Performance/RedundantBlockCall
282
+ end
283
+
284
+ # rubocop:enable Metrics/MethodLength
285
+ # rubocop:enable Metrics/PerceivedComplexity
286
+ # rubocop:enable Metrics/AbcSize
287
+ # rubocop:enable Metrics/CyclomaticComplexity
288
+
289
+ # Parses a semantic version string into its components as per the SemVer spec
290
+ # Example: "1.2.3-alpha+001" → ["1.2.3", "1", "2", "3", "alpha", "001"]
291
+ sig { params(full_version: T.nilable(String)).returns(T.nilable(T::Array[String])) }
292
+ def self.version_components(full_version)
293
+ return [] if full_version.nil?
294
+
295
+ match = full_version.match(SEMVER_REGEX)
296
+ return [] unless match
297
+
298
+ version = match[:version]
299
+ return [] unless version
300
+
301
+ major, minor, patch = version.split(".")
302
+ [version, major, minor, patch, match[:prerelease], match[:build]].compact
303
+ end
304
+ end
305
+ end
306
+ end
@@ -213,7 +213,7 @@ module Dependabot
213
213
 
214
214
  sig { returns(T.nilable(T.any(Integer, String))) }
215
215
  def bun_version
216
- return @bun_version = nil unless Experiments.enabled?(:bun_updates)
216
+ return @bun_version = nil unless allow_beta_ecosystems?
217
217
 
218
218
  @bun_version ||= T.let(
219
219
  package_manager_helper.setup(BunPackageManager::NAME),
@@ -453,6 +453,15 @@ module Dependabot
453
453
 
454
454
  resolution_deps = resolution_objects.flat_map(&:to_a)
455
455
  .map do |path, value|
456
+ # skip dependencies that contain invalid values such as inline comments, null, etc.
457
+
458
+ unless value.is_a?(String)
459
+ Dependabot.logger.warn("File fetcher: Skipping dependency \"#{path}\" " \
460
+ "with value: \"#{value}\"")
461
+
462
+ next
463
+ end
464
+
456
465
  convert_dependency_path_to_name(path, value)
457
466
  end
458
467
 
@@ -645,8 +654,8 @@ module Dependabot
645
654
  def parsed_pnpm_workspace_yaml
646
655
  return {} unless pnpm_workspace_yaml
647
656
 
648
- YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content))
649
- rescue Psych::SyntaxError
657
+ YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content), aliases: true)
658
+ rescue Psych::SyntaxError, Psych::BadAlias
650
659
  raise Dependabot::DependencyFileNotParseable, T.must(pnpm_workspace_yaml).path
651
660
  end
652
661
 
@@ -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 enable_pnpm_workspace_catalog?
62
63
 
63
64
  dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
64
65
 
@@ -93,6 +94,11 @@ module Dependabot
93
94
 
94
95
  private
95
96
 
97
+ sig { returns(T.nilable(T::Boolean)) }
98
+ def enable_pnpm_workspace_catalog?
99
+ pnpm_workspace_yml && Dependabot::Experiments.enabled?(:enable_pnpm_workspace_catalog)
100
+ end
101
+
96
102
  sig { returns(PackageManagerHelper) }
97
103
  def package_manager_helper
98
104
  @package_manager_helper ||= T.let(
@@ -143,56 +149,63 @@ module Dependabot
143
149
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
144
150
  def shrinkwrap
145
151
  @shrinkwrap ||= T.let(dependency_files.find do |f|
146
- f.name == NpmPackageManager::SHRINKWRAP_LOCKFILE_NAME
152
+ f.name.end_with?(NpmPackageManager::SHRINKWRAP_LOCKFILE_NAME)
147
153
  end, T.nilable(Dependabot::DependencyFile))
148
154
  end
149
155
 
150
156
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
151
157
  def package_lock
152
158
  @package_lock ||= T.let(dependency_files.find do |f|
153
- f.name == NpmPackageManager::LOCKFILE_NAME
159
+ f.name.end_with?(NpmPackageManager::LOCKFILE_NAME)
154
160
  end, T.nilable(Dependabot::DependencyFile))
155
161
  end
156
162
 
157
163
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
158
164
  def yarn_lock
159
165
  @yarn_lock ||= T.let(dependency_files.find do |f|
160
- f.name == YarnPackageManager::LOCKFILE_NAME
166
+ f.name.end_with?(YarnPackageManager::LOCKFILE_NAME)
161
167
  end, T.nilable(Dependabot::DependencyFile))
162
168
  end
163
169
 
164
170
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
165
171
  def pnpm_lock
166
172
  @pnpm_lock ||= T.let(dependency_files.find do |f|
167
- f.name == PNPMPackageManager::LOCKFILE_NAME
173
+ f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME)
174
+ end, T.nilable(Dependabot::DependencyFile))
175
+ end
176
+
177
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
178
+ def pnpm_workspace_yml
179
+ @pnpm_workspace_yml ||= T.let(dependency_files.find do |f|
180
+ f.name.end_with?(PNPMPackageManager::PNPM_WS_YML_FILENAME)
168
181
  end, T.nilable(Dependabot::DependencyFile))
169
182
  end
170
183
 
171
184
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
172
185
  def bun_lock
173
186
  @bun_lock ||= T.let(dependency_files.find do |f|
174
- f.name == BunPackageManager::LOCKFILE_NAME
187
+ f.name.end_with?(BunPackageManager::LOCKFILE_NAME)
175
188
  end, T.nilable(Dependabot::DependencyFile))
176
189
  end
177
190
 
178
191
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
179
192
  def npmrc
180
193
  @npmrc ||= T.let(dependency_files.find do |f|
181
- f.name == NpmPackageManager::RC_FILENAME
194
+ f.name.end_with?(NpmPackageManager::RC_FILENAME)
182
195
  end, T.nilable(Dependabot::DependencyFile))
183
196
  end
184
197
 
185
198
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
186
199
  def yarnrc
187
200
  @yarnrc ||= T.let(dependency_files.find do |f|
188
- f.name == YarnPackageManager::RC_FILENAME
201
+ f.name.end_with?(YarnPackageManager::RC_FILENAME)
189
202
  end, T.nilable(Dependabot::DependencyFile))
190
203
  end
191
204
 
192
205
  sig { returns(T.nilable(DependencyFile)) }
193
206
  def yarnrc_yml
194
207
  @yarnrc_yml ||= T.let(dependency_files.find do |f|
195
- f.name == YarnPackageManager::RC_YML_FILENAME
208
+ f.name.end_with?(YarnPackageManager::RC_YML_FILENAME)
196
209
  end, T.nilable(Dependabot::DependencyFile))
197
210
  end
198
211
 
@@ -212,7 +225,7 @@ module Dependabot
212
225
  next unless requirement.is_a?(String)
213
226
 
214
227
  # Skip dependencies using Yarn workspace cross-references as requirements
215
- next if requirement.start_with?("workspace:")
228
+ next if requirement.start_with?("workspace:", "catalog:")
216
229
 
217
230
  requirement = "*" if requirement == ""
218
231
  dep = build_dependency(
@@ -225,6 +238,30 @@ module Dependabot
225
238
  dependency_set
226
239
  end
227
240
 
241
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
242
+ def workspace_catalog_dependencies
243
+ dependency_set = DependencySet.new
244
+ workspace_config = YAML.safe_load(T.must(pnpm_workspace_yml&.content), aliases: true)
245
+
246
+ workspace_config["catalog"]&.each do |name, version|
247
+ dep = build_dependency(
248
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
249
+ )
250
+ dependency_set << dep if dep
251
+ end
252
+
253
+ workspace_config["catalogs"]&.each do |_, group_depenencies|
254
+ group_depenencies.each do |name, version|
255
+ dep = build_dependency(
256
+ file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
257
+ )
258
+ dependency_set << dep if dep
259
+ end
260
+ end
261
+
262
+ dependency_set
263
+ end
264
+
228
265
  sig { returns(LockfileParser) }
229
266
  def lockfile_parser
230
267
  @lockfile_parser ||= T.let(LockfileParser.new(
@@ -37,8 +37,13 @@ module Dependabot
37
37
  sig { returns(T::Array[Dependabot::Dependency]) }
38
38
  attr_reader :dependencies
39
39
 
40
+ # rubocop:disable Metrics/PerceivedComplexity
41
+
40
42
  sig { returns(T.nilable(String)) }
41
43
  def updated_package_json_content
44
+ # checks if we are updating single dependency in package.json
45
+ unique_deps_count = dependencies.map(&:name).to_a.uniq.compact.length
46
+
42
47
  dependencies.reduce(package_json.content.dup) do |content, dep|
43
48
  updated_requirements(dep)&.each do |new_req|
44
49
  old_req = old_requirement(dep, new_req)
@@ -50,7 +55,25 @@ module Dependabot
50
55
  new_req: new_req
51
56
  )
52
57
 
53
- raise "Expected content to change!" if content == new_content
58
+ if Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) &&
59
+ (content == new_content && unique_deps_count > 1)
60
+
61
+ # (we observed that) package.json does not always contains the same dependencies compared to
62
+ # "dependencies" list, for example, dependencies object can contain same name dependency "dep"=> "1.0.0"
63
+ # and "dev" => "1.0.1" while package.json can only contain "dep" => "1.0.0",the other dependency is
64
+ # not present in package.json so we don't have to update it, this is most likely (as observed)
65
+ # a transitive dependency which only needs update in lockfile, So we avoid throwing exception and let
66
+ # the update continue.
67
+
68
+ Dependabot.logger.info("experiment: avoid_duplicate_updates_package_json.
69
+ Updating package.json for #{dep.name} ")
70
+
71
+ raise "Expected content to change!"
72
+ end
73
+
74
+ if !Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) && (content == new_content)
75
+ raise "Expected content to change!"
76
+ end
54
77
 
55
78
  content = new_content
56
79
  end
@@ -69,7 +92,7 @@ module Dependabot
69
92
  content
70
93
  end
71
94
  end
72
-
95
+ # rubocop:enable Metrics/PerceivedComplexity
73
96
  sig do
74
97
  params(
75
98
  dependency: Dependabot::Dependency,