dependabot-npm_and_yarn 0.292.0 → 0.294.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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/npm/vulnerability-auditor.js +16 -16
  3. data/helpers/lib/npm6/updater.js +1 -1
  4. data/lib/dependabot/npm_and_yarn/bun_package_manager.rb +46 -0
  5. data/lib/dependabot/npm_and_yarn/dependency_files_filterer.rb +2 -1
  6. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +61 -35
  7. data/lib/dependabot/npm_and_yarn/file_parser/bun_lock.rb +141 -0
  8. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +33 -27
  9. data/lib/dependabot/npm_and_yarn/file_parser/pnpm_lock.rb +47 -0
  10. data/lib/dependabot/npm_and_yarn/file_parser.rb +17 -9
  11. data/lib/dependabot/npm_and_yarn/file_updater/bun_lockfile_updater.rb +144 -0
  12. data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +127 -12
  13. data/lib/dependabot/npm_and_yarn/file_updater.rb +66 -0
  14. data/lib/dependabot/npm_and_yarn/helpers.rb +54 -2
  15. data/lib/dependabot/npm_and_yarn/language.rb +45 -0
  16. data/lib/dependabot/npm_and_yarn/npm_package_manager.rb +70 -0
  17. data/lib/dependabot/npm_and_yarn/package_manager.rb +16 -196
  18. data/lib/dependabot/npm_and_yarn/pnpm_package_manager.rb +55 -0
  19. data/lib/dependabot/npm_and_yarn/sub_dependency_files_filterer.rb +1 -0
  20. data/lib/dependabot/npm_and_yarn/update_checker/dependency_files_builder.rb +14 -7
  21. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +14 -0
  22. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +19 -0
  23. data/lib/dependabot/npm_and_yarn/version.rb +4 -0
  24. data/lib/dependabot/npm_and_yarn/yarn_package_manager.rb +56 -0
  25. metadata +12 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e406eab7c13be2bea1200de0103017da062fcd4eda7b30652cc697cf2529c2de
4
- data.tar.gz: c41b184b80a82577f5ed87eb4df0c0c4bff862350afe5f992b75f04ac6e69f96
3
+ metadata.gz: 72e338772b3c3aac3cf86538fc2d70dbbc45f5f7cb854cd7fd74913b140fe056
4
+ data.tar.gz: 1856c138b871ebe80e5cc6faa5984ba80c10b342aa8bf0822e325e5a31a3f815
5
5
  SHA512:
