dependabot-npm_and_yarn 0.212.0 → 0.213.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/.eslintrc +1 -1
  3. data/helpers/README.md +2 -2
  4. data/helpers/lib/npm/vulnerability-auditor.js +7 -7
  5. data/helpers/package-lock.json +2585 -2386
  6. data/helpers/package.json +4 -4
  7. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +30 -5
  8. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +19 -4
  9. data/lib/dependabot/npm_and_yarn/file_parser.rb +17 -5
  10. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +35 -21
  11. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +7 -3
  12. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +2 -2
  13. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +83 -24
  14. data/lib/dependabot/npm_and_yarn/file_updater.rb +54 -0
  15. data/lib/dependabot/npm_and_yarn/helpers.rb +48 -0
  16. data/lib/dependabot/npm_and_yarn/package_name.rb +2 -2
  17. data/lib/dependabot/npm_and_yarn/requirement.rb +3 -3
  18. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +6 -1
  19. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +16 -3
  20. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +67 -19
  21. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +3 -4
  22. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +23 -1
  23. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +3 -3
  24. data/lib/dependabot/npm_and_yarn/update_checker/vulnerability_auditor.rb +33 -8
  25. data/lib/dependabot/npm_and_yarn/update_checker.rb +72 -19
  26. data/lib/dependabot/npm_and_yarn/version.rb +1 -1
  27. metadata +13 -55
data/helpers/package.json CHANGED
@@ -10,16 +10,16 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@dependabot/yarn-lib": "^1.22.19",
13
- "@npmcli/arborist": "^5.6.0",
13
+ "@npmcli/arborist": "^6.0.0",
14
14
  "detect-indent": "^6.1.0",
15
15
  "nock": "^13.2.9",
16
16
  "npm": "6.14.17",
17
- "semver": "^7.3.7"
17
+ "semver": "^7.3.8"
18
18
  },
19
19
  "devDependencies": {
20
- "eslint": "^8.22.0",
20
+ "eslint": "^8.26.0",
21
21
  "eslint-config-prettier": "^8.5.0",
22
- "jest": "^28.1.3",
22
+ "jest": "^29.2.1",
23
23
  "prettier": "^2.7.1",
24
24
  "rimraf": "^3.0.2"
25
25
  }
@@ -19,9 +19,8 @@ module Dependabot
19
19
  # when it specifies a path. Only include Yarn "link:"'s that start with a
20
20
  # path and ignore symlinked package names that have been registered with
21
21
  # "yarn link", e.g. "link:react"
22
- PATH_DEPENDENCY_STARTS =
23
- %w(file: link:. link:/ link:~/ / ./ ../ ~/).freeze
24
- PATH_DEPENDENCY_CLEAN_REGEX = /^file:|^link:/.freeze
22
+ PATH_DEPENDENCY_STARTS = %w(file: link:. link:/ link:~/ / ./ ../ ~/).freeze
23
+ PATH_DEPENDENCY_CLEAN_REGEX = /^file:|^link:/
25
24
 
26
25
  def self.required_files_in?(filenames)
27
26
  filenames.include?("package.json")
@@ -42,6 +41,7 @@ module Dependabot
42
41
  fetched_files << lerna_json if lerna_json
43
42
  fetched_files << npmrc if npmrc
44
43
  fetched_files << yarnrc if yarnrc
44
+ fetched_files << yarnrc_yml if yarnrc_yml
45
45
  fetched_files += workspace_package_jsons
46
46
  fetched_files += lerna_packages
47
47
  fetched_files += path_dependencies(fetched_files)
@@ -53,8 +53,8 @@ module Dependabot
53
53
  def instrument_package_manager_version
54
54
  package_managers = {}
55
55
 
56
- package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
57
- package_managers["yarn"] = 1 if yarn_lock
56
+ package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
57
+ package_managers["yarn"] = yarn_version if yarn_version
58
58
  package_managers["shrinkwrap"] = 1 if shrinkwrap
59
59
 
60
60
  Dependabot.instrument(
@@ -64,6 +64,22 @@ module Dependabot
64
64
  )
65
65
  end
66
66
 
