dependabot-npm_and_yarn 0.295.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: 14edb941111a95dc07d83e6449c01bd3b0f4b92c4e3168acd19d31ce1fa96183
4
- data.tar.gz: 0ec9a05bb2ebda169ad58028bfc2c0465ef0bbf192265f317570842f5aaa81b7
3
+ metadata.gz: 40c0445c84d264374459bc6e6d333d051a116ec598ae0e208536123242a2a56b
4
+ data.tar.gz: 208a49c91628dda0ada48108de1ef09b07b8ca7b4f12df008906689e5ad173f9
5
5
  SHA512:
6
- metadata.gz: 5822f02ef7a153c079c70f91724efe53250ea696af5c0a995cfb86537be568e155344eb9cbb1481c2f2be01c080d486a0b98300dd318aa72c6271b36bd0d906e
7
- data.tar.gz: 85bf7be8bb171f92b38a67d7c0b12b6c0e68056a6cdf09a0b20d8c2054517276e614e1bc7fd01bc366a53e917ba54c032ee816c977d47ca4eb11c22afda6c2ee
6
+ metadata.gz: c1a2bc9cfa98144e171f653efedd490ccf0eb02176c29496a5ee288d83a54b83f8390e3a7d67b98fa544ef3f5e4074080101d8115414fdc34cb4c3ec075a2f25
7
+ data.tar.gz: da594bed80a0ad0c2e0fef360bb026f176020c0edc1c665e4e4823f882edd72e85e07a52b5a2036c78620f0889a0e050d0342462b21f6671a8d6daaa5976cbb3
@@ -8,30 +8,51 @@ module Dependabot
8
8
  module ConstraintHelper
9
9
  extend T::Sig
10
10
 
11
- INVALID = "invalid" # Invalid constraint
12
11
  # Regex Components for Semantic Versioning
13
12
  DIGIT = "\\d+" # Matches a single number (e.g., "1")
14
13
  PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
15
14
  BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
16
- DOT = "\\." # Matches a literal dot "."
17
15
 
18
16
  # Matches semantic versions:
19
17
  VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
20
18
 
21
- VERSION_REGEX = T.let(/\A#{VERSION}\z/o, Regexp)
19
+ VERSION_REGEX = T.let(/^#{VERSION}$/, Regexp)
22
20
 
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.-]+))?$/
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 = /^(>=|<=|>|<|~|\^|=)$/
25
44
 
26
45
  # 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)
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)
34
53
  WILDCARD_REGEX = T.let(/^\*$/, Regexp)
54
+ LATEST_REGEX = T.let(/^latest$/, Regexp)
55
+ SEMVER_CONSTANTS = ["*", "latest"].freeze
35
56
 
36
57
  # Unified Regex for Valid Constraints
37
58
  VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
@@ -42,43 +63,10 @@ module Dependabot
42
63
  LESS_THAN_EQUAL_REGEX,
43
64
  GREATER_THAN_REGEX,
44
65
  LESS_THAN_REGEX,
45
- WILDCARD_REGEX
66
+ WILDCARD_REGEX,
67
+ LATEST_REGEX
46
68
  ).freeze, Regexp)
47
69
 
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
70
  # Extract unique constraints from the given constraint expression.
83
71
  # @param constraint_expression [T.nilable(String)] The semver constraint expression.
84
72
  # @return [T::Array[String]] The list of unique Ruby-compatible constraints.
@@ -89,17 +77,92 @@ module Dependabot
89
77
  )
90
78
  .returns(T.nilable(T::Array[String]))
91
79
  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)
80
+ def self.extract_ruby_constraints(constraint_expression, dependabot_versions = nil)
81
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
97
82
 
98
83
  return nil unless parsed_constraints
99
84
 
100
85
  parsed_constraints.filter_map { |parsed| parsed[:constraint] }
101
86
  end
102
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
+
103
166
  # Find the highest version from the given constraint expression.
104
167
  # @param constraint_expression [T.nilable(String)] The semver constraint expression.
105
168
  # @return [T.nilable(String)] The highest version, or nil if no versions are available.
@@ -111,10 +174,7 @@ module Dependabot
111
174
  .returns(T.nilable(String))
112
175
  end
113
176
  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)
177
+ parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
118
178
 
119
179
  return nil unless parsed_constraints
120
180
 
@@ -136,20 +196,11 @@ module Dependabot
136
196
  .returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
137
197
  end
138
198
  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?
199
+ splitted_constraints = split_constraints(constraint_expression)
143
200
 
144
- # Return nil for invalid constraints
145
- return nil unless valid_constraint_expression?(normalized_constraint)
201
+ return unless splitted_constraints
146
202
 
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
203
+ constraints = to_ruby_constraints_with_versions(splitted_constraints, dependabot_versions)
153
204
  constraints