6
- metadata.gz: 535024739c08d5e33e7a53a300a75f16009c8227a27b27c8c758501b6328865db2ebeaaace0bc8ae94d5f199d93bd63f76f98164e1524df7896c22784aa04975
7
- data.tar.gz: e12a28a7d0933ad3fc4ccff35d36948e42b9ea9c884a132f7aed5bd9c33b67ad61037f0b29975c5fc04329a64ef7bcc8703ce3c684e4e278168706eecd1a37a7
6
+ metadata.gz: 0e3267d0aafcf35e345505c87a23b2b783cfc36377e46f5f10875a4bd8b0c4fe201b6dea0dbed75463b75dccba175014af850451cfc52b39c0a2a410a5c5ab34
7
+ data.tar.gz: a1c6be5ccbebcf43a76a51d7871a37743428f052c734a985a48fc90d4b1e2944679a8b6eb17910a8ef7e14c69dbe46d9761319b28d6a57d7dcbac7e94fbb9c09
@@ -97,9 +97,9 @@ async function findVulnerableDependencies(directory, advisories) {
97
97
 
98
98
  for (const group of groupedFixUpdateChains.values()) {
99
99
  const fixUpdateNode = group[0].nodes[0]
100
- const groupTopLevelAncestors = group.reduce((anc, chain) => {
100
+ const groupTopLevelAncestors = group.reduce((ancestor, chain) => {
101
101
  const topLevelNode = chain.nodes[chain.nodes.length - 1]
102
- return anc.add(topLevelNode.name)
102
+ return ancestor.add(topLevelNode.name)
103
103
  }, new Set())
104
104
 
105
105
  // Add group's top-level ancestors to the set of all top-level ancestors of
@@ -269,23 +269,23 @@ const maybeReadFile = file => {
269
269
  }
270
270
 
271
271
  function loadCACerts(npmConfig) {
272
- if (npmConfig.ca) {
273
- return npmConfig.ca
274
- }
272
+ if (npmConfig.ca) {
273
+ return npmConfig.ca
274
+ }
275
275
 
276
- if (!npmConfig.cafile) {
277
- return
278
- }
276
+ if (!npmConfig.cafile) {
277
+ return
278
+ }
279
279
 
280
- const raw = maybeReadFile(npmConfig.cafile)
281
- if (!raw) {
282
- return
283
- }
280
+ const raw = maybeReadFile(npmConfig.cafile)
281
+ if (!raw) {
282
+ return
283
+ }
284
284
 
285
- const delim = '-----END CERTIFICATE-----'
286
- return raw.replace(/\r\n/g, '\n').split(delim)
287
- .filter(section => section.trim())
288
- .map(section => section.trimStart() + delim)
285
+ const delim = '-----END CERTIFICATE-----'
286
+ return raw.replace(/\r\n/g, '\n').split(delim)
287
+ .filter(section => section.trim())
288
+ .map(section => section.trimStart() + delim)
289
289
  }
290
290
 
291
291
  module.exports = { findVulnerableDependencies }
@@ -113,7 +113,7 @@ function flattenAllDependencies(manifest) {
113
113
  );
114
114
  }
115
115
 
116
- // NOTE: Re-used in npm 7 updater
116
+ // NOTE: Reused in npm 7 updater
117
117
  function installArgs(
118
118
  depName,
119
119
  desiredVersion,
@@ -0,0 +1,46 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module NpmAndYarn
6
+ class BunPackageManager < Ecosystem::VersionManager
7
+ extend T::Sig
8
+ NAME = "bun"
9
+ LOCKFILE_NAME = "bun.lock"
10
+
11
+ # In Bun 1.1.39, the lockfile format was changed from a binary bun.lockb to a text-based bun.lock.
12
+ # https://bun.sh/blog/bun-lock-text-lockfile
13
+ MIN_SUPPORTED_VERSION = Version.new("1.1.39")
14
+ SUPPORTED_VERSIONS = T.let([MIN_SUPPORTED_VERSION].freeze, T::Array[Dependabot::Version])
15
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
16
+
17
+ sig do
18
+ params(
19
+ detected_version: T.nilable(String),
20
+ raw_version: T.nilable(String),
21
+ requirement: T.nilable(Dependabot::NpmAndYarn::Requirement)
22
+ ).void
23
+ end
24
+ def initialize(detected_version: nil, raw_version: nil, requirement: nil)
25
+ super(
26
+ name: NAME,
27
+ detected_version: detected_version ? Version.new(detected_version) : nil,
28
+ version: raw_version ? Version.new(raw_version) : nil,
29
+ deprecated_versions: DEPRECATED_VERSIONS,
30
+ supported_versions: SUPPORTED_VERSIONS,
31
+ requirement: requirement
32
+ )
33
+ end
34
+
35
+ sig { override.returns(T::Boolean) }
36
+ def deprecated?
37
+ false
38
+ end
39
+
40
+ sig { override.returns(T::Boolean) }
41
+ def unsupported?
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
@@ -82,7 +82,7 @@ module Dependabot
82
82
 
83
83
  sig { params(lockfile: DependencyFile).returns(T::Boolean) }
84
84
  def workspaces_lockfile?(lockfile)
85
- return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"].include?(lockfile.name)
85
+ return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock"].include?(lockfile.name)
86
86
 
87
87
  return false unless parsed_root_package_json["workspaces"] || dependency_files.any? do |file|
88
88
  file.name.end_with?("pnpm-workspace.yaml") && File.dirname(file.name) == File.dirname(lockfile.name)
@@ -148,6 +148,7 @@ module Dependabot
148
148
  "package-lock.json",
149
149
  "yarn.lock",
150
150
  "pnpm-lock.yaml",
151
+ "bun.lock",
151
152
  "npm-shrinkwrap.json"
152
153
  )
153
154
  end
@@ -68,6 +68,7 @@ module Dependabot
68
68
  package_managers["npm"] = npm_version if npm_version
69
69
  package_managers["yarn"] = yarn_version if yarn_version
70
70
  package_managers["pnpm"] = pnpm_version if pnpm_version
71
+ package_managers["bun"] = bun_version if bun_version
71
72
  package_managers["unknown"] = 1 if package_managers.empty?
72
73
 
73
74
  {
@@ -83,6 +84,7 @@ module Dependabot
83
84
  fetched_files += npm_files if npm_version
84
85
  fetched_files += yarn_files if yarn_version
85
86
  fetched_files += pnpm_files if pnpm_version
87
+ fetched_files += bun_files if bun_version
86
88
  fetched_files += lerna_files
87
89
  fetched_files += workspace_package_jsons
88
90
  fetched_files += path_dependencies(fetched_files)
@@ -120,6 +122,13 @@ module Dependabot
120
122
  fetched_pnpm_files
121
123
  end
122
124
 
125
+ sig { returns(T::Array[DependencyFile]) }
126
+ def bun_files
127
+ fetched_bun_files = []
128
+ fetched_bun_files << bun_lock if bun_lock
129
+ fetched_bun_files
130
+ end
131
+
123
132
  sig { returns(T::Array[DependencyFile]) }
124
133
  def lerna_files
125
134
  fetched_lerna_files = []
@@ -202,6 +211,16 @@ module Dependabot
202
211
  )
203
212
  end
204
213
 
214
+ sig { returns(T.nilable(T.any(Integer, String))) }
215
+ def bun_version
216
+ return @bun_version = nil unless allow_beta_ecosystems?
217
+
218
+ @bun_version ||= T.let(
219
+ package_manager_helper.setup(BunPackageManager::NAME),
220
+ T.nilable(T.any(Integer, String))
221
+ )
222
+ end
223
+
205
224
  sig { returns(PackageManagerHelper) }
206
225
  def package_manager_helper
207
226
  @package_manager_helper ||= T.let(
@@ -219,7 +238,8 @@ module Dependabot
219
238
  {
220
239
  npm: package_lock || shrinkwrap,
221
240
  yarn: yarn_lock,
222
- pnpm: pnpm_lock
241
+ pnpm: pnpm_lock,
242
+ bun: bun_lock
223
243
  }
224
244
  end
225
245
 
@@ -261,17 +281,18 @@ module Dependabot
261
281
 
262
282
  return @pnpm_lock if @pnpm_lock || directory == "/"
263
283
 
264
- # Loop through parent directories looking for a pnpm-lock
265
- (1..directory.split("/").count).each do |i|
266
- @pnpm_lock = fetch_file_from_host(("../" * i) + PNPMPackageManager::LOCKFILE_NAME)
267
- .tap { |f| f.support_file = true }
268
- break if @pnpm_lock
269
- rescue Dependabot::DependencyFileNotFound
270
- # Ignore errors (pnpm_lock.yaml may not be present)
271
- nil
272
- end
284
+ @pnpm_lock = fetch_file_from_parent_directories(PNPMPackageManager::LOCKFILE_NAME)
285
+ end
286
+
287
+ sig { returns(T.nilable(DependencyFile)) }
288
+ def bun_lock
289
+ return @bun_lock if defined?(@bun_lock)
290
+
291
+ @bun_lock ||= T.let(fetch_file_if_present(BunPackageManager::LOCKFILE_NAME), T.nilable(DependencyFile))
292
+
293
+ return @bun_lock if @bun_lock || directory == "/"
273
294
 
274
- @pnpm_lock
295
+ @bun_lock = fetch_file_from_parent_directories(BunPackageManager::LOCKFILE_NAME)
275
296
  end
276
297
 
277
298
  sig { returns(T.nilable(DependencyFile)) }
@@ -294,17 +315,7 @@ module Dependabot
294
315
 
295
316
  return @npmrc if @npmrc || directory == "/"
296
317
 
297
- # Loop through parent directories looking for an npmrc
298
- (1..directory.split("/").count).each do |i|
299
- @npmrc = fetch_file_from_host(("../" * i) + NpmPackageManager::RC_FILENAME)
300
- .tap { |f| f.support_file = true }
301
- break if @npmrc
302
- rescue Dependabot::DependencyFileNotFound
303
- # Ignore errors (.npmrc may not be present)
304
- nil
305
- end
306
-
307
- @npmrc
318
+ @npmrc = fetch_file_from_parent_directories(NpmPackageManager::RC_FILENAME)
308
319
  end
309
320
 
310
321
  sig { returns(T.nilable(DependencyFile)) }
@@ -315,17 +326,7 @@ module Dependabot
315
326
 
316
327
  return @yarnrc if @yarnrc || directory == "/"
317
328
 
318
- # Loop through parent directories looking for an yarnrc
319
- (1..directory.split("/").count).each do |i|
320
- @yarnrc = fetch_file_from_host(("../" * i) + YarnPackageManager::RC_FILENAME)
321
- .tap { |f| f.support_file = true }
322
- break if @yarnrc
323
- rescue Dependabot::DependencyFileNotFound
324
- # Ignore errors (.yarnrc may not be present)
325
- nil
326
- end
327
-
328
- @yarnrc
329
+ @yarnrc = fetch_file_from_parent_directories(YarnPackageManager::RC_FILENAME)
329
330
  end
330
331
 
331
332
  sig { returns(T.nilable(DependencyFile)) }
@@ -452,6 +453,15 @@ module Dependabot
452
453
 
453
454
  resolution_deps = resolution_objects.flat_map(&:to_a)
454
455
  .map do |path, value|
456
+ # skip dependencies that contain invalid values such as inline comments, null, etc.
457
+
458
+ unless value.is_a?(String)
459
+ Dependabot.logger.warn("File fetcher: Skipping dependency \"#{path}\" " \
460
+ "with value: \"#{value}\"")
461
+
462
+ next
463
+ end
464
+
455
465
  convert_dependency_path_to_name(path, value)
456
466
  end
457
467
 
@@ -644,8 +654,8 @@ module Dependabot
644
654
  def parsed_pnpm_workspace_yaml
645
655
  return {} unless pnpm_workspace_yaml
646
656
 
647
- YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content))
648
- rescue Psych::SyntaxError
657
+ YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content), aliases: true)
658
+ rescue Psych::SyntaxError, Psych::BadAlias
649
659
  raise Dependabot::DependencyFileNotParseable, T.must(pnpm_workspace_yaml).path
