Skip to content

Commit 9b33750

Browse files
authored
Merge pull request #5660 from dependabot/jurre/yarn-berry
Initial yarn berry support
2 parents 04741be + 193d22d commit 9b33750

File tree

103 files changed

+101126
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+101126
-25
lines changed

Dockerfile

+6
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \
119119
&& npm install -g [email protected] \
120120
&& rm -rf ~/.npm
121121

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

123126
### ELM
124127

@@ -287,6 +290,9 @@ RUN bash /opt/pub/helpers/build
287290

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

291297
COPY --chown=dependabot:dependabot python/helpers /opt/python/helpers
292298
RUN bash /opt/python/helpers/build

npm_and_yarn/lib/dependabot/npm_and_yarn/file_fetcher.rb

+24-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def fetch_files
4242
fetched_files << lerna_json if lerna_json
4343
fetched_files << npmrc if npmrc
4444
fetched_files << yarnrc if yarnrc
45+
fetched_files << yarnrc_yml if yarnrc_yml
4546
fetched_files += workspace_package_jsons
4647
fetched_files += lerna_packages
4748
fetched_files += path_dependencies(fetched_files)
@@ -53,8 +54,8 @@ def fetch_files
5354
def instrument_package_manager_version
5455
package_managers = {}
5556

56-
package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
57-
package_managers["yarn"] = 1 if yarn_lock
57+
package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
58+
package_managers["yarn"] = yarn_version if yarn_version
5859
package_managers["shrinkwrap"] = 1 if shrinkwrap
5960

6061
Dependabot.instrument(
@@ -64,6 +65,22 @@ def instrument_package_manager_version
6465
)
6566
end
6667

68+
def yarn_version
69+
return @yarn_version if defined?(@yarn_version)
70+
71+
package = JSON.parse(package_json.content)
72+
if (pkgmanager = package.fetch("packageManager", nil))
73+
get_yarn_version_from_path(pkgmanager)
74+
elsif yarn_lock
75+
1
76+
end
77+
end
78+
79+
def get_yarn_version_from_path(path)
80+
version_match = path.match(/yarn@(?<version>\d+.\d+.\d+)/)
81+
version_match&.named_captures&.fetch("version", nil)
82+
end
83+
6784
def package_json
6885
@package_json ||= fetch_file_from_host("package.json")
6986
end
@@ -118,6 +135,11 @@ def yarnrc
118135
@yarnrc
119136
end
120137

138+
def yarnrc_yml
139+
@yarnrc_yml ||= fetch_file_if_present(".yarnrc.yml")&.
140+
tap { |f| f.support_file = true }
141+
end
142+
121143
def lerna_json
122144
@lerna_json ||= fetch_file_if_present("lerna.json")&.
123145
tap { |f| f.support_file = true }

npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser.rb

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# See https://docs.npmjs.com/files/package.json for package.json format docs.
44

55
require "dependabot/dependency"
6+
require "dependabot/experiments"
67
require "dependabot/file_parsers"
78
require "dependabot/file_parsers/base"
89
require "dependabot/shared_helpers"
@@ -94,6 +95,7 @@ def build_dependency(file:, type:, name:, requirement:)
9495
manifest_name: file.name
9596
)
9697
version = version_for(name, requirement, file.name)
98+
9799
return if lockfile_details && !version
98100
return if ignore_requirement?(requirement)
99101
return if workspace_package_names.include?(name)
@@ -326,6 +328,7 @@ def package_files
326328
dependency_files.
327329
select { |f| f.name.end_with?("package.json") }.
328330
reject { |f| f.name == "package.json" }.
331+
reject { |f| f.name.include?("node_modules/") if Experiments.enabled?(:yarn_berry) }.
329332
reject(&:support_file?)
330333

331334
[

npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb

+11-1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def yarn_lock_dependencies
9696
parse_yarn_lock(yarn_lock).each do |req, details|
9797
next unless semver_version_for(details["version"])
9898
next if alias_package?(req)
99+
next if Experiments.enabled?(:yarn_berry) && workspace_package?(req)
100+
next if Experiments.enabled?(:yarn_berry) && req == "__metadata"
99101

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

190192
def alias_package?(requirement)
191-
requirement.include?("@npm:")
193+
if Experiments.enabled?(:yarn_berry)
194+
requirement.match?(/@npm:(.+@(?!npm))/)
195+
else
196+
requirement.include?("@npm:")
197+
end
198+
end
199+
200+
def workspace_package?(requirement)
201+
requirement.include?("@workspace:")
192202
end
193203

194204
def parse_package_lock(package_lock)

npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater.rb

+46
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# frozen_string_literal: true
22

3+
require "dependabot/experiments"
34
require "dependabot/file_updaters"
45
require "dependabot/file_updaters/base"
6+
require "dependabot/file_updaters/vendor_updater"
57
require "dependabot/npm_and_yarn/dependency_files_filterer"
68
require "dependabot/npm_and_yarn/sub_dependency_files_filterer"
79

@@ -53,11 +55,54 @@ def updated_dependency_files
5355
)
5456
end
5557

