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
@@ -110,7 +110,8 @@ module Dependabot
110
110
  {
111
111
  npm: package_lock || shrinkwrap,
112
112
  yarn: yarn_lock,
113
- pnpm: pnpm_lock
113
+ pnpm: pnpm_lock,
114
+ bun: bun_lock
114
115
  }
115
116
  end
116
117
 
@@ -142,49 +143,56 @@ module Dependabot
142
143
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
143
144
  def shrinkwrap
144
145
  @shrinkwrap ||= T.let(dependency_files.find do |f|
145
- f.name == NpmPackageManager::SHRINKWRAP_LOCKFILE_NAME
146
+ f.name.end_with?(NpmPackageManager::SHRINKWRAP_LOCKFILE_NAME)
146
147
  end, T.nilable(Dependabot::DependencyFile))
147
148
  end
148
149
 
149
150
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
150
151
  def package_lock
151
152
  @package_lock ||= T.let(dependency_files.find do |f|
152
- f.name == NpmPackageManager::LOCKFILE_NAME
153
+ f.name.end_with?(NpmPackageManager::LOCKFILE_NAME)
153
154
  end, T.nilable(Dependabot::DependencyFile))
154
155
  end
155
156
 
156
157
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
157
158
  def yarn_lock
158
159
  @yarn_lock ||= T.let(dependency_files.find do |f|
159
- f.name == YarnPackageManager::LOCKFILE_NAME
160
+ f.name.end_with?(YarnPackageManager::LOCKFILE_NAME)
160
161
  end, T.nilable(Dependabot::DependencyFile))
161
162
  end
162
163
 
163
164
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
164
165
  def pnpm_lock
165
166
  @pnpm_lock ||= T.let(dependency_files.find do |f|
166
- f.name == PNPMPackageManager::LOCKFILE_NAME
167
+ f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME)
168
+ end, T.nilable(Dependabot::DependencyFile))
169
+ end
170
+
171
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
172
+ def bun_lock
173
+ @bun_lock ||= T.let(dependency_files.find do |f|
174
+ f.name.end_with?(BunPackageManager::LOCKFILE_NAME)
167
175
  end, T.nilable(Dependabot::DependencyFile))
168
176
  end
169
177
 
170
178
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
171
179
  def npmrc
172
180
  @npmrc ||= T.let(dependency_files.find do |f|
173
- f.name == NpmPackageManager::RC_FILENAME
181
+ f.name.end_with?(NpmPackageManager::RC_FILENAME)
174
182
  end, T.nilable(Dependabot::DependencyFile))
175
183
  end
176
184
 
177
185
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
178
186
  def yarnrc
179
187
  @yarnrc ||= T.let(dependency_files.find do |f|
180
- f.name == YarnPackageManager::RC_FILENAME
188
+ f.name.end_with?(YarnPackageManager::RC_FILENAME)
181
189
  end, T.nilable(Dependabot::DependencyFile))
182
190
  end
183
191
 
184
192
  sig { returns(T.nilable(DependencyFile)) }
185
193
  def yarnrc_yml
186
194
  @yarnrc_yml ||= T.let(dependency_files.find do |f|
187
- f.name == YarnPackageManager::RC_YML_FILENAME
195
+ f.name.end_with?(YarnPackageManager::RC_YML_FILENAME)
188
196
  end, T.nilable(Dependabot::DependencyFile))
189
197
  end
190
198
 
@@ -204,7 +212,7 @@ module Dependabot
204
212
  next unless requirement.is_a?(String)
205
213
 
206
214
  # Skip dependencies using Yarn workspace cross-references as requirements
207
- next if requirement.start_with?("workspace:")
215
+ next if requirement.start_with?("workspace:", "catalog:")
208
216
 
209
217
  requirement = "*" if requirement == ""
