Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial yarn berry support #5660

Merged
merged 27 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
161673a
Add yarn berry file parser test case
jurre Sep 7, 2022
f86a61b
Install yarn berry and run it when updating lockfile
jurre Sep 7, 2022
fe00215
Pass in requirement instead of version to yarn add cmd
jurre Sep 8, 2022
d248155
Clarify, cleanup a bit
jurre Sep 8, 2022
e96d9f7
Update yarn caches
jurre Sep 8, 2022
5c136af
We do not need the yarn berry parser
jurre Sep 8, 2022
67074d2
Rubocop
jurre Sep 9, 2022
2471df1
Use corepack to install yarn berry
pavera Sep 8, 2022
0487801
hacking in some dynamic yarn version detection
pavera Sep 8, 2022
8bcb5c7
Clone full repo so yarn cache is available
jurre Sep 9, 2022
f8f632c
Rubocop
jurre Sep 9, 2022
07dcff0
Basic support for yarn berry workspaces
jurre Sep 9, 2022
e8269cd
Implement yarn berry subdependency updates
jurre Sep 12, 2022
cb233ba
Fix file fetcher package manager instrumentation
jurre Sep 13, 2022
529b515
Pass correct basename to yarn 1 subdep updater
jurre Sep 13, 2022
d7b6bd5
Activate yarn 3.2 after installing native helpers
jurre Sep 13, 2022
1dcda58
Add test for peer dependency resolution in yarn berry
jurre Sep 13, 2022
c161ad1
Fix lockfile updater specs
jurre Sep 14, 2022
e21bc15
Fix typo in lockfile updater
jurre Sep 14, 2022
d5fd3e8
Only dedupe the package being updated for now
jurre Sep 14, 2022
0a7c7be
Fix aliased_package_name check
jurre Sep 21, 2022
4c2106b
Gate yarn berry changes behind feature-flag
jurre Sep 19, 2022
5786662
Run yarn commands without running scripts
jurre Sep 22, 2022
5f3dcd3
Improve readability of yarn_berry check in lockfile parser
jurre Sep 22, 2022
cbd4684
Make yarn version parsing a bit safer
jurre Sep 22, 2022
7ba3e3d
Remove unnecessary comment
jurre Sep 22, 2022
193d22d
Fix rubocop violation
jurre Sep 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
&& npm install -g [email protected] \
&& rm -rf ~/.npm

# Install yarn berry and set it to a stable version
RUN corepack enable \
&& corepack prepare [email protected] --activate

### ELM

Expand Down Expand Up @@ -287,6 +290,9 @@ RUN bash /opt/pub/helpers/build

COPY --chown=dependabot:dependabot npm_and_yarn/helpers /opt/npm_and_yarn/helpers
RUN bash /opt/npm_and_yarn/helpers/build
# Our native helpers pull in yarn 1, so we need to reset the version globally to
# 3.2.3.
RUN corepack prepare [email protected] --activate

COPY --chown=dependabot:dependabot python/helpers /opt/python/helpers
RUN bash /opt/python/helpers/build
Expand Down
26 changes: 24 additions & 2 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def fetch_files
fetched_files << lerna_json if lerna_json
fetched_files << npmrc if npmrc
fetched_files << yarnrc if yarnrc
fetched_files << yarnrc_yml if yarnrc_yml
fetched_files += workspace_package_jsons
fetched_files += lerna_packages
fetched_files += path_dependencies(fetched_files)
Expand All @@ -53,8 +54,8 @@ def fetch_files
def instrument_package_manager_version
package_managers = {}

package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
package_managers["yarn"] = 1 if yarn_lock
package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
package_managers["yarn"] = yarn_version if yarn_version
package_managers["shrinkwrap"] = 1 if shrinkwrap

Dependabot.instrument(
Expand All @@ -64,6 +65,22 @@ def instrument_package_manager_version
)
end

def yarn_version
return @yarn_version if defined?(@yarn_version)

package = JSON.parse(package_json.content)
if (pkgmanager = package.fetch("packageManager", nil))
get_yarn_version_from_path(pkgmanager)
elsif yarn_lock
1
end
end

def get_yarn_version_from_path(path)
version_match = path.match(/yarn@(?<version>\d+.\d+.\d+)/)
version_match&.named_captures&.fetch("version", nil)
end

def package_json
@package_json ||= fetch_file_from_host("package.json")
end
Expand Down Expand Up @@ -118,6 +135,11 @@ def yarnrc
@yarnrc
end

def yarnrc_yml
@yarnrc_yml ||= fetch_file_if_present(".yarnrc.yml")&.
tap { |f| f.support_file = true }
end

def lerna_json
@lerna_json ||= fetch_file_if_present("lerna.json")&.
tap { |f| f.support_file = true }
Expand Down
3 changes: 3 additions & 0 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# See https://docs.npmjs.com/files/package.json for package.json format docs.

