dependabot-npm_and_yarn 0.254.0 → 0.255.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,62 @@
1
+ const {
2
+ parseLockfile,
3
+ } = require("../../lib/pnpm");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ describe("generates an updated pnpm lock for the original file", () => {
9
+
10
+ let tempDir;
11
+ beforeEach(() => {
12
+ tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
13
+ });
14
+ afterEach(() => fs.rm(tempDir, { recursive: true }, () => {}));
15
+
16
+ function copyDependencies(sourceDir, destDir) {
17
+ const srcPnpmYaml = path.join(
18
+ __dirname,
19
+ `fixtures/parser/${sourceDir}/pnpm-lock.yaml`
20
+ );
21
+ fs.copyFileSync(srcPnpmYaml, `${destDir}/pnpm-lock.yaml`);
22
+ }
23
+
24
+ it("that contains duplicate dependencies", async () =>{
25
+ copyDependencies("no_lockfile_change", tempDir);
26
+ const result = await parseLockfile(tempDir);
27
+
28
+ expect(result.length).toEqual(398);
29
+ })
30
+
31
+ it("that contains only dev dependencies but no (prod) dependencies", async () =>{
32
+ copyDependencies("only_dev_dependencies", tempDir);
33
+ const result = await parseLockfile(tempDir);
34
+
35
+ expect(result).toEqual([
36
+ {
37
+ name: 'etag',
38
+ version: '1.8.0',
39
+ resolved: undefined,
40
+ dev: true,
41
+ specifiers: [ '^1.0.0' ],
42
+ aliased: false
43
+ }
44
+ ]);
45
+ })
46
+
47
+ it("that contains dependencies which locked to versions with peer disambiguation suffix", async () =>{
48
+ copyDependencies("peer_disambiguation", tempDir);
49
+ const result = await parseLockfile(tempDir);
50
+
51
+ expect(result.length).toEqual(122);
52
+ })
53
+
54
+ // Should have the version in the lock file.
55
+ it("that contains dependencies with an empty version", async () =>{
56
+ copyDependencies("empty_version", tempDir);
57
+ const result = await parseLockfile(tempDir);
58
+
59
+ expect(result.length).toEqual(9);
60
+ })
61
+
62
+ })
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@colend-contract-helpers",
3
+ "dependencies": {
4
+ "@commitlint/cli": "^15.0.0",
5
+ "is-positive": "^3.1.0",
6
+ "left-pad": "^1.1.3"
7
+ }
8
+ }
@@ -0,0 +1,14 @@
1
+ # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2
+ # yarn lockfile v1
3
+
4
+ "@commitlint/cli@^15.0.0":
5
+ version "15.0.0"
6
+ resolved "https://registry.yarnpkg.com/@commitlint/cli/-/cli-15.0.0.tgz#8e78e86ee2b6955c1a5d140e734a6c171ce367ee"
7
+
8
+ "is-positive@^3.1.0":
9
+ version "3.1.0"
10
+ resolved "https://registry.yarnpkg.com/is-positive/-/is-positive-3.1.0.tgz#857db584a1ba5d1cb2980527fc3b6c435d37b0fd"
11
+
12
+ "left-pad@^1.0.0":
13
+ version "1.0.0"
14
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.0.0.tgz#c84e2417581bbb8eaf2b9e3d7a122e572ab1af37"
@@ -85,4 +85,33 @@ describe("updater", () => {
85
85
  expect(error).not.toBeNull();
86
86
  }
87
87
  });
88
+
89
+ it("with a package.json which contains illegal character '@' in the name", async () => {
90
+ copyDependencies("illegal_character", tempDir);
91
+
92
+ try {
93
+ await updateDependencyFiles(tempDir, [
94
+ {
95
+ name: "@commitlint/cli",
96
+ version: "19.3.0",
97
+ requirements: [
98
+ {
99
+ requirement: "^19.3.0",
100
+ file: "package.json",
101
+ groups: ["devDependencies"],
102
+ source:
103
+ {
104
+ type: "registry",
105
+ url: "https://registry.yarnpkg.com"
106
+ }
107
+ }
108
+ ]
109
+ }
110
+ ]
111
+ );
112
+ } catch (error) {
113
+ expect(error).not.toBeNull();
114
+ expect(error.message).toEqual("package.json: Name contains illegal characters")
115
+ }
116
+ });
88
117
  });
