dependabot-npm_and_yarn 0.131.3 → 0.132.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/npm6/updater.js +1 -0
  3. data/helpers/lib/npm7/index.js +5 -0
  4. data/helpers/lib/npm7/peer-dependency-checker.js +77 -0
  5. data/helpers/test/npm7/conflicting-dependency-parser.test.js +67 -0
  6. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/deeply-nested/package-lock.json +1291 -0
  7. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/deeply-nested/package.json +14 -0
  8. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/nested/package-lock.json +411 -0
  9. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/nested/package.json +14 -0
  10. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/simple/package-lock.json +64 -0
  11. data/helpers/test/npm7/fixtures/conflicting-dependency-parser/simple/package.json +14 -0
  12. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency/package-lock.json +290 -0
  13. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency/package.json +23 -0
  14. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency_multiple/package-lock.json +965 -0
  15. data/helpers/test/npm7/fixtures/peer-dependency-checker/peer_dependency_multiple/package.json +10 -0
  16. data/helpers/test/npm7/helpers.js +21 -0
  17. data/helpers/test/npm7/peer-dependency-checker.test.js +107 -0
  18. data/helpers/yarn.lock +1198 -1232
  19. data/lib/dependabot/npm_and_yarn/dependency_files_filterer.rb +3 -3
  20. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +252 -53
  21. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +1 -2
  22. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +1 -2
  23. metadata +18 -3
@@ -20,7 +20,7 @@ module Dependabot
20
20
  dependency_files.select do |file|
21
21
  package_files_requiring_update.include?(file) ||
22
22
  package_required_lockfile?(file) ||
23
- yarn_workspaces_lockfile?(file)
23
+ workspaces_lockfile?(file)
24
24
  end
25
25
  end
26
26
  end
@@ -51,8 +51,8 @@ module Dependabot
51
51
  end
52
52
  end
53
53
 
54
- def yarn_workspaces_lockfile?(lockfile)
55
- return false unless lockfile.name == "yarn.lock"
54
+ def workspaces_lockfile?(lockfile)
55
+ return false unless ["yarn.lock", "package-lock.json"].include?(lockfile.name)
56
56
  return false unless parsed_root_package_json["workspaces"]
57
57
 
58
58
  updated_dependencies_in_lockfile?(lockfile)
@@ -37,7 +37,7 @@ module Dependabot
37
37
  run_current_npm_update(lockfile_name: lockfile_name, lockfile_content: lockfile.content)
38
38
  end
39
39
  updated_content = updated_files.fetch(lockfile_name)
40
- post_process_npm_lockfile(lockfile.content, updated_content)
40
+ post_process_npm_lockfile(lockfile.content, updated_content, lockfile.name)
41
41
  end
42
42
  rescue SharedHelpers::HelperSubprocessFailed => e
43
43
  handle_npm_updater_error(e, lockfile)
@@ -47,15 +47,22 @@ module Dependabot
47
47
 
48
48
  attr_reader :dependencies, :dependency_files, :credentials
49
49
 
