dependabot-javascript 0.296.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/lib/dependabot/bun.rb +49 -0
  3. data/lib/dependabot/javascript/bun/file_fetcher.rb +77 -0
  4. data/lib/dependabot/javascript/bun/file_parser/bun_lock.rb +156 -0
  5. data/lib/dependabot/javascript/bun/file_parser/lockfile_parser.rb +55 -0
  6. data/lib/dependabot/javascript/bun/file_parser.rb +74 -0
  7. data/lib/dependabot/javascript/bun/file_updater/lockfile_updater.rb +138 -0
  8. data/lib/dependabot/javascript/bun/file_updater.rb +75 -0
  9. data/lib/dependabot/javascript/bun/helpers.rb +72 -0
  10. data/lib/dependabot/javascript/bun/package_manager.rb +48 -0
  11. data/lib/dependabot/javascript/bun/requirement.rb +11 -0
  12. data/lib/dependabot/javascript/bun/update_checker/conflicting_dependency_resolver.rb +64 -0
  13. data/lib/dependabot/javascript/bun/update_checker/dependency_files_builder.rb +47 -0
  14. data/lib/dependabot/javascript/bun/update_checker/latest_version_finder.rb +450 -0
  15. data/lib/dependabot/javascript/bun/update_checker/library_detector.rb +76 -0
  16. data/lib/dependabot/javascript/bun/update_checker/requirements_updater.rb +203 -0
  17. data/lib/dependabot/javascript/bun/update_checker/subdependency_version_resolver.rb +144 -0
  18. data/lib/dependabot/javascript/bun/update_checker/version_resolver.rb +525 -0
  19. data/lib/dependabot/javascript/bun/update_checker/vulnerability_auditor.rb +165 -0
  20. data/lib/dependabot/javascript/bun/update_checker.rb +440 -0
  21. data/lib/dependabot/javascript/bun/version.rb +11 -0
  22. data/lib/dependabot/javascript/shared/constraint_helper.rb +359 -0
  23. data/lib/dependabot/javascript/shared/dependency_files_filterer.rb +164 -0
  24. data/lib/dependabot/javascript/shared/file_fetcher.rb +283 -0
  25. data/lib/dependabot/javascript/shared/file_parser/lockfile_parser.rb +106 -0
  26. data/lib/dependabot/javascript/shared/file_parser.rb +454 -0
  27. data/lib/dependabot/javascript/shared/file_updater/npmrc_builder.rb +394 -0
  28. data/lib/dependabot/javascript/shared/file_updater/package_json_preparer.rb +87 -0
  29. data/lib/dependabot/javascript/shared/file_updater/package_json_updater.rb +376 -0
  30. data/lib/dependabot/javascript/shared/file_updater.rb +179 -0
  31. data/lib/dependabot/javascript/shared/language.rb +45 -0
  32. data/lib/dependabot/javascript/shared/metadata_finder.rb +209 -0
  33. data/lib/dependabot/javascript/shared/native_helpers.rb +21 -0
  34. data/lib/dependabot/javascript/shared/package_manager_detector.rb +72 -0
  35. data/lib/dependabot/javascript/shared/package_name.rb +118 -0
  36. data/lib/dependabot/javascript/shared/registry_helper.rb +190 -0
  37. data/lib/dependabot/javascript/shared/registry_parser.rb +93 -0
  38. data/lib/dependabot/javascript/shared/requirement.rb +144 -0
  39. data/lib/dependabot/javascript/shared/sub_dependency_files_filterer.rb +79 -0
  40. data/lib/dependabot/javascript/shared/update_checker/dependency_files_builder.rb +87 -0
  41. data/lib/dependabot/javascript/shared/update_checker/registry_finder.rb +358 -0
  42. data/lib/dependabot/javascript/shared/version.rb +133 -0
  43. data/lib/dependabot/javascript/shared/version_selector.rb +60 -0
  44. data/lib/dependabot/javascript.rb +39 -0
  45. 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