210
218
  dep = build_dependency(
@@ -0,0 +1,144 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/npm_and_yarn/helpers"
5
+ require "dependabot/npm_and_yarn/update_checker/registry_finder"
6
+ require "dependabot/npm_and_yarn/registry_parser"
7
+ require "dependabot/shared_helpers"
8
+
9
+ module Dependabot
10
+ module NpmAndYarn
11
+ class FileUpdater < Dependabot::FileUpdaters::Base
12
+ class BunLockfileUpdater
13
+ require_relative "npmrc_builder"
14
+ require_relative "package_json_updater"
15
+
16
+ def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
17
+ @dependencies = dependencies
18
+ @dependency_files = dependency_files
19
+ @repo_contents_path = repo_contents_path
20
+ @credentials = credentials
21
+ end
22
+
23
+ def updated_bun_lock_content(bun_lock)
24
+ @updated_bun_lock_content ||= {}
25
+ return @updated_bun_lock_content[bun_lock.name] if @updated_bun_lock_content[bun_lock.name]
26
+
27
+ new_content = run_bun_update(bun_lock: bun_lock)
28
+ @updated_bun_lock_content[bun_lock.name] = new_content
29
+ rescue SharedHelpers::HelperSubprocessFailed => e
30
+ handle_bun_lock_updater_error(e, bun_lock)
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :dependencies
36
+ attr_reader :dependency_files
37
+ attr_reader :repo_contents_path
38
+ attr_reader :credentials
39
+
40
+ ERR_PATTERNS = {
41
+ /get .* 404/i => Dependabot::DependencyNotFound,
42
+ /installfailed cloning repository/i => Dependabot::DependencyNotFound,
43
+ /file:.* failed to resolve/i => Dependabot::DependencyNotFound,
44
+ /no version matching/i => Dependabot::DependencyFileNotResolvable,
45
+ /failed to resolve/i => Dependabot::DependencyFileNotResolvable
46
+ }.freeze
47
+
48
+ def run_bun_update(bun_lock:)
49
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
50
+ File.write(".npmrc", npmrc_content(bun_lock))
51
+
52
+ SharedHelpers.with_git_configured(credentials: credentials) do
53
+ run_bun_updater
54
+
55
+ write_final_package_json_files
56
+
57
+ run_bun_install
58
+
59
+ File.read(bun_lock.name)
60
+ end
61
+ end
62
+ end
63
+
64
+ def run_bun_updater
65
+ dependency_updates = dependencies.map do |d|
66
+ "#{d.name}@#{d.version}"
67
+ end.join(" ")
68
+
69
+ Helpers.run_bun_command(
70
+ "install #{dependency_updates} --save-text-lockfile",
71
+ fingerprint: "install <dependency_updates> --save-text-lockfile"
72
+ )
73
+ end
74
+
75
+ def run_bun_install
76
+ Helpers.run_bun_command(
77
+ "install --save-text-lockfile"
78
+ )
79
+ end
80
+
81
+ def lockfile_dependencies(lockfile)
82
+ @lockfile_dependencies ||= {}
83
+ @lockfile_dependencies[lockfile.name] ||=
84
+ NpmAndYarn::FileParser.new(
85
+ dependency_files: [lockfile, *package_files],
86
+ source: nil,
87
+ credentials: credentials
88
+ ).parse
89
+ end
90
+
91
+ def handle_bun_lock_updater_error(error, _bun_lock)
92
+ error_message = error.message
93
+
94
+ ERR_PATTERNS.each do |pattern, error_class|
95
+ raise error_class, error_message if error_message.match?(pattern)
96
+ end
97
+
98
+ raise error
99
+ end
100
+
101
+ def write_final_package_json_files
102
+ package_files.each do |file|
103
+ path = file.name
104
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
105
+ File.write(path, updated_package_json_content(file))
106
+ end
107
+ end
108
+
109
+ def npmrc_content(bun_lock)
110
+ NpmrcBuilder.new(
111
+ credentials: credentials,
112
+ dependency_files: dependency_files,
113
+ dependencies: lockfile_dependencies(bun_lock)
114
+ ).npmrc_content
115
+ end
116
+
117
+ def updated_package_json_content(file)
118
+ @updated_package_json_content ||= {}
119
+ @updated_package_json_content[file.name] ||=
120
+ PackageJsonUpdater.new(
121
+ package_json: file,
122
+ dependencies: dependencies
123
+ ).updated_package_json.content
124
+ end
125
+
126
+ def package_files
127
+ @package_files ||= dependency_files.select { |f| f.name.end_with?("package.json") }
128
+ end
129
+
130
+ def base_dir
131
+ dependency_files.first.directory
132
+ end
133
+
134
+ def npmrc_file
135
+ dependency_files.find { |f| f.name == ".npmrc" }
136
+ end
137
+
138
+ def sanitize_message(message)
139
+ message.gsub(/"|\[|\]|\}|\{/, "")
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -18,6 +18,10 @@ module Dependabot
18
18
  @dependency_files = dependency_files
19
19
  @repo_contents_path = repo_contents_path
20
20
  @credentials = credentials
21
+ @error_handler = PnpmErrorHandler.new(
22
+ dependencies: dependencies,
23
+ dependency_files: dependency_files
24
+ )
21
25
  end
22
26
 
23
27
  def updated_pnpm_lock_content(pnpm_lock)
@@ -36,6 +40,7 @@ module Dependabot
36
40
  attr_reader :dependency_files
37
41
  attr_reader :repo_contents_path
38
42
  attr_reader :credentials
43
+ attr_reader :error_handler
39
44
 
40
45
  IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION"
41
46
  INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER"
@@ -46,12 +51,12 @@ module Dependabot
46
51
  UNAUTHORIZED_PACKAGE = /ERR_PNPM_FETCH_401[ [^:print:]]+GET (?<dependency_url>.*): Unauthorized - 401/
47
52
 
48
53
  # ERR_PNPM_FETCH ERROR CODES
49
- ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*): - 401/
50
- ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*): - 403/
51
- ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*): - 404/
52
- ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*): - 500/
53
- ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*): - 502/
54
- ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*): - 503/
54
+ ERR_PNPM_FETCH_401 = /ERR_PNPM_FETCH_401.*GET (?<dependency_url>.*):/
55
+ ERR_PNPM_FETCH_403 = /ERR_PNPM_FETCH_403.*GET (?<dependency_url>.*):/
56
+ ERR_PNPM_FETCH_404 = /ERR_PNPM_FETCH_404.*GET (?<dependency_url>.*):/
57
+ ERR_PNPM_FETCH_500 = /ERR_PNPM_FETCH_500.*GET (?<dependency_url>.*):/
58
+ ERR_PNPM_FETCH_502 = /ERR_PNPM_FETCH_502.*GET (?<dependency_url>.*):/
59
+ ERR_PNPM_FETCH_503 = /ERR_PNPM_FETCH_503.*GET (?<dependency_url>.*):/
55
60
 
