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,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/npm_and_yarn/update_checker"
5
+ require "dependabot/npm_and_yarn/update_checker/registry_finder"
6
+ require "dependabot/npm_and_yarn/version"
7
+ require "dependabot/npm_and_yarn/requirement"
8
+ require "dependabot/shared_helpers"
9
+ require "dependabot/errors"
10
+
11
+ module Dependabot
12
+ module NpmAndYarn
13
+ class UpdateChecker
14
+ class LatestVersionFinder
15
+ class RegistryError < StandardError; end
16
+
17
+ def initialize(dependency:, credentials:, dependency_files:,
18
+ ignored_versions:)
19
+ @dependency = dependency
20
+ @credentials = credentials
21
+ @dependency_files = dependency_files
22
+ @ignored_versions = ignored_versions
23
+ end
24
+
25
+ def latest_version_details_from_registry
26
+ return nil unless npm_details&.fetch("dist-tags", nil)
27
+
28
+ dist_tag_version = version_from_dist_tags(npm_details)
29
+ return { version: dist_tag_version } if dist_tag_version
30
+ return nil if specified_dist_tag_requirement?
31
+
32
+ { version: version_from_versions_array }
33
+ rescue Excon::Error::Socket, Excon::Error::Timeout
34
+ raise if dependency_registry == "registry.npmjs.org"
35
+ # Custom registries can be flaky. We don't want to make that
36
+ # our problem, so we quietly return `nil` here.
37
+ end
38
+
39
+ def latest_resolvable_version_with_no_unlock
40
+ return unless npm_details
41
+
42
+ if specified_dist_tag_requirement?
43
+ return version_from_dist_tags(npm_details)
44
+ end
45
+
46
+ reqs = dependency.requirements.map do |r|
47
+ NpmAndYarn::Requirement.
48
+ requirements_array(r.fetch(:requirement))
49
+ end.compact
50
+
51
+ possible_versions.
52
+ find do |version|
53
+ reqs.all? { |r| r.any? { |opt| opt.satisfied_by?(version) } } &&
54
+ !yanked?(version)
55
+ end
56
+ rescue Excon::Error::Socket, Excon::Error::Timeout
57
+ raise if dependency_registry == "registry.npmjs.org"
58
+ # Sometimes custom registries are flaky. We don't want to make that
59
+ # our problem, so we quietly return `nil` here.
60
+ end
61
+
62
+ def possible_versions
63
+ npm_details.fetch("versions", {}).
64
+ reject { |_, details| details["deprecated"] }.
65
+ keys.map { |v| version_class.new(v) }.
66
+ reject { |v| v.prerelease? && !related_to_current_pre?(v) }.
67
+ reject { |v| ignore_reqs.any? { |r| r.satisfied_by?(v) } }.
68
+ sort.reverse
69
+ end
70
+
71
+ def possible_versions_with_details
72
+ npm_details.fetch("versions", {}).
73
+ reject { |_, details| details["deprecated"] }.
74
+ transform_keys { |k| version_class.new(k) }.
75
+ reject { |k, _| k.prerelease? && !related_to_current_pre?(k) }.
76
+ reject { |k, _| ignore_reqs.any? { |r| r.satisfied_by?(k) } }.
77
+ sort_by(&:first).reverse
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :dependency, :credentials, :dependency_files,
83
+ :ignored_versions
84
+
85
+ def version_from_dist_tags(npm_details)
86
+ dist_tags = npm_details["dist-tags"].keys
87
+
88
+ # Check if a dist tag was specified as a requirement. If it was, and
89
+ # it exists, use it.
90
+ dist_tag_req = dependency.requirements.
91
+ find { |r| dist_tags.include?(r[:requirement]) }&.
92
+ fetch(:requirement)
93
+
94
+ if dist_tag_req
95
+ tag_vers =
96
+ version_class.new(npm_details["dist-tags"][dist_tag_req])
97
+ return tag_vers unless yanked?(tag_vers)
98
+ end
99
+
100
+ # Use the latest dist tag unless there's a reason not to
101
+ return nil unless npm_details["dist-tags"]["latest"]
102
+
103
+ latest = version_class.new(npm_details["dist-tags"]["latest"])
104
+
105
+ wants_latest_dist_tag?(latest) ? latest : nil
106
+ end
107
+
108
+ def related_to_current_pre?(version)
109
+ current_version = dependency.version
110
+ if current_version &&
111
+ version_class.correct?(current_version) &&
112
+ version_class.new(current_version).prerelease? &&
113
+ version_class.new(current_version).release == version.release
114
+ return true
115
+ end
116
+
117
+ dependency.requirements.any? do |req|
118
+ next unless req[:requirement]&.match?(/\d-[A-Za-z]/)
119
+
120
+ NpmAndYarn::Requirement.
121
+ requirements_array(req.fetch(:requirement)).
122
+ any? do |r|
123
+ r.requirements.any? { |a| a.last.release == version.release }
124
+ end
125
+ rescue Gem::Requirement::BadRequirementError
126
+ false
127
+ end
128
+ end
129
+
130
+ def specified_dist_tag_requirement?
131
+ dependency.requirements.any? do |req|
132
+ next false if req[:requirement].nil?
133
+
134
+ req[:requirement].match?(/^[A-Za-z]/)
135
+ end
136
+ end
137
+
138
+ def wants_latest_dist_tag?(latest_version)
139
+ ver = latest_version
140
+ return false if related_to_current_pre?(ver) ^ ver.prerelease?
141
+ return false if current_version_greater_than?(ver)
142
+ return false if current_requirement_greater_than?(ver)
143
+ return false if ignore_reqs.any? { |r| r.satisfied_by?(ver) }
144
+ return false if yanked?(ver)
145
+
146
+ true
147
+ end
148
+
149
+ def current_version_greater_than?(version)
150
+ return false unless dependency.version
151
+ return false unless version_class.correct?(dependency.version)
152
+
153
+ version_class.new(dependency.version) > version
154
+ end
155
+
156
+ def current_requirement_greater_than?(version)
157
+ dependency.requirements.any? do |req|
158
+ next false unless req[:requirement]
159
+
160
+ req_version = req[:requirement].sub(/^\^|~|>=?/, "")
161
+ next false unless version_class.correct?(req_version)
162
+
163
+ version_class.new(req_version) > version
164
+ end
165
+ end
166
+
167
+ def version_from_versions_array
168
+ possible_versions.find { |version| !yanked?(version) }
169
+ end
170
+
171
+ def yanked?(version)
172
+ @yanked ||= {}
173
+ return @yanked[version] if @yanked.key?(version)
174
+
175
+ @yanked[version] =
176
+ begin
177
+ version_not_found =
178
+ Excon.get(
179
+ dependency_url + "/#{version}",
180
+ SharedHelpers.excon_defaults.merge(
181
+ headers: registry_auth_headers,
182
+ idempotent: true
183
+ )
184
+ ).status == 404
185
+ version_not_found && version_endpoint_working?
186
+ rescue Excon::Error::Timeout
187
+ # Give the benefit of the doubt if the registry is playing up
188
+ false
189
+ end
190
+ end
191
+
192
+ def version_endpoint_working?
193
+ return true if dependency_registry == "registry.npmjs.org"
194
+
195
+ if defined?(@version_endpoint_working)
196
+ return @version_endpoint_working
197
+ end
198
+
199
+ @version_endpoint_working =
200
+ begin
201
+ Excon.get(
202
+ dependency_url + "/latest",
203
+ SharedHelpers.excon_defaults.merge(
204
+ headers: registry_auth_headers,
205
+ idempotent: true
206
+ )
207
+ ).status < 400
208
+ rescue Excon::Error::Timeout
209
+ # Give the benefit of the doubt if the registry is playing up
210
+ true
211
+ end
212
+ end
213
+
214
+ def npm_details
215
+ return @npm_details if @npm_details_lookup_attempted
216
+
217
+ @npm_details_lookup_attempted = true
218
+ @npm_details ||=
219
+ begin
220
+ npm_response = fetch_npm_response
221
+
222
+ check_npm_response(npm_response)
223
+ JSON.parse(npm_response.body)
224
+ rescue JSON::ParserError, Excon::Error::Timeout,
225
+ RegistryError => error
226
+ retry_count ||= 0
227
+ retry_count += 1
228
+ raise_npm_details_error(error) if retry_count > 2
229
+ sleep(rand(3.0..10.0)) && retry
230
+ end
231
+ end
232
+
233
+ def fetch_npm_response
234
+ response = Excon.get(
235
+ dependency_url,
236
+ SharedHelpers.excon_defaults.merge(
237
+ headers: registry_auth_headers,
238
+ idempotent: true
239
+ )
240
+ )
241
+
242
+ return response unless response.status == 500
243
+ return response unless registry_auth_headers["Authorization"]
244
+
245
+ auth = registry_auth_headers["Authorization"]
246
+ return response unless auth.start_with?("Basic")
247
+
248
+ decoded_token = Base64.decode64(auth.gsub("Basic ", ""))
249
+ return unless decoded_token.include?(":")
250
+
251
+ username, password = decoded_token.split(":")
252
+ Excon.get(
253
+ dependency_url,
254
+ SharedHelpers.excon_defaults.merge(
255
+ user: username,
256
+ password: password,
257
+ idempotent: true
258
+ )
259
+ )
260
+ end
261
+
262
+ def check_npm_response(npm_response)
263
+ return if git_dependency?
264
+
265
+ if private_dependency_not_reachable?(npm_response)
266
+ raise PrivateSourceAuthenticationFailure, dependency_registry
267
+ end
268
+
269
+ status = npm_response.status
270
+ return if status.to_s.start_with?("2")
271
+
272
+ # Ignore 404s from the registry for updates where a lockfile doesn't
273
+ # need to be generated. The 404 won't cause problems later.
274
+ return if status == 404 && dependency.version.nil?
275
+
276
+ msg = "Got #{status} response with body #{npm_response.body}"
277
+ raise RegistryError, msg
278
+ end
279
+
280
+ def raise_npm_details_error(error)
281
+ raise if dependency_registry == "registry.npmjs.org"
282
+ raise unless error.is_a?(Excon::Error::Timeout)
283
+
284
+ raise PrivateSourceTimedOut, dependency_registry
285
+ end
286
+
287
+ def private_dependency_not_reachable?(npm_response)
288
+ # Check whether this dependency is (likely to be) private
289
+ if dependency_registry == "registry.npmjs.org" &&
290
+ !dependency.name.start_with?("@")
291
+ return false
292
+ end
293
+
294
+ [401, 402, 403, 404].include?(npm_response.status)
295
+ end
296
+
297
+ def dependency_url
298
+ registry_finder.dependency_url
299
+ end
300
+
301
+ def dependency_registry
302
+ registry_finder.registry
303
+ end
304
+
305
+ def registry_auth_headers
306
+ registry_finder.auth_headers
307
+ end
308
+
309
+ def registry_finder
310
+ @registry_finder ||=
311
+ RegistryFinder.new(
312
+ dependency: dependency,
313
+ credentials: credentials,
314
+ npmrc_file: dependency_files.
315
+ find { |f| f.name.end_with?(".npmrc") },
316
+ yarnrc_file: dependency_files.
317
+ find { |f| f.name.end_with?(".yarnrc") }
318
+ )
319
+ end
320
+
321
+ def ignore_reqs
322
+ ignored_versions.
323
+ map { |req| NpmAndYarn::Requirement.new(req.split(",")) }
324
+ end
325
+
326
+ def version_class
327
+ NpmAndYarn::Version
328
+ end
329
+
330
+ # TODO: Remove need for me
331
+ def git_dependency?
332
+ GitCommitChecker.new(
333
+ dependency: dependency,
334
+ credentials: credentials
335
+ ).git_dependency?
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/npm_and_yarn/update_checker"
5
+ require "dependabot/shared_helpers"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ class UpdateChecker
10
+ class LibraryDetector
11
+ def initialize(package_json_file:)
12
+ @package_json_file = package_json_file
13
+ end
14
+
15
+ def library?
16
+ return false unless package_json_may_be_for_library?
17
+
18
+ npm_response_matches_package_json?
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :package_json_file
24
+
25
+ def package_json_may_be_for_library?
26
+ return false unless project_name
27
+ return false if project_name.match?(/\{\{.*\}\}/)
28
+ return false unless parsed_package_json["version"]
29
+ return false if parsed_package_json["private"]
30
+
31
+ true
32
+ end
33
+
34
+ def npm_response_matches_package_json?
35
+ project_description = parsed_package_json["description"]
36
+ return false unless project_description
37
+
38
+ # Check if the project is listed on npm. If it is, it's a library
39
+ @project_npm_response ||= Excon.get(
40
+ "https://registry.npmjs.org/#{escaped_project_name}",
41
+ idempotent: true,
42
+ **SharedHelpers.excon_defaults
43
+ )
44
+
45
+ return false unless @project_npm_response.status == 200
46
+
47
+ @project_npm_response.body.force_encoding("UTF-8").encode.
48
+ include?(project_description)
49
+ rescue Excon::Error::Socket, Excon::Error::Timeout
50
+ false
51
+ end
52
+
53
+ def project_name
54
+ parsed_package_json.fetch("name", nil)
55
+ end
56
+
57
+ def escaped_project_name
58
+ project_name&.gsub("/", "%2F")
59
+ end
60
+
61
+ def parsed_package_json
62
+ @parsed_package_json ||= JSON.parse(package_json_file.content)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/npm_and_yarn/update_checker"
5
+ require "dependabot/shared_helpers"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ class UpdateChecker
10
+ class RegistryFinder
11
+ NPM_AUTH_TOKEN_REGEX =
12
+ %r{//(?<registry>.*)/:_authToken=(?<token>.*)$}.freeze
13
+ NPM_GLOBAL_REGISTRY_REGEX =
14
+ /^registry\s*=\s*(?<registry>.*)$/.freeze
15
+ YARN_GLOBAL_REGISTRY_REGEX =
16
+ /^registry\s+['"](?<registry>.*)['"]/.freeze
17
+
18
+ def initialize(dependency:, credentials:, npmrc_file: nil,
19
+ yarnrc_file: nil)
20
+ @dependency = dependency
21
+ @credentials = credentials
22
+ @npmrc_file = npmrc_file
23
+ @yarnrc_file = yarnrc_file
24
+ end
25
+
26
+ def registry
27
+ locked_registry || first_registry_with_dependency_details
28
+ end
29
+
30
+ def auth_headers
31
+ auth_header_for(auth_token)
32
+ end
33
+
34
+ def dependency_url
35
+ "#{registry_url.gsub(%r{/+$}, '')}/#{escaped_dependency_name}"
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :dependency, :credentials, :npmrc_file, :yarnrc_file
41
+
42
+ def first_registry_with_dependency_details
43
+ @first_registry_with_dependency_details ||=
44
+ known_registries.find do |details|
45
+ Excon.get(
46
+ "https://#{details['registry'].gsub(%r{/+$}, '')}/"\
47
+ "#{escaped_dependency_name}",
48
+ headers: auth_header_for(details["token"]),
49
+ idempotent: true,
50
+ **SharedHelpers.excon_defaults
51
+ ).status < 400
52
+ rescue Excon::Error::Timeout, Excon::Error::Socket
53
+ nil
54
+ end&.fetch("registry")
55
+
56
+ @first_registry_with_dependency_details ||= global_registry
57
+ end
58
+
59
+ def registry_url
60
+ protocol =
61
+ if private_registry_source_url
62
+ private_registry_source_url.split("://").first
63
+ else
64
+ "https"
65
+ end
66
+
67
+ "#{protocol}://#{registry}"
68
+ end
69
+
70
+ def auth_header_for(token)
71
+ return {} unless token
72
+
73
+ if token.include?(":")
74
+ encoded_token = Base64.encode64(token).delete("\n")
75
+ { "Authorization" => "Basic #{encoded_token}" }
76
+ elsif Base64.decode64(token).ascii_only? &&
77
+ Base64.decode64(token).include?(":")
78
+ { "Authorization" => "Basic #{token.delete("\n")}" }
79
+ else
80
+ { "Authorization" => "Bearer #{token}" }
81
+ end
82
+ end
83
+
84
+ def auth_token
85
+ known_registries.
86
+ find { |cred| cred["registry"] == registry }&.
87
+ fetch("token")
88
+ end
89
+
90
+ def locked_registry
91
+ return unless private_registry_source_url
92
+
93
+ lockfile_registry =
94
+ private_registry_source_url.
95
+ gsub("https://", "").
96
+ gsub("http://", "")
97
+ detailed_registry =
98
+ known_registries.
99
+ find { |h| h["registry"].include?(lockfile_registry) }&.
100
+ fetch("registry")
101
+
102
+ detailed_registry || lockfile_registry
103
+ end
104
+
105
+ def known_registries
106
+ @known_registries ||=
107
+ begin
108
+ registries = []
109
+ registries += credentials.
110
+ select { |cred| cred["type"] == "npm_registry" }
111
+ registries += npmrc_registries
112
+ registries += yarnrc_registries
113
+
114
+ unique_registries(registries)
115
+ end
116
+ end
117
+
118
+ def npmrc_registries
119
+ return [] unless npmrc_file
120
+
121
+ registries = []
122
+ npmrc_file.content.scan(NPM_AUTH_TOKEN_REGEX) do
123
+ next if Regexp.last_match[:registry].include?("${")
124
+
125
+ registries << {
126
+ "type" => "npm_registry",
127
+ "registry" => Regexp.last_match[:registry],
128
+ "token" => Regexp.last_match[:token]
129
+ }
130
+ end
131
+
132
+ npmrc_file.content.scan(NPM_GLOBAL_REGISTRY_REGEX) do
133
+ next if Regexp.last_match[:registry].include?("${")
134
+
135
+ registry = Regexp.last_match[:registry].strip.
136
+ sub(%r{/+$}, "").
137
+ sub(%r{^.*?//}, "")
138
+ next if registries.map { |r| r["registry"] }.include?(registry)
139
+
140
+ registries << {
141
+ "type" => "npm_registry",
142
+ "registry" => registry,
143
+ "token" => nil
144
+ }
145
+ end
146
+
147
+ registries
148
+ end
149
+
150
+ def yarnrc_registries
151
+ return [] unless yarnrc_file
152
+
153
+ registries = []
154
+ yarnrc_file.content.scan(YARN_GLOBAL_REGISTRY_REGEX) do
155
+ next if Regexp.last_match[:registry].include?("${")
156
+
157
+ registry = Regexp.last_match[:registry].strip.
158
+ sub(%r{/+$}, "").
159
+ sub(%r{^.*?//}, "")
160
+ registries << {
161
+ "type" => "npm_registry",
162
+ "registry" => registry,
163
+ "token" => nil
164
+ }
165
+ end
166
+
167
+ registries
168
+ end
169
+
170
+ def unique_registries(registries)
171
+ registries.uniq.reject do |registry|
172
+ next if registry["token"]
173
+
174
+ # Reject this entry if an identical one with a token exists
175
+ registries.any? do |r|
176
+ r["token"] && r["registry"] == registry["registry"]
177
+ end
178
+ end
179
+ end
180
+
181
+ def global_registry
182
+ npmrc_file&.content.to_s.scan(NPM_GLOBAL_REGISTRY_REGEX) do
183
+ next if Regexp.last_match[:registry].include?("${")
184
+
185
+ registry = Regexp.last_match[:registry].strip.
186
+ sub(%r{/+$}, "").
187
+ sub(%r{^.*?//}, "")
188
+ return registry
189
+ end
190
+
191
+ yarnrc_file&.content.to_s.scan(YARN_GLOBAL_REGISTRY_REGEX) do
192
+ next if Regexp.last_match[:registry].include?("${")
193
+
194
+ registry = Regexp.last_match[:registry].strip.
195
+ sub(%r{/+$}, "").
196
+ sub(%r{^.*?//}, "")
197
+ return registry
198
+ end
199
+
200
+ "registry.npmjs.org"
201
+ end
202
+
203
+ # npm registries expect slashes to be escaped
204
+ def escaped_dependency_name
205
+ dependency.name.gsub("/", "%2F")
206
+ end
207
+
208
+ def private_registry_source_url
209
+ sources = dependency.requirements.
210
+ map { |r| r.fetch(:source) }.uniq.compact
211
+
212
+ # If there are multiple source types, or multiple source URLs, then
213
+ # it's unclear how we should proceed
214
+ if sources.map { |s| [s[:type], s[:url]] }.uniq.count > 1
215
+ raise "Multiple sources! #{sources.join(', ')}"
216
+ end
217
+
218
+ # Otherwise we just take the URL of the first private registry
219
+ sources.find { |s| s[:type] == "private_registry" }&.fetch(:url)
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end