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.
@@ -18,6 +18,10 @@ module Dependabot
18
18
  @dependency_files = dependency_files
19
19
  @repo_contents_path = repo_contents_path
20
20
  @credentials = credentials
21
+ @error_handler = PnpmErrorHandler.new(
22
+ dependencies: dependencies,
23
+ dependency_files: dependency_files
24
+ )
21
25
  end
22
26
 
23
27
  def updated_pnpm_lock_content(pnpm_lock)
@@ -36,6 +40,7 @@ module Dependabot
36
40
  attr_reader :dependency_files
37
41
  attr_reader :repo_contents_path
38
42
  attr_reader :credentials
43
+ attr_reader :error_handler
39
44
 
40
45
  IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
41
46
  INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER"
@@ -46,12 +51,12 @@ module Dependabot
46
51
  UNAUTHORIZED_PACKAGE = /ERR_PNPM_FETCH_401[ [^:print:]]+GET (?<dependency_url>.*): Unauthorized - 401/
47
52
 
48
53
  # ERR_PNPM_FETCH ERROR CODES
49
- ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*): - 401/
50
- ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*): - 403/
51
- ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*): - 404/
52
- ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*): - 500/
53
- ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*): - 502/
54
- ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*): - 503/
54
+ ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*):/
55
+ ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*):/
56
+ ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*):/
57
+ ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*):/
58
+ ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*):/
59
+ ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*):/
55
60
 
56
61
  # ERR_PNPM_UNSUPPORTED_ENGINE
57
62
  ERR_PNPM_UNSUPPORTED_ENGINE = /ERR_PNPM_UNSUPPORTED_ENGINE/
@@ -100,7 +105,7 @@ module Dependabot
100
105
  File.write(".npmrc", npmrc_content(pnpm_lock))
101
106
 
102
107
  SharedHelpers.with_git_configured(credentials: credentials) do
103
- run_pnpm_updater
108
+ run_pnpm_update_packages
104
109
 
105
110
  write_final_package_json_files
106
111
 
@@ -111,15 +116,22 @@ module Dependabot
111
116
  end
112
117
  end
113
118
 
114
- def run_pnpm_updater
119
+ def run_pnpm_update_packages
115
120
  dependency_updates = dependencies.map do |d|
116
121
  "#{d.name}@#{d.version}"
117
122
  end.join(" ")
118
123
 
119
- Helpers.run_pnpm_command(
120
- "install #{dependency_updates} --lockfile-only --ignore-workspace-root-check",
121
- fingerprint: "install <dependency_updates> --lockfile-only --ignore-workspace-root-check"
122
- )
124
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
125
+ Helpers.run_pnpm_command(
126
+ "update #{dependency_updates} --lockfile-only --no-save -r",
127
+ fingerprint: "update <dependency_updates> --lockfile-only --no-save -r"
128
+ )
129
+ else
130
+ Helpers.run_pnpm_command(
131
+ "install #{dependency_updates} --lockfile-only --ignore-workspace-root-check",
132
+ fingerprint: "install <dependency_updates> --lockfile-only --ignore-workspace-root-check"
133
+ )
134
+ end
123
135
  end
124
136
 
125
137
  def run_pnpm_install
@@ -251,6 +263,8 @@ module Dependabot
251
263
  pnpm_lock)
252
264
  end
253
265
 
266
+ error_handler.handle_pnpm_error(error)
267
+
254
268
  raise
255
269
  end
256
270
  # rubocop:enable Metrics/AbcSize
@@ -360,5 +374,60 @@ module Dependabot
360
374
  end
361
375
  end
362
376
  end
