dependabot-npm_and_yarn 0.292.0 → 0.294.0

Sign up to get free protection for your applications and to get access to all the features.
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