56
61
  # ERR_PNPM_UNSUPPORTED_ENGINE
57
62
  ERR_PNPM_UNSUPPORTED_ENGINE = /ERR_PNPM_UNSUPPORTED_ENGINE/
@@ -62,6 +67,13 @@ module Dependabot
62
67
 
63
68
  ERR_PNPM_PATCH_NOT_APPLIED = /ERR_PNPM_PATCH_NOT_APPLIED/
64
69
 
70
+ # this intermittent issue is related with Node v20
71
+ ERR_INVALID_THIS = /ERR_INVALID_THIS/
72
+ URL_SEARCH_PARAMS = /URLSearchParams/
73
+
74
+ # A modules directory is present and is linked to a different store directory.
75
+ ERR_PNPM_UNEXPECTED_STORE = /ERR_PNPM_UNEXPECTED_STORE/
76
+
65
77
  # ERR_PNPM_UNSUPPORTED_PLATFORM
66
78
  ERR_PNPM_UNSUPPORTED_PLATFORM = /ERR_PNPM_UNSUPPORTED_PLATFORM/
67
79
  PLATFORM_PACAKGE_DEP = /Unsupported platform for (?<dep>.*)\: wanted/
@@ -78,12 +90,22 @@ module Dependabot
78
90
  ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND = /ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND*.*Could not install from \"(?<dir>.*)\" /
79
91
  ERR_PNPM_WORKSPACE_PKG_NOT_FOUND = /ERR_PNPM_WORKSPACE_PKG_NOT_FOUND/
80
92
 
93
+ # Unparsable package.json file
94
+ ERR_PNPM_INVALID_PACKAGE_JSON = /Invalid package.json in package/
95
+
96
+ # Unparsable lockfile
97
+ ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE = /ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE/
98
+ ERR_PNPM_OUTDATED_LOCKFILE = /ERR_PNPM_OUTDATED_LOCKFILE/
99
+
100
+ # Peer dependencies configuration error
101
+ ERR_PNPM_PEER_DEP_ISSUES = /ERR_PNPM_PEER_DEP_ISSUES/
102
+
81
103
  def run_pnpm_update(pnpm_lock:)
82
104
  SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
83
105
  File.write(".npmrc", npmrc_content(pnpm_lock))
84
106
 
85
107
  SharedHelpers.with_git_configured(credentials: credentials) do