650
660
  end
651
661
 
@@ -699,6 +709,22 @@ module Dependabot
699
709
  Dependabot.logger.info("Repository contents path does not exist")
700
710
  end
701
711
  end
712
+
713
+ sig { params(filename: String).returns(T.nilable(DependencyFile)) }
714
+ def fetch_file_with_support(filename)
715
+ fetch_file_from_host(filename).tap { |f| f.support_file = true }
716
+ rescue Dependabot::DependencyFileNotFound
717
+ nil
718
+ end
719
+
720
+ sig { params(filename: String).returns(T.nilable(DependencyFile)) }
721
+ def fetch_file_from_parent_directories(filename)
722
+ (1..directory.split("/").count).each do |i|
723
+ file = fetch_file_with_support(("../" * i) + filename)
724
+ return file if file
725
+ end
726
+ nil
727
+ end
702
728
  end
703
729
  end
704
730
  end
@@ -0,0 +1,141 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+ require "dependabot/errors"
6
+ require "dependabot/npm_and_yarn/helpers"
7
+ require "sorbet-runtime"
8
+
9
+ module Dependabot
10
+ module NpmAndYarn
11
+ class FileParser < Dependabot::FileParsers::Base
12
+ class BunLock
13
+ extend T::Sig
14
+
15
+ sig { params(dependency_file: DependencyFile).void }
16
+ def initialize(dependency_file)
17
+ @dependency_file = dependency_file
18
+ end
19
+
20
+ sig { returns(T::Hash[String, T.untyped]) }
21
+ def parsed
22
+ @parsed ||= begin
23
+ content = begin
24
+ # Since bun.lock is a JSONC file, which is a subset of YAML, we can use YAML to parse it
25
+ YAML.load(T.must(@dependency_file.content))
26
+ rescue Psych::SyntaxError => e
27
+ raise_invalid!("malformed JSONC at line #{e.line}, column #{e.column}")
28
+ end
29
+ raise_invalid!("expected to be an object") unless content.is_a?(Hash)
30
+
31
+ version = content["lockfileVersion"]
32
+ raise_invalid!("expected 'lockfileVersion' to be an integer") unless version.is_a?(Integer)
33
+ raise_invalid!("expected 'lockfileVersion' to be >= 0") unless version >= 0
34
+ raise_invalid!("unsupported 'lockfileVersion' = #{version}") unless version.zero?
35
+
36
+ T.let(content, T.untyped)
37
+ end
38
+ end
39
+
40
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
41
+ def dependencies
42
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
43
+
44
+ # bun.lock v0 format:
45
+ # https://github.com/oven-sh/bun/blob/c130df6c589fdf28f9f3c7f23ed9901140bc9349/src/install/bun.lock.zig#L595-L605
46
+
47
+ packages = parsed["packages"]
48
+ raise_invalid!("expected 'packages' to be an object") unless packages.is_a?(Hash)
49
+
50
+ packages.each do |key, details|
51
+ raise_invalid!("expected 'packages.#{key}' to be an array") unless details.is_a?(Array)
52
+
53
+ resolution = details.first
54
+ raise_invalid!("expected 'packages.#{key}[0]' to be a string") unless resolution.is_a?(String)
55
+
56
+ name, version = resolution.split(/(?<=\w)\@/)
57
+ next if name.empty?
58
+
59
+ semver = Version.semver_for(version)
60
+ next unless semver
61
+
62
+ dependency_set << Dependency.new(
63
+ name: name,
64
+ version: semver.to_s,
65
+ package_manager: "npm_and_yarn",
66
+ requirements: []
67
+ )
68
+ end
69
+
70
+ dependency_set
71
+ end
72
+
73
+ sig do
74
+ params(dependency_name: String, requirement: T.untyped, _manifest_name: String)
75
+ .returns(T.nilable(T::Hash[String, T.untyped]))
76
+ end
77
+ def details(dependency_name, requirement, _manifest_name)
78
+ packages = parsed["packages"]
79
+ return unless packages.is_a?(Hash)
80
+
81
+ candidates =
82
+ packages
83
+ .select { |name, _| name == dependency_name }
84
+ .values
85
+
86
+ # If there's only one entry for this dependency, use it, even if
87
+ # the requirement in the lockfile doesn't match
88
+ if candidates.one?
89
+ parse_details(candidates.first)
90
+ else
91
+ candidate = candidates.find do |label, _|
92
+ label.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
93
+ end&.last
94
+ parse_details(candidate)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ sig { params(message: String).void }
101
+ def raise_invalid!(message)
102
+ raise Dependabot::DependencyFileNotParseable.new(@dependency_file.path, "Invalid bun.lock file: #{message}")
103
+ end
104
+
105
+ sig do
106
+ params(entry: T.nilable(T::Array[T.untyped])).returns(T.nilable(T::Hash[String, T.untyped]))
107
+ end
108
+ def parse_details(entry)
109
+ return unless entry.is_a?(Array)
110
+
111
+ # Either:
112
+ # - "{name}@{version}", registry, details, integrity
113
+ # - "{name}@{resolution}", details
114
+ resolution = entry.first
115
+ return unless resolution.is_a?(String)
116
+
117
+ name, version = resolution.split(/(?<=\w)\@/)
118
+ semver = Version.semver_for(version)
119
+
120
+ if semver
121
+ registry, details, integrity = entry[1..3]
122
+ {
123
+ "name" => name,
124
+ "version" => semver.to_s,
125
+ "registry" => registry,
126
+ "details" => details,
127
+ "integrity" => integrity
128
+ }
129
+ else
130
+ details = entry[1]
131
+ {
132
+ "name" => name,
133
+ "resolution" => version,
134
+ "details" => details
135
+ }
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -15,6 +15,11 @@ module Dependabot
15
15
  require "dependabot/npm_and_yarn/file_parser/yarn_lock"
