Skip to content

Commit

Permalink
Add BunLockfileUpdater
Browse files Browse the repository at this point in the history
Co-authored-by: Ashcon Partovi <[email protected]>
  • Loading branch information
markhallen and Ashcon Partovi committed Jan 15, 2025
1 parent 794cb64 commit 728c785
Show file tree
Hide file tree
Showing 26 changed files with 612 additions and 7 deletions.
3 changes: 3 additions & 0 deletions npm_and_yarn/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ ARG PNPM_VERSION=9.15.3
# Check for updates at https://github.com/yarnpkg/berry/releases
ARG YARN_VERSION=4.5.3

# Check for updates at https://github.com/oven-sh/bun/releases
ARG BUN_VERSION=1.1.39

# See https://github.com/nodesource/distributions#installation-instructions
ARG NODEJS_VERSION=20
Expand All @@ -27,6 +29,7 @@ RUN mkdir -p /etc/apt/keyrings \
nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g corepack@$COREPACK_VERSION \
&& npm install -g corepack@$COREPACK_VERSION bun@$BUN_VERSION \
&& rm -rf ~/.npm

USER dependabot
Expand Down
48 changes: 48 additions & 0 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class FileUpdater < Dependabot::FileUpdaters::Base
require_relative "file_updater/npm_lockfile_updater"
require_relative "file_updater/yarn_lockfile_updater"
require_relative "file_updater/pnpm_lockfile_updater"
require_relative "file_updater/bun_lockfile_updater"

class NoChangeError < StandardError
extend T::Sig
Expand Down Expand Up @@ -189,6 +190,15 @@ def pnpm_locks
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def bun_locks
@bun_locks ||= T.let(
filtered_dependency_files
.select { |f| f.name.end_with?("bun.lock") },
T.nilable(T::Array[Dependabot::DependencyFile])
)
end

sig { returns(T::Array[Dependabot::DependencyFile]) }
def shrinkwraps
@shrinkwraps ||= T.let(
Expand Down Expand Up @@ -217,6 +227,11 @@ def pnpm_lock_changed?(pnpm_lock)
pnpm_lock.content != updated_pnpm_lock_content(pnpm_lock)
end

sig { params(bun_lock: Dependabot::DependencyFile).returns(T::Boolean) }
def bun_lock_changed?(bun_lock)
bun_lock.content != updated_bun_lock_content(bun_lock)
end

sig { params(package_lock: Dependabot::DependencyFile).returns(T::Boolean) }
def package_lock_changed?(package_lock)
package_lock.content != updated_lockfile_content(package_lock)
Expand All @@ -237,6 +252,8 @@ def updated_manifest_files
end
end

# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/PerceivedComplexity
sig { returns(T::Array[Dependabot::DependencyFile]) }
def updated_lockfiles
updated_files = []
Expand All @@ -259,6 +276,15 @@ def updated_lockfiles
)
end

bun_locks.each do |bun_lock|
next unless bun_lock_changed?(bun_lock)

updated_files << updated_file(
file: bun_lock,
content: updated_bun_lock_content(bun_lock)
)
end

package_locks.each do |package_lock|
next unless package_lock_changed?(package_lock)

Expand All @@ -279,6 +305,8 @@ def updated_lockfiles

updated_files
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/PerceivedComplexity

sig { params(yarn_lock: Dependabot::DependencyFile).returns(String) }
def updated_yarn_lock_content(yarn_lock)
Expand All @@ -294,6 +322,13 @@ def updated_pnpm_lock_content(pnpm_lock)
pnpm_lockfile_updater.updated_pnpm_lock_content(pnpm_lock)
end

sig { params(bun_lock: Dependabot::DependencyFile).returns(String) }
def updated_bun_lock_content(bun_lock)
@updated_bun_lock_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
@updated_bun_lock_content[bun_lock.name] ||=
bun_lockfile_updater.updated_bun_lock_content(bun_lock)
end

sig { returns(Dependabot::NpmAndYarn::FileUpdater::YarnLockfileUpdater) }
def yarn_lockfile_updater
@yarn_lockfile_updater ||= T.let(
Expand All @@ -320,6 +355,19 @@ def pnpm_lockfile_updater
)
end