86
- run_pnpm_updater
108
+ run_pnpm_update_packages
87
109
 
88
110
  write_final_package_json_files
89
111
 
@@ -94,15 +116,22 @@ module Dependabot
94
116
  end
95
117
  end
96
118
 
97
- def run_pnpm_updater
119
+ def run_pnpm_update_packages
98
120
  dependency_updates = dependencies.map do |d|
99
121
  "#{d.name}@#{d.version}"
100
122
  end.join(" ")
101
123
 
102
- Helpers.run_pnpm_command(
103
- "install #{dependency_updates} --lockfile-only --ignore-workspace-root-check",
104
- fingerprint: "install <dependency_updates> --lockfile-only --ignore-workspace-root-check"
105
- )
124
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
125
+ Helpers.run_pnpm_command(
126
+ "update #{dependency_updates} --lockfile-only --no-save -r",
127
+ fingerprint: "update <dependency_updates> --lockfile-only --no-save -r"
128
+ )
129
+ else
130
+ Helpers.run_pnpm_command(
131
+ "install #{dependency_updates} --lockfile-only --ignore-workspace-root-check",
132
+ fingerprint: "install <dependency_updates> --lockfile-only --ignore-workspace-root-check"
133
+ )
134
+ end
106
135
  end
107
136
 
108
137
  def run_pnpm_install
@@ -196,15 +225,46 @@ module Dependabot
196
225
  raise Dependabot::DependencyFileNotResolvable, msg
197
226
  end
198
227
 
228
+ if error_message.match?(ERR_PNPM_INVALID_PACKAGE_JSON) || error_message.match?(ERR_PNPM_UNEXPECTED_STORE)
229
+ msg = "Error while resolving package.json."
230
+ Dependabot.logger.warn(error_message)
231
+ raise Dependabot::DependencyFileNotResolvable, msg
232
+ end
233
+
234
+ [ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE, ERR_PNPM_OUTDATED_LOCKFILE]
235
+ .each do |regexp|
236
+ next unless error_message.match?(regexp)
237
+
238
+ error_msg = T.let("Error while resolving pnpm-lock.yaml file.", String)
239
+
240
+ Dependabot.logger.warn(error_message)
241
+ raise Dependabot::DependencyFileNotResolvable, error_msg
242
+ end
243
+
244
+ if error_message.match?(ERR_PNPM_PEER_DEP_ISSUES)
245
+ msg = "Missing or invalid configuration while installing peer dependencies."
246
+
247
+ Dependabot.logger.warn(error_message)
248
+ raise Dependabot::DependencyFileNotResolvable, msg
249
+ end
250
+
199
251
  raise_patch_dependency_error(error_message) if error_message.match?(ERR_PNPM_PATCH_NOT_APPLIED)
200
252
 
201
253
  raise_unsupported_engine_error(error_message, pnpm_lock) if error_message.match?(ERR_PNPM_UNSUPPORTED_ENGINE)
202
254
 
255
+ if error_message.match?(ERR_INVALID_THIS) && error_message.match?(URL_SEARCH_PARAMS)
256
+ msg = "Error while resolving dependencies."
257
+ Dependabot.logger.warn(error_message)
258
+ raise Dependabot::DependencyFileNotResolvable, msg
259
+ end
260
+
203
261
  if error_message.match?(ERR_PNPM_UNSUPPORTED_PLATFORM)
204
262
  raise_unsupported_platform_error(error_message,
205
263
  pnpm_lock)
206
264
  end
207
265
 
266
+ error_handler.handle_pnpm_error(error)
267
+
208
268
  raise
209
269
  end
210
270
  # rubocop:enable Metrics/AbcSize
@@ -314,5 +374,60 @@ module Dependabot
314
374
  end
315
375
  end
316
376
  end