16
16
  require "dependabot/npm_and_yarn/file_parser/pnpm_lock"
17
17
  require "dependabot/npm_and_yarn/file_parser/json_lock"
18
+ require "dependabot/npm_and_yarn/file_parser/bun_lock"
19
+
20
+ DEFAULT_LOCKFILES = %w(package-lock.json yarn.lock pnpm-lock.yaml bun.lock npm-shrinkwrap.json).freeze
21
+
22
+ LockFile = T.type_alias { T.any(JsonLock, YarnLock, PnpmLock, BunLock) }
18
23
 
19
24
  sig { params(dependency_files: T::Array[DependencyFile]).void }
20
25
  def initialize(dependency_files:)
@@ -29,7 +34,7 @@ module Dependabot
29
34
  # end up unique by name. That's not a perfect representation of
30
35
  # the nested nature of JS resolution, but it makes everything work
31
36
  # comparably to other flat-resolution strategies
32
- (yarn_locks + pnpm_locks + package_locks + shrinkwraps).each do |file|
37
+ (yarn_locks + pnpm_locks + package_locks + bun_locks + shrinkwraps).each do |file|
33
38
  dependency_set += lockfile_for(file).dependencies
34
39
  end
35
40
 
@@ -64,58 +69,59 @@ module Dependabot
64
69
  sig { params(manifest_filename: String).returns(T::Array[DependencyFile]) }