sig { returns(Dependabot::NpmAndYarn::FileUpdater::BunLockfileUpdater) }
def bun_lockfile_updater
@bun_lockfile_updater ||= T.let(
BunLockfileUpdater.new(
dependencies: dependencies,
dependency_files: dependency_files,
repo_contents_path: repo_contents_path,
credentials: credentials
),
T.nilable(Dependabot::NpmAndYarn::FileUpdater::BunLockfileUpdater)
)
end

sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) }
def updated_lockfile_content(file)
@updated_lockfile_content ||= T.let({}, T.nilable(T::Hash[String, T.nilable(String)]))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# typed: true
# frozen_string_literal: true

require "dependabot/npm_and_yarn/helpers"
require "dependabot/npm_and_yarn/update_checker/registry_finder"
require "dependabot/npm_and_yarn/registry_parser"
require "dependabot/shared_helpers"

module Dependabot
module NpmAndYarn
class FileUpdater < Dependabot::FileUpdaters::Base
class BunLockfileUpdater
require_relative "npmrc_builder"
require_relative "package_json_updater"

def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
@dependencies = dependencies
@dependency_files = dependency_files
@repo_contents_path = repo_contents_path
@credentials = credentials
end

def updated_bun_lock_content(bun_lock)
@updated_bun_lock_content ||= {}
return @updated_bun_lock_content[bun_lock.name] if @updated_bun_lock_content[bun_lock.name]

new_content = run_bun_update(bun_lock: bun_lock)
@updated_bun_lock_content[bun_lock.name] = new_content
rescue SharedHelpers::HelperSubprocessFailed => e
handle_bun_lock_updater_error(e, bun_lock)
end

private

attr_reader :dependencies
attr_reader :dependency_files
attr_reader :repo_contents_path
attr_reader :credentials

ERR_PATTERNS = {
/get .* 404/i => Dependabot::DependencyNotFound,
/installfailed cloning repository/i => Dependabot::DependencyNotFound,
/file:.* failed to resolve/i => Dependabot::DependencyNotFound,
/no version matching/i => Dependabot::DependencyFileNotResolvable,
/failed to resolve/i => Dependabot::DependencyFileNotResolvable
}.freeze

def run_bun_update(bun_lock:)
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
File.write(".npmrc", npmrc_content(bun_lock))

SharedHelpers.with_git_configured(credentials: credentials) do
run_bun_updater

write_final_package_json_files

run_bun_install

File.read(bun_lock.name)
end
end
end

def run_bun_updater
dependency_updates = dependencies.map do |d|
"#{d.name}@#{d.version}"
end.join(" ")

Helpers.run_bun_command(
"install #{dependency_updates} --save-text-lockfile",
fingerprint: "install <dependency_updates> --save-text-lockfile"
)
end

def run_bun_install
Helpers.run_bun_command(
"install --save-text-lockfile"
)
end

def lockfile_dependencies(lockfile)
@lockfile_dependencies ||= {}
@lockfile_dependencies[lockfile.name] ||=
NpmAndYarn::FileParser.new(
dependency_files: [lockfile, *package_files],
source: nil,
credentials: credentials
).parse
end

def handle_bun_lock_updater_error(error, _bun_lock)
error_message = error.message

ERR_PATTERNS.each do |pattern, error_class|
raise error_class, error_message if error_message.match?(pattern)
end

raise error
end

def write_final_package_json_files
package_files.each do |file|
path = file.name
FileUtils.mkdir_p(Pathname.new(path).dirname)
File.write(path, updated_package_json_content(file))
end
end

def npmrc_content(bun_lock)
NpmrcBuilder.new(
credentials: credentials,
dependency_files: dependency_files,
dependencies: lockfile_dependencies(bun_lock)
).npmrc_content
end

def updated_package_json_content(file)
@updated_package_json_content ||= {}
@updated_package_json_content[file.name] ||=
PackageJsonUpdater.new(
package_json: file,
dependencies: dependencies
).updated_package_json.content
end

def package_files
@package_files ||= dependency_files.select { |f| f.name.end_with?("package.json") }
end

def base_dir
dependency_files.first.directory
end

def npmrc_file
dependency_files.find { |f| f.name == ".npmrc" }
end