154
205
  end
155
206
 
@@ -162,7 +213,7 @@ module Dependabot
162
213
  def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
163
214
  constraints.filter_map do |constraint|
164
215
  parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
165
- parsed if parsed && parsed[:constraint] # Only include valid constraints
216
+ parsed if parsed
166
217
  end.uniq
167
218
  end
168
219
 
@@ -258,8 +309,10 @@ module Dependabot
258
309
  version < Version.new(constraint_version)
259
310
  end
260
311
  { 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
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
263
316
  end
264
317
  end
265
318
 
@@ -292,7 +345,7 @@ module Dependabot
292
345
  def self.version_components(full_version)
293
346
  return [] if full_version.nil?
294
347
 
295
- match = full_version.match(SEMVER_REGEX)
348
+ match = full_version.match(SEMVER_VALIDATION_REGEX)
296
349
  return [] unless match
297
350
 
298
351
  version = match[:version]
@@ -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)) }
@@ -59,7 +59,7 @@ module Dependabot
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
+ dependency_set += workspace_catalog_dependencies if pnpm_workspace_yml
63
63
 
64
64
  dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
65
65
 
@@ -94,11 +94,6 @@ module Dependabot
94
94
 
95
95
  private
96
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
-
102
97
  sig { returns(PackageManagerHelper) }
103
98
  def package_manager_helper
104
99
  @package_manager_helper ||= T.let(
@@ -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
@@ -115,6 +119,8 @@ module Dependabot
115
119
  def updated_requirements(dependency)
116
120
  return unless dependency.previous_requirements
117
121
 
122
+ preliminary_check_for_update(dependency)
123
+
118
124
  updated_requirement_pairs =
119
125
  dependency.requirements.zip(T.must(dependency.previous_requirements))
120
126
  .reject do |new_req, old_req|
@@ -341,6 +347,31 @@ module Dependabot
341
347
 
342
348
  0
343
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
344
375
  end
345
376
  end
346
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
@@ -50,32 +50,21 @@ module Dependabot
50
50
  ]
51
51
  end
52
52
 
53
- # rubocop:disable Metrics/PerceivedComplexity
54
53
  sig { override.returns(T::Array[DependencyFile]) }
55
54
  def updated_dependency_files
56
55
  updated_files = T.let([], T::Array[DependencyFile])
57
56
 
58
57
  updated_files += updated_manifest_files
59
- if Dependabot::Experiments.enabled?(:enable_pnpm_workspace_catalog)
60
- updated_files += updated_pnpm_workspace_files
61
- end
62
- updated_files += updated_lockfiles
58
+ updated_files += if pnpm_workspace.any?
59
+ update_pnpm_workspace_and_locks
60
+ else
61
+ updated_lockfiles
62
+ end
63
63
 
64
64
  if updated_files.none?
65
-
66
- if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
67
- # when all dependencies are transitive
68
- all_transitive = dependencies.none?(&:top_level?)
69
- # when there is no update in package.json
70
- no_package_json_update = package_files.empty?
71
- # handle the no change error for transitive dependency updates
72
- if pnpm_locks.any? && dependencies.length.positive? && all_transitive && no_package_json_update
73
- raise ToolFeatureNotSupported.new(
74
- tool_name: "pnpm",
75
- tool_type: "package_manager",
76
- feature: "updating transitive dependencies"
77
- )
78
- end
65
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error) && original_pnpm_locks.any?
66
+ raise_tool_not_supported_for_pnpm_if_transitive
67
+ raise_miss_configured_tooling_if_pnpm_subdirectory
79
68
  end
80
69
 