377
+
378
+ class PnpmErrorHandler
379
+ extend T::Sig
380
+
381
+ # remote connection closed
382
+ ECONNRESET_ERROR = /ECONNRESET/
383
+
384
+ # socket hang up error code
385
+ SOCKET_HANG_UP = /socket hang up/
386
+
387
+ # ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC error
388
+ ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC = /ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC/
389
+
390
+ # duplicate package error code
391
+ DUPLICATE_PACKAGE = /Found duplicates/
392
+
393
+ ERR_PNPM_NO_VERSIONS = /ERR_PNPM_NO_VERSIONS/
394
+
395
+ # Initializes the YarnErrorHandler with dependencies and dependency files
396
+ sig do
397
+ params(
398
+ dependencies: T::Array[Dependabot::Dependency],
399
+ dependency_files: T::Array[Dependabot::DependencyFile]
400
+ ).void
401
+ end
402
+ def initialize(dependencies:, dependency_files:)
403
+ @dependencies = dependencies
404
+ @dependency_files = dependency_files
405
+ end
406
+
407
+ private
408
+
409
+ sig { returns(T::Array[Dependabot::Dependency]) }
410
+ attr_reader :dependencies
411
+
412
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
413
+ attr_reader :dependency_files
414
+
415
+ public
416
+
417
+ # Handles errors with specific to yarn error codes
418
+ sig { params(error: SharedHelpers::HelperSubprocessFailed).void }
419
+ def handle_pnpm_error(error)
420
+ if error.message.match?(DUPLICATE_PACKAGE) || error.message.match?(ERR_PNPM_NO_VERSIONS) ||
421
+ error.message.match?(ERR_PNPM_CATALOG_ENTRY_NOT_FOUND_FOR_SPEC)
422
+
423
+ raise DependencyFileNotResolvable, "Error resolving dependency"
424
+ end
425
+
426
+ ## Clean error message from ANSI escape codes
427
+ return unless error.message.match?(ECONNRESET_ERROR) || error.message.match?(SOCKET_HANG_UP)
428
+
429
+ raise InconsistentRegistryResponse, "Inconsistent registry response while resolving dependency"
430
+ end
431
+ end
317
432
  end
318
433
  end
@@ -18,6 +18,7 @@ module Dependabot
18
18
  require_relative "file_updater/npm_lockfile_updater"
19
19
  require_relative "file_updater/yarn_lockfile_updater"
20
20
  require_relative "file_updater/pnpm_lockfile_updater"
21
+ require_relative "file_updater/bun_lockfile_updater"
21
22
 
22
23
  class NoChangeError < StandardError
23
24
  extend T::Sig
@@ -47,6 +48,7 @@ module Dependabot
47
48
  ]
48
49
  end
49
50
 
51
+ # rubocop:disable Metrics/PerceivedComplexity
50
52
  sig { override.returns(T::Array[DependencyFile]) }
51
53
  def updated_dependency_files
52
54
  updated_files = T.let([], T::Array[DependencyFile])
@@ -55,6 +57,22 @@ module Dependabot
55
57
  updated_files += updated_lockfiles
56
58
 
57
59
  if updated_files.none?
60
+
61
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
62
+ # when all dependencies are transitive
63
+ all_transitive = dependencies.none?(&:top_level?)
64
+ # when there is no update in package.json
65
+ no_package_json_update = package_files.empty?
66
+ # handle the no change error for transitive dependency updates
67
+ if pnpm_locks.any? && dependencies.length.positive? && all_transitive && no_package_json_update
68
+ raise ToolFeatureNotSupported.new(
69
+ tool_name: "pnpm",
70
+ tool_type: "package_manager",
71
+ feature: "updating transitive dependencies"
72
+ )
73
+ end
74
+ end
75
+
58
76
  raise NoChangeError.new(
59
77
  message: "No files were updated!",
60
78
  error_context: error_context(updated_files: updated_files)
@@ -71,6 +89,7 @@ module Dependabot
71
89
 
72
90
  vendor_updated_files(updated_files)
73
91
  end
92
+ # rubocop:enable Metrics/PerceivedComplexity
74
93
 
75
94
  private
76
95
 
@@ -189,6 +208,15 @@ module Dependabot
189
208
  )
190
209
  end
191
210
 
211
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
212
+ def bun_locks
213
+ @bun_locks ||= T.let(
214
+ filtered_dependency_files
215
+ .select { |f| f.name.end_with?("bun.lock") },
216
+ T.nilable(T::Array[Dependabot::DependencyFile])
217
+ )
218
+ end
219
+
192
220
  sig { returns(T::Array[Dependabot::DependencyFile]) }
193
221
  def shrinkwraps