50
- UNREACHABLE_GIT =
51
- /ls-remote (?:(-h -t)|(--tags --heads)) (?<url>.*)/.freeze
52
- FORBIDDEN_PACKAGE =
53
- %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}.freeze
50
+ UNREACHABLE_GIT = /fatal: repository '(?<url>.*)' not found/.freeze
51
+ FORBIDDEN_GIT = /fatal: Authentication failed for '(?<url>.*)'/.freeze
52
+ FORBIDDEN_PACKAGE = %r{(?<package_req>[^/]+) - (Forbidden|Unauthorized)}.freeze
54
53
  FORBIDDEN_PACKAGE_403 = %r{^403\sForbidden\s
55
54
  -\sGET\shttps?://(?<source>[^/]+)/(?<package_req>[^/\s]+)}x.freeze
56
55
  MISSING_PACKAGE = %r{(?<package_req>[^/]+) - Not found}.freeze
57
56
  INVALID_PACKAGE = /Can't install (?<package_req>.*): Missing/.freeze
58
57
 
58
+ # TODO: look into fixing this in npm, seems like a bug in the git
59
+ # downloader introduced in npm 7
60
+ #
61
+ # NOTE: error message returned from arborist/npm 7 when trying to
62
+ # fetching a invalid/non-existent git ref
63
+ NPM7_MISSING_GIT_REF = /already exists and is not an empty directory/.freeze
64
+ NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/.freeze
65
+
59
66
  def top_level_dependencies
60
67
  dependencies.select(&:top_level?)
61
68
  end
@@ -65,11 +72,9 @@ module Dependabot
65
72
  end
66
73
 
67
74
  def updatable_dependencies(lockfile)
68
- lockfile_dir = Pathname.new(lockfile.name).dirname.to_s
69
75
  dependencies.reject do |dependency|
70
76
  dependency_up_to_date?(lockfile, dependency) ||
71
- top_level_dependency_update_not_required?(dependency,
72
- lockfile_dir)
77
+ top_level_dependency_update_not_required?(dependency, lockfile)
73
78
  end
74
79
  end
75
80
 
@@ -96,17 +101,24 @@ module Dependabot
96
101
  existing_dep&.version == dependency.version
97
102
  end
98
103
 
99
- # Prevent changes to the lockfile when the dependency has been
104
+ # NOTE: Prevent changes to npm 6 lockfiles when the dependency has been
100
105
  # required in a package.json outside the current folder (e.g. lerna
101
- # proj)
102
- def top_level_dependency_update_not_required?(dependency,
103
- lockfile_dir)
106
+ # proj). npm 7 introduces workspace support so we explitly want to
107
+ # update the root lockfile and check if the dependency is in the
108
+ # lockfile
109
+ def top_level_dependency_update_not_required?(dependency, lockfile)
110
+ lockfile_dir = Pathname.new(lockfile.name).dirname.to_s
111
+
104
112
  requirements_for_path = dependency.requirements.select do |req|
105
113
  req_dir = Pathname.new(req[:file]).dirname.to_s
106
114
  req_dir == lockfile_dir
107
115
  end
108
116
 
109
- dependency.top_level? && requirements_for_path.empty?
117
+ dependency_in_lockfile = lockfile_dependencies(lockfile).any? do |dep|
118
+ dep.name == dependency.name
119
+ end
120
+
121
+ dependency.top_level? && requirements_for_path.empty? && !dependency_in_lockfile
110
122
  end
111
123
 
112
124
  def run_current_npm_update(lockfile_name:, lockfile_content:)
@@ -152,29 +164,133 @@ module Dependabot
152
164
  end
153
165
 
154
166
  def run_npm_top_level_updater(lockfile_name:, top_level_dependency_updates:, lockfile_content:)
155
- npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
156
- Dependabot.logger.info(npm_version)
157
-
158
- SharedHelpers.run_helper_subprocess(
159
- command: NativeHelpers.helper_path,
160
- function: "npm6:update",
161
- args: [
162
- Dir.pwd,
163
- lockfile_name,
164
- top_level_dependency_updates
165
- ]
167
+ if npm7?(lockfile_content)
168
+ run_npm_7_top_level_updater(
169
+ lockfile_name: lockfile_name,
170
+ top_level_dependency_updates: top_level_dependency_updates
171
+ )
172
+ else
173
+ SharedHelpers.run_helper_subprocess(
174
+ command: NativeHelpers.helper_path,
175
+ function: "npm6:update",
176
+ args: [
177
+ Dir.pwd,
178
+ lockfile_name,
179
+ top_level_dependency_updates
180
+ ]
181
+ )
182
+ end
183
+ end
184
+
185
+ def run_npm_7_top_level_updater(lockfile_name:, top_level_dependency_updates:)
186
+ # - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
187
+ # work around an issue in npm 6, we don't want that here
188
+ # - `--force` ignores checks for platform (os, cpu) and engines
189
+ # - `--ignore-scripts` disables prepare and prepack scripts which are run
190
+ # when installing git dependencies
191
+ flattenend_manifest_dependencies = flattenend_manifest_dependencies_for_lockfile_name(lockfile_name)
192
+ install_args = npm_top_level_updater_args(
193
+ top_level_dependency_updates: top_level_dependency_updates,
194
+ flattenend_manifest_dependencies: flattenend_manifest_dependencies
166
195
  )
196
+ command = [
197
+ "npm",
198
+ "install",
199
+ *install_args,
200
+ "--force",
201
+ "--dry-run",
202
+ "false",
203
+ "--ignore-scripts",
204
+ "--package-lock-only"
205
+ ].join(" ")
206
+ SharedHelpers.run_shell_command(command)
207
+ { lockfile_name => File.read(lockfile_name) }
167
208
  end
168
209
 
169
210
  def run_npm_subdependency_updater(lockfile_name:, lockfile_content:)
170
- npm_version = Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content)
171
- Dependabot.logger.info(npm_version)
211
+ if npm7?(lockfile_content)
212
+ run_npm_7_subdependency_updater(lockfile_name: lockfile_name)
213
+ else
214
+ SharedHelpers.run_helper_subprocess(
215
+ command: NativeHelpers.helper_path,
216
+ function: "npm6:updateSubdependency",
217
+ args: [Dir.pwd, lockfile_name, sub_dependencies.map(&:to_h)]
218
+ )
219
+ end
220
+ end
172
221
 
173
- SharedHelpers.run_helper_subprocess(
174
- command: NativeHelpers.helper_path,
175
- function: "npm6:updateSubdependency",
176
- args: [Dir.pwd, lockfile_name, sub_dependencies.map(&:to_h)]
177
- )
222
+ def run_npm_7_subdependency_updater(lockfile_name:)
223
+ dependency_names = sub_dependencies.map(&:name)
224
+ # - `--dry-run=false` the updater sets a global .npmrc with dry-run: true to
225
+ # work around an issue in npm 6, we don't want that here
226
+ # - `--force` ignores checks for platform (os, cpu) and engines
227
+ # - `--ignore-scripts` disables prepare and prepack scripts which are run
228
+ # when installing git dependencies
229
+ command = [
230
+ "npm",
231
+ "update",
232
+ *dependency_names,
233
+ "--force",
234
+ "--dry-run",
235
+ "false",
236
+ "--ignore-scripts",
237
+ "--package-lock-only"
238
+ ].join(" ")
239
+ SharedHelpers.run_shell_command(command)
240
+ { lockfile_name => File.read(lockfile_name) }
241
+ end
242
+
243
+ # TODO: Update the npm 6 updater to use these args as we currently do
244
+ # the same in the js updater helper, we've kept it seperate for the npm
245
+ # 7 rollout
246
+ def npm_top_level_updater_args(top_level_dependency_updates:, flattenend_manifest_dependencies:)
247
+ top_level_dependency_updates.map do |dependency|
248
+ # NOTE: For git dependencies we loose some information about the
249
+ # requirement that's only available in the package.json, e.g. when
250
+ # specifying a semver tag:
251
+ # `dependabot/depeendabot-core#semver:^0.1` - this is required to
252
+ # pass the correct install argument to `npm install`
253
+ existing_version_requirement = flattenend_manifest_dependencies[dependency.fetch(:name)]
254
+ npm_install_args(
255
+ dependency.fetch(:name),
256
+ dependency.fetch(:version),
257
+ dependency.fetch(:requirements),
258
+ existing_version_requirement
259
+ )
260
+ end
261
+ end
262
+
263
+ def flattenend_manifest_dependencies_for_lockfile_name(lockfile_name)
264
+ package_json_content = updated_package_json_content_for_lockfile_name(lockfile_name)
265
+ return {} unless package_json_content
266
+
267
+ parsed_package = JSON.parse(package_json_content)
268
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type|
269
+ deps.merge(parsed_package[type] || {})
270
+ end
271
+ end
272
+
273
+ def npm_install_args(dep_name, desired_version, requirements, existing_version_requirement)
274
+ git_requirement = requirements.find { |req| req[:source] && req[:source][:type] == "git" }
275
+
276
+ if git_requirement
277
+ existing_version_requirement ||= git_requirement[:source][:url]
278
+
279
+ # NOTE: Git is configured to auth over https while updating
280
+ existing_version_requirement = existing_version_requirement.gsub(
281
+ %r{git\+ssh://git@(.*?)[:/]}, 'https://\1/'
282
+ )
283
+
284
+ # NOTE: Keep any semver range that has already been updated by the
285
+ # PackageJsonUpdater when installing the new version
286
+ if existing_version_requirement.include?(desired_version)
287
+ "#{dep_name}@#{existing_version_requirement}"
288
+ else
289
+ "#{dep_name}@#{existing_version_requirement.sub(/#.*/, '')}##{desired_version}"
290
+ end
291
+ else
292
+ "#{dep_name}@#{desired_version}"
293
+ end
178
294
  end
179
295
 
180
296
  # rubocop:disable Metrics/AbcSize
@@ -198,7 +314,7 @@ module Dependabot
198
314
  # workspace project)
199
315
  sub_dep_local_path_error = "does not contain a package.json file"
200
316
  if error_message.match?(INVALID_PACKAGE) ||
201
- error_message.start_with?("Invalid package name") ||
317
+ error_message.include?("Invalid package name") ||
202
318
  error_message.include?(sub_dep_local_path_error)
203
319
  raise_resolvability_error(error_message, lockfile)
204
320
  end
@@ -222,7 +338,7 @@ module Dependabot
222
338
  # This happens if a new version has been published but npm is having
223
339
  # consistency issues and the version isn't fully available on all
224
340
  # queries
225
- if error_message.start_with?("No matching vers") &&
341
+ if error_message.include?("No matching vers") &&
226
342
  dependencies_in_error_message?(error_message) &&
227
343
  resolvable_before_update?(lockfile)
228
344
 
@@ -249,10 +365,8 @@ module Dependabot
249
365
  handle_missing_package(sanitized_name, sanitized_error, lockfile)
250
366
  end
251
367
 
252
- if error_message.match?(UNREACHABLE_GIT)
253
- dependency_url =
254
- error_message.match(UNREACHABLE_GIT).
255
- named_captures.fetch("url")
368
+ if (git_error = error_message.match(UNREACHABLE_GIT) || error_message.match(FORBIDDEN_GIT))
369
+ dependency_url = git_error.named_captures.fetch("url")
256
370
 
257
371
  raise Dependabot::GitDependenciesNotReachable, dependency_url
258
372
  end
@@ -270,14 +384,22 @@ module Dependabot
270
384
  lockfile)
271
385
  end
272
386
 
273
- if (error_message.start_with?("No matching vers", "404 Not Found") ||
274
- error_message.include?("not match any file(s) known to git") ||
387
+ if (error_message.include?("No matching vers") ||
388
+ error_message.include?("404 Not Found") ||
275
389
  error_message.include?("Non-registry package missing package") ||
276
- error_message.include?("Invalid tag name")) &&
390
+ error_message.include?("Invalid tag name") ||
391
+ error_message.match?(NPM6_MISSING_GIT_REF) ||
392
+ error_message.match?(NPM7_MISSING_GIT_REF)) &&
277
393
  !resolvable_before_update?(lockfile)
278
394
  raise_resolvability_error(error_message, lockfile)
279
395
  end
280
396
 
397
+ # NOTE: This check was introduced in npm7/arborist
398
+ if error_message.include?("must provide string spec")
399
+ msg = "Error parsing your package.json manifest: the version requirement must be a string"
400
+ raise Dependabot::DependencyFileNotParseable, msg
401
+ end
402
+
281
403
  raise error
282
404
  end
283
405
  # rubocop:enable Metrics/AbcSize
@@ -388,8 +510,11 @@ module Dependabot
388
510
  file.content
389
511
  end
390
512
 
391
- # When updating a package-lock.json we have to manually lock all
392
- # git dependencies, otherwise npm will (unhelpfully) update them
513
+ # TODO: Figure out if we need to lock git deps for npm 7 and can
514
+ # start deprecating this hornets nest
515
+ #
516
+ # NOTE: When updating a package-lock.json we have to manually lock
517
+ # all git dependencies, otherwise npm will (unhelpfully) update them
393
518
  updated_content = lock_git_deps(updated_content)
394
519
  updated_content = replace_ssh_sources(updated_content)
395
520
  updated_content = lock_deps_with_latest_reqs(updated_content)
@@ -502,46 +627,107 @@ module Dependabot
502
627
  @git_ssh_requirements_to_swap
503
628
  end
504
629
 
505
- def post_process_npm_lockfile(original_content, updated_content)
506
- updated_content =
507
- replace_project_metadata(updated_content, original_content)
630
+ def post_process_npm_lockfile(original_content, updated_content, lockfile_name)
631
+ updated_content = replace_project_metadata(updated_content, original_content)
508
632
 
509
633
  # Switch SSH requirements back for git dependencies
634
+ updated_content = replace_swapped_git_ssh_requirements(updated_content)
635
+
636
+ # Switch from details back for git dependencies (they will have
637
+ # changed because we locked them)
638
+ updated_content = replace_locked_git_dependencies(updated_content)
639
+
640
+ # Switch back npm 7 lockfile "pacakages" requirements from the package.json
641
+ updated_content = restore_locked_package_dependencies(lockfile_name, updated_content)
642
+
643
+ # Switch back the protocol of tarball resolutions if they've changed
644
+ # (fixes an npm bug, which appears to be applied inconsistently)
645
+ replace_tarball_urls(updated_content)
646
+ end
647
+
648
+ # NOTE: This is a workaround to "sync" what's in package.json
649
+ # requirements and the `packages.""` entry in npm 7 v2 lockfiles. These
650
+ # get out of sync because we lock git dependencies (that are not being
651
+ # updated) to a specific sha to prevent unrelated updates and the way we
652
+ # invoke the `npm install` cli, where we might tell npm to install a
653
+ # specific versionm e.g. `npm install eslint@1.1.8` but we keep the
654
+ # `package.json` requirement for eslint at `^1.0.0`, in which case we
655
+ # need to copy this from the manifest to the lockfile after the update
656
+ # has finished.
657
+ def restore_locked_package_dependencies(lockfile_name, lockfile_content)
658
+ return lockfile_content unless npm7?(lockfile_content)
659
+
660
+ original_package = updated_package_json_content_for_lockfile_name(lockfile_name)
661
+ return lockfile_content unless original_package
662
+
663
+ parsed_package = JSON.parse(original_package)
664
+ parsed_lockfile = JSON.parse(lockfile_content)
665
+ dependency_names_to_restore = (dependencies.map(&:name) + git_dependencies_to_lock.keys).uniq
666
+
667
+ NpmAndYarn::FileParser::DEPENDENCY_TYPES.each do |type|
668
+ parsed_package.fetch(type, {}).each do |dependency_name, original_requirement|
669
+ next unless dependency_names_to_restore.include?(dependency_name)
670
+
671
+ locked_requirement = parsed_lockfile.dig("packages", "", type, dependency_name)
672
+ next unless locked_requirement
673
+
674
+ locked_req = %("#{dependency_name}": "#{locked_requirement}")
675
+ original_req = %("#{dependency_name}": "#{original_requirement}")
676
+ lockfile_content = lockfile_content.gsub(locked_req, original_req)
677
+ end
678
+ end
679
+
680
+ lockfile_content
681
+ end
682
+
683
+ def replace_swapped_git_ssh_requirements(lockfile_content)
510
684
  git_ssh_requirements_to_swap.each do |req|
511
685
  new_r = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'git+https://\1/')