81
70
  raise NoChangeError.new(
@@ -94,10 +83,69 @@ module Dependabot
94
83
 
95
84
  vendor_updated_files(updated_files)
96
85
  end
97
- # rubocop:enable Metrics/PerceivedComplexity
98
86
 
99
87
  private
100
88
 
89
+ sig { void }
90
+ def raise_tool_not_supported_for_pnpm_if_transitive
91
+ # ✅ Ensure there are dependencies and check if all are transitive
92
+ return if dependencies.empty? || dependencies.any?(&:top_level?)
93
+
94
+ raise ToolFeatureNotSupported.new(
95
+ tool_name: "pnpm",
96
+ tool_type: "package_manager",
97
+ feature: "updating transitive dependencies"
98
+ )
99
+ end
100
+
101
+ # rubocop:disable Metrics/PerceivedComplexity
102
+ sig { void }
103
+ def raise_miss_configured_tooling_if_pnpm_subdirectory
104
+ workspace_files = original_pnpm_workspace
105
+ lockfiles = original_pnpm_locks
106
+
107
+ # ✅ Ensure `pnpm-workspace.yaml` is in a parent directory
108
+ return if workspace_files.empty?
109
+ return if workspace_files.any? { |f| f.directory == "/" }
110
+ return unless workspace_files.all? { |f| f.name.end_with?("../pnpm-workspace.yaml") }
111
+
112
+ # ✅ Ensure `pnpm-lock.yaml` is also in a parent directory
113
+ return if lockfiles.empty?
114
+ return if lockfiles.any? { |f| f.directory == "/" }
115
+ return unless lockfiles.all? { |f| f.name.end_with?("../pnpm-lock.yaml") }
116
+
117
+ # ❌ Raise error → Updating inside a subdirectory is misconfigured
118
+ raise MisconfiguredTooling.new(
119
+ "pnpm",
120
+ "Updating workspaces from inside a workspace subdirectory is not supported. " \
121
+ "Both `pnpm-lock.yaml` and `pnpm-workspace.yaml` exist in a parent directory. " \
122
+ "Dependabot should only update from the root workspace."
123
+ )
124
+ end
125
+ # rubocop:enable Metrics/PerceivedComplexity
126
+
127
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
128
+ def update_pnpm_workspace_and_locks
129
+ workspace_updates = updated_pnpm_workspace_files
130
+ lock_updates = update_pnpm_locks
131
+
132
+ workspace_updates + lock_updates
133
+ end
134
+
135
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
136
+ def update_pnpm_locks
137
+ updated_files = []
138
+ pnpm_locks.each do |pnpm_lock|
139
+ next unless pnpm_lock_changed?(pnpm_lock)
140
+
141
+ updated_files << updated_file(
142
+ file: pnpm_lock,
143
+ content: updated_pnpm_lock_content(pnpm_lock)
144
+ )
145
+ end
146
+ updated_files
147
+ end
148
+
101
149
  sig { params(updated_files: T::Array[Dependabot::DependencyFile]).returns(T::Array[Dependabot::DependencyFile]) }
102
150
  def vendor_updated_files(updated_files)
103
151
  base_dir = T.must(updated_files.first).directory
@@ -222,6 +270,24 @@ module Dependabot
222
270
  )
223
271
  end
224
272
 
273
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
274
+ def original_pnpm_locks
275
+ @original_pnpm_locks ||= T.let(
276
+ dependency_files
277
+ .select { |f| f.name.end_with?("pnpm-lock.yaml") },
278
+ T.nilable(T::Array[Dependabot::DependencyFile])
279
+ )
280
+ end
281
+
282
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
283
+ def original_pnpm_workspace
284
+ @original_pnpm_workspace ||= T.let(
285
+ dependency_files
286
+ .select { |f| f.name.end_with?("pnpm-workspace.yaml") },
287
+ T.nilable(T::Array[Dependabot::DependencyFile])
288
+ )
289
+ end
290
+
225
291
  sig { returns(T::Array[Dependabot::DependencyFile]) }
226
292
  def bun_locks
227
293
  @bun_locks ||= T.let(
@@ -294,8 +360,6 @@ module Dependabot
294
360
  end
295
361
  end
296
362
 
297
- # rubocop:disable Metrics/MethodLength
298
- # rubocop:disable Metrics/PerceivedComplexity
299
363
  sig { returns(T::Array[Dependabot::DependencyFile]) }
300
364
  def updated_lockfiles
301
365
  updated_files = []
@@ -309,14 +373,7 @@ module Dependabot
309
373
  )
310
374
  end
311
375
 
312
- pnpm_locks.each do |pnpm_lock|
313
- next unless pnpm_lock_changed?(pnpm_lock)
314
-
315
- updated_files << updated_file(
316
- file: pnpm_lock,
317
- content: updated_pnpm_lock_content(pnpm_lock)
318
- )
319
- end
376
+ updated_files.concat(update_pnpm_locks)
320
377
 
321
378
  bun_locks.each do |bun_lock|
322
379
  next unless bun_lock_changed?(bun_lock)
@@ -347,9 +404,6 @@ module Dependabot
347
404
 
348
405
  updated_files
349
406
  end
350
- # rubocop:enable Metrics/MethodLength
351
- # rubocop:enable Metrics/PerceivedComplexity
352
-
353
407
  sig { params(yarn_lock: Dependabot::DependencyFile).returns(String) }
354
408
  def updated_yarn_lock_content(yarn_lock)
355
409
  @updated_yarn_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
@@ -361,7 +415,10 @@ module Dependabot
361
415
  def updated_pnpm_lock_content(pnpm_lock)
362
416
  @updated_pnpm_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
363
417
  @updated_pnpm_lock_content[pnpm_lock.name] ||=