@@ -1,6 +1,8 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  require "dependabot/npm_and_yarn/file_updater"
5
7
 
6
8
  module Dependabot
@@ -10,13 +12,25 @@ module Dependabot
10
12
  # committed .npmrc
11
13
  # We should refactor this to use UpdateChecker::RegistryFinder
12
14
  class NpmrcBuilder
13
- CENTRAL_REGISTRIES = %w(
14
- registry.npmjs.org
15
- registry.yarnpkg.com
16
- ).freeze
15
+ extend T::Sig
16
+
17
+ CENTRAL_REGISTRIES = T.let(
18
+ %w(
19
+ registry.npmjs.org
20
+ registry.yarnpkg.com
21
+ ).freeze,
22
+ T::Array[String]
23
+ )
17
24
 
18
25
  SCOPED_REGISTRY = /^\s*@(?<scope>\S+):registry\s*=\s*(?<registry>\S+)/
19
26
 
27
+ sig do
28
+ params(
29
+ dependency_files: T::Array[Dependabot::DependencyFile],
30
+ credentials: T::Array[Dependabot::Credential],
31
+ dependencies: T::Array[Dependabot::Dependency]
32
+ ).void
33
+ end
20
34
  def initialize(dependency_files:, credentials:, dependencies: [])
21
35
  @dependency_files = dependency_files
22
36
  @credentials = credentials
@@ -24,6 +38,7 @@ module Dependabot
24
38
  end
25
39
 
26
40
  # PROXY WORK
41
+ sig { returns(String) }
27
42
  def npmrc_content
28
43
  initial_content =
29
44
  if npmrc_file then complete_npmrc_from_credentials
@@ -48,6 +63,7 @@ module Dependabot
48
63
  # PROXY WORK
49
64
  # Yarn allows registries to be defined either in an .npmrc or .yarnrc
50
65
  # so we need to parse both files for registry keys
66
+ sig { returns(String) }
51
67
  def yarnrc_content
52
68
  initial_content =
53
69
  if npmrc_file then complete_yarnrc_from_credentials
@@ -61,65 +77,83 @@ module Dependabot
61
77
 
62
78
  private
63
79
 
80
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
64
81
  attr_reader :dependency_files
82
+
83
+ sig { returns(T::Array[Dependabot::Credential]) }
65
84
  attr_reader :credentials
85
+
86
+ sig { returns(T::Array[Dependabot::Dependency]) }
66
87
  attr_reader :dependencies
67
88
 
89
+ sig { returns(T.nilable(String)) }
68
90
  def build_npmrc_content_from_lockfile
69
91
  return unless yarn_lock || package_lock || shrinkwrap
70
92
  return unless global_registry
71
93
 
72
- registry = global_registry["registry"]
73
- registry = "https://#{registry}" unless registry.start_with?("http")
94
+ registry = T.must(global_registry)["registry"]
95
+ registry = "https://#{registry}" unless registry&.start_with?("http")
74
96
  "registry = #{registry}\n" \
75
97
  "#{npmrc_global_registry_auth_line}" \
76
98
  "always-auth = true"
77
99
  end
78
100
 
101
+ sig { returns(T.nilable(String)) }
79
102
  def build_yarnrc_content_from_lockfile
80
103
  return unless yarn_lock || package_lock
81
104
  return unless global_registry
82
105
 
83
- "registry \"https://#{global_registry['registry']}\"\n" \
106
+ "registry \"https://#{T.must(global_registry)['registry']}\"\n" \
84
107
  "#{yarnrc_global_registry_auth_line}" \
85
108
  "npmAlwaysAuth: true"