require "dependabot/dependency"
require "dependabot/experiments"
require "dependabot/file_parsers"
require "dependabot/file_parsers/base"
require "dependabot/shared_helpers"
Expand Down Expand Up @@ -94,6 +95,7 @@ def build_dependency(file:, type:, name:, requirement:)
manifest_name: file.name
)
version = version_for(name, requirement, file.name)

return if lockfile_details && !version
return if ignore_requirement?(requirement)
return if workspace_package_names.include?(name)
Expand Down Expand Up @@ -326,6 +328,7 @@ def package_files
dependency_files.
select { |f| f.name.end_with?("package.json") }.
reject { |f| f.name == "package.json" }.
reject { |f| f.name.include?("node_modules/") if Experiments.enabled?(:yarn_berry) }.
reject(&:support_file?)

[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ def yarn_lock_dependencies
parse_yarn_lock(yarn_lock).each do |req, details|
next unless semver_version_for(details["version"])
next if alias_package?(req)
next if Experiments.enabled?(:yarn_berry) && workspace_package?(req)
next if Experiments.enabled?(:yarn_berry) && req == "__metadata"

# NOTE: The DependencySet will de-dupe our dependencies, so they
# end up unique by name. That's not a perfect representation of
Expand Down Expand Up @@ -188,7 +190,15 @@ def semver_version_for(version_string)
end

def alias_package?(requirement)
requirement.include?("@npm:")
if Experiments.enabled?(:yarn_berry)
requirement.match?(/@npm:(.+@(?!npm))/)
else
requirement.include?("@npm:")
end
end

def workspace_package?(requirement)
requirement.include?("@workspace:")
end

def parse_package_lock(package_lock)
Expand Down
46 changes: 46 additions & 0 deletions npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# frozen_string_literal: true

require "dependabot/experiments"
require "dependabot/file_updaters"
require "dependabot/file_updaters/base"
require "dependabot/file_updaters/vendor_updater"
require "dependabot/npm_and_yarn/dependency_files_filterer"
require "dependabot/npm_and_yarn/sub_dependency_files_filterer"

Expand Down Expand Up @@ -53,11 +55,54 @@ def updated_dependency_files
)
end

if Experiments.enabled?(:yarn_berry)
base_dir = updated_files.first.directory
vendor_updater.updated_vendor_cache_files(base_directory: base_dir).each { |file| updated_files << file }
install_state_updater.updated_vendor_cache_files(base_directory: base_dir).each do |file|
updated_files << file
end
end

updated_files
end

private

# Dynamically fetch the vendor cache folder from yarn
def vendor_cache_dir
return @vendor_cache_dir if defined?(@vendor_cache_dir)

@vendor_cache_dir = if File.exist?(".yarnrc.yml")
YAML.load_file(".yarnrc.yml").fetch("cacheFolder", "./.yarn/cache")
else
"./.yarn/cache"
end
end

def install_state_path
return @install_state_path if defined?(@install_state_path)

@install_state_path = if File.exist?(".yarnrc.yml")
YAML.load_file(".yarnrc.yml").fetch("installStatePath", "./.yarn/install-state.gz")
else
"./.yarn/install-state.gz"
end
end

def vendor_updater
Dependabot::FileUpdaters::VendorUpdater.new(
repo_contents_path: repo_contents_path,
vendor_dir: vendor_cache_dir
)
end

def install_state_updater
Dependabot::FileUpdaters::VendorUpdater.new(
repo_contents_path: repo_contents_path,
vendor_dir: install_state_path
)
end

def filtered_dependency_files
@filtered_dependency_files ||=
if dependencies.select(&:top_level?).any?
Expand Down Expand Up @@ -175,6 +220,7 @@ def yarn_lockfile_updater
YarnLockfileUpdater.new(
dependencies: dependencies,
dependency_files: dependency_files,
repo_contents_path: repo_contents_path,
credentials: credentials
)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

require "dependabot/npm_and_yarn/file_updater"
require "dependabot/npm_and_yarn/file_parser"
require "dependabot/npm_and_yarn/helpers"
require "dependabot/npm_and_yarn/update_checker/registry_finder"
require "dependabot/npm_and_yarn/native_helpers"
require "dependabot/shared_helpers"
require "dependabot/errors"
require "dependabot/experiments"

# rubocop:disable Metrics/ClassLength
module Dependabot
Expand All @@ -17,9 +19,10 @@ class YarnLockfileUpdater
require_relative "npmrc_builder"
require_relative "package_json_updater"

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

Expand All @@ -35,7 +38,7 @@ def updated_yarn_lock_content(yarn_lock)

private

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

UNREACHABLE_GIT = /ls-remote --tags --heads (?<url>.*)/.freeze
TIMEOUT_FETCHING_PACKAGE =
Expand All @@ -51,21 +54,22 @@ def sub_dependencies
end