67
+ def yarn_version
68
+ return @yarn_version if defined?(@yarn_version)
69
+
70
+ package = JSON.parse(package_json.content)
71
+ if Experiments.enabled?(:yarn_berry) && (package_manager = package.fetch("packageManager", nil))
72
+ get_yarn_version_from_package_json(package_manager)
73
+ elsif yarn_lock
74
+ 1
75
+ end
76
+ end
77
+
78
+ def get_yarn_version_from_package_json(package_manager)
79
+ version_match = package_manager.match(/yarn@(?<version>\d+.\d+.\d+)/)
80
+ version_match&.named_captures&.fetch("version", nil)
81
+ end
82
+
67
83
  def package_json
68
84
  @package_json ||= fetch_file_from_host("package.json")
69
85
  end
@@ -118,6 +134,11 @@ module Dependabot
118
134
  @yarnrc
119
135
  end
120
136
 
137
+ def yarnrc_yml
138
+ @yarnrc_yml ||= fetch_file_if_present(".yarnrc.yml")&.
139
+ tap { |f| f.support_file = true }
140
+ end
141
+
121
142
  def lerna_json
122
143
  @lerna_json ||= fetch_file_if_present("lerna.json")&.
123
144
  tap { |f| f.support_file = true }
@@ -137,6 +158,8 @@ module Dependabot
137
158
 
138
159
  path_dependency_details(fetched_files).each do |name, path|
139
160
  path = path.gsub(PATH_DEPENDENCY_CLEAN_REGEX, "")
161
+ raise PathDependenciesNotReachable, "#{name} at #{path}" if path.start_with?("/")
162
+
140
163
  filename = path
141
164
  # NPM/Yarn support loading path dependencies from tarballs:
142
165
  # https://docs.npmjs.com/cli/pack.html
@@ -212,6 +235,8 @@ module Dependabot
212
235
  select { |_, v| v.is_a?(String) && v.start_with?(*path_starts) }.
213
236
  map do |name, path|
214
237
  path = path.gsub(PATH_DEPENDENCY_CLEAN_REGEX, "")
238
+ raise PathDependenciesNotReachable, "#{name} at #{path}" if path.start_with?("/")
239
+
215
240
  path = File.join(current_dir, path) unless current_dir.nil?
216
241
  [name, Pathname.new(path).cleanpath.to_path]
217
242
  end
@@ -12,12 +12,17 @@ module Dependabot
12
12
  @dependency_files = dependency_files
13
13
  end
14
14
 
15
- def parse
15
+ def parse_set
16
16
  dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
17
17
  dependency_set += yarn_lock_dependencies if yarn_locks.any?
18
18
  dependency_set += package_lock_dependencies if package_locks.any?
19
19
  dependency_set += shrinkwrap_dependencies if shrinkwraps.any?
20
- dependency_set.dependencies
20
+
21
+ dependency_set
22
+ end
23
+
24
+ def parse
25
+ Helpers.dependencies_with_all_versions_metadata(parse_set)
21
26
  end
22
27
 
23
28
  def lockfile_details(dependency_name:, requirement:, manifest_name:)
@@ -77,7 +82,7 @@ module Dependabot
77
82
  details_candidates.first.last
78
83
  else
79
84
  details_candidates.find do |k, _|
80
- k.split(/(?<=\w)\@/)[1..-1].join("@") == requirement
85
+ k.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
81
86
  end&.last
82
87
  end
83
88
  end
@@ -96,6 +101,8 @@ module Dependabot
96
101
  parse_yarn_lock(yarn_lock).each do |req, details|
97
102
  next unless semver_version_for(details["version"])
98
103
  next if alias_package?(req)
104
+ next if Experiments.enabled?(:yarn_berry) && workspace_package?(req)
105
+ next if Experiments.enabled?(:yarn_berry) && req == "__metadata"
99
106
 
100
107
  # NOTE: The DependencySet will de-dupe our dependencies, so they
101
108
  # end up unique by name. That's not a perfect representation of
@@ -188,7 +195,15 @@ module Dependabot
188
195
  end
189
196
 
190
197
  def alias_package?(requirement)
191
- requirement.include?("@npm:")
198
+ if Experiments.enabled?(:yarn_berry)
199
+ requirement.match?(/@npm:(.+@(?!npm))/)
200
+ else
201
+ requirement.include?("@npm:")
202
+ end
203
+ end
204
+
205
+ def workspace_package?(requirement)
206
+ requirement.include?("@workspace:")
192
207
  end