86
109
  end
87
110
 
88
- def global_registry # rubocop:disable Metrics/PerceivedComplexity
111
+ # rubocop:disable Metrics/PerceivedComplexity
112
+ # rubocop:disable Metrics/CyclomaticComplexity
113
+ # rubocop:disable Metrics/AbcSize
114
+ sig { returns(T.nilable(Dependabot::Credential)) }
115
+ def global_registry
89
116
  return @global_registry if defined?(@global_registry)
90
117
 
91
- @global_registry =
118
+ @global_registry = T.let(
92
119
  registry_credentials.find do |cred|
93
120
  next false if CENTRAL_REGISTRIES.include?(cred["registry"])
94
121
 
95
122
  # If all the URLs include this registry, it's global
96
- next true if dependency_urls.size.positive? && dependency_urls.all? do |url|
97
- url.include?(cred["registry"])
123
+ next true if dependency_urls&.size&.positive? && dependency_urls&.all? do |url|
124
+ url.include?(T.must(cred["registry"]))
98
125
  end
99
126
 
100
127
  # Check if this registry has already been defined in .npmrc as a scoped registry
101
- next false if npmrc_scoped_registries.any? { |sr| sr.include?(cred["registry"]) }
128
+ next false if npmrc_scoped_registries&.any? { |sr| sr.include?(T.must(cred["registry"])) }
102
129
 
103
- next false if yarnrc_scoped_registries.any? { |sr| sr.include?(cred["registry"]) }
130
+ next false if yarnrc_scoped_registries&.any? { |sr| sr.include?(T.must(cred["registry"])) }
104
131
 
105
132
  # If any unscoped URLs include this registry, assume it's global
106
133
  dependency_urls
107
- .reject { |u| u.include?("@") || u.include?("%40") }
108
- .any? { |url| url.include?(cred["registry"]) }
109
- end
134
+ &.reject { |u| u.include?("@") || u.include?("%40") }
135
+ &.any? { |url| url.include?(T.must(cred["registry"])) }
136
+ end,
137
+ T.nilable(Dependabot::Credential)
138
+ )
110
139
  end
140
+ # rubocop:enable Metrics/PerceivedComplexity
141
+ # rubocop:enable Metrics/CyclomaticComplexity
142
+ # rubocop:enable Metrics/AbcSize
111
143
 
144
+ sig { returns(String) }
112
145
  def npmrc_global_registry_auth_line
113
146
  # This token is passed in from the Dependabot Config
114
147
  # We write it to the .npmrc file so that it can be used by the VulnerabilityAuditor
115
- token = global_registry.fetch("token", nil)
148
+ token = global_registry&.fetch("token", nil)
116
149
  return "" unless token
117
150
 
118
- auth_line(token, global_registry.fetch("registry")) + "\n"
151
+ auth_line(token, global_registry&.fetch("registry")) + "\n"
119
152
  end
120
153
 
154
+ sig { returns(String) }
121
155
  def yarnrc_global_registry_auth_line
122
- token = global_registry.fetch("token", nil)
156
+ token = global_registry&.fetch("token", nil)
123
157
  return "" unless token
124
158
 
125
159
  if token.include?(":")
@@ -133,6 +167,8 @@ module Dependabot
133
167
  end
134
168
  end
135
169
 
170
+ # rubocop:disable Metrics/AbcSize
171
+ sig { returns(T.nilable(T::Array[String])) }
136
172
  def dependency_urls
137
173
  return @dependency_urls if defined?(@dependency_urls)
138
174
 
@@ -154,55 +190,62 @@ module Dependabot
154
190
  npm_lockfile = package_lock || shrinkwrap
155
191
  if npm_lockfile
156
192
  @dependency_urls +=
157
- npm_lockfile.content.scan(/"resolved"\s*:\s*"(.*)"/)
158
- .flatten
159
- .select { |url| url.is_a?(String) }
160
- .reject { |url| url.start_with?("git") }
193
+ T.must(npm_lockfile.content).scan(/"resolved"\s*:\s*"(.*)"/)
194
+ .flatten
195
+ .select { |url| url.is_a?(String) }
196
+ .reject { |url| url.start_with?("git") }
161
197
  end