65
70
  def potential_lockfiles_for_manifest(manifest_filename)
66
71
  dir_name = File.dirname(manifest_filename)
67
- possible_lockfile_names =
68
- %w(package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock).map do |f|
69
- Pathname.new(File.join(dir_name, f)).cleanpath.to_path
70
- end +
71
- %w(yarn.lock pnpm-lock.yaml package-lock.json npm-shrinkwrap.json)
72
+ possible_lockfile_names = DEFAULT_LOCKFILES.map do |f|
73
+ Pathname.new(File.join(dir_name, f)).cleanpath.to_path
74
+ end + DEFAULT_LOCKFILES
72
75
 
73
76
  possible_lockfile_names.uniq
74
77
  .filter_map { |nm| dependency_files.find { |f| f.name == nm } }
75
78
  end
76
79
 
77
- sig { params(file: DependencyFile).returns(T.any(JsonLock, YarnLock, PnpmLock)) }
80
+ sig { params(file: DependencyFile).returns(LockFile) }
78
81
  def lockfile_for(file)
79
- @lockfiles ||= T.let({}, T.nilable(T::Hash[String, T.any(JsonLock, YarnLock, PnpmLock)]))
80
- @lockfiles[file.name] ||= if [*package_locks, *shrinkwraps].include?(file)
82
+ @lockfiles ||= T.let({}, T.nilable(T::Hash[String, LockFile]))
83
+ @lockfiles[file.name] ||= case file.name
84
+ when *package_locks.map(&:name), *shrinkwraps.map(&:name)
81
85
  JsonLock.new(file)
