dependabot-npm_and_yarn 0.91.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/helpers/build +14 -0
  3. data/helpers/npm/.eslintrc +14 -0
  4. data/helpers/npm/bin/run.js +34 -0
  5. data/helpers/npm/lib/helpers.js +25 -0
  6. data/helpers/npm/lib/peer-dependency-checker.js +102 -0
  7. data/helpers/npm/lib/subdependency-updater.js +48 -0
  8. data/helpers/npm/lib/updater.js +101 -0
  9. data/helpers/npm/package-lock.json +8868 -0
  10. data/helpers/npm/package.json +17 -0
  11. data/helpers/npm/test/fixtures/npm-left-pad.json +1 -0
  12. data/helpers/npm/test/fixtures/updater/original/package-lock.json +16 -0
  13. data/helpers/npm/test/fixtures/updater/original/package.json +9 -0
  14. data/helpers/npm/test/fixtures/updater/updated/package-lock.json +16 -0
  15. data/helpers/npm/test/helpers.js +7 -0
  16. data/helpers/npm/test/updater.test.js +50 -0
  17. data/helpers/npm/yarn.lock +6176 -0
  18. data/helpers/yarn/.eslintrc +14 -0
  19. data/helpers/yarn/bin/run.js +36 -0
  20. data/helpers/yarn/lib/fix-duplicates.js +78 -0
  21. data/helpers/yarn/lib/helpers.js +5 -0
  22. data/helpers/yarn/lib/lockfile-parser.js +21 -0
  23. data/helpers/yarn/lib/peer-dependency-checker.js +130 -0
  24. data/helpers/yarn/lib/replace-lockfile-declaration.js +57 -0
  25. data/helpers/yarn/lib/subdependency-updater.js +69 -0
  26. data/helpers/yarn/lib/updater.js +266 -0
  27. data/helpers/yarn/package.json +17 -0
  28. data/helpers/yarn/test/fixtures/updater/original/package.json +6 -0
  29. data/helpers/yarn/test/fixtures/updater/original/yarn.lock +11 -0
  30. data/helpers/yarn/test/fixtures/updater/updated/yarn.lock +12 -0
  31. data/helpers/yarn/test/fixtures/updater/with-version-comments/package.json +5 -0
  32. data/helpers/yarn/test/fixtures/updater/with-version-comments/yarn.lock +13 -0
  33. data/helpers/yarn/test/fixtures/yarnpkg-is-positive.json +1 -0
  34. data/helpers/yarn/test/fixtures/yarnpkg-left-pad.json +1 -0
  35. data/helpers/yarn/test/helpers.js +7 -0
  36. data/helpers/yarn/test/updater.test.js +93 -0
  37. data/helpers/yarn/yarn.lock +4760 -0
  38. data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +146 -0
  39. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +332 -0
  40. data/lib/dependabot/npm_and_yarn/file_parser.rb +397 -0
  41. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +527 -0
  42. data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +190 -0
  43. data/lib/dependabot/npm_and_yarn/file_updater/package_json_preparer.rb +87 -0
  44. data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +218 -0
  45. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +471 -0
  46. data/lib/dependabot/npm_and_yarn/file_updater.rb +189 -0
  47. data/lib/dependabot/npm_and_yarn/metadata_finder.rb +217 -0
  48. data/lib/dependabot/npm_and_yarn/native_helpers.rb +28 -0
  49. data/lib/dependabot/npm_and_yarn/requirement.rb +145 -0
  50. data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +340 -0
  51. data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +67 -0
  52. data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +224 -0
  53. data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +193 -0
  54. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +223 -0
  55. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +495 -0
  56. data/lib/dependabot/npm_and_yarn/update_checker.rb +282 -0
  57. data/lib/dependabot/npm_and_yarn/version.rb +34 -0
  58. data/lib/dependabot/npm_and_yarn.rb +11 -0
  59. metadata +226 -0
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ ################################################################################
4
+ # For more details on npm version constraints, see: #
5
+ # https://docs.npmjs.com/misc/semver #
6
+ ################################################################################
7
+
8
+ require "dependabot/npm_and_yarn/update_checker"
9
+ require "dependabot/npm_and_yarn/version"
10
+ require "dependabot/npm_and_yarn/requirement"
11
+
12
+ module Dependabot
13
+ module NpmAndYarn
14
+ class UpdateChecker
15
+ class RequirementsUpdater
16
+ VERSION_REGEX = /[0-9]+(?:\.[A-Za-z0-9\-_]+)*/.freeze
17
+ SEPARATOR = /(?<=[a-zA-Z0-9*])[\s|]+(?![\s|-])/.freeze
18
+ ALLOWED_UPDATE_STRATEGIES =
19
+ %i(widen_ranges bump_versions bump_versions_if_necessary).freeze
20
+
21
+ def initialize(requirements:, updated_source:, update_strategy:,
22
+ latest_version:, latest_resolvable_version:)
23
+ @requirements = requirements
24
+ @updated_source = updated_source
25
+ @update_strategy = update_strategy
26
+
27
+ check_update_strategy
28
+
29
+ @latest_version = version_class.new(latest_version) if latest_version
30
+ return unless latest_resolvable_version
31
+
32
+ @latest_resolvable_version =
33
+ version_class.new(latest_resolvable_version)
34
+ end
35
+
36
+ def updated_requirements
37
+ requirements.map do |req|
38
+ req = req.merge(source: updated_source)
39
+ next req unless latest_resolvable_version
40
+ next initial_req_after_source_change(req) unless req[:requirement]
41
+ next req if req[:requirement].match?(/^([A-Za-uw-z]|v[^\d])/)
42
+
43
+ case update_strategy
44
+ when :widen_ranges then widen_requirement(req)
45
+ when :bump_versions then update_version_requirement(req)
46
+ when :bump_versions_if_necessary
47
+ update_version_requirement_if_needed(req)
48
+ else raise "Unexpected update strategy: #{update_strategy}"
49
+ end
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :requirements, :updated_source, :update_strategy,
56
+ :latest_version, :latest_resolvable_version
57
+
58
+ def check_update_strategy
59
+ return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
60
+
61
+ raise "Unknown update strategy: #{update_strategy}"
62
+ end
63
+
64
+ def updating_from_git_to_npm?
65
+ return false unless updated_source.nil?
66
+
67
+ original_source = requirements.map { |r| r[:source] }.compact.first
68
+ original_source&.fetch(:type) == "git"
69
+ end
70
+
71
+ def initial_req_after_source_change(req)
72
+ return req unless updating_from_git_to_npm?
73
+ return req unless req[:requirement].nil?
74
+
75
+ req.merge(requirement: "^#{latest_resolvable_version}")
76
+ end
77
+
78
+ def update_version_requirement(req)
79
+ current_requirement = req[:requirement]
80
+
81
+ if current_requirement.match?(/(<|-\s)/i)
82
+ ruby_req = ruby_requirements(current_requirement).first
83
+ return req if ruby_req.satisfied_by?(latest_resolvable_version)
84
+
85
+ updated_req = update_range_requirement(current_requirement)
86
+ return req.merge(requirement: updated_req)
87
+ end
88
+
89
+ reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
90
+ req.merge(requirement: update_version_string(reqs.first))
91
+ end
92
+
93
+ def update_version_requirement_if_needed(req)
94
+ current_requirement = req[:requirement]
95
+ version = latest_resolvable_version
96
+ return req if current_requirement.strip == ""
97
+
98
+ ruby_reqs = ruby_requirements(current_requirement)
99
+ return req if ruby_reqs.any? { |r| r.satisfied_by?(version) }
100
+
101
+ update_version_requirement(req)
102
+ end
103
+
104
+ def widen_requirement(req)
105
+ current_requirement = req[:requirement]
106
+ version = latest_resolvable_version
107
+ return req if current_requirement.strip == ""
108
+
109
+ ruby_reqs = ruby_requirements(current_requirement)
110
+ return req if ruby_reqs.any? { |r| r.satisfied_by?(version) }
111
+
112
+ reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
113
+
114
+ updated_requirement =
115
+ if reqs.any? { |r| r.match?(/(<|-\s)/i) }
116
+ update_range_requirement(current_requirement)
117
+ elsif current_requirement.strip.split(SEPARATOR).count == 1
118
+ update_version_string(current_requirement)
119
+ else
120
+ current_requirement
121
+ end
122
+
123
+ req.merge(requirement: updated_requirement)
124
+ end
125
+
126
+ def ruby_requirements(requirement_string)
127
+ NpmAndYarn::Requirement.
128
+ requirements_array(requirement_string)
129
+ end
130
+
131
+ def update_range_requirement(req_string)
132
+ range_requirements =
133
+ req_string.split(SEPARATOR).select { |r| r.match?(/<|(\s+-\s+)/) }
134
+
135
+ if range_requirements.count == 1
136
+ range_requirement = range_requirements.first
137
+ versions = range_requirement.scan(VERSION_REGEX)
138
+ upper_bound = versions.map { |v| version_class.new(v) }.max
139
+ new_upper_bound = update_greatest_version(
140
+ upper_bound,
141
+ latest_resolvable_version
142
+ )
143
+
144
+ req_string.sub(
145
+ upper_bound.to_s,
146
+ new_upper_bound.to_s
147
+ )
148
+ else
149
+ req_string + " || ^#{latest_resolvable_version}"
150
+ end
151
+ end
152
+
153
+ def update_version_string(req_string)
154
+ req_string.
155
+ sub(VERSION_REGEX) do |old_version|
156
+ if old_version.match?(/\d-/) ||
157
+ latest_resolvable_version.to_s.match?(/\d-/)
158
+ latest_resolvable_version.to_s
159
+ else
160
+ old_parts = old_version.split(".")
161
+ new_parts = latest_resolvable_version.to_s.split(".").
162
+ first(old_parts.count)
163
+ new_parts.map.with_index do |part, i|
164
+ old_parts[i].match?(/^x\b/) ? "x" : part
165
+ end.join(".")
166
+ end
167
+ end
168
+ end
169
+
170
+ def update_greatest_version(old_version, version_to_be_permitted)
171
+ version = version_class.new(old_version)
172
+ version = version.release if version.prerelease?
173
+
174
+ index_to_update =
175
+ version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
176
+
177
+ version.segments.map.with_index do |_, index|
178
+ if index < index_to_update
179
+ version_to_be_permitted.segments[index]
180
+ elsif index == index_to_update
181
+ version_to_be_permitted.segments[index] + 1
182
+ else 0
183
+ end
184
+ end.join(".")
185
+ end
186
+
187
+ def version_class
188
+ NpmAndYarn::Version
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/npm_and_yarn/update_checker"
4
+ require "dependabot/npm_and_yarn/file_parser"
5
+ require "dependabot/npm_and_yarn/version"
6
+ require "dependabot/npm_and_yarn/native_helpers"
7
+ require "dependabot/shared_helpers"
8
+ require "dependabot/errors"
9
+ require "dependabot/npm_and_yarn/file_updater/npmrc_builder"
10
+ require "dependabot/npm_and_yarn/file_updater/package_json_preparer"
11
+
12
+ module Dependabot
13
+ module NpmAndYarn
14
+ class UpdateChecker
15
+ class SubdependencyVersionResolver
16
+ def initialize(dependency:, credentials:, dependency_files:,
17
+ ignored_versions:)
18
+ @dependency = dependency
19
+ @credentials = credentials
20
+ @dependency_files = dependency_files
21
+ @ignored_versions = ignored_versions
22
+ end
23
+
24
+ def latest_resolvable_version
25
+ raise "Not a subdependency!" if dependency.requirements.any?
26
+
27
+ lockfiles = [*package_locks, *shrinkwraps, *yarn_locks]
28
+ updated_lockfiles = lockfiles.map do |lockfile|
29
+ updated_content = update_subdependency_in_lockfile(lockfile)
30
+ updated_lockfile = lockfile.dup
31
+ updated_lockfile.content = updated_content
32
+ updated_lockfile
33
+ end
34
+
35
+ version_from_updated_lockfiles(updated_lockfiles)
36
+ rescue SharedHelpers::HelperSubprocessFailed
37
+ # TODO: Move error handling logic from the FileUpdater to this class
38
+
39
+ # Return nil (no update possible) if an unknown error occurred
40
+ nil
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :dependency, :credentials, :dependency_files,
46
+ :ignored_versions
47
+
48
+ def update_subdependency_in_lockfile(lockfile)
49
+ SharedHelpers.in_a_temporary_directory do
50
+ write_temporary_dependency_files
51
+ lockfile_name = Pathname.new(lockfile.name).basename.to_s
52
+ path = Pathname.new(lockfile.name).dirname.to_s
53
+
54
+ updated_files = if lockfile.name.end_with?("yarn.lock")
55
+ run_yarn_updater(path, lockfile_name)
56
+ else
57
+ run_npm_updater(path, lockfile_name)
58
+ end
59
+
60
+ updated_files.fetch(lockfile_name)
61
+ end
62
+ end
63
+
64
+ def version_from_updated_lockfiles(updated_lockfiles)
65
+ updated_files = dependency_files -
66
+ yarn_locks -
67
+ package_locks -
68
+ shrinkwraps +
69
+ updated_lockfiles
70
+
71
+ updated_version = NpmAndYarn::FileParser.new(
72
+ dependency_files: updated_files,
73
+ source: nil,
74
+ credentials: credentials
75
+ ).parse.find { |d| d.name == dependency.name }&.version
76
+ return unless updated_version
77
+
78
+ version_class.new(updated_version)
79
+ end
80
+
81
+ # rubocop:disable Metrics/CyclomaticComplexity
82
+ # rubocop:disable Metrics/PerceivedComplexity
83
+ def run_yarn_updater(path, lockfile_name)
84
+ SharedHelpers.with_git_configured(credentials: credentials) do
85
+ Dir.chdir(path) do
86
+ SharedHelpers.run_helper_subprocess(
87
+ command: "node #{yarn_helper_path}",
88
+ function: "updateSubdependency",
89
+ args: [Dir.pwd, lockfile_name]
90
+ )
91
+ end
92
+ end
93
+ rescue SharedHelpers::HelperSubprocessFailed => error
94
+ unfindable_str = "find package \"#{dependency.name}"
95
+ raise unless error.message.include?("The registry may be down") ||
96
+ error.message.include?("ETIMEDOUT") ||
97
+ error.message.include?("ENOBUFS") ||
98
+ error.message.include?(unfindable_str)
99
+
100
+ retry_count ||= 0
101
+ retry_count += 1
102
+ raise if retry_count > 2
103
+
104
+ sleep(rand(3.0..10.0)) && retry
105
+ end
106
+ # rubocop:enable Metrics/CyclomaticComplexity
107
+ # rubocop:enable Metrics/PerceivedComplexity
108
+
109
+ def run_npm_updater(path, lockfile_name)
110
+ SharedHelpers.with_git_configured(credentials: credentials) do
111
+ Dir.chdir(path) do
112
+ SharedHelpers.run_helper_subprocess(
113
+ command: "node #{npm_helper_path}",
114
+ function: "updateSubdependency",
115
+ args: [Dir.pwd, lockfile_name]
116
+ )
117
+ end
118
+ end
119
+ end
120
+
121
+ def write_temporary_dependency_files
122
+ write_lock_files
123
+
124
+ File.write(".npmrc", npmrc_content)
125
+
126
+ package_files.each do |file|
127
+ path = file.name
128
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
129
+ File.write(file.name, prepared_package_json_content(file))
130
+ end
131
+ end
132
+
133
+ def write_lock_files
134
+ yarn_locks.each do |f|
135
+ FileUtils.mkdir_p(Pathname.new(f.name).dirname)
136
+ File.write(f.name, prepared_yarn_lockfile_content(f.content))
137
+ end
138
+
139
+ [*package_locks, *shrinkwraps].each do |f|
140
+ FileUtils.mkdir_p(Pathname.new(f.name).dirname)
141
+ File.write(f.name, prepared_npm_lockfile_content(f.content))
142
+ end
143
+ end
144
+
145
+ # Duplicated in NpmLockfileUpdater
146
+ # Remove the dependency we want to update from the lockfile and let
147
+ # yarn find the latest resolvable version and fix the lockfile
148
+ def prepared_yarn_lockfile_content(content)
149
+ content.gsub(/^#{Regexp.quote(dependency.name)}\@.*?\n\n/m, "")
150
+ end
151
+
152
+ def prepared_npm_lockfile_content(content)
153
+ JSON.dump(
154
+ remove_dependency_from_npm_lockfile(JSON.parse(content))
155
+ )
156
+ end
157
+
158
+ # Duplicated in NpmLockfileUpdater
159
+ # Remove the dependency we want to update from the lockfile and let
160
+ # npm find the latest resolvable version and fix the lockfile
161
+ def remove_dependency_from_npm_lockfile(npm_lockfile)
162
+ return npm_lockfile unless npm_lockfile.key?("dependencies")
163
+
164
+ dependencies =
165
+ npm_lockfile["dependencies"].
166
+ reject { |key, _| key == dependency.name }.
167
+ map { |k, v| [k, remove_dependency_from_npm_lockfile(v)] }.
168
+ to_h
169
+ npm_lockfile.merge("dependencies" => dependencies)
170
+ end
171
+
172
+ def prepared_package_json_content(file)
173
+ NpmAndYarn::FileUpdater::PackageJsonPreparer.new(
174
+ package_json_content: file.content
175
+ ).prepared_content
176
+ end
177
+
178
+ def npmrc_content
179
+ NpmAndYarn::FileUpdater::NpmrcBuilder.new(
180
+ credentials: credentials,
181
+ dependency_files: dependency_files
182
+ ).npmrc_content
183
+ end
184
+
185
+ def version_class
186
+ NpmAndYarn::Version
187
+ end
188
+
189
+ def package_locks
190
+ @package_locks ||=
191
+ dependency_files.
192
+ select { |f| f.name.end_with?("package-lock.json") }
193
+ end
194
+
195
+ def yarn_locks
196
+ @yarn_locks ||=
197
+ dependency_files.
198
+ select { |f| f.name.end_with?("yarn.lock") }
199
+ end
200
+
201
+ def shrinkwraps
202
+ @shrinkwraps ||=
203
+ dependency_files.
204
+ select { |f| f.name.end_with?("npm-shrinkwrap.json") }
205
+ end
206
+
207
+ def package_files
208
+ @package_files ||=
209
+ dependency_files.
210
+ select { |f| f.name.end_with?("package.json") }
211
+ end
212
+
213
+ def yarn_helper_path
214
+ NativeHelpers.yarn_helper_path
215
+ end
216
+
217
+ def npm_helper_path
218
+ NativeHelpers.npm_helper_path
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,495 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/git_commit_checker"
4
+ require "dependabot/npm_and_yarn/update_checker"
5
+ require "dependabot/npm_and_yarn/file_parser"
6
+ require "dependabot/npm_and_yarn/version"
7
+ require "dependabot/npm_and_yarn/requirement"
8
+ require "dependabot/npm_and_yarn/native_helpers"
9
+ require "dependabot/shared_helpers"
10
+ require "dependabot/errors"
11
+ require "dependabot/npm_and_yarn/file_updater/npmrc_builder"
12
+ require "dependabot/npm_and_yarn/file_updater/package_json_preparer"
13
+
14
+ # rubocop:disable Metrics/ClassLength
15
+ module Dependabot
16
+ module NpmAndYarn
17
+ class UpdateChecker
18
+ class VersionResolver
19
+ require_relative "latest_version_finder"
20
+
21
+ TIGHTLY_COUPLED_MONOREPOS = {
22
+ "vue" => %w(vue vue-template-compiler)
23
+ }.freeze
24
+
25
+ # Error message from yarn add:
26
+ # " > @reach/router@1.2.1" has incorrect \
27
+ # peer dependency "react@15.x || 16.x || 16.4.0-alpha.0911da3"
28
+ # " > react-burger-menu@1.9.9" has unmet \
29
+ # peer dependency "react@>=0.14.0 <16.0.0".
30
+ YARN_PEER_DEP_ERROR_REGEX =
31
+ /
32
+ "\s>\s(?<requiring_dep>[^"]+)"\s
33
+ has\s(incorrect|unmet)\speer\sdependency\s
34
+ "(?<required_dep>[^"]+)"
35
+ /x.freeze
36
+
37
+ # Error message from npm install:
38
+ # react-dom@15.2.0 requires a peer of react@^15.2.0 \
39
+ # but none is installed. You must install peer dependencies yourself.
40
+ NPM_PEER_DEP_ERROR_REGEX =
41
+ /
42
+ (?<requiring_dep>[^\s]+)\s
43
+ requires\sa\speer\sof\s
44
+ (?<required_dep>.+?)\sbut\snone\sis\sinstalled.
45
+ /x.freeze
46
+
47
+ def initialize(dependency:, credentials:, dependency_files:,
48
+ latest_allowable_version:, latest_version_finder:)
49
+ @dependency = dependency
50
+ @credentials = credentials
51
+ @dependency_files = dependency_files
52
+ @latest_allowable_version = latest_allowable_version
53
+
54
+ @latest_version_finder = {}
55
+ @latest_version_finder[dependency] = latest_version_finder
56
+ end
57
+
58
+ def latest_resolvable_version
59
+ return latest_allowable_version if git_dependency?(dependency)
60
+ return if part_of_tightly_locked_monorepo?
61
+
62
+ unless relevant_unmet_peer_dependencies.any?
63
+ return latest_allowable_version
64
+ end
65
+
66
+ satisfying_versions.first
67
+ end
68
+
69
+ def latest_version_resolvable_with_full_unlock?
70
+ return false if dependency_updates_from_full_unlock.nil?
71
+
72
+ true
73
+ end
74
+
75
+ def dependency_updates_from_full_unlock
76
+ return if git_dependency?(dependency)
77
+ if part_of_tightly_locked_monorepo?
78
+ return updated_monorepo_dependencies
79
+ end
80
+ return if newly_broken_peer_reqs_from_dep.any?
81
+
82
+ updates =
83
+ [{ dependency: dependency, version: latest_allowable_version }]
84
+ newly_broken_peer_reqs_on_dep.each do |peer_req|
85
+ dep_name = peer_req.fetch(:requiring_dep_name)
86
+ dep = top_level_dependencies.find { |d| d.name == dep_name }
87
+
88
+ # Can't handle reqs from sub-deps or git source deps (yet)
89
+ return nil if dep.nil?
90
+ return nil if git_dependency?(dep)
91
+
92
+ updated_version =
93
+ latest_version_of_dep_with_satisfied_peer_reqs(dep)
94
+ return nil unless updated_version
95
+
96
+ updates << { dependency: dep, version: updated_version }
97
+ end
98
+
99
+ updates
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :dependency, :credentials, :dependency_files,
105
+ :latest_allowable_version
106
+
107
+ def latest_version_finder(dep)
108
+ @latest_version_finder[dep] ||=
109
+ LatestVersionFinder.new(
110
+ dependency: dep,
111
+ credentials: credentials,
112
+ dependency_files: dependency_files,
113
+ ignored_versions: []
114
+ )
115
+ end
116
+
117
+ def part_of_tightly_locked_monorepo?
118
+ monorepo_dep_names =
119
+ TIGHTLY_COUPLED_MONOREPOS.values.
120
+ find { |deps| deps.include?(dependency.name) }
121
+ return false unless monorepo_dep_names
122
+
123
+ deps_to_update =
124
+ top_level_dependencies.
125
+ select { |d| monorepo_dep_names.include?(d.name) }
126
+
127
+ deps_to_update.count > 1
128
+ end
129
+
130
+ def updated_monorepo_dependencies
131
+ monorepo_dep_names =
132
+ TIGHTLY_COUPLED_MONOREPOS.values.
133
+ find { |deps| deps.include?(dependency.name) }
134
+
135
+ deps_to_update =
136
+ top_level_dependencies.
137
+ select { |d| monorepo_dep_names.include?(d.name) }
138
+
139
+ updates = []
140
+ deps_to_update.each do |dep|
141
+ next if git_dependency?(dep)
142
+ next if dep.version &&
143
+ version_class.new(dep.version) >= latest_allowable_version
144
+
145
+ updated_version =
146
+ latest_version_finder(dep).
147
+ possible_versions.
148
+ find { |v| v == latest_allowable_version }
149
+ next unless updated_version
150
+
151
+ updates << { dependency: dep, version: updated_version }
152
+ end
153
+
154
+ updates
155
+ end
156
+
157
+ def peer_dependency_errors
158
+ return @peer_dependency_errors if @peer_dependency_errors_checked
159
+
160
+ @peer_dependency_errors_checked = true
161
+
162
+ @peer_dependency_errors =
163
+ fetch_peer_dependency_errors(version: latest_allowable_version)
164
+ end
165
+
166
+ def old_peer_dependency_errors
167
+ if @old_peer_dependency_errors_checked
168
+ return @old_peer_dependency_errors
169
+ end
170
+
171
+ @old_peer_dependency_errors_checked = true
172
+
173
+ @old_peer_dependency_errors =
174
+ fetch_peer_dependency_errors(version: dependency.version)
175
+ end
176
+
177
+ def fetch_peer_dependency_errors(version:)
178
+ # TODO: Add all of the error handling that the FileUpdater does
179
+ # here (since problematic repos will be resolved here before they're
180
+ # seen by the FileUpdater)
181
+ SharedHelpers.in_a_temporary_directory do
182
+ write_temporary_dependency_files
183
+
184
+ package_files.flat_map do |file|
185
+ path = Pathname.new(file.name).dirname
186
+ run_checker(path: path, version: version)
187
+ rescue SharedHelpers::HelperSubprocessFailed => error
188
+ errors = []
189
+ if error.message.match?(NPM_PEER_DEP_ERROR_REGEX)
190
+ error.message.scan(NPM_PEER_DEP_ERROR_REGEX) do
191
+ errors << Regexp.last_match.named_captures
192
+ end
193
+ elsif error.message.match?(YARN_PEER_DEP_ERROR_REGEX)
194
+ error.message.scan(YARN_PEER_DEP_ERROR_REGEX) do
195
+ errors << Regexp.last_match.named_captures
196
+ end
197
+ else raise
198
+ end
199
+ errors
200
+ end.compact
201
+ end
202
+ rescue SharedHelpers::HelperSubprocessFailed
203
+ # Fall back to allowing the version through. Whatever error
204
+ # occurred should be properly handled by the FileUpdater. We
205
+ # can slowly migrate error handling to this class over time.
206
+ []
207
+ end
208
+
209
+ def unmet_peer_dependencies
210
+ peer_dependency_errors.
211
+ map { |captures| error_details_from_captures(captures) }
212
+ end
213
+
214
+ def old_unmet_peer_dependencies
215
+ old_peer_dependency_errors.
216
+ map { |captures| error_details_from_captures(captures) }
217
+ end
218
+
219
+ def error_details_from_captures(captures)
220
+ {
221
+ requirement_name:
222
+ captures.fetch("required_dep").sub(/@[^@]+$/, ""),
223
+ requirement_version:
224
+ captures.fetch("required_dep").split("@").last,
225
+ requiring_dep_name:
226
+ captures.fetch("requiring_dep").sub(/@[^@]+$/, "")
227
+ }
228
+ end
229
+
230
+ def relevant_unmet_peer_dependencies
231
+ relevant_unmet_peer_dependencies =
232
+ unmet_peer_dependencies.select do |dep|
233
+ dep[:requirement_name] == dependency.name ||
234
+ dep[:requiring_dep_name] == dependency.name
235
+ end
236
+
237
+ return [] if relevant_unmet_peer_dependencies.empty?
238
+
239
+ # Prune out any pre-existing warnings
240
+ relevant_unmet_peer_dependencies.reject do |issue|
241
+ old_unmet_peer_dependencies.any? do |old_issue|
242
+ old_issue.slice(:requirement_name, :requiring_dep_name) ==
243
+ issue.slice(:requirement_name, :requiring_dep_name)
244
+ end
245
+ end
246
+ end
247
+
248
+ def satisfying_versions
249
+ latest_version_finder(dependency).
250
+ possible_versions_with_details.
251
+ select do |version, details|
252
+ next false unless satisfies_peer_reqs_on_dep?(version)
253
+ next true unless details["peerDependencies"]
254
+
255
+ details["peerDependencies"].all? do |dep, req|
256
+ dep = top_level_dependencies.find { |d| d.name == dep }
257
+ next false unless dep
258
+ next git_dependency?(dep) if req.include?("/")
259
+
260
+ reqs = requirement_class.requirements_array(req)
261
+ next false unless version_for_dependency(dep)
262
+
263
+ reqs.any? { |r| r.satisfied_by?(version_for_dependency(dep)) }
264
+ rescue Gem::Requirement::BadRequirementError
265
+ false
266
+ end
267
+ end.
268
+ map(&:first)
269
+ end
270
+
271
+ def satisfies_peer_reqs_on_dep?(version)
272
+ newly_broken_peer_reqs_on_dep.all? do |peer_req|
273
+ req = peer_req.fetch(:requirement_version)
274
+
275
+ # Git requirements can't be satisfied by a version
276
+ next false if req.include?("/")
277
+
278
+ reqs = requirement_class.requirements_array(req)
279
+ reqs.any? { |r| r.satisfied_by?(version) }
280
+ end
281
+ end
282
+
283
+ def latest_version_of_dep_with_satisfied_peer_reqs(dep)
284
+ latest_version_finder(dep).
285
+ possible_versions_with_details.
286
+ find do |version, details|
287
+ next false unless version > version_class.new(dep.version)
288
+ next true unless details["peerDependencies"]
289
+
290
+ details["peerDependencies"].all? do |peer_dep_name, req|
291
+ # Can't handle multiple peer dependencies
292
+ next false unless peer_dep_name == dependency.name
293
+ next git_dependency?(dependency) if req.include?("/")
294
+
295
+ reqs = requirement_class.requirements_array(req)
296
+
297
+ reqs.any? { |r| r.satisfied_by?(latest_allowable_version) }
298
+ end
299
+ end&.
300
+ first
301
+ end
302
+
303
+ def git_dependency?(dep)
304
+ GitCommitChecker.
305
+ new(dependency: dep, credentials: credentials).
306
+ git_dependency?
307
+ end
308
+
309
+ def newly_broken_peer_reqs_on_dep
310
+ relevant_unmet_peer_dependencies.
311
+ select { |dep| dep[:requirement_name] == dependency.name }
312
+ end
313
+
314
+ def newly_broken_peer_reqs_from_dep
315
+ relevant_unmet_peer_dependencies.
316
+ select { |dep| dep[:requiring_dep_name] == dependency.name }
317
+ end
318
+
319
+ def run_checker(path:, version:)
320
+ if [*package_locks, *shrinkwraps].any?
321
+ run_npm_checker(path: path, version: version)
322
+ end
323
+
324
+ run_yarn_checker(path: path, version: version) if yarn_locks.any?
325
+ run_yarn_checker(path: path, version: version) if lockfiles.none?
326
+ end
327
+
328
+ def run_yarn_checker(path:, version:)
329
+ SharedHelpers.with_git_configured(credentials: credentials) do
330
+ Dir.chdir(path) do
331
+ SharedHelpers.run_helper_subprocess(
332
+ command: "node #{yarn_helper_path}",
333
+ function: "checkPeerDependencies",
334
+ args: [
335
+ Dir.pwd,
336
+ dependency.name,
337
+ version,
338
+ requirements_for_path(dependency.requirements, path)
339
+ ]
340
+ )
341
+ end
342
+ end
343
+ end
344
+
345
+ def run_npm_checker(path:, version:)
346
+ SharedHelpers.with_git_configured(credentials: credentials) do
347
+ Dir.chdir(path) do
348
+ SharedHelpers.run_helper_subprocess(
349
+ command: "node #{npm_helper_path}",
350
+ function: "checkPeerDependencies",
351
+ args: [
352
+ Dir.pwd,
353
+ dependency.name,
354
+ version,
355
+ requirements_for_path(dependency.requirements, path),
356
+ top_level_dependencies.map(&:to_h)
357
+ ]
358
+ )
359
+ end
360
+ end
361
+ end
362
+
363
+ def requirements_for_path(requirements, path)
364
+ return requirements if path.to_s == "."
365
+
366
+ requirements.map do |r|
367
+ next unless r[:file].start_with?("#{path}/")
368
+
369
+ r.merge(file: r[:file].gsub(/^#{Regexp.quote("#{path}/")}/, ""))
370
+ end.compact
371
+ end
372
+
373
+ def write_temporary_dependency_files
374
+ write_lock_files
375
+
376
+ File.write(".npmrc", npmrc_content)
377
+
378
+ package_files.each do |file|
379
+ path = file.name
380
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
381
+ File.write(file.name, prepared_package_json_content(file))
382
+ end
383
+ end
384
+
385
+ def write_lock_files
386
+ yarn_locks.each do |f|
387
+ FileUtils.mkdir_p(Pathname.new(f.name).dirname)
388
+ File.write(f.name, prepared_yarn_lockfile_content(f.content))
389
+ end
390
+
391
+ package_locks.each do |f|
392
+ FileUtils.mkdir_p(Pathname.new(f.name).dirname)
393
+ File.write(f.name, f.content)
394
+ end
395
+
396
+ shrinkwraps.each do |f|
397
+ FileUtils.mkdir_p(Pathname.new(f.name).dirname)
398
+ File.write(f.name, f.content)
399
+ end
400
+ end
401
+
402
+ def prepared_yarn_lockfile_content(content)
403
+ content.gsub(/^#{Regexp.quote(dependency.name)}\@.*?\n\n/m, "")
404
+ end
405
+
406
+ def prepared_package_json_content(file)
407
+ NpmAndYarn::FileUpdater::PackageJsonPreparer.new(
408
+ package_json_content: file.content
409
+ ).prepared_content
410
+ end
411
+
412
+ def npmrc_content
413
+ NpmAndYarn::FileUpdater::NpmrcBuilder.new(
414
+ credentials: credentials,
415
+ dependency_files: dependency_files
416
+ ).npmrc_content
417
+ end
418
+
419
+ # Top level dependecies are required in the peer dep checker
420
+ # to fetch the manifests for all top level deps which may contain
421
+ # "peerDependency" requirements
422
+ def top_level_dependencies
423
+ @top_level_dependencies ||= NpmAndYarn::FileParser.new(
424
+ dependency_files: dependency_files,
425
+ source: nil,
426
+ credentials: credentials
427
+ ).parse.select(&:top_level?)
428
+ end
429
+
430
+ def lockfiles
431
+ [*yarn_locks, *package_locks, *shrinkwraps]
432
+ end
433
+
434
+ def package_locks
435
+ @package_locks ||=
436
+ dependency_files.
437
+ select { |f| f.name.end_with?("package-lock.json") }
438
+ end
439
+
440
+ def yarn_locks
441
+ @yarn_locks ||=
442
+ dependency_files.
443
+ select { |f| f.name.end_with?("yarn.lock") }
444
+ end
445
+
446
+ def shrinkwraps
447
+ @shrinkwraps ||=
448
+ dependency_files.
449
+ select { |f| f.name.end_with?("npm-shrinkwrap.json") }
450
+ end
451
+
452
+ def package_files
453
+ @package_files ||=
454
+ dependency_files.
455
+ select { |f| f.name.end_with?("package.json") }
456
+ end
457
+
458
+ def yarn_helper_path
459
+ NativeHelpers.yarn_helper_path
460
+ end
461
+
462
+ def npm_helper_path
463
+ NativeHelpers.npm_helper_path
464
+ end
465
+
466
+ def version_for_dependency(dep)
467
+ if dep.version && version_class.correct?(dep.version)
468
+ return version_class.new(dep.version)
469
+ end
470
+
471
+ dep.requirements.map { |r| r[:requirement] }.compact.
472
+ reject { |req_string| req_string.start_with?("<") }.
473
+ select { |req_string| req_string.match?(version_regex) }.
474
+ map { |req_string| req_string.match(version_regex) }.
475
+ select { |version| version_class.correct?(version.to_s) }.
476
+ map { |version| version_class.new(version.to_s) }.
477
+ max
478
+ end
479
+
480
+ def version_class
481
+ NpmAndYarn::Version
482
+ end
483
+
484
+ def requirement_class
485
+ NpmAndYarn::Requirement
486
+ end
487
+
488
+ def version_regex
489
+ version_class::VERSION_PATTERN
490
+ end
491
+ end
492
+ end
493
+ end
494
+ end
495
+ # rubocop:enable Metrics/ClassLength