193
208
 
194
209
  def parse_package_lock(package_lock)
@@ -3,9 +3,11 @@
3
3
  # See https://docs.npmjs.com/files/package.json for package.json format docs.
4
4
 
5
5
  require "dependabot/dependency"
6
+ require "dependabot/experiments"
6
7
  require "dependabot/file_parsers"
7
8
  require "dependabot/file_parsers/base"
8
9
  require "dependabot/shared_helpers"
10
+ require "dependabot/npm_and_yarn/helpers"
9
11
  require "dependabot/npm_and_yarn/native_helpers"
10
12
  require "dependabot/npm_and_yarn/version"
11
13
  require "dependabot/git_metadata_fetcher"
@@ -29,13 +31,14 @@ module Dependabot
29
31
  (?:\#(?=[\^~=<>*])(?<semver>.+))|
30
32
  (?:\#(?<ref>.+))
31
33
  )?$
32
- }ix.freeze
34
+ }ix
33
35
 
34
36
  def parse
35
37
  dependency_set = DependencySet.new
36
38
  dependency_set += manifest_dependencies
37
39
  dependency_set += lockfile_dependencies
38
- dependencies = dependency_set.dependencies
40
+
41
+ dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
39
42
 
40
43
  # TODO: Currently, Dependabot can't handle dependencies that have both
41
44
  # a git source *and* a non-git source. Fix that!
@@ -84,7 +87,7 @@ module Dependabot
84
87
  end
85
88
 
86
89
  def lockfile_dependencies
87
- DependencySet.new(lockfile_parser.parse)
90
+ lockfile_parser.parse_set
88
91
  end
89
92
 
90
93
  def build_dependency(file:, type:, name:, requirement:)
@@ -94,6 +97,7 @@ module Dependabot
94
97
  manifest_name: file.name
95
98
  )
96
99
  version = version_for(name, requirement, file.name)
100
+
97
101
  return if lockfile_details && !version
98
102
  return if ignore_requirement?(requirement)
99
103
  return if workspace_package_names.include?(name)
@@ -239,11 +243,18 @@ module Dependabot
239
243
  def source_for(name, requirement, manifest_name)
240
244
  return git_source_for(requirement) if git_url?(requirement)
241
245
 
242
- resolved_url = lockfile_parser.lockfile_details(
246
+ lockfile_details = lockfile_parser.lockfile_details(
243
247
  dependency_name: name,
244
248
  requirement: requirement,
245
249
  manifest_name: manifest_name
246
- )&.fetch("resolved", nil)
250
+ )
251
+ resolved_url = lockfile_details&.fetch("resolved", nil)
252
+
253
+ if Experiments.enabled?(:yarn_berry) && resolved_url.nil?
254
+ resolution = lockfile_details&.fetch("resolution", nil)
255
+ package_match = resolution&.match(/__archiveUrl=(?<package_url>.+)/)
256
+ resolved_url = CGI.unescape(package_match.named_captures.fetch("package_url", "")) if package_match
257
+ end
247
258
 
248
259
  return unless resolved_url
249
260
  return unless resolved_url.start_with?("http")
@@ -326,6 +337,7 @@ module Dependabot
326
337
  dependency_files.
327
338
  select { |f| f.name.end_with?("package.json") }.
328
339
  reject { |f| f.name == "package.json" }.
340
+ reject { |f| f.name.include?("node_modules/") if Experiments.enabled?(:yarn_berry) }.
329
341
  reject(&:support_file?)
330
342
 
