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 +4 -4
- data/lib/dependabot/npm_and_yarn/constraint_helper.rb +359 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +5 -0
- data/lib/dependabot/npm_and_yarn/file_parser/bun_lock.rb +0 -1
- data/lib/dependabot/npm_and_yarn/file_parser.rb +33 -1
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +57 -3
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +17 -7
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_workspace_updater.rb +140 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +126 -32
- data/lib/dependabot/npm_and_yarn/helpers.rb +7 -0
- data/lib/dependabot/npm_and_yarn/npm_package_manager.rb +4 -10
- data/lib/dependabot/npm_and_yarn/package_manager.rb +70 -27
- data/lib/dependabot/npm_and_yarn/version_selector.rb +32 -7
- data/lib/dependabot/npm_and_yarn.rb +19 -0
- metadata +7 -5
@@ -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,34 +44,27 @@ 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
|
|
51
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
52
53
|
sig { override.returns(T::Array[DependencyFile]) }
|
53
54
|
def updated_dependency_files
|
54
55
|
updated_files = T.let([], T::Array[DependencyFile])
|
55
56
|
|
56
57
|
updated_files += updated_manifest_files
|
57
|
-
updated_files +=
|
58
|
+
updated_files += if pnpm_workspace.any?
|
59
|
+
update_pnpm_workspace_and_locks
|
60
|
+
else
|
61
|
+
updated_lockfiles
|
62
|
+
end
|
58
63
|
|
59
64
|
if updated_files.none?
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
all_transitive = dependencies.none?(&:top_level?)
|
64
|
-
# when there is no update in package.json
|
65
|
-
no_package_json_update = package_files.empty?
|
66
|
-
# handle the no change error for transitive dependency updates
|
67
|
-
if pnpm_locks.any? && dependencies.length.positive? && all_transitive && no_package_json_update
|
68
|
-
raise ToolFeatureNotSupported.new(
|
69
|
-
tool_name: "pnpm",
|
70
|
-
tool_type: "package_manager",
|
71
|
-
feature: "updating transitive dependencies"
|
72
|
-
)
|
73
|
-
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
|
74
68
|
end
|
75
69
|
|
76
70
|
raise NoChangeError.new(
|
@@ -89,10 +83,69 @@ module Dependabot
|
|
89
83
|
|
90
84
|
vendor_updated_files(updated_files)
|
91
85
|
end
|
92
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
93
86
|
|
94
87
|
private
|
95
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
|
+
|
96
149
|
sig { params(updated_files: T::Array[Dependabot::DependencyFile]).returns(T::Array[Dependabot::DependencyFile]) }
|
97
150
|
def vendor_updated_files(updated_files)
|
98
151
|
base_dir = T.must(updated_files.first).directory
|
@@ -208,6 +261,33 @@ module Dependabot
|
|
208
261
|
)
|
209
262
|
end
|
210
263
|
|
264
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
265
|
+
def pnpm_workspace
|
266
|
+
@pnpm_workspace ||= T.let(
|
267
|
+
filtered_dependency_files
|
268
|
+
.select { |f| f.name.end_with?("pnpm-workspace.yaml") },
|
269
|
+
T.nilable(T::Array[Dependabot::DependencyFile])
|
270
|
+
)
|
271
|
+
end
|
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
|
+
|
211
291
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
212
292
|
def bun_locks
|
213
293
|
@bun_locks ||= T.let(
|
@@ -270,8 +350,16 @@ module Dependabot
|
|
270
350
|
end
|
271
351
|
end
|
272
352
|
|
273
|
-
|
274
|
-
|
353
|
+
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
354
|
+
def updated_pnpm_workspace_files
|
355
|
+
pnpm_workspace.filter_map do |file|
|
356
|
+
updated_content = updated_pnpm_workspace_content(file)
|
357
|
+
next if updated_content == file.content
|
358
|
+
|
359
|
+
updated_file(file: file, content: T.must(updated_content))
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
275
363
|
sig { returns(T::Array[Dependabot::DependencyFile]) }
|
276
364
|
def updated_lockfiles
|
277
365
|
updated_files = []
|
@@ -285,14 +373,7 @@ module Dependabot
|
|
285
373
|
)
|
286
374
|
end
|
287
375
|
|
288
|
-
|
289
|
-
next unless pnpm_lock_changed?(pnpm_lock)
|
290
|
-
|
291
|
-
updated_files << updated_file(
|
292
|
-
file: pnpm_lock,
|
293
|
-
content: updated_pnpm_lock_content(pnpm_lock)
|
294
|
-
)
|
295
|
-
end
|
376
|
+
updated_files.concat(update_pnpm_locks)
|
296
377
|
|
297
378
|
bun_locks.each do |bun_lock|
|
298
379
|
next unless bun_lock_changed?(bun_lock)
|
@@ -323,9 +404,6 @@ module Dependabot
|
|
323
404
|
|
324
405
|
updated_files
|
325
406
|
end
|
326
|
-
# rubocop:enable Metrics/MethodLength
|
327
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
328
|
-
|
329
407
|
sig { params(yarn_lock: Dependabot::DependencyFile).returns(String) }
|
330
408
|
def updated_yarn_lock_content(yarn_lock)
|
331
409
|
@updated_yarn_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
|
@@ -337,7 +415,10 @@ module Dependabot
|
|
337
415
|
def updated_pnpm_lock_content(pnpm_lock)
|
338
416
|
@updated_pnpm_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
|
339
417
|
@updated_pnpm_lock_content[pnpm_lock.name] ||=
|
340
|
-
pnpm_lockfile_updater.updated_pnpm_lock_content(
|
418
|
+
pnpm_lockfile_updater.updated_pnpm_lock_content(
|
419
|
+
pnpm_lock,
|
420
|
+
updated_pnpm_workspace_content: @updated_pnpm_workspace_content
|
421
|
+
)
|
341
422
|
end
|
342
423
|
|
343
424
|
sig { params(bun_lock: Dependabot::DependencyFile).returns(String) }
|
@@ -407,6 +488,19 @@ module Dependabot
|
|
407
488
|
dependencies: dependencies
|
408
489
|
).updated_package_json.content
|
409
490
|
end
|
491
|
+
|
492
|
+
sig do
|
493
|
+
params(file: Dependabot::DependencyFile)
|
494
|
+
.returns(T.nilable(String))
|
495
|
+
end
|
496
|
+
def updated_pnpm_workspace_content(file)
|
497
|
+
@updated_pnpm_workspace_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
|
498
|
+
@updated_pnpm_workspace_content[file.name] ||=
|
499
|
+
PnpmWorkspaceUpdater.new(
|
500
|
+
workspace_file: file,
|
501
|
+
dependencies: dependencies
|
502
|
+
).updated_pnpm_workspace.content
|
503
|
+
end
|
410
504
|
end
|
411
505
|
end
|
412
506
|
end
|
@@ -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
|
|
@@ -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
|
-
|
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
|
-
|
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
|
@@ -188,6 +189,8 @@ module Dependabot
|
|
188
189
|
@language_requirement ||= find_engine_constraints_as_requirement(Language::NAME)
|
189
190
|
end
|
190
191
|
|
192
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
193
|
+
# rubocop:disable Metrics/AbcSize
|
191
194
|
sig { params(name: String).returns(T.nilable(Requirement)) }
|
192
195
|
def find_engine_constraints_as_requirement(name)
|
193
196
|
Dependabot.logger.info("Processing engine constraints for #{name}")
|
@@ -197,27 +200,42 @@ module Dependabot
|
|
197
200
|
raw_constraint = @engines[name].to_s.strip
|
198
201
|
return nil if raw_constraint.empty?
|
199
202
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
constraint
|
203
|
+
if Dependabot::Experiments.enabled?(:enable_engine_version_detection)
|
204
|
+
constraints = ConstraintHelper.extract_ruby_constraints(raw_constraint)
|
205
|
+
# When constraints are invalid we return constraints array nil
|
206
|
+
if constraints.nil?
|
207
|
+
Dependabot.logger.warn(
|
208
|
+
"Unrecognized constraint format for #{name}: #{raw_constraint}"
|
209
|
+
)
|
210
|
+
end
|
211
|
+
else
|
212
|
+
raw_constraints = raw_constraint.split
|
213
|
+
constraints = raw_constraints.map do |constraint|
|
214
|
+
case constraint
|
215
|
+
when /^\d+$/
|
216
|
+
">=#{constraint}.0.0 <#{constraint.to_i + 1}.0.0"
|
217
|
+
when /^\d+\.\d+$/
|
218
|
+
">=#{constraint} <#{constraint.split('.').first.to_i + 1}.0.0"
|
219
|
+
when /^\d+\.\d+\.\d+$/
|
220
|
+
"=#{constraint}"
|
221
|
+
else
|
222
|
+
Dependabot.logger.warn("Unrecognized constraint format for #{name}: #{constraint}")
|
223
|
+
constraint
|
224
|
+
end
|
212
225
|
end
|
226
|
+
|
213
227
|
end
|
214
228
|
|
215
|
-
|
216
|
-
|
229
|
+
if constraints && !constraints.empty?
|
230
|
+
Dependabot.logger.info("Parsed constraints for #{name}: #{constraints.join(', ')}")
|
231
|
+
Requirement.new(constraints)
|
232
|
+
end
|
217
233
|
rescue StandardError => e
|
218
234
|
Dependabot.logger.error("Error processing constraints for #{name}: #{e.message}")
|
219
235
|
nil
|
220
236
|
end
|
237
|
+
# rubocop:enable Metrics/AbcSize
|
238
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
221
239
|
|
222
240
|
# rubocop:disable Metrics/CyclomaticComplexity
|
223
241
|
# rubocop:disable Metrics/AbcSize
|
@@ -289,19 +307,24 @@ module Dependabot
|
|
289
307
|
|
290
308
|
sig { params(name: String).returns(T.nilable(String)) }
|
291
309
|
def detect_version(name)
|
292
|
-
#
|
310
|
+
# Prioritize version mentioned in "packageManager" instead of "engines"
|
293
311
|
if @manifest_package_manager&.start_with?("#{name}@")
|
294
312
|
detected_version = @manifest_package_manager.split("@").last.to_s
|
295
313
|
end
|
296
314
|
|
297
|
-
#
|
298
|
-
detected_version
|
315
|
+
# If "packageManager" has no version specified, check if we can extract "engines" information
|
316
|
+
detected_version ||= check_engine_version(name) if detected_version.to_s.empty?
|
317
|
+
|
318
|
+
# If neither "packageManager" nor "engines" have versions, infer version from lockfileVersion
|
319
|
+
detected_version ||= guessed_version(name) if detected_version.to_s.empty?
|
320
|
+
|
321
|
+
# Strip and validate version format
|
322
|
+
detected_version_string = detected_version.to_s.strip
|
299
323
|
|
300
|
-
#
|
301
|
-
|
302
|
-
detected_version = guessed_version(name) if !detected_version || detected_version.empty?
|
324
|
+
# Ensure detected_version is neither "0" nor invalid format
|
325
|
+
return if detected_version_string == "0" || !detected_version_string.match?(ConstraintHelper::VERSION_REGEX)
|
303
326
|
|
304
|
-
|
327
|
+
detected_version_string
|
305
328
|
end
|
306
329
|
|
307
330
|
sig { params(name: T.nilable(String)).returns(Ecosystem::VersionManager) }
|
@@ -332,7 +355,7 @@ module Dependabot
|
|
332
355
|
end
|
333
356
|
|
334
357
|
package_manager_class.new(
|
335
|
-
detected_version: detected_version
|
358
|
+
detected_version: detected_version,
|
336
359
|
raw_version: installed_version,
|
337
360
|
requirement: package_manager_requirement
|
338
361
|
)
|
@@ -394,10 +417,15 @@ module Dependabot
|
|
394
417
|
|
395
418
|
Dependabot.logger.info("Installing \"#{name}@#{version}\"")
|
396
419
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
401
429
|
end
|
402
430
|
|
403
431
|
sig { params(name: T.nilable(String)).returns(String) }
|
@@ -434,7 +462,8 @@ module Dependabot
|
|
434
462
|
return if @package_json.nil?
|
435
463
|
|
436
464
|
version_selector = VersionSelector.new
|
437
|
-
|
465
|
+
|
466
|
+
engine_versions = version_selector.setup(@package_json, name, dependabot_versions(name))
|
438
467
|
|
439
468
|
return if engine_versions.empty?
|
440
469
|
|
@@ -442,6 +471,20 @@ module Dependabot
|
|
442
471
|
Dependabot.logger.info("Returned (#{MANIFEST_ENGINES_KEY}) info \"#{name}\" : \"#{version}\"")
|
443
472
|
version
|
444
473
|
end
|
474
|
+
|
475
|
+
sig { params(name: String).returns(T.nilable(T::Array[Dependabot::Version])) }
|
476
|
+
def dependabot_versions(name)
|
477
|
+
case name
|
478
|
+
when "npm"
|
479
|
+
NpmPackageManager::SUPPORTED_VERSIONS
|
480
|
+
when "yarn"
|
481
|
+
YarnPackageManager::SUPPORTED_VERSIONS
|
482
|
+
when "bun"
|
483
|
+
BunPackageManager::SUPPORTED_VERSIONS
|
484
|
+
when "pnpm"
|
485
|
+
PNPMPackageManager::SUPPORTED_VERSIONS
|
486
|
+
end
|
487
|
+
end
|
445
488
|
end
|
446
489
|
end
|
447
490
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require "dependabot/shared_helpers"
|
5
|
+
require "dependabot/npm_and_yarn/constraint_helper"
|
5
6
|
|
6
7
|
module Dependabot
|
7
8
|
module NpmAndYarn
|
@@ -13,18 +14,42 @@ module Dependabot
|
|
13
14
|
# such as "20.8.7", "8.1.2", "8.21.2",
|
14
15
|
NODE_ENGINE_SUPPORTED_REGEX = /^\d+(?:\.\d+)*$/
|
15
16
|
|
16
|
-
|
17
|
-
|
17
|
+
# Sets up engine versions from the given manifest JSON.
|
18
|
+
#
|
19
|
+
# @param manifest_json [Hash] The manifest JSON containing version information.
|
20
|
+
# @param name [String] The engine name to match.
|
21
|
+
# @return [Hash] A hash with selected versions, if found.
|
22
|
+
sig do
|
23
|
+
params(
|
24
|
+
manifest_json: T::Hash[String, T.untyped],
|
25
|
+
name: String,
|
26
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
27
|
+
)
|
28
|
+
.returns(T::Hash[Symbol, T.untyped])
|
29
|
+
end
|
30
|
+
def setup(manifest_json, name, dependabot_versions = nil)
|
18
31
|
engine_versions = manifest_json["engines"]
|
19
32
|
|
33
|
+
# Return an empty hash if no engine versions are specified
|
20
34
|
return {} if engine_versions.nil?
|
21
35
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
36
|
+
versions = {}
|
37
|
+
|
38
|
+
if Dependabot::Experiments.enabled?(:enable_engine_version_detection)
|
39
|
+
engine_versions.each do |engine, value|
|
40
|
+
next unless engine.to_s.match(name)
|
41
|
+
|
42
|
+
versions[name] = ConstraintHelper.find_highest_version_from_constraint_expression(
|
43
|
+
value, dependabot_versions
|
44
|
+
)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
versions = engine_versions.select do |engine, value|
|
48
|
+
engine.to_s.match(name) && valid_extracted_version?(value)
|
49
|
+
end
|
50
|
+
end
|
26
51
|
|
27
|
-
|
52
|
+
versions
|
28
53
|
end
|
29
54
|
|
30
55
|
sig { params(version: String).returns(T::Boolean) }
|