512
686
  old_r = req.gsub(%r{git@(.*?)[:/]}, 'git@\1/')
513
- updated_content = updated_content.gsub(new_r, old_r)
687
+ lockfile_content = lockfile_content.gsub(new_r, old_r)
514
688
  end
515
689
 
690
+ lockfile_content
691
+ end
692
+
693
+ def replace_locked_git_dependencies(lockfile_content)
516
694
  # Switch from details back for git dependencies (they will have
517
695
  # changed because we locked them)
518
- git_dependencies_to_lock.each do |_, details|
696
+ git_dependencies_to_lock.each do |dependency_name, details|
519
697
  next unless details[:version] && details[:from]
520
698
 
521
699
  # When locking git dependencies in package.json we set the version
522
700
  # to be the git commit from the lockfile "version" field which
523
701
  # updates the lockfile "from" field to the new git commit when we
524
702
  # run npm install
525
- locked_from = %("from": "#{details[:version]}")
526
703
  original_from = %("from": "#{details[:from]}")
527
- updated_content = updated_content.gsub(locked_from, original_from)
704
+ if npm7?(lockfile_content)
705
+ # NOTE: The `from` syntax has changed in npm 7 to inclued the dependency name
706
+ npm7_locked_from = %("from": "#{dependency_name}@#{details[:version]}")
707
+ lockfile_content = lockfile_content.gsub(npm7_locked_from, original_from)
708
+ else
709
+ npm6_locked_from = %("from": "#{details[:version]}")
710
+ lockfile_content = lockfile_content.gsub(npm6_locked_from, original_from)
711
+ end
528
712
  end