194
222
  @shrinkwraps ||= T.let(
@@ -217,6 +245,11 @@ module Dependabot
217
245
  pnpm_lock.content != updated_pnpm_lock_content(pnpm_lock)
218
246
  end
219
247
 
248
+ sig { params(bun_lock: Dependabot::DependencyFile).returns(T::Boolean) }
249
+ def bun_lock_changed?(bun_lock)
250
+ bun_lock.content != updated_bun_lock_content(bun_lock)
251
+ end
252
+
220
253
  sig { params(package_lock: Dependabot::DependencyFile).returns(T::Boolean) }
221
254
  def package_lock_changed?(package_lock)
222
255
  package_lock.content != updated_lockfile_content(package_lock)
@@ -237,6 +270,8 @@ module Dependabot
237
270
  end
238
271
  end
239
272
 
273
+ # rubocop:disable Metrics/MethodLength
274
+ # rubocop:disable Metrics/PerceivedComplexity
240
275
  sig { returns(T::Array[Dependabot::DependencyFile]) }
241
276
  def updated_lockfiles
242
277
  updated_files = []
@@ -259,6 +294,15 @@ module Dependabot
259
294
  )
260
295
  end
261
296
 
297
+ bun_locks.each do |bun_lock|
298
+ next unless bun_lock_changed?(bun_lock)
299
+
300
+ updated_files << updated_file(
301
+ file: bun_lock,
302
+ content: updated_bun_lock_content(bun_lock)
303
+ )
304
+ end
305
+
262
306
  package_locks.each do |package_lock|
263
307
  next unless package_lock_changed?(package_lock)
264
308
 
@@ -279,6 +323,8 @@ module Dependabot
279
323
 
280
324
  updated_files
281
325
  end
326
+ # rubocop:enable Metrics/MethodLength
327
+ # rubocop:enable Metrics/PerceivedComplexity
282
328
 
283
329
  sig { params(yarn_lock: Dependabot::DependencyFile).returns(String) }
284
330
  def updated_yarn_lock_content(yarn_lock)
@@ -294,6 +340,13 @@ module Dependabot
294
340
  pnpm_lockfile_updater.updated_pnpm_lock_content(pnpm_lock)
295
341
  end
296
342
 
343
+ sig { params(bun_lock: Dependabot::DependencyFile).returns(String) }
344
+ def updated_bun_lock_content(bun_lock)
345
+ @updated_bun_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
346
+ @updated_bun_lock_content[bun_lock.name] ||=
347
+ bun_lockfile_updater.updated_bun_lock_content(bun_lock)
348
+ end
349
+
297
350
  sig { returns(Dependabot::NpmAndYarn::FileUpdater::YarnLockfileUpdater) }
298
351
  def yarn_lockfile_updater
299
352
  @yarn_lockfile_updater ||= T.let(
@@ -320,6 +373,19 @@ module Dependabot
320
373
  )
321
374
  end
322
375
 
376
+ sig { returns(Dependabot::NpmAndYarn::FileUpdater::BunLockfileUpdater) }
377
+ def bun_lockfile_updater
378
+ @bun_lockfile_updater ||= T.let(
379
+ BunLockfileUpdater.new(
380
+ dependencies: dependencies,
381
+ dependency_files: dependency_files,
382
+ repo_contents_path: repo_contents_path,
383
+ credentials: credentials
384
+ ),
385
+ T.nilable(Dependabot::NpmAndYarn::FileUpdater::BunLockfileUpdater)
386
+ )
387
+ end
388
+
323
389
  sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
324
390
  def updated_lockfile_content(file)
325
391
  @updated_lockfile_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
@@ -29,6 +29,10 @@ module Dependabot
29
29
  PNPM_DEFAULT_VERSION = PNPM_V9
30
30
  PNPM_FALLBACK_VERSION = PNPM_V6
31
31
 
32
+ # BUN Version Constants
33
+ BUN_V1 = 1
34
+ BUN_DEFAULT_VERSION = BUN_V1
35
+
32
36
  # YARN Version Constants
33
37
  YARN_V3 = 3
34
38
  YARN_V2 = 2
@@ -36,6 +40,9 @@ module Dependabot
36
40
  YARN_DEFAULT_VERSION = YARN_V3
37
41
  YARN_FALLBACK_VERSION = YARN_V1
38
42
 
43
+ # corepack supported package managers
44
+ SUPPORTED_COREPACK_PACKAGE_MANAGERS = %w(npm yarn pnpm).freeze
45
+
39
46
  # Determines the npm version depends to the feature flag
40
47
  # If the feature flag is enabled, we are going to use the minimum version npm 8