162
198
  if yarn_lock
163
199
  @dependency_urls +=
164
- yarn_lock.content.scan(/ resolved "(.*?)"/).flatten
200
+ T.must(T.must(yarn_lock).content).scan(/ resolved "(.*?)"/).flatten
165
201
  end
166
202
 
167
203
  # The registry URL for Bintray goes into the lockfile in a
168
204
  # modified format, so we modify it back before checking against
169
205
  # our credentials
170
- @dependency_urls =
206
+ @dependency_urls = T.let(
171
207
  @dependency_urls.map do |url|
172
208
  url.gsub("dl.bintray.com//", "api.bintray.com/npm/")
173
- end
209
+ end,
210
+ T.nilable(T::Array[String])
211
+ )
174
212
  end
213
+ # rubocop:enable Metrics/AbcSize
175
214
 
215
+ sig { returns(String) }
176
216
  def complete_npmrc_from_credentials
177
- initial_content = npmrc_file.content
178
- .gsub(/^.*\$\{.*\}.*/, "").strip + "\n"
217
+ initial_content = T.must(T.must(npmrc_file).content)
218
+ .gsub(/^.*\$\{.*\}.*/, "").strip + "\n"
179
219
  return initial_content unless yarn_lock || package_lock
180
220
  return initial_content unless global_registry
181
221
 
182
- registry = global_registry["registry"]
183
- registry = "https://#{registry}" unless registry.start_with?("http")
222
+ registry = T.must(global_registry)["registry"]
223
+ registry = "https://#{registry}" unless registry&.start_with?("http")
184
224
  initial_content +
185
225
  "registry = #{registry}\n" \
186
226
  "#{npmrc_global_registry_auth_line}" \
187
227
  "always-auth = true\n"
188
228
  end
189
229
 
230
+ sig { returns(String) }
190
231
  def complete_yarnrc_from_credentials
191
- initial_content = yarnrc_file.content
192
- .gsub(/^.*\$\{.*\}.*/, "").strip + "\n"
232
+ initial_content = T.must(T.must(yarnrc_file).content)
233
+ .gsub(/^.*\$\{.*\}.*/, "").strip + "\n"
193
234
  return initial_content unless yarn_lock || package_lock
194
235
  return initial_content unless global_registry
195
236
 
196
237
  initial_content +
197
- "registry: \"https://#{global_registry['registry']}\"\n" \
238
+ "registry: \"https://#{T.must(global_registry)['registry']}\"\n" \
198
239
  "#{yarnrc_global_registry_auth_line}" \
199
240
  "npmAlwaysAuth: true\n"
200
241
  end
201
242
 
243
+ sig { returns(T.nilable(String)) }
202
244
  def build_npmrc_from_yarnrc
203
245
  yarnrc_global_registry =
204
- yarnrc_file.content
205
- .lines.find { |line| line.match?(/^\s*registry\s/) }
246
+ yarnrc_file&.content
247
+ &.lines
248
+ &.find { |line| line.match?(/^\s*registry\s/) }
206
249
  &.match(NpmAndYarn::UpdateChecker::RegistryFinder::YARN_GLOBAL_REGISTRY_REGEX)
207
250
  &.named_captures&.fetch("registry")
208
251
 
@@ -211,10 +254,12 @@ module Dependabot
211
254
  build_npmrc_content_from_lockfile
212
255
  end
213
256
 
257
+ sig { returns(T.nilable(String)) }
214
258
  def build_yarnrc_from_yarnrc
215
259
  yarnrc_global_registry =
216
- yarnrc_file.content
217
- .lines.find { |line| line.match?(/^\s*registry\s/) }
260
+ yarnrc_file&.content
261
+ &.lines
262
+ &.find { |line| line.match?(/^\s*registry\s/) }
218
263
  &.match(/^\s*registry\s+"(?<registry>[^"]+)"/)