377
+
378
+ class PnpmErrorHandler
379
+ extend T::Sig
380
+
381
+ # remote connection closed
382
+ ECONNRESET_ERROR = /ECONNRESET/
383
+
384
+ # socket hang up error code
385
+ SOCKET_HANG_UP = /socket hang up/
386
+
387
+ # ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC error
388
+ ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC = /ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC/
389
+
390
+ # duplicate package error code
391
+ DUPLICATE_PACKAGE = /Found duplicates/
392
+
393
+ ERR_PNPM_NO_VERSIONS = /ERR_PNPM_NO_VERSIONS/
394
+
395
+ # Initializes the YarnErrorHandler with dependencies and dependency files
396
+ sig do
397
+ params(
398
+ dependencies: T::Array[Dependabot::Dependency],
399
+ dependency_files: T::Array[Dependabot::DependencyFile]
400
+ ).void
401
+ end
402
+ def initialize(dependencies:, dependency_files:)
403
+ @dependencies = dependencies
404
+ @dependency_files = dependency_files
405
+ end
406
+
407
+ private
408
+
409
+ sig { returns(T::Array[Dependabot::Dependency]) }
410
+ attr_reader :dependencies
411
+
412
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
413
+ attr_reader :dependency_files
414
+
415
+ public
416
+
417
+ # Handles errors with specific to yarn error codes
418
+ sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
419
+ def handle_pnpm_error(error)
420
+ if error.message.match?(DUPLICATE_PACKAGE) || error.message.match?(ERR_PNPM_NO_VERSIONS) ||
421
+ error.message.match?(ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC)
422
+
423
+ raise DependencyFileNotResolvable, "Error resolving dependency"
424
+ end
425
+
426
+ ## Clean error message from ANSI escape codes
427
+ return unless error.message.match?(ECONNRESET_ERROR) || error.message.match?(SOCKET_HANG_UP)
428
+
429
+ raise InconsistentRegistryResponse, "Inconsistent registry response while resolving dependency"
430
+ end
431
+ end
363
432
  end
364
433
  end