331
343
  [
@@ -34,21 +34,21 @@ module Dependabot
34
34
 
35
35
  attr_reader :lockfile, :dependencies, :dependency_files, :credentials
36
36
 
37
- UNREACHABLE_GIT = /fatal: repository '(?<url>.*)' not found/.freeze
38
- FORBIDDEN_GIT = /fatal: Authentication failed for '(?<url>.*)'/.freeze
39
- FORBIDDEN_PACKAGE = %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}.freeze
37
+ UNREACHABLE_GIT = /fatal: repository '(?<url>.*)' not found/
38
+ FORBIDDEN_GIT = /fatal: Authentication failed for '(?<url>.*)'/
39
+ FORBIDDEN_PACKAGE = %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}
40
40
  FORBIDDEN_PACKAGE_403 = %r{^403\sForbidden\s
41
- -\sGET\shttps?://(?<source>[^/]+)/(?<package_req>[^/\s]+)}x.freeze
42
- MISSING_PACKAGE = %r{(?<package_req>[^/]+) - Not found}.freeze
43
- INVALID_PACKAGE = /Can't install (?<package_req>.*): Missing/.freeze
41
+ -\sGET\shttps?://(?<source>[^/]+)/(?<package_req>[^/\s]+)}x
42
+ MISSING_PACKAGE = %r{(?<package_req>[^/]+) - Not found}
43
+ INVALID_PACKAGE = /Can't install (?<package_req>.*): Missing/
44
44
 
45
45
  # TODO: look into fixing this in npm, seems like a bug in the git
46
46
  # downloader introduced in npm 7
47
47
  #
48
48
  # NOTE: error message returned from arborist/npm 8 when trying to
49
49
  # fetching a invalid/non-existent git ref
50
- NPM8_MISSING_GIT_REF = /already exists and is not an empty directory/.freeze
51
- NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/.freeze
50
+ NPM8_MISSING_GIT_REF = /already exists and is not an empty directory/
51
+ NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/
52
52
 
53
53
  def updated_lockfile_content
54
54
  return lockfile.content if npmrc_disables_lockfile?
@@ -112,7 +112,7 @@ module Dependabot
112
112
  end
113
113
 
114
114
  def run_current_npm_update
115
- run_npm_updater(top_level_dependencies: top_level_dependencies)
115
+ run_npm_updater(top_level_dependencies: top_level_dependencies, sub_dependencies: sub_dependencies)
116
116
  end
117
117
 
118
118
  def run_previous_npm_update
@@ -127,16 +127,31 @@ module Dependabot
127
127
  )
128
128
  end
129
129
 
130
- run_npm_updater(top_level_dependencies: previous_top_level_dependencies)
130
+ previous_sub_dependencies = sub_dependencies.map do |d|
131
+ Dependabot::Dependency.new(
132
+ name: d.name,
133
+ package_manager: d.package_manager,
134
+ version: d.previous_version,
135
+ previous_version: d.previous_version,
136
+ requirements: [],
137
+ previous_requirements: []
138
+ )
139
+ end
140
+
141
+ run_npm_updater(top_level_dependencies: previous_top_level_dependencies,
142
+ sub_dependencies: previous_sub_dependencies)
131
143
  end
132
144
 
133
- def run_npm_updater(top_level_dependencies:)
145
+ def run_npm_updater(top_level_dependencies:, sub_dependencies:)
134
146
  SharedHelpers.with_git_configured(credentials: credentials) do
147
+ updated_files = {}
135
148
  if top_level_dependencies.any?
136
- run_npm_top_level_updater(top_level_dependencies: top_level_dependencies)
137
- else
138
- run_npm_subdependency_updater
149
+ updated_files.merge!(run_npm_top_level_updater(top_level_dependencies: top_level_dependencies))
150
+ end
151
+ if sub_dependencies.any?
152
+ updated_files.merge!(run_npm_subdependency_updater(sub_dependencies: sub_dependencies))
139
153
  end
154
+ updated_files
140
155
  end
141
156
  end
142
157
 
@@ -194,9 +209,9 @@ module Dependabot
194
209
  { lockfile_basename => File.read(lockfile_basename) }
195
210
  end
196
211
 
197
- def run_npm_subdependency_updater
212
+ def run_npm_subdependency_updater(sub_dependencies:)
198
213
  if npm8?
199
- run_npm8_subdependency_updater
214
+ run_npm8_subdependency_updater(sub_dependencies: sub_dependencies)
200
215
  else
