dependabot-npm_and_yarn 0.91.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/helpers/build +14 -0
- data/helpers/npm/.eslintrc +14 -0
- data/helpers/npm/bin/run.js +34 -0
- data/helpers/npm/lib/helpers.js +25 -0
- data/helpers/npm/lib/peer-dependency-checker.js +102 -0
- data/helpers/npm/lib/subdependency-updater.js +48 -0
- data/helpers/npm/lib/updater.js +101 -0
- data/helpers/npm/package-lock.json +8868 -0
- data/helpers/npm/package.json +17 -0
- data/helpers/npm/test/fixtures/npm-left-pad.json +1 -0
- data/helpers/npm/test/fixtures/updater/original/package-lock.json +16 -0
- data/helpers/npm/test/fixtures/updater/original/package.json +9 -0
- data/helpers/npm/test/fixtures/updater/updated/package-lock.json +16 -0
- data/helpers/npm/test/helpers.js +7 -0
- data/helpers/npm/test/updater.test.js +50 -0
- data/helpers/npm/yarn.lock +6176 -0
- data/helpers/yarn/.eslintrc +14 -0
- data/helpers/yarn/bin/run.js +36 -0
- data/helpers/yarn/lib/fix-duplicates.js +78 -0
- data/helpers/yarn/lib/helpers.js +5 -0
- data/helpers/yarn/lib/lockfile-parser.js +21 -0
- data/helpers/yarn/lib/peer-dependency-checker.js +130 -0
- data/helpers/yarn/lib/replace-lockfile-declaration.js +57 -0
- data/helpers/yarn/lib/subdependency-updater.js +69 -0
- data/helpers/yarn/lib/updater.js +266 -0
- data/helpers/yarn/package.json +17 -0
- data/helpers/yarn/test/fixtures/updater/original/package.json +6 -0
- data/helpers/yarn/test/fixtures/updater/original/yarn.lock +11 -0
- data/helpers/yarn/test/fixtures/updater/updated/yarn.lock +12 -0
- data/helpers/yarn/test/fixtures/updater/with-version-comments/package.json +5 -0
- data/helpers/yarn/test/fixtures/updater/with-version-comments/yarn.lock +13 -0
- data/helpers/yarn/test/fixtures/yarnpkg-is-positive.json +1 -0
- data/helpers/yarn/test/fixtures/yarnpkg-left-pad.json +1 -0
- data/helpers/yarn/test/helpers.js +7 -0
- data/helpers/yarn/test/updater.test.js +93 -0
- data/helpers/yarn/yarn.lock +4760 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb +146 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +332 -0
- data/lib/dependabot/npm_and_yarn/file_parser.rb +397 -0
- data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +527 -0
- data/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +190 -0
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_preparer.rb +87 -0
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +218 -0
- data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +471 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +189 -0
- data/lib/dependabot/npm_and_yarn/metadata_finder.rb +217 -0
- data/lib/dependabot/npm_and_yarn/native_helpers.rb +28 -0
- data/lib/dependabot/npm_and_yarn/requirement.rb +145 -0
- data/lib/dependabot/npm_and_yarn/update_checker/latest_version_finder.rb +340 -0
- data/lib/dependabot/npm_and_yarn/update_checker/library_detector.rb +67 -0
- data/lib/dependabot/npm_and_yarn/update_checker/registry_finder.rb +224 -0
- data/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +193 -0
- data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +223 -0
- data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +495 -0
- data/lib/dependabot/npm_and_yarn/update_checker.rb +282 -0
- data/lib/dependabot/npm_and_yarn/version.rb +34 -0
- data/lib/dependabot/npm_and_yarn.rb +11 -0
- 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
|
+
)
|