@@ -0,0 +1,140 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/npm_and_yarn/helpers"
5
+ require "dependabot/npm_and_yarn/update_checker/registry_finder"
6
+ require "dependabot/npm_and_yarn/registry_parser"
7
+ require "dependabot/shared_helpers"
8
+
9
+ class DependencyRequirement < T::Struct
10
+ const :file, String
11
+ const :requirement, String
12
+ const :groups, T::Array[String]
13
+ const :source, T.nilable(String)
14
+ end
15
+
16
+ module Dependabot
17
+ module NpmAndYarn
18
+ class FileUpdater
19
+ class PnpmWorkspaceUpdater
20
+ extend T::Sig
21
+
22
+ sig do
23
+ params(
24
+ workspace_file: Dependabot::DependencyFile,
25
+ dependencies: T::Array[Dependabot::Dependency]
26
+ ) .void
27
+ end
28
+ def initialize(workspace_file:, dependencies:)
29
+ @dependencies = dependencies
30
+ @workspace_file = workspace_file
31
+ end
32
+
33
+ sig { returns(Dependabot::DependencyFile) }
34
+ def updated_pnpm_workspace
35
+ updated_file = workspace_file.dup
36
+ updated_file.content = updated_pnpm_workspace_content
37
+ updated_file
38
+ end
39
+
40
+ private
41
+
42
+ sig { returns(Dependabot::DependencyFile) }
43
+ attr_reader :workspace_file
44
+
45
+ sig { returns(T::Array[Dependabot::Dependency]) }
46
+ attr_reader :dependencies
47
+
48
+ sig { returns(T.nilable(String)) }
49
+ def updated_pnpm_workspace_content
50
+ content = workspace_file.content.dup
51
+ dependencies.each do |dependency|
52
+ content = update_dependency_versions(T.must(content), dependency)
53
+ end
54
+
55
+ content
56
+ end
57
+
58
+ sig { params(content: String, dependency: Dependabot::Dependency).returns(String) }
59
+ def update_dependency_versions(content, dependency)
60
+ new_requirements(dependency).each do |requirement|
61
+ content = replace_version_in_content(
62
+ content: content,
63
+ dependency: dependency,
64
+ old_requirement: T.must(old_requirement(dependency, requirement)),
65
+ new_requirement: requirement
66
+ )
67
+ end
68
+
69
+ content
70
+ end
71
+
72
+ sig do
73
+ params(
74
+ content: String,
75
+ dependency: Dependabot::Dependency,
76
+ old_requirement: DependencyRequirement,
77
+ new_requirement: DependencyRequirement
78
+ ).returns(String)
79
+ end
80
+ def replace_version_in_content(content:, dependency:, old_requirement:, new_requirement:)
81
+ old_version = old_requirement.requirement
82
+ new_version = new_requirement.requirement
83
+
84
+ pattern = build_replacement_pattern(
85
+ dependency_name: dependency.name,
86
+ version: old_version
87
+ )
88
+
89
+ replacement = build_replacement_string(
90
+ dependency_name: dependency.name,
91
+ version: new_version
92
+ )
93
+
94
+ content.gsub(pattern, replacement)
95
+ end
96
+
97
+ sig { params(dependency_name: String, version: String).returns(Regexp) }
98
+ def build_replacement_pattern(dependency_name:, version:)
99
+ /(["']?)#{dependency_name}\1:\s*(["']?)#{Regexp.escape(version)}\2/
100
+ end
101
+
102
+ sig { params(dependency_name: String, version: String).returns(String) }
103
+ def build_replacement_string(dependency_name:, version:)
104
+ "\\1#{dependency_name}\\1: \\2#{version}\\2"
105
+ end
106
+
107
+ sig { params(dependency: Dependabot::Dependency).returns(T::Array[DependencyRequirement]) }
108
+ def new_requirements(dependency)
109
+ dependency.requirements
110
+ .select { |r| r[:file] == workspace_file.name }
111
+ .map do |r|
112
+ DependencyRequirement.new(
113
+ file: r[:file],
114
+ requirement: r[:requirement],
115
+ groups: r[:groups],
116
+ source: r[:source]
117
+ )
118
+ end
119
+ end
120
+
121
+ sig do
122
+ params(dependency: Dependabot::Dependency,
123
+ new_requirement: DependencyRequirement).returns(T.nilable(DependencyRequirement))
124
+ end
125
+ def old_requirement(dependency, new_requirement)
126
+ matching_req = T.must(dependency.previous_requirements).find { |r| r[:groups] == new_requirement.groups }
127
+
128
+ return nil if matching_req.nil?
129
+
130
+ DependencyRequirement.new(
131
+ file: matching_req[:file],
132
+ requirement: matching_req[:requirement],
133
+ groups: matching_req[:groups],
134
+ source: matching_req[:source]
135
+ )
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -11,7 +11,7 @@ require "sorbet-runtime"
11
11
 
12
12
  module Dependabot
13
13
  module NpmAndYarn
14
- class FileUpdater < Dependabot::FileUpdaters::Base
14
+ class FileUpdater < Dependabot::FileUpdaters::Base # rubocop:disable Metrics/ClassLength
15
15
  extend T::Sig
16
16
 
17
17
  require_relative "file_updater/package_json_updater"
@@ -19,6 +19,7 @@ module Dependabot
19
19
  require_relative "file_updater/yarn_lockfile_updater"
20
20
  require_relative "file_updater/pnpm_lockfile_updater"
21
21
  require_relative "file_updater/bun_lockfile_updater"
22
+ require_relative "file_updater/pnpm_workspace_updater"
22
23
 
23
24
  class NoChangeError < StandardError
24
25
  extend T::Sig
@@ -43,19 +44,40 @@ module Dependabot
43
44
  %r{^(?:.*/)?npm-shrinkwrap\.json$},
44
45
  %r{^(?:.*/)?yarn\.lock$},
45
46
  %r{^(?:.*/)?pnpm-lock\.yaml$},
47
+ %r{^(?:.*/)?pnpm-workspace\.yaml$},
46
48
  %r{^(?:.*/)?\.yarn/.*}, # Matches any file within the .yarn/ directory
47
49
  %r{^(?:.*/)?\.pnp\.(?:js|cjs)$} # Matches .pnp.js or .pnp.cjs files
48
50
  ]
49
51
  end
50
52
 
53
+ # rubocop:disable Metrics/PerceivedComplexity
51
54
  sig { override.returns(T::Array[DependencyFile]) }
52
55
  def updated_dependency_files
53
56
  updated_files = T.let([], T::Array[DependencyFile])
54
57
 
55
58
  updated_files += updated_manifest_files
59
+ if Dependabot::Experiments.enabled?(:enable_pnpm_workspace_catalog)
60
+ updated_files += updated_pnpm_workspace_files
61
+ end
56
62
  updated_files += updated_lockfiles
57
63
 
58
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
79
+ end
80
+
59
81
  raise NoChangeError.new(
60
82
  message: "No files were updated!",
61
83
  error_context: error_context(updated_files: updated_files)
@@ -72,6 +94,7 @@ module Dependabot
72
94
 
73
95
  vendor_updated_files(updated_files)
74
96
  end
97
+ # rubocop:enable Metrics/PerceivedComplexity
75
98
 
76
99
  private
77
100
 
@@ -190,6 +213,15 @@ module Dependabot
190
213
  )
191
214
  end
192
215
 
216
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
217
+ def pnpm_workspace
218
+ @pnpm_workspace ||= T.let(
219
+ filtered_dependency_files
220
+ .select { |f| f.name.end_with?("pnpm-workspace.yaml") },
221
+ T.nilable(T::Array[Dependabot::DependencyFile])
222
+ )
223
+ end
224
+
193
225
  sig { returns(T::Array[Dependabot::DependencyFile]) }
194
226
  def bun_locks
195
227
  @bun_locks ||= T.let(
@@ -252,6 +284,16 @@ module Dependabot
252
284
  end
253
285
  end
254
286
 
287
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
288
+ def updated_pnpm_workspace_files
289
+ pnpm_workspace.filter_map do |file|
290
+ updated_content = updated_pnpm_workspace_content(file)
291
+ next if updated_content == file.content
292
+
293
+ updated_file(file: file, content: T.must(updated_content))
294
+ end
295
+ end
296
+
255
297
  # rubocop:disable Metrics/MethodLength
256
298
  # rubocop:disable Metrics/PerceivedComplexity
257
299
  sig { returns(T::Array[Dependabot::DependencyFile]) }
@@ -389,6 +431,19 @@ module Dependabot
389
431
  dependencies: dependencies
390
432
  ).updated_package_json.content
391
433
  end
434
+
435
+ sig do
436
+ params(file: Dependabot::DependencyFile)
437
+ .returns(T.nilable(String))
438
+ end
439
+ def updated_pnpm_workspace_content(file)
440
+ @updated_pnpm_workspace_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
441
+ @updated_pnpm_workspace_content[file.name] ||=
442
+ PnpmWorkspaceUpdater.new(
443
+ workspace_file: file,
444
+ dependencies: dependencies
445
+ ).updated_pnpm_workspace.content
446
+ end
392
447
  end
393
448
  end
394
449
  end
@@ -40,6 +40,9 @@ module Dependabot
40
40
  YARN_DEFAULT_VERSION = YARN_V3
41
41
  YARN_FALLBACK_VERSION = YARN_V1
42
42
 
43
+ # corepack supported package managers
44
+ SUPPORTED_COREPACK_PACKAGE_MANAGERS = %w(npm yarn pnpm).freeze
45
+
43
46
  # Determines the npm version depends to the feature flag
44
47
  # If the feature flag is enabled, we are going to use the minimum version npm 8
45
48
  # Otherwise, we are going to use old versionining npm 6
@@ -324,8 +327,8 @@ module Dependabot
324
327
  package_manager_run_command(NpmPackageManager::NAME, command, fingerprint: fingerprint)
325
328
  else
326
329
  Dependabot::SharedHelpers.run_shell_command(
327
- "corepack npm #{command}",
328
- fingerprint: "corepack npm #{fingerprint}"
330
+ "npm #{command}",
331
+ fingerprint: "npm #{fingerprint}"
329
332
  )
330
333
  end
331
334
  end
@@ -484,6 +487,8 @@ module Dependabot
484
487
  .returns(String)
485
488
  end
486
489
  def self.package_manager_install(name, version, env: {})
490
+ return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)
491
+
487
492
  Dependabot::SharedHelpers.run_shell_command(
488
493
  "corepack install #{name}@#{version} --global --cache-only",
489
494
  fingerprint: "corepack install <name>@<version> --global --cache-only",
@@ -494,6 +499,8 @@ module Dependabot
494
499
  # Prepare the package manager for use by using corepack
495
500
  sig { params(name: String, version: String).returns(String) }
496
501
  def self.package_manager_activate(name, version)
502
+ return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)
503
+
497
504
  Dependabot::SharedHelpers.run_shell_command(
498
505
  "corepack prepare #{name}@#{version} --activate",
499
506
  fingerprint: "corepack prepare <name>@<version> --activate"
@@ -566,6 +573,11 @@ module Dependabot
566
573
  dependency
567
574
  end
568
575
  end
576
+
577
+ sig { params(name: String).returns(T::Boolean) }
578
+ def self.corepack_supported_package_manager?(name)
579
+ SUPPORTED_COREPACK_PACKAGE_MANAGERS.include?(name)
580
+ end
569
581
  end
570
582
  end
571
583
  end
@@ -38,8 +38,8 @@ module Dependabot
38
38
  def initialize(detected_version: nil, raw_version: nil, requirement: nil)
39
39
  super(
40
40
  name: NAME,
41
- detected_version: detected_version ? Version.new(detected_version) : nil,
42
- version: raw_version ? Version.new(raw_version) : nil,
41
+ detected_version: detected_version && !detected_version.empty? ? Version.new(detected_version) : nil,
42
+ version: raw_version && !raw_version.empty? ? Version.new(raw_version) : nil,
43
43
  deprecated_versions: DEPRECATED_VERSIONS,
44
44
  supported_versions: SUPPORTED_VERSIONS,
45
45
  requirement: requirement
@@ -48,22 +48,16 @@ module Dependabot
48
48
 
49
49
  sig { override.returns(T::Boolean) }
50
50
  def deprecated?
51
- return false unless detected_version
52
-
53
- return false if unsupported?
54
-
55
51
  return false unless Dependabot::Experiments.enabled?(:npm_v6_deprecation_warning)
56
52
 
57
- deprecated_versions.include?(detected_version)
53
+ super
58
54
  end
59
55
 
60
56
  sig { override.returns(T::Boolean) }
61
57
  def unsupported?
62
- return false unless detected_version
63
-
64
58
  return false unless Dependabot::Experiments.enabled?(:npm_v6_unsupported_error)
65
59
 
66
- supported_versions.all? { |supported| supported > detected_version }
60
+ super
67
61
  end
68
62
  end
69
63
  end
@@ -11,6 +11,7 @@ require "dependabot/npm_and_yarn/yarn_package_manager"
11
11
  require "dependabot/npm_and_yarn/pnpm_package_manager"
12
12
  require "dependabot/npm_and_yarn/bun_package_manager"
13
13
  require "dependabot/npm_and_yarn/language"
14
+ require "dependabot/npm_and_yarn/constraint_helper"
14
15
 
15
16
  module Dependabot
16
17
  module NpmAndYarn
@@ -59,14 +60,16 @@ module Dependabot
59
60
  T.any(
60
61
  T.class_of(Dependabot::NpmAndYarn::NpmPackageManager),
61
62
  T.class_of(Dependabot::NpmAndYarn::YarnPackageManager),
62
- T.class_of(Dependabot::NpmAndYarn::PNPMPackageManager)
63
+ T.class_of(Dependabot::NpmAndYarn::PNPMPackageManager),
64
+ T.class_of(Dependabot::NpmAndYarn::BunPackageManager)
63
65
  )
64
66
  end
65
67
 
66
68
  PACKAGE_MANAGER_CLASSES = T.let({
67
69
  NpmPackageManager::NAME => NpmPackageManager,
68
70
  YarnPackageManager::NAME => YarnPackageManager,
69
- PNPMPackageManager::NAME => PNPMPackageManager
71
+ PNPMPackageManager::NAME => PNPMPackageManager,
72
+ BunPackageManager::NAME => BunPackageManager
70
73
  }.freeze, T::Hash[String, NpmAndYarnPackageManagerClassType])
71
74
 
72
75
  # Error malformed version number string
@@ -187,7 +190,7 @@ module Dependabot
187
190
  end
188
191
 
189
192
  sig { params(name: String).returns(T.nilable(Requirement)) }
190
- def find_engine_constraints_as_requirement(name)
193
+ def find_engine_constraints_as_requirement(name) # rubocop:disable Metrics/PerceivedComplexity
191
194
  Dependabot.logger.info("Processing engine constraints for #{name}")
192
195
 
193
196
  return nil unless @engines.is_a?(Hash) && @engines[name]
@@ -195,19 +198,31 @@ module Dependabot
195
198
  raw_constraint = @engines[name].to_s.strip
196
199
  return nil if raw_constraint.empty?
197
200
 
198
- raw_constraints = raw_constraint.split
199
- constraints = raw_constraints.map do |constraint|
200
- case constraint
201
- when /^\d+$/
202
- ">=#{constraint}.0.0 <#{constraint.to_i + 1}.0.0"
203
- when /^\d+\.\d+$/
204
- ">=#{constraint} <#{constraint.split('.').first.to_i + 1}.0.0"
205
- when /^\d+\.\d+\.\d+$/
206
- "=#{constraint}"
207
- else
208
- Dependabot.logger.warn("Unrecognized constraint format for #{name}: #{constraint}")
209
- constraint
201
+ if Dependabot::Experiments.enabled?(:enable_engine_version_detection)
202
+ constraints = ConstraintHelper.extract_constraints(raw_constraint)
203
+
204
+ # When constraints are invalid we return constraints array nil
205
+ if constraints.nil?
206
+ Dependabot.logger.warn(
207
+ "Unrecognized constraint format for #{name}: #{raw_constraint}"
208
+ )
210
209
  end
210
+ else
211
+ raw_constraints = raw_constraint.split
212
+ constraints = raw_constraints.map do |constraint|
213
+ case constraint
214
+ when /^\d+$/
215
+ ">=#{constraint}.0.0 <#{constraint.to_i + 1}.0.0"
216
+ when /^\d+\.\d+$/
217
+ ">=#{constraint} <#{constraint.split('.').first.to_i + 1}.0.0"
218
+ when /^\d+\.\d+\.\d+$/
219
+ "=#{constraint}"
220
+ else
221
+ Dependabot.logger.warn("Unrecognized constraint format for #{name}: #{constraint}")
222
+ constraint
223
+ end
224
+ end
225
+
211
226
  end
212
227
 
213
228
  Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
@@ -287,19 +302,24 @@ module Dependabot
287
302
 
288
303
  sig { params(name: String).returns(T.nilable(String)) }
289
304
  def detect_version(name)
290
- # we prioritize version mentioned in "packageManager" instead of "engines"
305
+ # Prioritize version mentioned in "packageManager" instead of "engines"
291
306
  if @manifest_package_manager&.start_with?("#{name}@")
292
307
  detected_version = @manifest_package_manager.split("@").last.to_s
293
308
  end
294
309
 
295
- # if "packageManager" have no version specified, we check if we can extract "engines" information
296
- detected_version = check_engine_version(name) if !detected_version || detected_version.empty?
310
+ # If "packageManager" has no version specified, check if we can extract "engines" information
311
+ detected_version ||= check_engine_version(name) if detected_version.to_s.empty?
297
312
 
298
- # if "packageManager" and "engines" both are not present, we check if we can infer the version
299
- # from the manifest file lockfileVersion
300
- detected_version = guessed_version(name) if !detected_version || detected_version.empty?
313
+ # If neither "packageManager" nor "engines" have versions, infer version from lockfileVersion
314
+ detected_version ||= guessed_version(name) if detected_version.to_s.empty?
301
315
 
302
- detected_version&.to_s
316
+ # Strip and validate version format
317
+ detected_version_string = detected_version.to_s.strip
318
+
319
+ # Ensure detected_version is neither "0" nor invalid format
320
+ return if detected_version_string == "0" || !detected_version_string.match?(ConstraintHelper::VERSION_REGEX)
321
+
322
+ detected_version_string
303
323
  end
304
324
 
305
325
  sig { params(name: T.nilable(String)).returns(Ecosystem::VersionManager) }
@@ -330,7 +350,7 @@ module Dependabot
330
350
  end
331
351
 
332
352
  package_manager_class.new(
333
- detected_version: detected_version.to_s,
353
+ detected_version: detected_version,
334
354
  raw_version: installed_version,
335
355
  requirement: package_manager_requirement
336
356
  )
@@ -432,7 +452,8 @@ module Dependabot
432
452
  return if @package_json.nil?
433
453
 
434
454
  version_selector = VersionSelector.new
435
- engine_versions = version_selector.setup(@package_json, name)
455
+
456
+ engine_versions = version_selector.setup(@package_json, name, dependabot_versions(name))
436
457
 
437
458
  return if engine_versions.empty?
438
459
 
@@ -440,6 +461,20 @@ module Dependabot
440
461
  Dependabot.logger.info("Returned (#{MANIFEST_ENGINES_KEY}) info \"#{name}\" : \"#{version}\"")
441
462
  version
442
463
  end
464
+
465
+ sig { params(name: String).returns(T.nilable(T::Array[Dependabot::Version])) }
466
+ def dependabot_versions(name)
467
+ case name
468
+ when "npm"
469
+ NpmPackageManager::SUPPORTED_VERSIONS
470
+ when "yarn"
471
+ YarnPackageManager::SUPPORTED_VERSIONS
472
+ when "bun"
473
+ BunPackageManager::SUPPORTED_VERSIONS
474
+ when "pnpm"
475
+ PNPMPackageManager::SUPPORTED_VERSIONS
476
+ end
477
+ end
443
478
  end
444
479
  end
445
480
  end
@@ -80,6 +80,10 @@ module Dependabot
80
80
  # Matches @ followed by x.y.z (digits separated by dots)
81
81
  if (match = version.match(/@(\d+\.\d+\.\d+)/))
82
82
  version = match[1] # Just "4.5.3"
83
+
84
+ # Extract version in case the output contains Corepack verbose data
85
+ elsif version.include?("Corepack")
86
+ version = T.must(T.must(version.tr("\n", " ").match(/(\d+\.\d+\.\d+)/))[-1])
83
87
  end
84
88
  version = version&.gsub(/^v/, "")
85
89
  end