201
216
  SharedHelpers.run_helper_subprocess(
202
217
  command: NativeHelpers.helper_path,
@@ -206,7 +221,7 @@ module Dependabot
206
221
  end
207
222
  end
208
223
 
209
- def run_npm8_subdependency_updater
224
+ def run_npm8_subdependency_updater(sub_dependencies:)
210
225
  dependency_names = sub_dependencies.map(&:name)
211
226
  SharedHelpers.run_shell_command(NativeHelpers.npm8_subdependency_update_command(dependency_names))
212
227
  { lockfile_basename => File.read(lockfile_basename) }
@@ -414,10 +429,9 @@ module Dependabot
414
429
  reg = NpmAndYarn::UpdateChecker::RegistryFinder.new(
415
430
  dependency: missing_dep,
416
431
  credentials: credentials,
417
- npmrc_file: dependency_files.
418
- find { |f| f.name.end_with?(".npmrc") },
419
- yarnrc_file: dependency_files.
420
- find { |f| f.name.end_with?(".yarnrc") }
432
+ npmrc_file: dependency_files. find { |f| f.name.end_with?(".npmrc") },
433
+ yarnrc_file: dependency_files. find { |f| f.name.end_with?(".yarnrc") },
434
+ yarnrc_yml_file: dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
421
435
  ).registry
422
436
 
423
437
  return if UpdateChecker::RegistryFinder.central_registry?(reg) && !package_name.start_with?("@")
@@ -13,7 +13,7 @@ module Dependabot
13
13
  registry.yarnpkg.com
14
14
  ).freeze
15
15
 
16
- SCOPED_REGISTRY = /^\s*@(?<scope>\S+):registry\s*=\s*(?<registry>\S+)/.freeze
16
+ SCOPED_REGISTRY = /^\s*@(?<scope>\S+):registry\s*=\s*(?<registry>\S+)/
17
17
 
18
18
  def initialize(dependency_files:, credentials:)
19
19
  @dependency_files = dependency_files
@@ -42,7 +42,9 @@ module Dependabot
42
42
  return unless yarn_lock || package_lock
43
43
  return unless global_registry
44
44
 
45
- "registry = https://#{global_registry['registry']}\n" \
45
+ registry = global_registry["registry"]
46
+ registry = "https://#{registry}" unless registry.start_with?("http")
47
+ "registry = #{registry}\n" \
46
48
  "#{global_registry_auth_line}" \
47
49
  "always-auth = true"
48
50
  end
@@ -113,8 +115,10 @@ module Dependabot
113
115
  return initial_content unless yarn_lock || package_lock
114
116
  return initial_content unless global_registry
115
117
 
118
+ registry = global_registry["registry"]
119
+ registry = "https://#{registry}" unless registry.start_with?("http")
116
120
  initial_content +
117
- "registry = https://#{global_registry['registry']}\n" \
121
+ "registry = #{registry}\n" \
118
122
  "#{global_registry_auth_line}" \
119
123
  "always-auth = true\n"
120
124
  end
@@ -183,8 +183,8 @@ module Dependabot
183
183
  end
184
184
 