58+
if Experiments.enabled?(:yarn_berry)
59+
base_dir = updated_files.first.directory
60+
vendor_updater.updated_vendor_cache_files(base_directory: base_dir).each { |file| updated_files << file }
61+
install_state_updater.updated_vendor_cache_files(base_directory: base_dir).each do |file|
62+
updated_files << file
63+
end
64+
end
65+
5666
updated_files
5767
end
5868

5969
private
6070

71+
# Dynamically fetch the vendor cache folder from yarn
72+
def vendor_cache_dir
73+
return @vendor_cache_dir if defined?(@vendor_cache_dir)
74+
75+
@vendor_cache_dir = if File.exist?(".yarnrc.yml")
76+
YAML.load_file(".yarnrc.yml").fetch("cacheFolder", "./.yarn/cache")
77+
else
78+
"./.yarn/cache"
79+
end
80+
end
81+
82+
def install_state_path
83+
return @install_state_path if defined?(@install_state_path)
84+
85+
@install_state_path = if File.exist?(".yarnrc.yml")
86+
YAML.load_file(".yarnrc.yml").fetch("installStatePath", "./.yarn/install-state.gz")
87+
else
88+
"./.yarn/install-state.gz"
89+
end
90+
end
91+
92+
def vendor_updater
93+
Dependabot::FileUpdaters::VendorUpdater.new(
94+
repo_contents_path: repo_contents_path,
95+
vendor_dir: vendor_cache_dir
96+
)
97+
end
98+
99+
def install_state_updater
100+
Dependabot::FileUpdaters::VendorUpdater.new(
101+
repo_contents_path: repo_contents_path,
102+
vendor_dir: install_state_path
103+
)
104+
end
105+
61106
def filtered_dependency_files
62107
@filtered_dependency_files ||=
63108
if dependencies.select(&:top_level?).any?
@@ -175,6 +220,7 @@ def yarn_lockfile_updater
175220
YarnLockfileUpdater.new(
176221
dependencies: dependencies,
177222
dependency_files: dependency_files,
223+
repo_contents_path: repo_contents_path,
178224
credentials: credentials
179225
)
180226
end

npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb

+64-19
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
require "dependabot/npm_and_yarn/file_updater"
66
require "dependabot/npm_and_yarn/file_parser"
7+
require "dependabot/npm_and_yarn/helpers"
78
require "dependabot/npm_and_yarn/update_checker/registry_finder"
89
require "dependabot/npm_and_yarn/native_helpers"
910
require "dependabot/shared_helpers"
1011
require "dependabot/errors"
12+
require "dependabot/experiments"
1113

1214
# rubocop:disable Metrics/ClassLength
1315
module Dependabot
@@ -17,9 +19,10 @@ class YarnLockfileUpdater
1719
require_relative "npmrc_builder"
1820
require_relative "package_json_updater"
1921

20-
def initialize(dependencies:, dependency_files:, credentials:)
22+
def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
2123
@dependencies = dependencies
2224
@dependency_files = dependency_files
25+
@repo_contents_path = repo_contents_path
2326
@credentials = credentials
2427
end
2528

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

3639
private
3740

38-
attr_reader :dependencies, :dependency_files, :credentials
41+
attr_reader :dependencies, :dependency_files, :repo_contents_path, :credentials
3942

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

5356
def updated_yarn_lock(yarn_lock)
54-
SharedHelpers.in_a_temporary_directory do
57+
base_dir = dependency_files.first.directory
58+
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
5559
write_temporary_dependency_files
5660
lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
5761
path = Pathname.new(yarn_lock.name).dirname.to_s
5862
updated_files = run_current_yarn_update(
5963
path: path,
60-
lockfile_name: lockfile_name
64+
yarn_lock: yarn_lock
6165
)
6266
updated_files.fetch(lockfile_name)
6367
end
6468
rescue SharedHelpers::HelperSubprocessFailed => e
6569
handle_yarn_lock_updater_error(e, yarn_lock)
6670
end
6771

