class Dependabot::NpmAndYarn::FileUpdater::PackageJsonUpdater

Attributes

dependencies[R]
package_json[R]

Public Class Methods

new(package_json:, dependencies:) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 9
def initialize(package_json:, dependencies:)
  @package_json = package_json
  @dependencies = dependencies
end

Public Instance Methods

updated_package_json() click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 14
def updated_package_json
  updated_file = package_json.dup
  updated_file.content = updated_package_json_content
  updated_file
end

Private Instance Methods

closing_bracket_index(string) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 233
def closing_bracket_index(string)
  closes_required = 1

  string.chars.each_with_index do |char, index|
    closes_required += 1 if char == "{"
    closes_required -= 1 if char == "}"
    return index if closes_required.zero?
  end

  0
end
declaration_line(dependency_name:, dependency_req:, content:) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 141
def declaration_line(dependency_name:, dependency_req:, content:)
  git_dependency = dependency_req.dig(:source, :type) == "git"

  unless git_dependency
    requirement = dependency_req.fetch(:requirement)
    return content.match(/"#{Regexp.escape(dependency_name)}"\s*:\s*
                          "#{Regexp.escape(requirement)}"/x).to_s
  end

  username, repo =
    dependency_req.dig(:source, :url).split("/").last(2)

  content.match(
    %r{"#{Regexp.escape(dependency_name)}"\s*:\s*
       ".*?#{Regexp.escape(username)}/#{Regexp.escape(repo)}.*"}x
  ).to_s
end
new_requirements(dependency) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 62
def new_requirements(dependency)
  dependency.requirements.select { |r| r[:file] == package_json.name }
end
old_requirement(dependency, new_requirement) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 56
def old_requirement(dependency, new_requirement)
  dependency.previous_requirements.
    select { |r| r[:file] == package_json.name }.
    find { |r| r[:groups] == new_requirement[:groups] }
end
replacement_declaration_line(original_line:, old_req:, new_req:) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 159
def replacement_declaration_line(original_line:, old_req:, new_req:)
  was_git_dependency = old_req.dig(:source, :type) == "git"
  now_git_dependency = new_req.dig(:source, :type) == "git"

  unless was_git_dependency
    return original_line.gsub(
      %("#{old_req.fetch(:requirement)}"),
      %("#{new_req.fetch(:requirement)}")
    )
  end

  unless now_git_dependency
    return original_line.gsub(
      /(?<=\s").*[^\\](?=")/,
      new_req.fetch(:requirement)
    )
  end

  if original_line.match?(/#[\^~=<>]|semver:/)
    return update_git_semver_requirement(
      original_line: original_line,
      old_req: old_req,
      new_req: new_req
    )
  end

  original_line.gsub(
    %(\##{old_req.dig(:source, :ref)}"),
    %(\##{new_req.dig(:source, :ref)}")
  )
end
update_git_semver_requirement(original_line:, old_req:, new_req:) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 191
def update_git_semver_requirement(original_line:, old_req:, new_req:)
  if original_line.include?("semver:")
    return original_line.gsub(
      %(semver:#{old_req.fetch(:requirement)}"),
      %(semver:#{new_req.fetch(:requirement)}")
    )
  end

  raise "Not a semver req!" unless original_line.match?(/#[\^~=<>]/)

  original_line.gsub(
    %(##{old_req.fetch(:requirement)}"),
    %(##{new_req.fetch(:requirement)}")
  )
end
update_package_json_declaration(package_json_content:, new_req:, dependency_name:, old_req:) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 81
def update_package_json_declaration(package_json_content:, new_req:,
                                    dependency_name:, old_req:)
  original_line = declaration_line(
    dependency_name: dependency_name,
    dependency_req: old_req,
    content: package_json_content
  )

  replacement_line = replacement_declaration_line(
    original_line: original_line,
    old_req: old_req,
    new_req: new_req
  )

  groups = new_req.fetch(:groups)

  update_package_json_sections(
    groups,
    package_json_content,
    original_line,
    replacement_line
  )
end
update_package_json_resolutions(package_json_content:, new_req:, dependency:, old_req:) click to toggle source

For full details on how Yarn resolutions work, see github.com/yarnpkg/rfcs/blob/master/implemented/ 0000-selective-versions-resolutions.md

# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 108
def update_package_json_resolutions(package_json_content:, new_req:,
                                    dependency:, old_req:)
  dep = dependency
  resolutions =
    JSON.parse(package_json_content).fetch("resolutions", {}).
    reject { |_, v| v != old_req && v != dep.previous_version }.
    select { |k, _| k == dep.name || k.end_with?("/#{dep.name}") }

  return package_json_content unless resolutions.any?

  content = package_json_content
  resolutions.each do |_, resolution|
    original_line = declaration_line(
      dependency_name: dep.name,
      dependency_req: { requirement: resolution },
      content: content
    )

    new_resolution = resolution == old_req ? new_req : dep.version

    replacement_line = replacement_declaration_line(
      original_line: original_line,
      old_req: { requirement: resolution },
      new_req: { requirement: new_resolution }
    )

    content = update_package_json_sections(
      ["resolutions"], content, original_line, replacement_line
    )
  end
  content
end
update_package_json_sections(sections, content, old_line, new_line) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 207
def update_package_json_sections(sections, content, old_line,
                                 new_line)
  # Currently, Dependabot doesn't update peerDependencies. However,
  # if a development dependency is being updated and its requirement
  # matches the requirement on a peer dependency we probably want to
  # update the peer too.
  #
  # TODO: Move this logic to the UpdateChecker (and parse peer deps)
  sections += ["peerDependencies"]
  sections_regex = /#{sections.join("|")}/

  declaration_blocks = []

  content.scan(/['"]#{sections_regex}['"]\s*:\s*\{/m) do
    mtch = Regexp.last_match
    declaration_blocks <<
      mtch.to_s +
      mtch.post_match[0..closing_bracket_index(mtch.post_match)]
  end

  declaration_blocks.reduce(content.dup) do |new_content, block|
    updated_block = block.sub(old_line, new_line)
    new_content.sub!(block, updated_block)
  end
end
updated_package_json_content() click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 24
def updated_package_json_content
  dependencies.reduce(package_json.content.dup) do |content, dep|
    updated_requirements(dep).each do |new_req|
      old_req = old_requirement(dep, new_req)

      new_content = update_package_json_declaration(
        package_json_content: content,
        dependency_name: dep.name,
        old_req: old_req,
        new_req: new_req
      )

      raise "Expected content to change!" if content == new_content

      content = new_content
    end

    new_requirements(dep).each do |new_req|
      old_req = old_requirement(dep, new_req)

      content = update_package_json_resolutions(
        package_json_content: content,
        new_req: new_req,
        dependency: dep,
        old_req: old_req
      )
    end

    content
  end
end
updated_requirements(dependency) click to toggle source
# File lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb, line 66
def updated_requirements(dependency)
  updated_requirement_pairs =
    dependency.requirements.zip(dependency.previous_requirements).
    reject do |new_req, old_req|
      next true if new_req == old_req
      next false unless old_req[:source].nil?

      new_req[:requirement] == old_req[:requirement]
    end

  updated_requirement_pairs.
    map(&:first).
    select { |r| r[:file] == package_json.name }
end