219
264
  &.named_captures&.fetch("registry")
220
265
 
@@ -223,12 +268,13 @@ module Dependabot
223
268
  build_yarnrc_content_from_lockfile
224
269
  end
225
270
 
271
+ sig { returns(T::Array[String]) }
226
272
  def credential_lines_for_npmrc
227
- lines = []
273
+ lines = T.let([], T::Array[String])
228
274
  registry_credentials.each do |cred|
229
275
  registry = cred.fetch("registry")
230
276
 
231
- lines += registry_scopes(registry) if registry_scopes(registry)
277
+ lines += T.must(registry_scopes(registry)) if registry_scopes(registry)
232
278
 
233
279
  token = cred.fetch("token", nil)
234
280
  next unless token
@@ -242,6 +288,7 @@ module Dependabot
242
288
  ["always-auth = true"] + lines
243
289
  end
244
290
 
291
+ sig { params(token: String, registry: T.nilable(String)).returns(String) }
245
292
  def auth_line(token, registry = nil)
246
293
  auth = if token.include?(":")
247
294
  encoded_token = Base64.encode64(token).delete("\n")
@@ -262,23 +309,30 @@ module Dependabot
262
309
  "//#{registry_with_trailing_slash}:#{auth}"
263
310
  end
264
311
 
312
+ sig { returns(T.nilable(T::Array[String])) }
265
313
  def npmrc_scoped_registries
266
314
  return [] unless npmrc_file
267
315
 
268
- @npmrc_scoped_registries ||=
269
- npmrc_file.content.lines.select { |line| line.match?(SCOPED_REGISTRY) }
270
- .filter_map { |line| line.match(SCOPED_REGISTRY)&.named_captures&.fetch("registry") }
316
+ @npmrc_scoped_registries ||= T.let(
317
+ T.must(T.must(npmrc_file).content).lines.select { |line| line.match?(SCOPED_REGISTRY) }
318
+ .filter_map { |line| line.match(SCOPED_REGISTRY)&.named_captures&.fetch("registry") },
319
+ T.nilable(T::Array[String])
320
+ )
271
321
  end
272
322
 
323
+ sig { returns(T.nilable(T::Array[String])) }
273
324
  def yarnrc_scoped_registries
274
325
  return [] unless yarnrc_file
275
326
 
276
- @yarnrc_scoped_registries ||=
277
- yarnrc_file.content.lines.select { |line| line.match?(SCOPED_REGISTRY) }
278
- .filter_map { |line| line.match(SCOPED_REGISTRY)&.named_captures&.fetch("registry") }
327
+ @yarnrc_scoped_registries ||= T.let(
328
+ T.must(T.must(yarnrc_file).content).lines.select { |line| line.match?(SCOPED_REGISTRY) }
329
+ .filter_map { |line| line.match(SCOPED_REGISTRY)&.named_captures&.fetch("registry") },
330
+ T.nilable(T::Array[String])
331
+ )
279
332
  end
280
333
 
281
334
  # rubocop:disable Metrics/PerceivedComplexity
335
+ sig { params(registry: String).returns(T.nilable(T::Array[String])) }
282
336
  def registry_scopes(registry)
283
337
  # Central registries don't just apply to scopes
284
338
  return if CENTRAL_REGISTRIES.include?(registry)
@@ -289,13 +343,13 @@ module Dependabot
289
343
  [registry]
290
344
  affected_urls =
291
345
  dependency_urls
292
- .select do |url|
346
+ &.select do |url|
293
347
  next false unless url.include?(registry)
294
348
 
295
349
  other_regs.none? { |r| r.include?(registry) && url.include?(r) }
296
350
  end
297
351
 
298
- scopes = affected_urls.map do |url|
352
+ scopes = T.must(affected_urls).map do |url|
299
353
  url.split(/\%40|@/)[1]&.split(%r{\%2[fF]|/})&.first
300
354
  end.uniq