185
185
  original_line.gsub(
186
- %(\##{old_req.dig(:source, :ref)}"),
187
- %(\##{new_req.dig(:source, :ref)}")
186
+ %(##{old_req.dig(:source, :ref)}"),
187
+ %(##{new_req.dig(:source, :ref)}")
188
188
  )
189
189
  end
190
190
 
@@ -4,10 +4,12 @@ require "uri"
4
4
 
5
5
  require "dependabot/npm_and_yarn/file_updater"
6
6
  require "dependabot/npm_and_yarn/file_parser"
7
+ require "dependabot/npm_and_yarn/helpers"
7
8
  require "dependabot/npm_and_yarn/update_checker/registry_finder"
8
9
  require "dependabot/npm_and_yarn/native_helpers"
9
10
  require "dependabot/shared_helpers"
10
11
  require "dependabot/errors"
12
+ require "dependabot/experiments"
11
13
 
12
14
  # rubocop:disable Metrics/ClassLength
13
15
  module Dependabot
@@ -17,9 +19,10 @@ module Dependabot
17
19
  require_relative "npmrc_builder"
18
20
  require_relative "package_json_updater"
19
21
 
20
- def initialize(dependencies:, dependency_files:, credentials:)
22
+ def initialize(dependencies:, dependency_files:, repo_contents_path:, credentials:)
21
23
  @dependencies = dependencies
22
24
  @dependency_files = dependency_files
25
+ @repo_contents_path = repo_contents_path
23
26
  @credentials = credentials
24
27
  end
25
28
 
@@ -35,12 +38,11 @@ module Dependabot
35
38
 
36
39
  private
37
40
 
38
- attr_reader :dependencies, :dependency_files, :credentials
41
+ attr_reader :dependencies, :dependency_files, :repo_contents_path, :credentials
39
42
 
40
- UNREACHABLE_GIT = /ls-remote --tags --heads (?<url>.*)/.freeze
41
- TIMEOUT_FETCHING_PACKAGE =
42
- %r{(?<url>.+)/(?<package>[^/]+): ETIMEDOUT}.freeze
43
- INVALID_PACKAGE = /Can't add "(?<package_req>.*)": invalid/.freeze
43
+ UNREACHABLE_GIT = /ls-remote --tags --heads (?<url>.*)/
44
+ TIMEOUT_FETCHING_PACKAGE = %r{(?<url>.+)/(?<package>[^/]+): ETIMEDOUT}
45
+ INVALID_PACKAGE = /Can't add "(?<package_req>.*)": invalid/
44
46
 
45
47
  def top_level_dependencies
46
48
  dependencies.select(&:top_level?)
@@ -51,13 +53,14 @@ module Dependabot
51
53
  end
52
54
 
53
55
  def updated_yarn_lock(yarn_lock)
54
- SharedHelpers.in_a_temporary_directory do
56
+ base_dir = dependency_files.first.directory
57
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
55
58
  write_temporary_dependency_files
56
59
  lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
57
60
  path = Pathname.new(yarn_lock.name).dirname.to_s
58
61
  updated_files = run_current_yarn_update(
59
62
  path: path,
60
- lockfile_name: lockfile_name
63
+ yarn_lock: yarn_lock
61
64
  )
62
65
  updated_files.fetch(lockfile_name)
63
66
  end
@@ -65,7 +68,7 @@ module Dependabot
65
68
  handle_yarn_lock_updater_error(e, yarn_lock)
66
69
  end
67
70
 
68
- def run_current_yarn_update(path:, lockfile_name:)
71
+ def run_current_yarn_update(path:, yarn_lock:)
69
72
  top_level_dependency_updates = top_level_dependencies.map do |d|
70
73
  {
71
74
  name: d.name,
@@ -76,12 +79,12 @@ module Dependabot
76
79
 
77
80
  run_yarn_updater(
78
81
  path: path,
79
- lockfile_name: lockfile_name,
82
+ yarn_lock: yarn_lock,
80
83
  top_level_dependency_updates: top_level_dependency_updates
81
84
  )
82
85
  end
83
86
 
84
- def run_previous_yarn_update(path:, lockfile_name:)
87
+ def run_previous_yarn_update(path:, yarn_lock:)
85
88
  previous_top_level_dependencies = top_level_dependencies.map do |d|
86
89
  {
87
90
  name: d.name,
@@ -94,22 +97,29 @@ module Dependabot
94
97
 
95
98
  run_yarn_updater(
96
99
  path: path,
97
- lockfile_name: lockfile_name,
100
+ yarn_lock: yarn_lock,
98
101
  top_level_dependency_updates: previous_top_level_dependencies
99
102
  )
100
103
  end
101
104
 
102
105
  # rubocop:disable Metrics/PerceivedComplexity
103
- def run_yarn_updater(path:, lockfile_name:,
104
- top_level_dependency_updates:)
106
+ def run_yarn_updater(path:, yarn_lock:, top_level_dependency_updates:)
105
107
  SharedHelpers.with_git_configured(credentials: credentials) do
106
108
  Dir.chdir(path) do
107
109
  if top_level_dependency_updates.any?
108
- run_yarn_top_level_updater(
109
- top_level_dependency_updates: top_level_dependency_updates
110
- )
110
+ if yarn_berry?(yarn_lock)
111
+ run_yarn_berry_top_level_updater(top_level_dependency_updates: top_level_dependency_updates,
112
+ yarn_lock: yarn_lock)
113
+ else
114
+
115
+ run_yarn_top_level_updater(
116
+ top_level_dependency_updates: top_level_dependency_updates
117
+ )
118
+ end
119
+ elsif yarn_berry?(yarn_lock)
120
+ run_yarn_berry_subdependency_updater(yarn_lock: yarn_lock)
111
121
  else
112
- run_yarn_subdependency_updater(lockfile_name: lockfile_name)
122
+ run_yarn_subdependency_updater(yarn_lock: yarn_lock)
113
123
  end
114
124
  end
115
125
  end
@@ -133,6 +143,50 @@ module Dependabot
133
143
 
134
144
  # rubocop:enable Metrics/PerceivedComplexity
135
145
 
146
+ def yarn_berry?(yarn_lock)
147
+ return false unless Experiments.enabled?(:yarn_berry)
148
+
149
+ yaml = YAML.safe_load(yarn_lock.content)
150
+ yaml.key?("__metadata")
151
+ rescue StandardError
152
+ false
153
+ end
154
+
155
+ def run_yarn_berry_top_level_updater(top_level_dependency_updates:, yarn_lock:)
156
+ # If the requirements have changed, it means we've updated the
157
+ # package.json file(s), and we can just run yarn install to get the
158
+ # lockfile in the right state. Otherwise we'll need to manually update
159
+ # the lockfile.
160
+
161
+ command = if top_level_dependency_updates.all? { |dep| requirements_changed?(dep[:name]) }
162
+ "yarn install"
163
+ else
164
+ updates = top_level_dependency_updates.collect do |dep|
165
+ dep[:requirements].map { |req| "#{dep[:name]}@#{req[:requirement]}" }.join(" ")
166
+ end
167
+ "yarn up #{updates.join(' ')}"
168
+ end
169
+ Helpers.run_yarn_commands(command)
170
+ { yarn_lock.name => File.read(yarn_lock.name) }
171
+ end
172
+
173
+ def requirements_changed?(dependency_name)
174
+ dep = top_level_dependencies.first { |d| d.name == dependency_name }
175
+ dep.requirements != dep.previous_requirements
176
+ end
177
+
178
+ def run_yarn_berry_subdependency_updater(yarn_lock:)
179
+ dep = sub_dependencies.first
180
+ update = "#{dep.name}@#{dep.version}"
181
+
182
+ Helpers.run_yarn_commands(
183
+ "yarn add #{update}",
184
+ "yarn dedupe #{dep.name}",
185
+ "yarn remove #{dep.name}"
186
+ )
187
+ { yarn_lock.name => File.read(yarn_lock.name) }
188
+ end
189
+
136
190
  def run_yarn_top_level_updater(top_level_dependency_updates:)
137
191
  SharedHelpers.run_helper_subprocess(
138
192
  command: NativeHelpers.helper_path,
@@ -144,7 +198,8 @@ module Dependabot
144
198
  )
145
199
  end
146
200
 
147
- def run_yarn_subdependency_updater(lockfile_name:)
201
+ def run_yarn_subdependency_updater(yarn_lock:)
202
+ lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
148
203
  SharedHelpers.run_helper_subprocess(
149
204
  command: NativeHelpers.helper_path,
150
205
  function: "yarn:updateSubdependency",
@@ -259,12 +314,11 @@ module Dependabot
259
314
 
260
315
  @resolvable_before_update[yarn_lock.name] =
261
316
  begin
262
- SharedHelpers.in_a_temporary_directory do
317
+ base_dir = dependency_files.first.directory
318
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
263
319
  write_temporary_dependency_files(update_package_json: false)
264
- lockfile_name = Pathname.new(yarn_lock.name).basename.to_s
265
320
  path = Pathname.new(yarn_lock.name).dirname.to_s
266
- run_previous_yarn_update(path: path,
267
- lockfile_name: lockfile_name)
321
+ run_previous_yarn_update(path: path, yarn_lock: yarn_lock)
268
322
  end
269
323
 
270
324
  true
@@ -420,7 +474,8 @@ module Dependabot
420
474
  dependency: missing_dep,
421
475
  credentials: credentials,
422
476
  npmrc_file: npmrc_file,
423
- yarnrc_file: yarnrc_file
477
+ yarnrc_file: yarnrc_file,
478
+ yarnrc_yml_file: yarnrc_yml_file
424
479
  ).registry
425
480
 
426
481
  return if UpdateChecker::RegistryFinder.central_registry?(reg) && !package_name.start_with?("@")
@@ -524,6 +579,10 @@ module Dependabot
524
579
  def npmrc_file
525
580
  dependency_files.find { |f| f.name == ".npmrc" }
526
581
  end
582
+
583
+ def yarnrc_yml_file
584
+ dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") }
585
+ end
527
586
  end
528
587
  end
529
588
  end