class Dependabot::NpmAndYarn::FileUpdater::YarnLockfileUpdater
Constants
- INVALID_PACKAGE
- TIMEOUT_FETCHING_PACKAGE
- UNREACHABLE_GIT
Attributes
credentials[R]
dependencies[R]
dependency_files[R]
Public Class Methods
new(dependencies:, dependency_files:, credentials:)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 18 def initialize(dependencies:, dependency_files:, credentials:) @dependencies = dependencies @dependency_files = dependency_files @credentials = credentials end
Public Instance Methods
updated_yarn_lock_content(yarn_lock)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 24 def updated_yarn_lock_content(yarn_lock) @updated_yarn_lock_content ||= {} return @updated_yarn_lock_content[yarn_lock.name] if @updated_yarn_lock_content[yarn_lock.name] new_content = updated_yarn_lock(yarn_lock) @updated_yarn_lock_content[yarn_lock.name] = post_process_yarn_lockfile(new_content) end
Private Instance Methods
dependencies_in_error_message?(message)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 274 def dependencies_in_error_message?(message) names = dependencies.map { |dep| dep.name.split("/").first } # Example format: Couldn't find any versions for # "@dependabot/dummy-pkg-b" that matches "^1.3.0" names.any? do |name| message.match?(%r{"#{Regexp.quote(name)}["\/]}) end end
git_ssh_requirements_to_swap()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 347 def git_ssh_requirements_to_swap return @git_ssh_requirements_to_swap if @git_ssh_requirements_to_swap git_dependencies = dependencies. select do |dep| dep.requirements.any? { |r| r.dig(:source, :type) == "git" } end @git_ssh_requirements_to_swap = [] package_files.each do |file| NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |t| JSON.parse(file.content).fetch(t, {}).each do |nm, requirement| next unless git_dependencies.map(&:name).include?(nm) next unless requirement.start_with?("git+ssh:") req = requirement.split("#").first @git_ssh_requirements_to_swap << req end end end @git_ssh_requirements_to_swap end
handle_missing_package(package_name, error_message, yarn_lock)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 410 def handle_missing_package(package_name, error_message, yarn_lock) missing_dep = lockfile_dependencies(yarn_lock). find { |dep| dep.name == package_name } raise_resolvability_error(error_message, yarn_lock) unless missing_dep reg = NpmAndYarn::UpdateChecker::RegistryFinder.new( dependency: missing_dep, credentials: credentials, npmrc_file: npmrc_file, yarnrc_file: yarnrc_file ).registry return if UpdateChecker::RegistryFinder.central_registry?(reg) && !package_name.start_with?("@") raise PrivateSourceAuthenticationFailure, reg end
handle_timeout(error_message, yarn_lock)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 435 def handle_timeout(error_message, yarn_lock) url = error_message.match(TIMEOUT_FETCHING_PACKAGE). named_captures["url"] return if url.start_with?("https://registry.npmjs.org") package_name = error_message.match(TIMEOUT_FETCHING_PACKAGE). named_captures["package"] sanitized_name = sanitize_package_name(package_name) dep = lockfile_dependencies(yarn_lock). find { |d| d.name == sanitized_name } return unless dep raise PrivateSourceTimedOut, url.gsub(%r{https?://}, "") end
handle_yarn_lock_updater_error(error, yarn_lock)
click to toggle source
rubocop:disable Metrics/AbcSize rubocop:disable Metrics/PerceivedComplexity rubocop:disable Metrics/MethodLength
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 166 def handle_yarn_lock_updater_error(error, yarn_lock) error_message = error.message # Invalid package: When package.json doesn't include a name or version # Local path error: When installing a git dependency which # is using local file paths for sub-dependencies (e.g. unbuilt yarn # workspace project) sub_dep_local_path_err = 'Package "" refers to a non-existing file' if error_message.match?(INVALID_PACKAGE) || error_message.start_with?(sub_dep_local_path_err) raise_resolvability_error(error_message, yarn_lock) end if error_message.include?("Couldn't find package") package_name = error_message.match(/package "(?<package_req>.*?)"/). named_captures["package_req"]. split(/(?<=\w)\@/).first sanitized_name = sanitize_package_name(package_name) sanitized_error = error_message.gsub(package_name, sanitized_name) handle_missing_package(sanitized_name, sanitized_error, yarn_lock) end if error_message.match?(%r{/[^/]+: Not found}) package_name = error_message. match(%r{/(?<package_name>[^/]+): Not found}). named_captures["package_name"] sanitized_name = sanitize_package_name(package_name) sanitized_error = error_message.gsub(package_name, sanitized_name) handle_missing_package(sanitized_name, sanitized_error, yarn_lock) end # TODO: Move this logic to the version resolver and check if a new # version and all of its subdependencies are resolvable # Make sure the error in question matches the current list of # dependencies or matches an existing scoped package, this handles the # case where a new version (e.g. @angular-devkit/build-angular) relies # on a added dependency which hasn't been published yet under the same # scope (e.g. @angular-devkit/build-optimizer) # # This seems to happen when big monorepo projects publish all of their # packages sequentially, which might take enough time for Dependabot # to hear about a new version before all of its dependencies have been # published # # OR # # This happens if a new version has been published but npm is having # consistency issues and the version isn't fully available on all # queries if error_message.start_with?("Couldn't find any versions") && dependencies_in_error_message?(error_message) && resolvable_before_update?(yarn_lock) # Raise a bespoke error so we can capture and ignore it if # we're trying to create a new PR (which will be created # successfully at a later date) raise Dependabot::InconsistentRegistryResponse, error_message end if error_message.include?("Workspaces can only be enabled in priva") raise Dependabot::DependencyFileNotEvaluatable, error_message end if error_message.match?(UNREACHABLE_GIT) dependency_url = error_message.match(UNREACHABLE_GIT). named_captures.fetch("url") raise Dependabot::GitDependenciesNotReachable, dependency_url end handle_timeout(error_message, yarn_lock) if error_message.match?(TIMEOUT_FETCHING_PACKAGE) if error_message.start_with?("Couldn't find any versions") || error_message.include?(": Not found") raise_resolvability_error(error_message, yarn_lock) unless resolvable_before_update?(yarn_lock) # Dependabot has probably messed something up with the update and we # want to hear about it raise error end raise error end
lockfile_dependencies(lockfile)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 400 def lockfile_dependencies(lockfile) @lockfile_dependencies ||= {} @lockfile_dependencies[lockfile.name] ||= NpmAndYarn::FileParser.new( dependency_files: [lockfile, *package_files], source: nil, credentials: credentials ).parse end
npmrc_content()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 451 def npmrc_content NpmrcBuilder.new( credentials: credentials, dependency_files: dependency_files ).npmrc_content end
npmrc_disables_lockfile?()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 467 def npmrc_disables_lockfile? npmrc_content.match?(/^package-lock\s*=\s*false/) end
npmrc_file()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 521 def npmrc_file dependency_files.find { |f| f.name == ".npmrc" } end
package_files()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 513 def package_files dependency_files.select { |f| f.name.end_with?("package.json") } end
post_process_yarn_lockfile(lockfile_content)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 373 def post_process_yarn_lockfile(lockfile_content) updated_content = lockfile_content git_ssh_requirements_to_swap.each do |req| new_req = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'https://\1/') updated_content = updated_content.gsub(new_req, req) end # Enforce https for most common hostnames updated_content = updated_content.gsub( %r{http://(.*?(?:yarnpkg\.com|npmjs\.org|npmjs\.com))/}, 'https://\1/' ) updated_content = remove_integrity_lines(updated_content) if remove_integrity_lines? updated_content end
raise_resolvability_error(error_message, yarn_lock)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 428 def raise_resolvability_error(error_message, yarn_lock) dependency_names = dependencies.map(&:name).join(", ") msg = "Error whilst updating #{dependency_names} in "\ "#{yarn_lock.path}:\n#{error_message}" raise Dependabot::DependencyFileNotResolvable, msg end
remove_integrity_lines(content)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 396 def remove_integrity_lines(content) content.lines.reject { |l| l.match?(/\s*integrity sha/) }.join end
remove_integrity_lines?()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 392 def remove_integrity_lines? yarn_locks.none? { |f| f.content.include?(" integrity sha") } end
remove_workspace_path_prefixes(content)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 329 def remove_workspace_path_prefixes(content) json = JSON.parse(content) return content unless json.key?("workspaces") workspace_object = json.fetch("workspaces") paths_array = if workspace_object.is_a?(Hash) workspace_object.values_at("packages", "nohoist"). flatten.compact elsif workspace_object.is_a?(Array) then workspace_object else raise "Unexpected workspace object" end paths_array.each { |path| path.gsub!(%r{^\./}, "") } json.to_json end
replace_ssh_sources(content)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 318 def replace_ssh_sources(content) updated_content = content git_ssh_requirements_to_swap.each do |req| new_req = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'https://\1/') updated_content = updated_content.gsub(req, new_req) end updated_content end
requirements_for_path(requirements, path)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 153 def requirements_for_path(requirements, path) return requirements if path.to_s == "." requirements.map do |r| next unless r[:file].start_with?("#{path}/") r.merge(file: r[:file].gsub(/^#{Regexp.quote("#{path}/")}/, "")) end.compact end
resolvable_before_update?(yarn_lock)
click to toggle source
rubocop:enable Metrics/AbcSize rubocop:enable Metrics/PerceivedComplexity rubocop:enable Metrics/MethodLength
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 254 def resolvable_before_update?(yarn_lock) @resolvable_before_update ||= {} return @resolvable_before_update[yarn_lock.name] if @resolvable_before_update.key?(yarn_lock.name) @resolvable_before_update[yarn_lock.name] = begin SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files(update_package_json: false) lockfile_name = Pathname.new(yarn_lock.name).basename.to_s path = Pathname.new(yarn_lock.name).dirname.to_s run_previous_yarn_update(path: path, lockfile_name: lockfile_name) end true rescue SharedHelpers::HelperSubprocessFailed false end end
run_current_yarn_update(path:, lockfile_name:)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 66 def run_current_yarn_update(path:, lockfile_name:) top_level_dependency_updates = top_level_dependencies.map do |d| { name: d.name, version: d.version, requirements: requirements_for_path(d.requirements, path) } end run_yarn_updater( path: path, lockfile_name: lockfile_name, top_level_dependency_updates: top_level_dependency_updates ) end
run_previous_yarn_update(path:, lockfile_name:)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 82 def run_previous_yarn_update(path:, lockfile_name:) previous_top_level_dependencies = top_level_dependencies.map do |d| { name: d.name, version: d.previous_version, requirements: requirements_for_path( d.previous_requirements, path ) } end run_yarn_updater( path: path, lockfile_name: lockfile_name, top_level_dependency_updates: previous_top_level_dependencies ) end
run_yarn_subdependency_updater(lockfile_name:)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 145 def run_yarn_subdependency_updater(lockfile_name:) SharedHelpers.run_helper_subprocess( command: NativeHelpers.helper_path, function: "yarn:updateSubdependency", args: [Dir.pwd, lockfile_name, sub_dependencies.first.to_h] ) end
run_yarn_top_level_updater(top_level_dependency_updates:)
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 134 def run_yarn_top_level_updater(top_level_dependency_updates:) SharedHelpers.run_helper_subprocess( command: NativeHelpers.helper_path, function: "yarn:update", args: [ Dir.pwd, top_level_dependency_updates ] ) end
run_yarn_updater(path:, lockfile_name:, top_level_dependency_updates:)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 101 def run_yarn_updater(path:, lockfile_name:, top_level_dependency_updates:) SharedHelpers.with_git_configured(credentials: credentials) do Dir.chdir(path) do if top_level_dependency_updates.any? run_yarn_top_level_updater( top_level_dependency_updates: top_level_dependency_updates ) else run_yarn_subdependency_updater(lockfile_name: lockfile_name) end end end rescue SharedHelpers::HelperSubprocessFailed => e names = dependencies.map(&:name) package_missing = names.any? do |name| e.message.include?("find package \"#{name}") end raise unless e.message.include?("The registry may be down") || e.message.include?("ETIMEDOUT") || e.message.include?("ENOBUFS") || package_missing retry_count ||= 0 retry_count += 1 raise if retry_count > 2 sleep(rand(3.0..10.0)) && retry end
sanitize_package_name(package_name)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 503 def sanitize_package_name(package_name) package_name.gsub("%2f", "/").gsub("%2F", "/") end
sanitized_package_json_content(content)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 491 def sanitized_package_json_content(content) updated_content = content. gsub(/\{\{[^\}]*?\}\}/, "something"). # {{ nm }} syntax not allowed gsub(/(?<!\\)\\ /, " "). # escaped whitespace not allowed gsub(%r{^\s*//.*}, " ") # comments are not allowed json = JSON.parse(updated_content) json["name"] = json["name"].delete(" ") if json["name"].is_a?(String) json.to_json end
sub_dependencies()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 47 def sub_dependencies dependencies.reject(&:top_level?) end
top_level_dependencies()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 43 def top_level_dependencies dependencies.select(&:top_level?) end
updated_package_json_content(file)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 458 def updated_package_json_content(file) @updated_package_json_content ||= {} @updated_package_json_content[file.name] ||= PackageJsonUpdater.new( package_json: file, dependencies: top_level_dependencies ).updated_package_json.content end
updated_yarn_lock(yarn_lock)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 51 def updated_yarn_lock(yarn_lock) SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files lockfile_name = Pathname.new(yarn_lock.name).basename.to_s path = Pathname.new(yarn_lock.name).dirname.to_s updated_files = run_current_yarn_update( path: path, lockfile_name: lockfile_name ) updated_files.fetch(lockfile_name) end rescue SharedHelpers::HelperSubprocessFailed => e handle_yarn_lock_updater_error(e, yarn_lock) end
write_lockfiles()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 311 def write_lockfiles yarn_locks.each do |f| FileUtils.mkdir_p(Pathname.new(f.name).dirname) File.write(f.name, f.content) end end
write_temporary_dependency_files(update_package_json: true)
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 283 def write_temporary_dependency_files(update_package_json: true) write_lockfiles File.write(".npmrc", npmrc_content) File.write(".yarnrc", yarnrc_content) if yarnrc_specifies_npm_reg? package_files.each do |file| path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) updated_content = if update_package_json && top_level_dependencies.any? updated_package_json_content(file) else file.content end updated_content = replace_ssh_sources(updated_content) # A bug prevents Yarn recognising that a directory is part of a # workspace if it is specified with a `./` prefix. updated_content = remove_workspace_path_prefixes(updated_content) updated_content = sanitized_package_json_content(updated_content) File.write(file.name, updated_content) end end
yarn_locks()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 507 def yarn_locks @yarn_locks ||= dependency_files. select { |f| f.name.end_with?("yarn.lock") } end
yarnrc_content()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 487 def yarnrc_content 'registry "https://registry.npmjs.org"' end
yarnrc_file()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 517 def yarnrc_file dependency_files.find { |f| f.name == ".yarnrc" } end
yarnrc_specifies_npm_reg?()
click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb, line 471 def yarnrc_specifies_npm_reg? return false unless yarnrc_file regex = UpdateChecker::RegistryFinder::YARN_GLOBAL_REGISTRY_REGEX yarnrc_global_registry = yarnrc_file.content. lines.find { |line| line.match?(regex) }&. match(regex)&. named_captures&. fetch("registry") return false unless yarnrc_global_registry yarnrc_global_registry.include?("registry.npmjs.org") end