def sanitize_message(message)
message.gsub(/"|\[|\]|\}|\{/, "")
end
end
end
end
end
31 changes: 31 additions & 0 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,35 @@ def self.run_node_command(command, fingerprint: nil)
raise
end

sig { returns(T.nilable(String)) }
def self.bun_version
version = run_bun_command("--version", fingerprint: "--version").strip
if version.include?("+")
version.split("+").first # Remove build info, if present
end
rescue StandardError => e
Dependabot.logger.error("Error retrieving Bun version: #{e.message}")
nil
end

sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
def self.run_bun_command(command, fingerprint: nil)
full_command = "bun #{command}"

Dependabot.logger.info("Running bun command: #{full_command}")

result = Dependabot::SharedHelpers.run_shell_command(
full_command,
fingerprint: "bun #{fingerprint || command}"
)

Dependabot.logger.info("Command executed successfully: #{full_command}")
result
rescue StandardError => e
Dependabot.logger.error("Error running bun command: #{full_command}, Error: #{e.message}")
raise
end

# Setup yarn and run a single yarn command returning stdout/stderr
sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
def self.run_yarn_command(command, fingerprint: nil)
Expand Down Expand Up @@ -505,6 +534,8 @@ def self.package_manager_version(name)
).returns(String)
end
def self.package_manager_run_command(name, command, fingerprint: nil)
return run_bun_command(command, fingerprint: fingerprint) if name == BunPackageManager::NAME

full_command = "corepack #{name} #{command}"

result = Dependabot::SharedHelpers.run_shell_command(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def pnpm_locks
.select { |f| f.name.end_with?("pnpm-lock.yaml") }
end

def bun_locks
@bun_locks ||=
dependency_files
.select { |f| f.name.end_with?("bun.lock") }
end

def root_yarn_lock
@root_yarn_lock ||=
dependency_files
Expand All @@ -61,14 +67,20 @@ def root_pnpm_lock
.find { |f| f.name == "pnpm-lock.yaml" }
end

def root_bun_lock
@root_bun_lock ||=
dependency_files
.find { |f| f.name == "bun.lock" }
end

def shrinkwraps
@shrinkwraps ||=
dependency_files
.select { |f| f.name.end_with?("npm-shrinkwrap.json") }
end

def lockfiles
[*package_locks, *shrinkwraps, *yarn_locks, *pnpm_locks]
[*package_locks, *shrinkwraps, *yarn_locks, *pnpm_locks, *bun_locks]
end

def package_files
Expand All @@ -89,12 +101,7 @@ def write_lockfiles
File.write(f.name, prepared_yarn_lockfile_content(f.content))
end

pnpm_locks.each do |f|
FileUtils.mkdir_p(Pathname.new(f.name).dirname)
File.write(f.name, f.content)
end

[*package_locks, *shrinkwraps].each do |f|
[*package_locks, *shrinkwraps, *pnpm_locks, *bun_locks].each do |f|
FileUtils.mkdir_p(Pathname.new(f.name).dirname)
File.write(f.name, f.content)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def update_subdependency_in_lockfile(lockfile)
run_yarn_updater(path, lockfile_name)
elsif lockfile.name.end_with?("pnpm-lock.yaml")
run_pnpm_updater(path, lockfile_name)
elsif lockfile.name.end_with?("bun.lock")
run_bun_updater(path, lockfile_name)
elsif !Helpers.npm8?(lockfile)
run_npm6_updater(path, lockfile_name)
else
Expand Down Expand Up @@ -153,6 +155,18 @@ def run_npm_updater(path, lockfile_name)
end
end

def run_bun_updater(path, lockfile_name)
SharedHelpers.with_git_configured(credentials: credentials) do
Dir.chdir(path) do
Helpers.run_bun_command(
"update #{dependency.name} --save-text-lockfile",
fingerprint: "update <dependency_name> --save-text-lockfile"
)
{ lockfile_name => File.read(lockfile_name) }
end
end
end

def run_npm6_updater(path, lockfile_name)
SharedHelpers.with_git_configured(credentials: credentials) do
Dir.chdir(path) do
Expand Down
Loading

0 comments on commit 728c785

Please sign in to comment.