41
48
  # Otherwise, we are going to use old versionining npm 6
@@ -159,6 +166,11 @@ module Dependabot
159
166
  PNPM_FALLBACK_VERSION
160
167
  end
161
168
 
169
+ sig { params(_bun_lock: T.nilable(DependencyFile)).returns(Integer) }
170
+ def self.bun_version_numeric(_bun_lock)
171
+ BUN_DEFAULT_VERSION
172
+ end
173
+
162
174
  sig { params(key: String, default_value: String).returns(T.untyped) }
163
175
  def self.fetch_yarnrc_yml_value(key, default_value)
164
176
  if File.exist?(".yarnrc.yml") && (yarnrc = YAML.load_file(".yarnrc.yml"))
@@ -315,8 +327,8 @@ module Dependabot
315
327
  package_manager_run_command(NpmPackageManager::NAME, command, fingerprint: fingerprint)
316
328
  else
317
329
  Dependabot::SharedHelpers.run_shell_command(
318
- "corepack npm #{command}",
319
- fingerprint: "corepack npm #{fingerprint}"
330
+ "npm #{command}",
331
+ fingerprint: "npm #{fingerprint}"
320
332
  )
321
333
  end
322
334
  end
@@ -352,6 +364,35 @@ module Dependabot
352
364
  raise
353
365
  end
354
366
 
367
+ sig { returns(T.nilable(String)) }
368
+ def self.bun_version
369
+ version = run_bun_command("--version", fingerprint: "--version").strip
370
+ if version.include?("+")
371
+ version.split("+").first # Remove build info, if present
372
+ end
373
+ rescue StandardError => e
374
+ Dependabot.logger.error("Error retrieving Bun version: #{e.message}")
375
+ nil
376
+ end
377
+
378
+ sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
379
+ def self.run_bun_command(command, fingerprint: nil)
380
+ full_command = "bun #{command}"
381
+
382
+ Dependabot.logger.info("Running bun command: #{full_command}")
383
+
384
+ result = Dependabot::SharedHelpers.run_shell_command(
385
+ full_command,
386
+ fingerprint: "bun #{fingerprint || command}"
387
+ )
388
+
389
+ Dependabot.logger.info("Command executed successfully: #{full_command}")
390
+ result
391
+ rescue StandardError => e
392
+ Dependabot.logger.error("Error running bun command: #{full_command}, Error: #{e.message}")
393
+ raise
394
+ end
395
+
355
396
  # Setup yarn and run a single yarn command returning stdout/stderr
356
397
  sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
357
398
  def self.run_yarn_command(command, fingerprint: nil)
@@ -446,6 +487,8 @@ module Dependabot
446
487
  .returns(String)
447
488
  end
448
489
  def self.package_manager_install(name, version, env: {})
490
+ return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)
491
+
449
492
  Dependabot::SharedHelpers.run_shell_command(
450
493
  "corepack install #{name}@#{version} --global --cache-only",
451
494
  fingerprint: "corepack install <name>@<version> --global --cache-only",
@@ -456,6 +499,8 @@ module Dependabot
456
499
  # Prepare the package manager for use by using corepack
457
500
  sig { params(name: String, version: String).returns(String) }
458
501
  def self.package_manager_activate(name, version)
502
+ return "Corepack does not support #{name}" unless corepack_supported_package_manager?(name)
503
+
459
504
  Dependabot::SharedHelpers.run_shell_command(
460
505
  "corepack prepare #{name}@#{version} --activate",
461
506
  fingerprint: "corepack prepare <name>@<version> --activate"
@@ -496,6 +541,8 @@ module Dependabot
496
541
  ).returns(String)
497
542
  end
498
543
  def self.package_manager_run_command(name, command, fingerprint: nil)
544
+ return run_bun_command(command, fingerprint: fingerprint) if name == BunPackageManager::NAME
545
+
499
546
  full_command = "corepack #{name} #{command}"
500
547
 
501
548
  result = Dependabot::SharedHelpers.run_shell_command(
@@ -526,6 +573,11 @@ module Dependabot
526
573
  dependency
527
574
  end
528
575
  end
576
+
577
+ sig { params(name: String).returns(T::Boolean) }
578
+ def self.corepack_supported_package_manager?(name)
579
+ SUPPORTED_COREPACK_PACKAGE_MANAGERS.include?(name)
580
+ end
529
581
  end
530
582
  end
531
583
  end