def updated_yarn_lock(yarn_lock)
SharedHelpers.in_a_temporary_directory do
base_dir = dependency_files.first.directory
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) 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
yarn_lock: yarn_lock
)
updated_files.fetch(lockfile_name)
end
rescue SharedHelpers::HelperSubprocessFailed => e
handle_yarn_lock_updater_error(e, yarn_lock)
end

def run_current_yarn_update(path:, lockfile_name:)
def run_current_yarn_update(path:, yarn_lock:)
top_level_dependency_updates = top_level_dependencies.map do |d|
{
name: d.name,
Expand All @@ -76,12 +80,12 @@ def run_current_yarn_update(path:, lockfile_name:)

run_yarn_updater(
path: path,
lockfile_name: lockfile_name,
yarn_lock: yarn_lock,
top_level_dependency_updates: top_level_dependency_updates
)
end

def run_previous_yarn_update(path:, lockfile_name:)
def run_previous_yarn_update(path:, yarn_lock:)
previous_top_level_dependencies = top_level_dependencies.map do |d|
{
name: d.name,
Expand All @@ -94,22 +98,29 @@ def run_previous_yarn_update(path:, lockfile_name:)

run_yarn_updater(
path: path,
lockfile_name: lockfile_name,
yarn_lock: yarn_lock,
top_level_dependency_updates: previous_top_level_dependencies
)
end

# rubocop:disable Metrics/PerceivedComplexity
def run_yarn_updater(path:, lockfile_name:,
top_level_dependency_updates:)
def run_yarn_updater(path:, yarn_lock:, 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
)
if yarn_berry?(yarn_lock)
run_yarn_berry_top_level_updater(top_level_dependency_updates: top_level_dependency_updates,
yarn_lock: yarn_lock)
else

run_yarn_top_level_updater(
top_level_dependency_updates: top_level_dependency_updates
)
end
elsif yarn_berry?(yarn_lock)
run_yarn_berry_subdependency_updater(yarn_lock: yarn_lock)
else
run_yarn_subdependency_updater(lockfile_name: lockfile_name)
run_yarn_subdependency_updater(yarn_lock: yarn_lock)
end
end
end
Expand All @@ -133,6 +144,40 @@ def run_yarn_updater(path:, lockfile_name:,

# rubocop:enable Metrics/PerceivedComplexity

def yarn_berry?(yarn_lock)
return false unless Experiments.enabled?(:yarn_berry)

yaml = YAML.safe_load(yarn_lock.content)
yaml.key?("__metadata")
rescue StandardError
false
end

def run_yarn_berry_top_level_updater(top_level_dependency_updates:, yarn_lock:)
updates = top_level_dependency_updates.collect do |dep|
# when there are multiple requirements, we're dealing with a
# workspace-like setup, where there are multiple package.json files
# that pull in the same dependency. It appears that these are always
# updated to a single new version, so we just pick the first one.
"#{dep[:name]}@#{dep[:requirements].first[:requirement]}"
end
command = "yarn add #{updates.join(' ')}"
Helpers.run_yarn_commands(command)
{ yarn_lock.name => File.read(yarn_lock.name) }
end

def run_yarn_berry_subdependency_updater(yarn_lock:)
dep = sub_dependencies.first
update = "#{dep.name}@#{dep.version}"

Helpers.run_yarn_commands(
"yarn add #{update}",
"yarn dedupe #{dep.name}",
"yarn remove #{dep.name}"
)
{ yarn_lock.name => File.read(yarn_lock.name) }
end

def run_yarn_top_level_updater(top_level_dependency_updates:)
SharedHelpers.run_helper_subprocess(
command: NativeHelpers.helper_path,
Expand All @@ -144,7 +189,8 @@ def run_yarn_top_level_updater(top_level_dependency_updates:)
)
end

def run_yarn_subdependency_updater(lockfile_name:)
def run_yarn_subdependency_updater(yarn_lock:)
lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
SharedHelpers.run_helper_subprocess(
command: NativeHelpers.helper_path,
function: "yarn:updateSubdependency",
Expand Down Expand Up @@ -259,12 +305,11 @@ def resolvable_before_update?(yarn_lock)

@resolvable_before_update[yarn_lock.name] =
begin
SharedHelpers.in_a_temporary_directory do
base_dir = dependency_files.first.directory
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) 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)
run_previous_yarn_update(path: path, yarn_lock: yarn_lock)
end

true
Expand Down
10 changes: 10 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 @@ -15,6 +15,16 @@ def self.npm_version_numeric(lockfile_content)
rescue JSON::ParserError
6
end

# Run any number of yarn commands while ensuring that `enableScripts` is
# set to false. Yarn commands should _not_ be ran outside of this helper
# to ensure that postinstall scripts are never executed, as they could
# contain malicious code.
def self.run_yarn_commands(*commands)
# We never want to execute postinstall scripts
SharedHelpers.run_shell_command("yarn config set enableScripts false")
commands.each { |cmd| SharedHelpers.run_shell_command(cmd) }
end
end
end
end
Loading