364
- pnpm_lockfile_updater.updated_pnpm_lock_content(pnpm_lock)
418
+ pnpm_lockfile_updater.updated_pnpm_lock_content(
419
+ pnpm_lock,
420
+ updated_pnpm_workspace_content: @updated_pnpm_workspace_content
421
+ )
365
422
  end
366
423
 
367
424
  sig { params(bun_lock: Dependabot::DependencyFile).returns(String) }
@@ -467,6 +467,8 @@ module Dependabot
467
467
  # Attempt to activate the local version of the package manager
468
468
  sig { params(name: String).void }
469
469
  def self.fallback_to_local_version(name)
470
+ return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)
471
+
470
472
  Dependabot.logger.info("Falling back to activate the currently installed version of #{name}.")
471
473
 
472
474
  # Fetch the currently installed version directly from the environment
@@ -553,6 +555,11 @@ module Dependabot
553
555
  result
554
556
  rescue StandardError => e
555
557
  Dependabot.logger.error("Error running package manager command: #{full_command}, Error: #{e.message}")
558
+ if e.message.match?(/Response Code.*:.*404.*\(Not Found\)/) &&
559
+ e.message.include?("The remote server failed to provide the requested resource")
560
+ raise RegistryError.new(404, "The remote server failed to provide the requested resource")
561
+ end
562
+
556
563
  raise
557
564
  end
558
565
 
@@ -189,8 +189,10 @@ module Dependabot
189
189
  @language_requirement ||= find_engine_constraints_as_requirement(Language::NAME)
190
190
  end
191
191
 
192
+ # rubocop:disable Metrics/PerceivedComplexity
193
+ # rubocop:disable Metrics/AbcSize
192
194
  sig { params(name: String).returns(T.nilable(Requirement)) }
193
- def find_engine_constraints_as_requirement(name) # rubocop:disable Metrics/PerceivedComplexity
195
+ def find_engine_constraints_as_requirement(name)
194
196
  Dependabot.logger.info("Processing engine constraints for #{name}")
195
197
 
196
198
  return nil unless @engines.is_a?(Hash) && @engines[name]
@@ -199,8 +201,7 @@ module Dependabot
199
201
  return nil if raw_constraint.empty?
200
202
 
201
203
  if Dependabot::Experiments.enabled?(:enable_engine_version_detection)
202
- constraints = ConstraintHelper.extract_constraints(raw_constraint)
203
-
204
+ constraints = ConstraintHelper.extract_ruby_constraints(raw_constraint)
204
205
  # When constraints are invalid we return constraints array nil
205
206
  if constraints.nil?
206
207
  Dependabot.logger.warn(
@@ -225,12 +226,16 @@ module Dependabot
225
226
 
226
227
  end
227
228
 
228
- Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
229
- Requirement.new(constraints)
229
+ if constraints && !constraints.empty?
230
+ Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
231
+ Requirement.new(constraints)
232
+ end
230
233
  rescue StandardError => e
231
234
  Dependabot.logger.error("Error processing constraints for #{name}: #{e.message}")
232
235
  nil
233
236
  end
237
+ # rubocop:enable Metrics/AbcSize
238
+ # rubocop:enable Metrics/PerceivedComplexity
234
239
 
235
240
  # rubocop:disable Metrics/CyclomaticComplexity
236
241
  # rubocop:disable Metrics/AbcSize
@@ -412,10 +417,15 @@ module Dependabot
412
417
 
413
418
  Dependabot.logger.info("Installing \"#{name}@#{version}\"")
414
419
 
415
- SharedHelpers.run_shell_command(
416
- "corepack install #{name}@#{version} --global --cache-only",
417
- fingerprint: "corepack install <name>@<version> --global --cache-only"
418
- )
420
+ begin
421
+ SharedHelpers.run_shell_command(
422
+ "corepack install #{name}@#{version} --global --cache-only",
423
+ fingerprint: "corepack install <name>@<version> --global --cache-only"
424
+ )
425
+ rescue SharedHelpers::HelperSubprocessFailed => e
426
+ Dependabot.logger.error("Error installing #{name}@#{version}: #{e.message}")
427
+ Helpers.fallback_to_local_version(name)
428
+ end
419
429
  end
420
430
 
421
431
  sig { params(name: T.nilable(String)).returns(String) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.295.0
4
+ version: 0.296.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-30 00:00:00.000000000 Z
11
+ date: 2025-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dependabot-common
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 0.295.0
19
+ version: 0.296.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 0.295.0
26
+ version: 0.296.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: debug
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -356,7 +356,7 @@ licenses:
356
356
  - MIT
357
357
  metadata:
358
358
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
359
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.295.0
359
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.296.0
360
360
  post_install_message:
361
361
  rdoc_options: []
362
362
  require_paths: