dependabot-javascript 0.296.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/dependabot/bun.rb +49 -0
- data/lib/dependabot/javascript/bun/file_fetcher.rb +77 -0
- data/lib/dependabot/javascript/bun/file_parser/bun_lock.rb +156 -0
- data/lib/dependabot/javascript/bun/file_parser/lockfile_parser.rb +55 -0
- data/lib/dependabot/javascript/bun/file_parser.rb +74 -0
- data/lib/dependabot/javascript/bun/file_updater/lockfile_updater.rb +138 -0
- data/lib/dependabot/javascript/bun/file_updater.rb +75 -0
- data/lib/dependabot/javascript/bun/helpers.rb +72 -0
- data/lib/dependabot/javascript/bun/package_manager.rb +48 -0
- data/lib/dependabot/javascript/bun/requirement.rb +11 -0
- data/lib/dependabot/javascript/bun/update_checker/conflicting_dependency_resolver.rb +64 -0
- data/lib/dependabot/javascript/bun/update_checker/dependency_files_builder.rb +47 -0
- data/lib/dependabot/javascript/bun/update_checker/latest_version_finder.rb +450 -0
- data/lib/dependabot/javascript/bun/update_checker/library_detector.rb +76 -0
- data/lib/dependabot/javascript/bun/update_checker/requirements_updater.rb +203 -0
- data/lib/dependabot/javascript/bun/update_checker/subdependency_version_resolver.rb +144 -0
- data/lib/dependabot/javascript/bun/update_checker/version_resolver.rb +525 -0
- data/lib/dependabot/javascript/bun/update_checker/vulnerability_auditor.rb +165 -0
- data/lib/dependabot/javascript/bun/update_checker.rb +440 -0
- data/lib/dependabot/javascript/bun/version.rb +11 -0
- data/lib/dependabot/javascript/shared/constraint_helper.rb +359 -0
- data/lib/dependabot/javascript/shared/dependency_files_filterer.rb +164 -0
- data/lib/dependabot/javascript/shared/file_fetcher.rb +283 -0
- data/lib/dependabot/javascript/shared/file_parser/lockfile_parser.rb +106 -0
- data/lib/dependabot/javascript/shared/file_parser.rb +454 -0
- data/lib/dependabot/javascript/shared/file_updater/npmrc_builder.rb +394 -0
- data/lib/dependabot/javascript/shared/file_updater/package_json_preparer.rb +87 -0
- data/lib/dependabot/javascript/shared/file_updater/package_json_updater.rb +376 -0
- data/lib/dependabot/javascript/shared/file_updater.rb +179 -0
- data/lib/dependabot/javascript/shared/language.rb +45 -0
- data/lib/dependabot/javascript/shared/metadata_finder.rb +209 -0
- data/lib/dependabot/javascript/shared/native_helpers.rb +21 -0
- data/lib/dependabot/javascript/shared/package_manager_detector.rb +72 -0
- data/lib/dependabot/javascript/shared/package_name.rb +118 -0
- data/lib/dependabot/javascript/shared/registry_helper.rb +190 -0
- data/lib/dependabot/javascript/shared/registry_parser.rb +93 -0
- data/lib/dependabot/javascript/shared/requirement.rb +144 -0
- data/lib/dependabot/javascript/shared/sub_dependency_files_filterer.rb +79 -0
- data/lib/dependabot/javascript/shared/update_checker/dependency_files_builder.rb +87 -0
- data/lib/dependabot/javascript/shared/update_checker/registry_finder.rb +358 -0
- data/lib/dependabot/javascript/shared/version.rb +133 -0
- data/lib/dependabot/javascript/shared/version_selector.rb +60 -0
- data/lib/dependabot/javascript.rb +39 -0
- metadata +327 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "excon"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Javascript
|
8
|
+
module Shared
|
9
|
+
module UpdateChecker
|
10
|
+
class RegistryFinder
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
CENTRAL_REGISTRIES = %w(
|
14
|
+
https://registry.npmjs.org
|
15
|
+
http://registry.npmjs.org
|
16
|
+
https://registry.yarnpkg.com
|
17
|
+
http://registry.yarnpkg.com
|
18
|
+
).freeze
|
19
|
+
NPM_AUTH_TOKEN_REGEX = %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}
|
20
|
+
NPM_GLOBAL_REGISTRY_REGEX = /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
21
|
+
YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+((['"](?<registry>.*)['"])|(?<registry>.*))/
|
22
|
+
NPM_SCOPED_REGISTRY_REGEX = /^(?<scope>@[^:]+)\s*:registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
23
|
+
YARN_SCOPED_REGISTRY_REGEX = /['"](?<scope>@[^:]+):registry['"]\s((['"](?<registry>.*)['"])|(?<registry>.*))/
|
24
|
+
|
25
|
+
Registry = T.type_alias { String }
|
26
|
+
RegistrySyntax = T.type_alias { T.any(Regexp, String) }
|
27
|
+
|
28
|
+
sig do
|
29
|
+
params(
|
30
|
+
dependency: T.nilable(Dependency),
|
31
|
+
credentials: T::Array[Credential],
|
32
|
+
rc_file: T.nilable(DependencyFile)
|
33
|
+
).void
|
34
|
+
end
|
35
|
+
def initialize(dependency:, credentials:, rc_file:)
|
36
|
+
@dependency = dependency
|
37
|
+
@credentials = credentials
|
38
|
+
@rc_file = rc_file
|
39
|
+
|
40
|
+
@npmrc_file = T.let(
|
41
|
+
rc_file&.name&.end_with?(".npmrc") ? rc_file : nil,
|
42
|
+
T.nilable(DependencyFile)
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
sig { returns(T.nilable(Registry)) }
|
47
|
+
def registry
|
48
|
+
@registry ||= T.let(
|
49
|
+
locked_registry || configured_registry || first_registry_with_dependency_details,
|
50
|
+
T.nilable(Registry)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { returns(T::Hash[String, String]) }
|
55
|
+
def auth_headers
|
56
|
+
auth_header_for(auth_token)
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { returns(String) }
|
60
|
+
def dependency_url
|
61
|
+
"#{registry_url}/#{escaped_dependency_name}"
|
62
|
+
end
|
63
|
+
|
64
|
+
sig { params(version: Version).returns(String) }
|
65
|
+
def tarball_url(version)
|
66
|
+
version_without_build_metadata = version.to_s.gsub(/\+.*/, "")
|
67
|
+
|
68
|
+
# Dependency name needs to be unescaped since tarball URLs don't always work with escaped slashes
|
69
|
+
"#{registry_url}/#{dependency&.name}/-/#{scopeless_name}-#{version_without_build_metadata}.tgz"
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(registry: String).returns(T::Boolean) }
|
73
|
+
def self.central_registry?(registry)
|
74
|
+
CENTRAL_REGISTRIES.any? do |r|
|
75
|
+
r.include?(registry)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
sig { params(dependency_name: String).returns(T.nilable(String)) }
|
80
|
+
def registry_from_rc(dependency_name)
|
81
|
+
explicit_registry_from_rc(dependency_name) || global_registry
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
sig { returns(T.nilable(Dependency)) }
|
87
|
+
attr_reader :dependency
|
88
|
+
|
89
|
+
sig { returns(T::Array[Credential]) }
|
90
|
+
attr_reader :credentials
|
91
|
+
|
92
|
+
sig { returns(T.nilable(DependencyFile)) }
|
93
|
+
attr_reader :rc_file
|
94
|
+
|
95
|
+
sig { returns(T.nilable(DependencyFile)) }
|
96
|
+
attr_reader :npmrc_file
|
97
|
+
|
98
|
+
sig { params(dependency_name: T.nilable(String)).returns(T.nilable(String)) }
|
99
|
+
def explicit_registry_from_rc(dependency_name)
|
100
|
+
if dependency_name&.start_with?("@") && dependency_name.include?("/")
|
101
|
+
scope = dependency_name.split("/").first
|
102
|
+
scoped_registry(scope) || configured_global_registry
|
103
|
+
else
|
104
|
+
configured_global_registry
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
sig { returns(T.nilable(Registry)) }
|
109
|
+
def first_registry_with_dependency_details
|
110
|
+
@first_registry_with_dependency_details ||= T.let(
|
111
|
+
known_registries.find do |details|
|
112
|
+
url = "#{details['registry'].gsub(%r{/+$}, '')}/#{escaped_dependency_name}"
|
113
|
+
url = "https://#{url}" unless url.start_with?("http")
|
114
|
+
response = Dependabot::RegistryClient.get(
|
115
|
+
url: url,
|
116
|
+
headers: auth_header_for(details["token"])
|
117
|
+
)
|
118
|
+
response.status < 400 && JSON.parse(response.body)
|
119
|
+
rescue Excon::Error::Timeout,
|
120
|
+
Excon::Error::Socket,
|
121
|
+
JSON::ParserError
|
122
|
+
nil
|
123
|
+
rescue URI::InvalidURIError => e
|
124
|
+
raise DependencyFileNotResolvable, e.message
|
125
|
+
end&.fetch("registry"),
|
126
|
+
T.nilable(Registry)
|
127
|
+
)
|
128
|
+
|
129
|
+
@first_registry_with_dependency_details ||= global_registry.to_s.sub(%r{/+$}, "").sub(%r{^.*?//}, "")
|
130
|
+
end
|
131
|
+
|
132
|
+
sig { returns(T.nilable(String)) }
|
133
|
+
def registry_url
|
134
|
+
url =
|
135
|
+
if registry&.start_with?("http")
|
136
|
+
registry
|
137
|
+
else
|
138
|
+
protocol =
|
139
|
+
if registry_source_url
|
140
|
+
registry_source_url&.split("://")&.first
|
141
|
+
else
|
142
|
+
"https"
|
143
|
+
end
|
144
|
+
|
145
|
+
"#{protocol}://#{registry}"
|
146
|
+
end
|
147
|
+
|
148
|
+
url&.gsub(%r{/+$}, "")
|
149
|
+
end
|
150
|
+
|
151
|
+
sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) }
|
152
|
+
def auth_header_for(token)
|
153
|
+
return {} unless token
|
154
|
+
|
155
|
+
if token.include?(":")
|
156
|
+
encoded_token = Base64.encode64(token).delete("\n")
|
157
|
+
{ "Authorization" => "Basic #{encoded_token}" }
|
158
|
+
elsif Base64.decode64(token).ascii_only? &&
|
159
|
+
Base64.decode64(token).include?(":")
|
160
|
+
{ "Authorization" => "Basic #{token.delete("\n")}" }
|
161
|
+
else
|
162
|
+
{ "Authorization" => "Bearer #{token}" }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
sig { returns(T.nilable(String)) }
|
167
|
+
def auth_token
|
168
|
+
known_registries
|
169
|
+
.find { |cred| cred["registry"] == registry }
|
170
|
+
&.fetch("token", nil)
|
171
|
+
end
|
172
|
+
|
173
|
+
sig { returns(T.nilable(Registry)) }
|
174
|
+
def locked_registry
|
175
|
+
return unless registry_source_url
|
176
|
+
|
177
|
+
lockfile_registry =
|
178
|
+
T.must(registry_source_url)
|
179
|
+
.gsub("https://", "")
|
180
|
+
.gsub("http://", "")
|
181
|
+
detailed_registry =
|
182
|
+
known_registries
|
183
|
+
.find { |h| h["registry"].include?(lockfile_registry) }
|
184
|
+
&.fetch("registry")
|
185
|
+
|
186
|
+
detailed_registry || lockfile_registry
|
187
|
+
end
|
188
|
+
|
189
|
+
sig { returns(T.nilable(String)) }
|
190
|
+
def configured_registry
|
191
|
+
configured_registry_url = explicit_registry_from_rc(dependency&.name)
|
192
|
+
return unless configured_registry_url
|
193
|
+
|
194
|
+
normalize_configured_registry(configured_registry_url)
|
195
|
+
end
|
196
|
+
|
197
|
+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
|
198
|
+
def known_registries
|
199
|
+
@known_registries ||= T.let(
|
200
|
+
begin
|
201
|
+
registries = []
|
202
|
+
registries += credentials
|
203
|
+
.select { |cred| cred["type"] == "npm_registry" && cred["registry"] }
|
204
|
+
.tap { |arr| arr.each { |c| c["token"] ||= nil } }
|
205
|
+
registries += npmrc_registries
|
206
|
+
|
207
|
+
unique_registries(registries)
|
208
|
+
end,
|
209
|
+
T.nilable(T::Array[T::Hash[String, T.untyped]])
|
210
|
+
)
|
211
|
+
end
|
212
|
+
|
213
|
+
sig { returns(T::Array[T::Hash[String, T.untyped]]) }
|
214
|
+
def npmrc_registries
|
215
|
+
return [] unless npmrc_file
|
216
|
+
|
217
|
+
registries = []
|
218
|
+
T.must(npmrc_file).content&.scan(NPM_AUTH_TOKEN_REGEX) do
|
219
|
+
next if Regexp.last_match&.[](:registry)&.include?("${")
|
220
|
+
|
221
|
+
registry = T.must(Regexp.last_match)[:registry]
|
222
|
+
token = T.must(Regexp.last_match)[:token]&.strip
|
223
|
+
|
224
|
+
registries << {
|
225
|
+
"type" => "npm_registry",
|
226
|
+
"registry" => registry&.gsub(/\s+/, "%20"),
|
227
|
+
"token" => token
|
228
|
+
}
|
229
|
+
end
|
230
|
+
|
231
|
+
registries += npmrc_global_registries
|
232
|
+
end
|
233
|
+
|
234
|
+
sig { params(registries: T::Array[T::Hash[String, T.untyped]]).returns(T::Array[T::Hash[String, T.untyped]]) }
|
235
|
+
def unique_registries(registries)
|
236
|
+
registries.uniq.reject do |registry|
|
237
|
+
next if registry["token"]
|
238
|
+
|
239
|
+
# Reject this entry if an identical one with a token exists
|
240
|
+
registries.any? do |r|
|
241
|
+
r["token"] && r["registry"] == registry["registry"]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
sig { returns(T.nilable(String)) }
|
247
|
+
def global_registry
|
248
|
+
return @global_registry if defined? @global_registry
|
249
|
+
|
250
|
+
@global_registry ||= T.let(configured_global_registry || "https://registry.npmjs.org", T.nilable(String))
|
251
|
+
end
|
252
|
+
|
253
|
+
sig { returns(T.nilable(String)) }
|
254
|
+
def configured_global_registry
|
255
|
+
return @configured_global_registry if defined? @configured_global_registry
|
256
|
+
|
257
|
+
@configured_global_registry = T.let(
|
258
|
+
npmrc_file && npmrc_global_registries.first&.fetch("url"),
|
259
|
+
T.nilable(String)
|
260
|
+
)
|
261
|
+
return @configured_global_registry if @configured_global_registry
|
262
|
+
|
263
|
+
replaces_base = credentials.find { |cred| cred["type"] == "npm_registry" && cred.replaces_base? }
|
264
|
+
if replaces_base
|
265
|
+
registry = replaces_base["registry"]
|
266
|
+
registry = "https://#{registry}" unless registry&.start_with?("http")
|
267
|
+
return @configured_global_registry = registry
|
268
|
+
end
|
269
|
+
|
270
|
+
@configured_global_registry = nil
|
271
|
+
end
|
272
|
+
|
273
|
+
sig { returns(T::Array[T::Hash[String, T.nilable(String)]]) }
|
274
|
+
def npmrc_global_registries
|
275
|
+
return [] unless npmrc_file
|
276
|
+
|
277
|
+
global_rc_registries(T.must(npmrc_file), syntax: NPM_GLOBAL_REGISTRY_REGEX)
|
278
|
+
end
|
279
|
+
|
280
|
+
sig { params(scope: T.nilable(String)).returns(T.nilable(String)) }
|
281
|
+
def scoped_registry(scope)
|
282
|
+
scoped_rc_registry(npmrc_file, syntax: NPM_SCOPED_REGISTRY_REGEX, scope: scope)
|
283
|
+
end
|
284
|
+
|
285
|
+
sig do
|
286
|
+
params(file: DependencyFile, syntax: RegistrySyntax)
|
287
|
+
.returns(T::Array[T::Hash[String, T.nilable(String)]])
|
288
|
+
end
|
289
|
+
def global_rc_registries(file, syntax:)
|
290
|
+
registries = []
|
291
|
+
|
292
|
+
file.content&.scan(syntax) do
|
293
|
+
next if Regexp.last_match&.[](:registry)&.include?("${")
|
294
|
+
|
295
|
+
url = T.must(T.must(Regexp.last_match)[:registry]).strip
|
296
|
+
registry = normalize_configured_registry(url)
|
297
|
+
registries << {
|
298
|
+
"type" => "npm_registry",
|
299
|
+
"registry" => registry,
|
300
|
+
"url" => url,
|
301
|
+
"token" => nil
|
302
|
+
}
|
303
|
+
end
|
304
|
+
|
305
|
+
registries
|
306
|
+
end
|
307
|
+
|
308
|
+
sig do
|
309
|
+
params(file: T.nilable(DependencyFile), syntax: RegistrySyntax, scope: T.nilable(String))
|
310
|
+
.returns(T.nilable(String))
|
311
|
+
end
|
312
|
+
def scoped_rc_registry(file, syntax:, scope:)
|
313
|
+
file&.content.to_s.scan(syntax) do
|
314
|
+
next if Regexp.last_match&.[](:registry)&.include?("${") || Regexp.last_match&.[](:scope) != scope
|
315
|
+
|
316
|
+
return T.must(T.must(Regexp.last_match)[:registry]).strip
|
317
|
+
end
|
318
|
+
|
319
|
+
nil
|
320
|
+
end
|
321
|
+
|
322
|
+
# npm registries expect slashes to be escaped
|
323
|
+
sig { returns(T.nilable(String)) }
|
324
|
+
def escaped_dependency_name
|
325
|
+
return unless dependency
|
326
|
+
|
327
|
+
T.must(dependency).name.gsub("/", "%2F")
|
328
|
+
end
|
329
|
+
|
330
|
+
sig { returns(T.nilable(String)) }
|
331
|
+
def scopeless_name
|
332
|
+
return unless dependency
|
333
|
+
|
334
|
+
T.must(dependency).name.split("/").last
|
335
|
+
end
|
336
|
+
|
337
|
+
sig { returns(T.nilable(String)) }
|
338
|
+
def registry_source_url
|
339
|
+
return unless dependency
|
340
|
+
|
341
|
+
sources = T.must(dependency).requirements
|
342
|
+
.map { |r| r.fetch(:source) }.uniq.compact
|
343
|
+
.sort_by { |source| self.class.central_registry?(source[:url]) ? 1 : 0 }
|
344
|
+
|
345
|
+
sources.find { |s| s[:type] == "registry" }&.fetch(:url)
|
346
|
+
end
|
347
|
+
|
348
|
+
sig { params(url: String).returns(String) }
|
349
|
+
def normalize_configured_registry(url)
|
350
|
+
url.sub(%r{/+$}, "")
|
351
|
+
.sub(%r{^.*?//}, "")
|
352
|
+
.gsub(/\s+/, "%20")
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# JavaScript pre-release versions use 1.0.1-rc1 syntax, which Gem::Version
|
5
|
+
# converts into 1.0.1.pre.rc1. We override the `to_s` method to stop that
|
6
|
+
# alteration.
|
7
|
+
#
|
8
|
+
# See https://semver.org/ for details of node's version syntax.
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Javascript
|
12
|
+
module Shared
|
13
|
+
class Version < Dependabot::Version
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
sig { returns(T.nilable(String)) }
|
17
|
+
attr_reader :build_info
|
18
|
+
|
19
|
+
# These are possible npm versioning tags that can be used in place of a version.
|
20
|
+
# See https://docs.npmjs.com/cli/v10/commands/npm-dist-tag#purpose for more details.
|
21
|
+
VERSION_TAGS = T.let([
|
22
|
+
"alpha", # Alpha version, early testing phase
|
23
|
+
"beta", # Beta version, more stable than alpha
|
24
|
+
"canary", # Canary version, often used for cutting-edge builds
|
25
|
+
"dev", # Development version, ongoing development
|
26
|
+
"experimental", # Experimental version, unstable and new features
|
27
|
+
"latest", # Latest stable version, used by npm to identify the current version of a package
|
28
|
+
"legacy", # Legacy version, older version maintained for compatibility
|
29
|
+
"next", # Next version, used by some projects to identify the upcoming version
|
30
|
+
"nightly", # Nightly build, daily builds often including latest changes
|
31
|
+
"rc", # Release candidate, potential final version
|
32
|
+
"release", # General release version
|
33
|
+
"stable" # Stable version, thoroughly tested and stable
|
34
|
+
].freeze.map(&:freeze), T::Array[String])
|
35
|
+
|
36
|
+
VERSION_PATTERN = T.let(Gem::Version::VERSION_PATTERN + '(\+[0-9a-zA-Z\-.]+)?', String)
|
37
|
+
ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/
|
38
|
+
|
39
|
+
sig { override.params(version: VersionParameter).returns(T::Boolean) }
|
40
|
+
def self.correct?(version)
|
41
|
+
version = version.gsub(/^v/, "") if version.is_a?(String)
|
42
|
+
|
43
|
+
return false if version.nil?
|
44
|
+
|
45
|
+
version.to_s.match?(ANCHORED_VERSION_PATTERN)
|
46
|
+
end
|
47
|
+
|
48
|
+
sig { params(version: VersionParameter).returns(VersionParameter) }
|
49
|
+
def self.semver_for(version)
|
50
|
+
# The next two lines are to guard against improperly formatted
|
51
|
+
# versions in a lockfile, such as an empty string or additional
|
52
|
+
# characters. NPM/yarn fixes these when running an update, so we can
|
53
|
+
# safely ignore these versions.
|
54
|
+
return if version == ""
|
55
|
+
return unless correct?(version)
|
56
|
+
|
57
|
+
version
|
58
|
+
end
|
59
|
+
|
60
|
+
sig { override.params(version: VersionParameter).void }
|
61
|
+
def initialize(version)
|
62
|
+
version = clean_version(version)
|
63
|
+
|
64
|
+
@version_string = T.let(version.to_s, String)
|
65
|
+
|
66
|
+
@build_info = T.let(nil, T.nilable(String))
|
67
|
+
|
68
|
+
version, @build_info = version.to_s.split("+") if version.to_s.include?("+")
|
69
|
+
|
70
|
+
super(T.must(version))
|
71
|
+
end
|
72
|
+
|
73
|
+
sig { params(version: VersionParameter).returns(VersionParameter) }
|
74
|
+
def clean_version(version)
|
75
|
+
# Check if version is a string before attempting to match
|
76
|
+
if version.is_a?(String)
|
77
|
+
# Matches @ followed by x.y.z (digits separated by dots)
|
78
|
+
if (match = version.match(/@(\d+\.\d+\.\d+)/))
|
79
|
+
version = match[1] # Just "4.5.3"
|
80
|
+
|
81
|
+
# Extract version in case the output contains Corepack verbose data
|
82
|
+
elsif version.include?("Corepack")
|
83
|
+
version = T.must(T.must(version.tr("\n", " ").match(/(\d+\.\d+\.\d+)/))[-1])
|
84
|
+
end
|
85
|
+
version = version&.gsub(/^v/, "")
|
86
|
+
end
|
87
|
+
|
88
|
+
version
|
89
|
+
end
|
90
|
+
|
91
|
+
sig { override.params(version: VersionParameter).returns(Version) }
|
92
|
+
def self.new(version)
|
93
|
+
T.cast(super, Version)
|
94
|
+
end
|
95
|
+
|
96
|
+
sig { returns(Integer) }
|
97
|
+
def major
|
98
|
+
@major ||= T.let(segments[0].to_i, T.nilable(Integer))
|
99
|
+
end
|
100
|
+
|
101
|
+
sig { returns(Integer) }
|
102
|
+
def minor
|
103
|
+
@minor ||= T.let(segments[1].to_i, T.nilable(Integer))
|
104
|
+
end
|
105
|
+
|
106
|
+
sig { returns(Integer) }
|
107
|
+
def patch
|
108
|
+
@patch ||= T.let(segments[2].to_i, T.nilable(Integer))
|
109
|
+
end
|
110
|
+
|
111
|
+
sig { params(other: Version).returns(T::Boolean) }
|
112
|
+
def backwards_compatible_with?(other)
|
113
|
+
case major
|
114
|
+
when 0
|
115
|
+
self == other
|
116
|
+
else
|
117
|
+
major == other.major && minor >= other.minor
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { override.returns(String) }
|
122
|
+
def to_s
|
123
|
+
@version_string
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { override.returns(String) }
|
127
|
+
def inspect
|
128
|
+
"#<#{self.class} #{@version_string}>"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Dependabot
|
5
|
+
module Javascript
|
6
|
+
module Shared
|
7
|
+
class VersionSelector
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
|
11
|
+
# For limited testing, allowing only specific versions defined in engines in package.json
|
12
|
+
# such as "20.8.7", "8.1.2", "8.21.2",
|
13
|
+
NODE_ENGINE_SUPPORTED_REGEX = /^\d+(?:\.\d+)*$/
|
14
|
+
|
15
|
+
# Sets up engine versions from the given manifest JSON.
|
16
|
+
#
|
17
|
+
# @param manifest_json [Hash] The manifest JSON containing version information.
|
18
|
+
# @param name [String] The engine name to match.
|
19
|
+
# @return [Hash] A hash with selected versions, if found.
|
20
|
+
sig do
|
21
|
+
params(
|
22
|
+
manifest_json: T::Hash[String, T.untyped],
|
23
|
+
name: String,
|
24
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
25
|
+
)
|
26
|
+
.returns(T::Hash[Symbol, T.untyped])
|
27
|
+
end
|
28
|
+
def setup(manifest_json, name, dependabot_versions = nil)
|
29
|
+
engine_versions = manifest_json["engines"]
|
30
|
+
|
31
|
+
# Return an empty hash if no engine versions are specified
|
32
|
+
return {} if engine_versions.nil?
|
33
|
+
|
34
|
+
versions = {}
|
35
|
+
|
36
|
+
if Dependabot::Experiments.enabled?(:enable_engine_version_detection)
|
37
|
+
engine_versions.each do |engine, value|
|
38
|
+
next unless engine.to_s.match(name)
|
39
|
+
|
40
|
+
versions[name] = ConstraintHelper.find_highest_version_from_constraint_expression(
|
41
|
+
value, dependabot_versions
|
42
|
+
)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
versions = engine_versions.select do |engine, value|
|
46
|
+
engine.to_s.match(name) && valid_extracted_version?(value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
versions
|
51
|
+
end
|
52
|
+
|
53
|
+
sig { params(version: String).returns(T::Boolean) }
|
54
|
+
def valid_extracted_version?(version)
|
55
|
+
version.match?(NODE_ENGINE_SUPPORTED_REGEX)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strong
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dependabot/bun"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module Javascript
|
8
|
+
DEFAULT_PACKAGE_MANAGER = "npm"
|
9
|
+
ERROR_MALFORMED_VERSION_NUMBER = "Malformed version number"
|
10
|
+
MANIFEST_ENGINES_KEY = "engines"
|
11
|
+
MANIFEST_FILENAME = "package.json"
|
12
|
+
MANIFEST_PACKAGE_MANAGER_KEY = "packageManager"
|
13
|
+
|
14
|
+
# Define a type alias for the expected class interface
|
15
|
+
JavascriptPackageManagerClassType = T.type_alias do
|
16
|
+
T.class_of(Bun::PackageManager)
|
17
|
+
end
|
18
|
+
|
19
|
+
PACKAGE_MANAGER_CLASSES = T.let({
|
20
|
+
Bun::PackageManager::NAME => Bun::PackageManager
|
21
|
+
}.freeze, T::Hash[String, JavascriptPackageManagerClassType])
|
22
|
+
|
23
|
+
PACKAGE_MANAGER_VERSION_REGEX = /
|
24
|
+
^ # Start of string
|
25
|
+
(?<major>\d+) # Major version (required, numeric)
|
26
|
+
\. # Separator between major and minor versions
|
27
|
+
(?<minor>\d+) # Minor version (required, numeric)
|
28
|
+
\. # Separator between minor and patch versions
|
29
|
+
(?<patch>\d+) # Patch version (required, numeric)
|
30
|
+
( # Start pre-release section
|
31
|
+
-(?<pre_release>[a-zA-Z0-9.]+) # Pre-release label (optional, alphanumeric or dot-separated)
|
32
|
+
)?
|
33
|
+
( # Start build metadata section
|
34
|
+
\+(?<build>[a-zA-Z0-9.]+) # Build metadata (optional, alphanumeric or dot-separated)
|
35
|
+
)?
|
36
|
+
$ # End of string
|
37
|
+
/x # Extended mode for readability
|
38
|
+
end
|
39
|
+
end
|