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
@@ -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