301
355
 
@@ -306,41 +360,65 @@ module Dependabot
306
360
  end
307
361
  # rubocop:enable Metrics/PerceivedComplexity
308
362
 
363
+ sig { returns(T::Array[Dependabot::Credential]) }
309
364
  def registry_credentials
310
365
  credentials.select { |cred| cred.fetch("type") == "npm_registry" }
311
366
  end
312
367
 
368
+ sig { returns(T.nilable(T::Hash[String, T.untyped])) }
313
369
  def parsed_package_lock
314
- @parsed_package_lock ||= JSON.parse(package_lock.content)
370
+ @parsed_package_lock ||= T.let(
371
+ JSON.parse(T.must(T.must(package_lock).content)),
372
+ T.nilable(T::Hash[String, T.untyped])
373
+ )
315
374
  end
316
375
 
376
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
317
377
  def npmrc_file
318
- @npmrc_file ||= dependency_files
319
- .find { |f| f.name.end_with?(".npmrc") }
378
+ @npmrc_file ||= T.let(
379
+ dependency_files.find { |f| f.name.end_with?(".npmrc") },
380
+ T.nilable(Dependabot::DependencyFile)
381
+ )
320
382
  end
321
383
 
384
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
322
385
  def yarnrc_file
323
- @yarnrc_file ||= dependency_files
324
- .find { |f| f.name.end_with?(".yarnrc") }
386
+ @yarnrc_file ||= T.let(
387
+ dependency_files.find { |f| f.name.end_with?(".yarnrc") },
388
+ T.nilable(Dependabot::DependencyFile)
389
+ )
325
390
  end
326
391
 
392
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
327
393
  def yarnrc_yml_file
328
- @yarnrc_yml_file ||= dependency_files
329
- .find { |f| f.name.end_with?(".yarnrc.yml") }
394
+ @yarnrc_yml_file ||= T.let(
395
+ dependency_files.find { |f| f.name.end_with?(".yarnrc.yml") },
396
+ T.nilable(Dependabot::DependencyFile)
397
+ )
330
398
  end
331
399
 
400
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
332
401
  def yarn_lock
333
- @yarn_lock ||= dependency_files.find { |f| f.name == "yarn.lock" }
402
+ @yarn_lock ||= T.let(
403
+ dependency_files.find { |f| f.name == "yarn.lock" },
404
+ T.nilable(Dependabot::DependencyFile)
405
+ )
334
406
  end
335
407
 
408
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
336
409
  def package_lock
337
- @package_lock ||=
338
- dependency_files.find { |f| f.name == "package-lock.json" }
410
+ @package_lock ||= T.let(
411
+ dependency_files.find { |f| f.name == "package-lock.json" },
412
+ T.nilable(Dependabot::DependencyFile)
413
+ )
339
414
  end
340
415
 
416
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
341
417
  def shrinkwrap
342
- @shrinkwrap ||=
343
- dependency_files.find { |f| f.name == "npm-shrinkwrap.json" }
418
+ @shrinkwrap ||= T.let(
419
+ dependency_files.find { |f| f.name == "npm-shrinkwrap.json" },
420
+ T.nilable(Dependabot::DependencyFile)
421
+ )
344
422
  end
345
423
  end
346
424
  end
@@ -128,6 +128,11 @@ module Dependabot
128
128
  end
129
129
  end
130
130
  rescue SharedHelpers::HelperSubprocessFailed => e
131
+ # package.json name cannot contain characters like empty string or @.
132
+ if e.message.include?("Name contains illegal characters")
133
+ raise Dependabot::DependencyFileNotParseable, e.message
134
+ end
135
+
131
136
  names = dependencies.map(&:name)
132
137
  package_missing = names.any? do |name|
133
138
  e.message.include?("find package \"#{name}")
@@ -12,18 +12,21 @@ module Dependabot
12
12
  # - https://github.com/npm/npm-user-validate
13
13
  # - https://github.com/npm/validate-npm-package-name