529
713
 
530
- # Switch back the protocol of tarball resolutions if they've changed
531
- # (fixes an npm bug, which appears to be applied inconsistently)
714
+ lockfile_content
715
+ end
716
+
717
+ def replace_tarball_urls(lockfile_content)
532
718
  tarball_urls.each do |url|
533
719
  trimmed_url = url.gsub(/(\d+\.)*tgz$/, "")
534
720
  incorrect_url = if url.start_with?("https")
535
721
  trimmed_url.gsub(/^https:/, "http:")
536
722
  else trimmed_url.gsub(/^http:/, "https:")
537
723
  end
538
- updated_content = updated_content.gsub(
724
+ lockfile_content = lockfile_content.gsub(
539
725
  /#{Regexp.quote(incorrect_url)}(?=(\d+\.)*tgz")/,
540
726
  trimmed_url
541
727
  )
542
728
  end
543
729
 
544
- updated_content
730
+ lockfile_content
545
731
  end
546
732
 
547
733
  def replace_project_metadata(new_content, old_content)
@@ -579,6 +765,15 @@ module Dependabot
579
765
  ).npmrc_content
580
766
  end
581
767
 
768
+ def updated_package_json_content_for_lockfile_name(lockfile_name)
769
+ lockfile_basename = Pathname.new(lockfile_name).basename.to_s
770
+ package_name = lockfile_name.sub(lockfile_basename, "package.json")
771
+ package_json = package_files.find { |f| f.name == package_name }
772
+ return unless package_json
773
+
774
+ updated_package_json_content(package_json)
775
+ end
776
+
582
777
  def updated_package_json_content(file)
583
778
  @updated_package_json_content ||= {}
584
779
  @updated_package_json_content[file.name] ||=
@@ -592,6 +787,10 @@ module Dependabot
592
787
  npmrc_content.match?(/^package-lock\s*=\s*false/)
593
788
  end
594
789
 
790
+ def npm7?(lockfile_content)
791
+ Dependabot::NpmAndYarn::Helpers.npm_version(lockfile_content) == "npm7"
792
+ end
793
+
595
794
  def sanitized_package_json_content(content)
596
795
  content.
597
796
  gsub(/\{\{[^\}]*?\}\}/, "something"). # {{ nm }} syntax not allowed