68-
def run_current_yarn_update(path:, lockfile_name:)
72+
def run_current_yarn_update(path:, yarn_lock:)
6973
top_level_dependency_updates = top_level_dependencies.map do |d|
7074
{
7175
name: d.name,
@@ -76,12 +80,12 @@ def run_current_yarn_update(path:, lockfile_name:)
7680

7781
run_yarn_updater(
7882
path: path,
79-
lockfile_name: lockfile_name,
83+
yarn_lock: yarn_lock,
8084
top_level_dependency_updates: top_level_dependency_updates
8185
)
8286
end
8387

84-
def run_previous_yarn_update(path:, lockfile_name:)
88+
def run_previous_yarn_update(path:, yarn_lock:)
8589
previous_top_level_dependencies = top_level_dependencies.map do |d|
8690
{
8791
name: d.name,
@@ -94,22 +98,29 @@ def run_previous_yarn_update(path:, lockfile_name:)
9498

9599
run_yarn_updater(
96100
path: path,
97-
lockfile_name: lockfile_name,
101+
yarn_lock: yarn_lock,
98102
top_level_dependency_updates: previous_top_level_dependencies
99103
)
100104
end
101105

102106
# rubocop:disable Metrics/PerceivedComplexity
103-
def run_yarn_updater(path:, lockfile_name:,
104-
top_level_dependency_updates:)
107+
def run_yarn_updater(path:, yarn_lock:, top_level_dependency_updates:)
105108
SharedHelpers.with_git_configured(credentials: credentials) do
106109
Dir.chdir(path) do
107110
if top_level_dependency_updates.any?
108-
run_yarn_top_level_updater(
109-
top_level_dependency_updates: top_level_dependency_updates
110-
)
111+
if yarn_berry?(yarn_lock)
112+
run_yarn_berry_top_level_updater(top_level_dependency_updates: top_level_dependency_updates,
113+
yarn_lock: yarn_lock)
114+
else
115+
116+
run_yarn_top_level_updater(
117+
top_level_dependency_updates: top_level_dependency_updates
118+
)
119+
end
120+
elsif yarn_berry?(yarn_lock)
121+
run_yarn_berry_subdependency_updater(yarn_lock: yarn_lock)
111122
else
112-
run_yarn_subdependency_updater(lockfile_name: lockfile_name)
123+
run_yarn_subdependency_updater(yarn_lock: yarn_lock)
113124
end
114125
end
115126
end
@@ -133,6 +144,40 @@ def run_yarn_updater(path:, lockfile_name:,
133144

134145
# rubocop:enable Metrics/PerceivedComplexity
135146

147+
def yarn_berry?(yarn_lock)
148+
return false unless Experiments.enabled?(:yarn_berry)
149+
150+
yaml = YAML.safe_load(yarn_lock.content)
151+
yaml.key?("__metadata")
152+
rescue StandardError
153+
false
154+
end
155+
156+
def run_yarn_berry_top_level_updater(top_level_dependency_updates:, yarn_lock:)
157+
updates = top_level_dependency_updates.collect do |dep|
158+
# when there are multiple requirements, we're dealing with a
159+
# workspace-like setup, where there are multiple package.json files
160+
# that pull in the same dependency. It appears that these are always
161+
# updated to a single new version, so we just pick the first one.
162+
"#{dep[:name]}@#{dep[:requirements].first[:requirement]}"
163+
end
164+
command = "yarn add #{updates.join(' ')}"
165+
Helpers.run_yarn_commands(command)
166+
{ yarn_lock.name => File.read(yarn_lock.name) }
167+
end
168+
169+
def run_yarn_berry_subdependency_updater(yarn_lock:)
170+
dep = sub_dependencies.first
171+
update = "#{dep.name}@#{dep.version}"
172+
173+
Helpers.run_yarn_commands(
174+
"yarn add #{update}",
175+
"yarn dedupe #{dep.name}",
176+
"yarn remove #{dep.name}"
177+
)
178+
{ yarn_lock.name => File.read(yarn_lock.name) }
179+
end
180+
136181
def run_yarn_top_level_updater(top_level_dependency_updates:)
137182
SharedHelpers.run_helper_subprocess(
138183
command: NativeHelpers.helper_path,
@@ -144,7 +189,8 @@ def run_yarn_top_level_updater(top_level_dependency_updates:)
144189
)
145190
end
146191

147-
def run_yarn_subdependency_updater(lockfile_name:)
192+
def run_yarn_subdependency_updater(yarn_lock:)
193+
lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
148194
SharedHelpers.run_helper_subprocess(
149195
command: NativeHelpers.helper_path,
150196
function: "yarn:updateSubdependency",
@@ -259,12 +305,11 @@ def resolvable_before_update?(yarn_lock)
259305

260306
@resolvable_before_update[yarn_lock.name] =
261307
begin
262-
SharedHelpers.in_a_temporary_directory do
308+
base_dir = dependency_files.first.directory
309+
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
263310
write_temporary_dependency_files(update_package_json: false)
264-
lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
265311
path = Pathname.new(yarn_lock.name).dirname.to_s
266-
run_previous_yarn_update(path: path,
267-
lockfile_name: lockfile_name)
312+
run_previous_yarn_update(path: path, yarn_lock: yarn_lock)
268313
end
269314

270315
true

npm_and_yarn/lib/dependabot/npm_and_yarn/helpers.rb

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ def self.npm_version_numeric(lockfile_content)
1515
rescue JSON::ParserError
1616
6
1717
end
18+
19+
# Run any number of yarn commands while ensuring that `enableScripts` is
20+
# set to false. Yarn commands should _not_ be ran outside of this helper
21+
# to ensure that postinstall scripts are never executed, as they could
22+
# contain malicious code.
23+
def self.run_yarn_commands(*commands)
24+
# We never want to execute postinstall scripts
25+
SharedHelpers.run_shell_command("yarn config set enableScripts false")
26+
commands.each { |cmd| SharedHelpers.run_shell_command(cmd) }
27+
end
1828
end
1929
end
2030
end

0 commit comments

Comments
 (0)