82
- elsif yarn_locks.include?(file)
86
+ when *yarn_locks.map(&:name)
83
87
  YarnLock.new(file)
84
- else
88
+ when *pnpm_locks.map(&:name)
85
89
  PnpmLock.new(file)
90
+ when *bun_locks.map(&:name)
91
+ BunLock.new(file)
92
+ else
93
+ raise "Unexpected lockfile: #{file.name}"
86
94
  end
87
95
  end
88
96
 
97
+ sig { params(extension: String).returns(T::Array[DependencyFile]) }
98
+ def select_files_by_extension(extension)
99
+ dependency_files.select { |f| f.name.end_with?(extension) }
100
+ end
101
+
89
102
  sig { returns(T::Array[DependencyFile]) }
90
103
  def package_locks
91
- @package_locks ||= T.let(
92
- dependency_files
93
- .select { |f| f.name.end_with?("package-lock.json") }, T.nilable(T::Array[DependencyFile])
94
- )
104
+ @package_locks ||= T.let(select_files_by_extension("package-lock.json"), T.nilable(T::Array[DependencyFile]))
95
105
  end
96
106
 
97
107
  sig { returns(T::Array[DependencyFile]) }
98
108
  def pnpm_locks
99
- @pnpm_locks ||= T.let(
100
- dependency_files
101
- .select { |f| f.name.end_with?("pnpm-lock.yaml") }, T.nilable(T::Array[DependencyFile])
102
- )
109
+ @pnpm_locks ||= T.let(select_files_by_extension("pnpm-lock.yaml"), T.nilable(T::Array[DependencyFile]))
110
+ end
111
+
112
+ sig { returns(T::Array[DependencyFile]) }
113
+ def bun_locks
114
+ @bun_locks ||= T.let(select_files_by_extension("bun.lock"), T.nilable(T::Array[DependencyFile]))
103
115
  end