14
14
  PACKAGE_NAME_REGEX = %r{
15
- \A # beginning of string
16
- (?=.{1,214}\z) # enforce length (1 - 214)
17
- (@(?<scope> # capture 'scope' if present
18
- (?=[^\.]) # reject leading dot
19
- [a-z0-9\-\_\.\!\~\*\'\(\)]+ # URL-safe characters
20
- )\/)?
21
- (?<name> # capture package name
22
- (?=[^\.\_]) # reject leading dot or underscore
23
- [a-z0-9\-\_\.\!\~\*\'\(\)]+ # URL-safe characters
15
+ \A # beginning of string
16
+ (?=.{1,214}\z) # enforce length (1 - 214)
17
+ (@(?<scope> # capture 'scope' if present
18
+ [a-z0-9\-\_\.\!\~\*\'\(\)]+ # URL-safe characters in scope
19
+ )\/)? # scope must be followed by slash
20
+ (?<name> # capture package name
21
+ (?(<scope>) # if scope is present
22
+ [a-z0-9\-\_\.\!\~\*\'\(\)]+ # scoped names can start with any URL-safe character
23
+ | # if no scope
24
+ [a-z0-9\-\!\~\*\'\(\)] # non-scoped names cannot start with . or _
25
+ [a-z0-9\-\_\.\!\~\*\'\(\)]* # subsequent characters can be any URL-safe character
24
26
  )
25
- \z # end of string
26
- }xi # multi-line/case-insensitive
27
+ )
28
+ \z # end of string
29
+ }xi # multi-line/case-insensitive
27
30
 
28
31
  TYPES_PACKAGE_NAME_REGEX = %r{
29
32
  \A # beginning of string
@@ -31,7 +34,7 @@ module Dependabot
31
34
  ((?<scope>.+)__)? # capture scope
32
35
  (?<name>.+) # capture name
33
36
  \z # end of string
34
- }xi # multi-line/case-insensitive
37
+ }xi # multi-line/case-insensitive
35
38
 
36
39
  class InvalidPackageName < StandardError; end
37
40
 
@@ -44,7 +44,15 @@ module Dependabot
44
44
  # what ts-jest requests\n
45
45
  YARN_BERRY_PEER_DEP_ERROR_REGEX =
46
46
  /
47
- YN0060:\s|\s.+\sprovides\s(?<required_dep>.+?)\s\((?<info_hash>\w+)\).+what\s(?<requiring_dep>.+?)\srequests
47
+ YN0060:.+\sprovides\s(?<required_dep>.+?)\s\((?<info_hash>\w+)\).+what\s(?<requiring_dep>.+?)\srequests
48
+ /x
49
+
50
+ # Error message returned by `yarn add` (for Yarn berry v4):
51
+ # YN0060: │ react is listed by your project with version 15.2.0, \
52
+ # which doesn't satisfy what react-dom (p89012) requests (^16.0.0).
53
+ YARN_BERRY_V4_PEER_DEP_ERROR_REGEX =
54
+ /
55
+ YN0060:.+\s(?<required_dep>.+?)\sis\s.+what\s(?<requiring_dep>.+?)\s\((?<info_hash>\w+)\)\srequests
48
56
  /x
49
57
 
50
58
  # Error message returned by `pnpm update`:
@@ -366,6 +374,10 @@ module Dependabot
366
374
  message.scan(YARN_BERRY_PEER_DEP_ERROR_REGEX) do
367
375
  errors << Regexp.last_match.named_captures
368
376
  end
377
+ elsif message.match?(YARN_BERRY_V4_PEER_DEP_ERROR_REGEX)
378
+ message.scan(YARN_BERRY_V4_PEER_DEP_ERROR_REGEX) do
379
+ errors << Regexp.last_match.named_captures
380
+ end
369
381
  elsif message.match?(PNPM_PEER_DEP_ERROR_REGEX)
370
382
  message.scan(PNPM_PEER_DEP_ERROR_REGEX) do
371
383
  captures = Regexp.last_match.named_captures