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,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+
6
+ module Dependabot
7
+ module NpmAndYarn
8
+ class FileUpdater < Dependabot::FileUpdaters::Base
9
+ require_relative "file_updater/package_json_updater"
10
+ require_relative "file_updater/npm_lockfile_updater"
11
+ require_relative "file_updater/yarn_lockfile_updater"
12
+
13
+ class NoChangeError < StandardError
14
+ def initialize(message:, error_context:)
15
+ super(message)
16
+ @error_context = error_context
17
+ end
18
+
19
+ def raven_context
20
+ { extra: @error_context }
21
+ end
22
+ end
23
+
24
+ def self.updated_files_regex
25
+ [
26
+ /^package\.json$/,
27
+ /^package-lock\.json$/,
28
+ /^npm-shrinkwrap\.json$/,
29
+ /^yarn\.lock$/
30
+ ]
31
+ end
32
+
33
+ def updated_dependency_files
34
+ updated_files = []
35
+
36
+ updated_files += updated_manifest_files
37
+ updated_files += updated_lockfiles
38
+
39
+ if updated_files.none?
40
+ raise NoChangeError.new(
41
+ message: "No files where updated!",
42
+ error_context: error_context(updated_files: updated_files)
43
+ )
44
+ end
45
+
46
+ if updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
47
+ raise NoChangeError.new(
48
+ message: "Updated files are unchanged!",
49
+ error_context: error_context(updated_files: updated_files)
50
+ )
51
+ end
52
+
53
+ updated_files
54
+ end
55
+
56
+ private
57
+
58
+ def check_required_files
59
+ raise "No package.json!" unless get_original_file("package.json")
60
+ end
61
+
62
+ def error_context(updated_files:)
63
+ {
64
+ dependencies: dependencies.map(&:to_h),
65
+ updated_files: updated_files.map(&:name),
66
+ dependency_files: dependency_files.map(&:name)
67
+ }
68
+ end
69
+
70
+ def package_locks
71
+ @package_locks ||=
72
+ dependency_files.
73
+ select { |f| f.name.end_with?("package-lock.json") }
74
+ end
75
+
76
+ def yarn_locks
77
+ @yarn_locks ||=
78
+ dependency_files.
79
+ select { |f| f.name.end_with?("yarn.lock") }
80
+ end
81
+
82
+ def shrinkwraps
83
+ @shrinkwraps ||=
84
+ dependency_files.
85
+ select { |f| f.name.end_with?("npm-shrinkwrap.json") }
86
+ end
87
+
88
+ def package_files
89
+ dependency_files.select { |f| f.name.end_with?("package.json") }
90
+ end
91
+
92
+ def yarn_lock_changed?(yarn_lock)
93
+ yarn_lock.content != updated_yarn_lock_content(yarn_lock)
94
+ end
95
+
96
+ def package_lock_changed?(package_lock)
97
+ package_lock.content != updated_package_lock_content(package_lock)
98
+ end
99
+
100
+ def shrinkwrap_changed?(shrinkwrap)
101
+ shrinkwrap.content != updated_package_lock_content(shrinkwrap)
102
+ end
103
+
104
+ def updated_manifest_files
105
+ package_files.map do |file|
106
+ updated_content = updated_package_json_content(file)
107
+ next if updated_content == file.content
108
+
109
+ updated_file(file: file, content: updated_content)
110
+ end.compact
111
+ end
112
+
113
+ def updated_lockfiles
114
+ updated_files = []
115
+
116
+ yarn_locks.each do |yarn_lock|
117
+ next unless yarn_lock_changed?(yarn_lock)
118
+
119
+ updated_files << updated_file(
120
+ file: yarn_lock,
121
+ content: updated_yarn_lock_content(yarn_lock)
122
+ )
123
+ end
124
+
125
+ package_locks.each do |package_lock|
126
+ next unless package_lock_changed?(package_lock)
127
+
128
+ updated_files << updated_file(
129
+ file: package_lock,
130
+ content: updated_package_lock_content(package_lock)
131
+ )
132
+ end
133
+
134
+ shrinkwraps.each do |shrinkwrap|
135
+ next unless shrinkwrap_changed?(shrinkwrap)
136
+
137
+ updated_files << updated_file(
138
+ file: shrinkwrap,
139
+ content: updated_shrinkwrap_content(shrinkwrap)
140
+ )
141
+ end
142
+
143
+ updated_files
144
+ end
145
+
146
+ def updated_yarn_lock_content(yarn_lock)
147
+ yarn_lockfile_updater.updated_yarn_lock_content(yarn_lock)
148
+ end
149
+
150
+ def yarn_lockfile_updater
151
+ @yarn_lockfile_updater ||=
152
+ YarnLockfileUpdater.new(
153
+ dependencies: dependencies,
154
+ dependency_files: dependency_files,
155
+ credentials: credentials
156
+ )
157
+ end
158
+
159
+ def updated_package_lock_content(package_lock)
160
+ npm_lockfile_updater.updated_lockfile_content(package_lock)
161
+ end
162
+
163
+ def updated_shrinkwrap_content(shrinkwrap)
164
+ npm_lockfile_updater.updated_lockfile_content(shrinkwrap)
165
+ end
166
+
167
+ def npm_lockfile_updater
168
+ @npm_lockfile_updater ||=
169
+ NpmLockfileUpdater.new(
170
+ dependencies: dependencies,
171
+ dependency_files: dependency_files,
172
+ credentials: credentials
173
+ )
174
+ end
175
+
176
+ def updated_package_json_content(file)
177
+ @updated_package_json_content ||= {}
178
+ @updated_package_json_content[file.name] ||=
179
+ PackageJsonUpdater.new(
180
+ package_json: file,
181
+ dependencies: dependencies
182
+ ).updated_package_json.content
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ Dependabot::FileUpdaters.
189
+ register("npm_and_yarn", Dependabot::NpmAndYarn::FileUpdater)
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "time"
5
+
6
+ require "dependabot/metadata_finders"
7
+ require "dependabot/metadata_finders/base"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/npm_and_yarn/version"
10
+
11
+ module Dependabot
12
+ module NpmAndYarn
13
+ class MetadataFinder < Dependabot::MetadataFinders::Base
14
+ def homepage_url
15
+ # Attempt to use version_listing first, as fetching the entire listing
16
+ # array can be slow (if it's large)
17
+ if latest_version_listing["homepage"]
18
+ return latest_version_listing["homepage"]
19
+ end
20
+
21
+ listing = all_version_listings.find { |_, l| l["homepage"] }
22
+ listing&.last&.fetch("homepage", nil) || super
23
+ end
24
+
25
+ def maintainer_changes
26
+ return unless npm_releaser
27
+ return unless npm_listing.dig("time", dependency.version)
28
+ return if previous_releasers.include?(npm_releaser)
29
+
30
+ "This version was pushed to npm by "\
31
+ "[#{npm_releaser}](https://www.npmjs.com/~#{npm_releaser}), a new "\
32
+ "releaser for #{dependency.name} since your current version."
33
+ end
34
+
35
+ private
36
+
37
+ def look_up_source
38
+ return find_source_from_registry if new_source.nil?
39
+
40
+ source_type = new_source[:type] || new_source.fetch("type")
41
+
42
+ case source_type
43
+ when "git" then find_source_from_git_url
44
+ when "private_registry" then find_source_from_registry
45
+ else raise "Unexpected source type: #{source_type}"
46
+ end
47
+ end
48
+
49
+ def npm_releaser
50
+ all_version_listings.
51
+ find { |v, _| v == dependency.version }&.
52
+ last&.fetch("_npmUser", nil)&.fetch("name", nil)
53
+ end
54
+
55
+ def previous_releasers
56
+ times = npm_listing.fetch("time")
57
+
58
+ cutoff =
59
+ if dependency.previous_version && times[dependency.previous_version]
60
+ Time.parse(times[dependency.previous_version])
61
+ elsif times[dependency.version]
62
+ Time.parse(times[dependency.version]) - 1
63
+ end
64
+ return unless cutoff
65
+
66
+ all_version_listings.
67
+ reject { |v, _| Time.parse(times[v]) > cutoff }.
68
+ map { |_, d| d.fetch("_npmUser", nil)&.fetch("name", nil) }.compact
69
+ end
70
+
71
+ def find_source_from_registry
72
+ # Attempt to use version_listing first, as fetching the entire listing
73
+ # array can be slow (if it's large)
74
+ potential_source_urls =
75
+ [
76
+ get_url(latest_version_listing["repository"]),
77
+ get_url(latest_version_listing["homepage"]),
78
+ get_url(latest_version_listing["bugs"])
79
+ ].compact
80
+
81
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
82
+ return Source.from_url(source_url) if Source.from_url(source_url)
83
+
84
+ potential_source_urls =
85
+ all_version_listings.flat_map do |_, listing|
86
+ [
87
+ get_url(listing["repository"]),
88
+ get_url(listing["homepage"]),
89
+ get_url(listing["bugs"])
90
+ ]
91
+ end.compact
92
+
93
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
94
+ Source.from_url(source_url)
95
+ end
96
+
97
+ def new_source
98
+ sources = dependency.requirements.
99
+ map { |r| r.fetch(:source) }.uniq.compact
100
+
101
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
102
+
103
+ sources.first
104
+ end
105
+
106
+ def get_url(details)
107
+ case details
108
+ when String then details
109
+ when Hash then details.fetch("url", nil)
110
+ end
111
+ end
112
+
113
+ def find_source_from_git_url
114
+ url = new_source[:url] || new_source.fetch("url")
115
+ Source.from_url(url)
116
+ end
117
+
118
+ def latest_version_listing
119
+ return @latest_version_listing if defined?(@latest_version_listing)
120
+
121
+ response = Excon.get(
122
+ "#{dependency_url}/latest",
123
+ headers: registry_auth_headers,
124
+ idempotent: true,
125
+ **SharedHelpers.excon_defaults
126
+ )
127
+
128
+ if response.status == 200
129
+ return @latest_version_listing = JSON.parse(response.body)
130
+ end
131
+
132
+ @latest_version_listing = {}
133
+ rescue JSON::ParserError, Excon::Error::Timeout
134
+ @latest_version_listing = {}
135
+ end
136
+
137
+ def all_version_listings
138
+ return [] if npm_listing["versions"].nil?
139
+
140
+ npm_listing["versions"].
141
+ reject { |_, details| details["deprecated"] }.
142
+ sort_by { |version, _| NpmAndYarn::Version.new(version) }.
143
+ reverse
144
+ end
145
+
146
+ def npm_listing
147
+ return @npm_listing unless @npm_listing.nil?
148
+
149
+ response = Excon.get(
150
+ dependency_url,
151
+ headers: registry_auth_headers,
152
+ idempotent: true,
153
+ **SharedHelpers.excon_defaults
154
+ )
155
+
156
+ return @npm_listing = {} if response.status >= 500
157
+
158
+ begin
159
+ @npm_listing = JSON.parse(response.body)
160
+ rescue JSON::ParserError
161
+ raise unless non_standard_registry?
162
+
163
+ @npm_listing = {}
164
+ end
165
+ rescue Excon::Error::Timeout
166
+ @npm_listing = {}
167
+ end
168
+
169
+ def dependency_url
170
+ registry_url =
171
+ if new_source.nil? then "https://registry.npmjs.org"
172
+ else new_source.fetch(:url)
173
+ end
174
+
175
+ # NPM registries expect slashes to be escaped
176
+ escaped_dependency_name = dependency.name.gsub("/", "%2F")
177
+ "#{registry_url}/#{escaped_dependency_name}"
178
+ end
179
+
180
+ def registry_auth_headers
181
+ return {} unless auth_token
182
+
183
+ { "Authorization" => "Bearer #{auth_token}" }
184
+ end
185
+
186
+ def dependency_registry
187
+ if new_source.nil? then "registry.npmjs.org"
188
+ else new_source.fetch(:url).gsub("https://", "").gsub("http://", "")
189
+ end
190
+ end
191
+
192
+ def auth_token
193
+ credentials.
194
+ select { |cred| cred["type"] == "npm_registry" }.
195
+ find { |cred| cred["registry"] == dependency_registry }&.
196
+ fetch("token")
197
+ end
198
+
199
+ def private_dependency_not_reachable?(npm_response)
200
+ # Check whether this dependency is (likely to be) private
201
+ if dependency_registry == "registry.npmjs.org" &&
202
+ !dependency.name.start_with?("@")
203
+ return false
204
+ end
205
+
206
+ [401, 403, 404].include?(npm_response.status)
207
+ end
208
+
209
+ def non_standard_registry?
210
+ dependency_registry != "registry.npmjs.org"
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ Dependabot::MetadataFinders.
217
+ register("npm_and_yarn", Dependabot::NpmAndYarn::MetadataFinder)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module NpmAndYarn
5
+ module NativeHelpers
6
+ def self.npm_helper_path
7
+ File.join(npm_helpers_dir, "bin/run.js")
8
+ end
9
+
10
+ def self.npm_helpers_dir
11
+ File.join(native_helpers_root, "npm_and_yarn/helpers/npm")
12
+ end
13
+
14
+ def self.yarn_helper_path
15
+ File.join(yarn_helpers_dir, "bin/run.js")
16
+ end
17
+
18
+ def self.yarn_helpers_dir
19
+ File.join(native_helpers_root, "npm_and_yarn/helpers/yarn")
20
+ end
21
+
22
+ def self.native_helpers_root
23
+ default_path = File.join(__dir__, "../../../..")
24
+ ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/utils"
4
+ require "dependabot/npm_and_yarn/version"
5
+
6
+ module Dependabot
7
+ module NpmAndYarn
8
+ class Requirement < Gem::Requirement
9
+ AND_SEPARATOR = /(?<=[a-zA-Z0-9*])\s+(?:&+\s+)?(?!\s*[|-])/.freeze
10
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|+/.freeze
11
+
12
+ # Override the version pattern to allow a 'v' prefix
13
+ quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
14
+ version_pattern = "v?#{Gem::Version::VERSION_PATTERN}"
15
+
16
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
17
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
18
+
19
+ def self.parse(obj)
20
+ if obj.is_a?(Gem::Version)
21
+ return ["=", NpmAndYarn::Version.new(obj.to_s)]
22
+ end
23
+
24
+ unless (matches = PATTERN.match(obj.to_s))
25
+ msg = "Illformed requirement [#{obj.inspect}]"
26
+ raise BadRequirementError, msg
27
+ end
28
+
29
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
30
+
31
+ [matches[1] || "=", NpmAndYarn::Version.new(matches[2])]
32
+ end
33
+
34
+ # Returns an array of requirements. At least one requirement from the
35
+ # returned array must be satisfied for a version to be valid.
36
+ def self.requirements_array(requirement_string)
37
+ return [new(nil)] if requirement_string.nil?
38
+
39
+ # Removing parentheses is technically wrong but they are extremely
40
+ # rarely used.
41
+ # TODO: Handle complicated parenthesised requirements
42
+ requirement_string = requirement_string.gsub(/[()]/, "")
43
+ requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
44
+ requirements = req_string.strip.split(AND_SEPARATOR)
45
+ new(requirements)
46
+ end
47
+ end
48
+
49
+ def initialize(*requirements)
50
+ requirements = requirements.flatten.flat_map do |req_string|
51
+ convert_js_constraint_to_ruby_constraint(req_string)
52
+ end
53
+
54
+ super(requirements)
55
+ end
56
+
57
+ private
58
+
59
+ # rubocop:disable Metrics/PerceivedComplexity
60
+ # rubocop:disable Metrics/CyclomaticComplexity
61
+ def convert_js_constraint_to_ruby_constraint(req_string)
62
+ return req_string if req_string.match?(/^([A-Za-uw-z]|v[^\d])/)
63
+
64
+ req_string = req_string.gsub(/(?:\.|^)[xX*]/, "")
65
+
66
+ if req_string.empty? then ">= 0"
67
+ elsif req_string.start_with?("~>") then req_string
68
+ elsif req_string.start_with?("~") then convert_tilde_req(req_string)
69
+ elsif req_string.start_with?("^") then convert_caret_req(req_string)
70
+ elsif req_string.include?(" - ") then convert_hyphen_req(req_string)
71
+ elsif req_string.match?(/[<>]/) then req_string
72
+ else ruby_range(req_string)
73
+ end
74
+ end
75
+ # rubocop:enable Metrics/PerceivedComplexity
76
+ # rubocop:enable Metrics/CyclomaticComplexity
77
+
78
+ def convert_tilde_req(req_string)
79
+ version = req_string.gsub(/^~\>?/, "")
80
+ parts = version.split(".")
81
+ parts << "0" if parts.count < 3
82
+ "~> #{parts.join('.')}"
83
+ end
84
+
85
+ def convert_hyphen_req(req_string)
86
+ lower_bound, upper_bound = req_string.split(/\s+-\s+/)
87
+ lower_bound_parts = lower_bound.split(".")
88
+ lower_bound_parts.fill("0", lower_bound_parts.length...3)
89
+
90
+ upper_bound_parts = upper_bound.split(".")
91
+ upper_bound_range =
92
+ if upper_bound_parts.length < 3
93
+ # When upper bound is a partial version treat these as an X-range
94
+ if upper_bound_parts[-1].to_i.positive?
95
+ upper_bound_parts[-1] = upper_bound_parts[-1].to_i + 1
96
+ end
97
+ upper_bound_parts.fill("0", upper_bound_parts.length...3)
98
+ "< #{upper_bound_parts.join('.')}.a"
99
+ else
100
+ "<= #{upper_bound_parts.join('.')}"
101
+ end
102
+
103
+ [">= #{lower_bound_parts.join('.')}", upper_bound_range]
104
+ end
105
+
106
+ def ruby_range(req_string)
107
+ parts = req_string.split(".")
108
+ # If we have three or more parts then this is an exact match
109
+ return req_string if parts.count >= 3
110
+
111
+ # If we have fewer than three parts we do a partial match
112
+ parts << "0"
113
+ "~> #{parts.join('.')}"
114
+ end
115
+
116
+ # rubocop:disable Metrics/PerceivedComplexity
117
+ def convert_caret_req(req_string)
118
+ version = req_string.gsub(/^\^/, "")
119
+ parts = version.split(".")
120
+ parts = parts.fill("x", parts.length...3)
121
+ first_non_zero = parts.find { |d| d != "0" }
122
+ first_non_zero_index =
123
+ first_non_zero ? parts.index(first_non_zero) : parts.count - 1
124
+ # If the requirement has a blank minor or patch version increment the
125
+ # previous index value with 1
126
+ first_non_zero_index -= 1 if first_non_zero == "x"
127
+ upper_bound = parts.map.with_index do |part, i|
128
+ if i < first_non_zero_index then part
129
+ elsif i == first_non_zero_index then (part.to_i + 1).to_s
130
+ elsif i > first_non_zero_index && i == 2 then "0.a"
131
+ else 0
132
+ end
133
+ end.join(".")
134
+
135
+ [">= #{version}", "< #{upper_bound}"]
136
+ end
137
+ # rubocop:enable Metrics/PerceivedComplexity
138
+ end
139
+ end
140
+ end
141
+
142
+ Dependabot::Utils.register_requirement_class(
143
+ "npm_and_yarn",
144
+ Dependabot::NpmAndYarn::Requirement
145
+ )