104
116
 
105
117
  sig { returns(T::Array[DependencyFile]) }
106
118
  def yarn_locks
107
- @yarn_locks ||= T.let(
108
- dependency_files
109
- .select { |f| f.name.end_with?("yarn.lock") }, T.nilable(T::Array[DependencyFile])
110
- )
119
+ @yarn_locks ||= T.let(select_files_by_extension("yarn.lock"), T.nilable(T::Array[DependencyFile]))
111
120
  end
112
121
 
113
122
  sig { returns(T::Array[DependencyFile]) }
114
123
  def shrinkwraps
115
- @shrinkwraps ||= T.let(
116
- dependency_files
117
- .select { |f| f.name.end_with?("npm-shrinkwrap.json") }, T.nilable(T::Array[DependencyFile])
118
- )
124
+ @shrinkwraps ||= T.let(select_files_by_extension("npm-shrinkwrap.json"), T.nilable(T::Array[DependencyFile]))
119
125
  end
120
126
 
121
127
  sig { returns(T.class_of(Dependabot::NpmAndYarn::Version)) }
@@ -26,6 +26,10 @@ module Dependabot
26
26
  end
27
27
 
28
28
  def dependencies
29
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
30
+ return dependencies_with_prioritization
31
+ end
32
+
29
33
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
30
34
 
31
35
  parsed.each do |details|
@@ -52,6 +56,49 @@ module Dependabot
52
56
  dependency_set
53
57
  end
54
58
 
59
+ def dependencies_with_prioritization
60
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
61
+
62
+ # Separate dependencies into two categories: with specifiers and without specifiers.
63
+ dependencies_with_specifiers = [] # Main dependencies with specifiers.
64
+ dependencies_without_specifiers = [] # Subdependencies without specifiers.
65
+
66
+ parsed.each do |details|
67
+ next if details["aliased"]
68
+
69
+ name = details["name"]
70
+ version = details["version"]
71
+
72
+ dependency_args = {
73
+ name: name,
74
+ version: version,
75
+ package_manager: "npm_and_yarn",
76
+ requirements: []
77
+ }
78
+
79
+ # Add metadata for subdependencies if marked as a dev dependency.
80
+ dependency_args[:subdependency_metadata] = [{ production: !details["dev"] }] if details["dev"]
81
+
82
+ specifiers = details["specifiers"]
83
+ if specifiers&.any?
84
+ dependencies_with_specifiers << dependency_args
85
+ else
86
+ dependencies_without_specifiers << dependency_args
87
+ end
88
+ end
89
+
90
+ # Add prioritized dependencies to the dependency set.
91
+ dependencies_with_specifiers.each do |dependency_args|
92
+ dependency_set << Dependency.new(**dependency_args)
93
+ end
94
+
95
+ dependencies_without_specifiers.each do |dependency_args|
96
+ dependency_set << Dependency.new(**dependency_args)
97
+ end
98
+
99
+ dependency_set
100
+ end
101
+
55
102
  def details(dependency_name, requirement, _manifest_name)
56
103
  details_candidates = parsed.select { |info| info["name"] == dependency_name }
57
104