dependabot-npm_and_yarn 0.216.2 → 0.218.0

Sign up to get free protection for your applications and to get access to all the features.
data/helpers/package.json CHANGED
@@ -14,12 +14,14 @@
14
14
  "detect-indent": "^6.1.0",
15
15
  "nock": "^13.3.0",
16
16
  "npm": "6.14.18",
17
+ "@pnpm/lockfile-file": "^8.0.2",
18
+ "@pnpm/dependency-path": "^2.1.1",
17
19
  "semver": "^7.4.0"
18
20
  },
19
21
  "devDependencies": {
20
- "eslint": "^8.38.0",
22
+ "eslint": "^8.39.0",
21
23
  "eslint-config-prettier": "^8.8.0",
22
24
  "jest": "^29.5.0",
23
- "prettier": "^2.8.7"
25
+ "prettier": "^2.8.8"
24
26
  }
25
27
  }
@@ -14,6 +14,10 @@ module Dependabot
14
14
  @updated_dependencies = updated_dependencies
15
15
  end
16
16
 
17
+ def paths_requiring_update_check
18
+ @paths_requiring_update_check ||= fetch_paths_requiring_update_check
19
+ end
20
+
17
21
  def files_requiring_update
18
22
  @files_requiring_update ||=
19
23
  dependency_files.select do |file|
@@ -34,6 +38,15 @@ module Dependabot
34
38
 
35
39
  attr_reader :dependency_files, :updated_dependencies
36
40
 
41
+ def fetch_paths_requiring_update_check
42
+ # if only a root lockfile exists, it tracks all dependencies
43
+ return [File.dirname(root_lockfile.name)] if lockfiles == [root_lockfile]
44
+
45
+ package_files_requiring_update.map do |file|
46
+ File.dirname(file.name)
47
+ end
48
+ end
49
+
37
50
  def dependency_manifest_requirements
38
51
  @dependency_manifest_requirements ||=
39
52
  updated_dependencies.flat_map do |dep|
@@ -50,12 +63,29 @@ module Dependabot
50
63
  end
51
64
 
52
65
  def workspaces_lockfile?(lockfile)
53
- return false unless ["yarn.lock", "package-lock.json"].include?(lockfile.name)
54
- return false unless parsed_root_package_json["workspaces"]
66
+ return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"].include?(lockfile.name)
67
+
68
+ return false unless parsed_root_package_json["workspaces"] || dependency_files.any? do |file|
69
+ file.name.end_with?("pnpm-workspace.yaml") && File.dirname(file.name) == File.dirname(lockfile.name)
70
+ end
55
71
 
56
72
  updated_dependencies_in_lockfile?(lockfile)
57
73
  end
58
74
 
75
+ def root_lockfile
76
+ @root_lockfile ||=
77
+ lockfiles.find do |file|
78
+ File.dirname(file.name) == "."
79
+ end
80
+ end
81
+
82
+ def lockfiles
83
+ @lockfiles ||=
84
+ dependency_files.select do |file|
85
+ lockfile?(file)
86
+ end
87
+ end
88
+
59
89
  def parsed_root_package_json
60
90
  @parsed_root_package_json ||=
61
91
  begin
@@ -88,6 +118,7 @@ module Dependabot
88
118
  file.name.end_with?(
89
119
  "package-lock.json",
90
120
  "yarn.lock",
121
+ "pnpm-lock.yaml",
91
122
  "npm-shrinkwrap.json"
92
123
  )
93
124
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "dependabot/experiments"
4
5
  require "dependabot/logger"
5
6
  require "dependabot/file_fetchers"
6
7
  require "dependabot/file_fetchers/base"
7
8
  require "dependabot/npm_and_yarn/helpers"
9
+ require "dependabot/npm_and_yarn/package_manager"
8
10
  require "dependabot/npm_and_yarn/file_parser"
9
11
  require "dependabot/npm_and_yarn/file_parser/lockfile_parser"
10
12
 
@@ -55,6 +57,7 @@ module Dependabot
55
57
 
56
58
  package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
57
59
  package_managers["yarn"] = yarn_version if yarn_version
60
+ package_managers["pnpm"] = pnpm_version if pnpm_version
58
61
  package_managers["shrinkwrap"] = 1 if shrinkwrap
59
62
  package_managers["unknown"] = 1 if package_managers.empty?
60
63
 
@@ -69,22 +72,48 @@ module Dependabot
69
72
  def fetch_files
70
73
  fetched_files = []
71
74
  fetched_files << package_json
72
- fetched_files << package_lock if package_lock && !ignore_package_lock?
73
- fetched_files << yarn_lock if yarn_lock
74
- fetched_files << shrinkwrap if shrinkwrap
75
- fetched_files << lerna_json if lerna_json
76
- fetched_files << npmrc if npmrc
77
- fetched_files << yarnrc if yarnrc
78
- fetched_files << yarnrc_yml if yarnrc_yml
75
+ fetched_files += npm_files
76
+ fetched_files += yarn_files
77
+ fetched_files += pnpm_files if Experiments.enabled?(:pnpm_updates)
78
+ fetched_files += lerna_files
79
79
  fetched_files += workspace_package_jsons
80
- fetched_files += lerna_packages
81
80
  fetched_files += path_dependencies(fetched_files)
82
81
 
83
- fetched_files << inferred_npmrc if inferred_npmrc
84
-
85
82
  fetched_files.uniq
86
83
  end
87
84
 
85
+ def npm_files
86
+ fetched_npm_files = []
87
+ fetched_npm_files << package_lock if package_lock
88
+ fetched_npm_files << shrinkwrap if shrinkwrap
89
+ fetched_npm_files << npmrc if npmrc
90
+ fetched_npm_files << inferred_npmrc if inferred_npmrc
91
+ fetched_npm_files
92
+ end
93
+
94
+ def yarn_files
95
+ fetched_yarn_files = []
96
+ fetched_yarn_files << yarn_lock if yarn_lock
97
+ fetched_yarn_files << yarnrc if yarnrc
98
+ fetched_yarn_files << yarnrc_yml if yarnrc_yml
99
+ fetched_yarn_files
100
+ end
101
+
102
+ def pnpm_files
103
+ fetched_pnpm_files = []
104
+ fetched_pnpm_files << pnpm_lock if pnpm_lock
105
+ fetched_pnpm_files << pnpm_workspace_yaml if pnpm_workspace_yaml
106
+ fetched_pnpm_files += pnpm_workspace_package_jsons
107
+ fetched_pnpm_files
108
+ end
109
+
110
+ def lerna_files
111
+ fetched_lerna_files = []
112
+ fetched_lerna_files << lerna_json if lerna_json
113
+ fetched_lerna_files += lerna_packages
114
+ fetched_lerna_files
115
+ end
116
+
88
117
  # If every entry in the lockfile uses the same registry, we can infer
89
118
  # that there is a global .npmrc file, so add it here as if it were in the repo.
90
119
 
@@ -135,17 +164,29 @@ module Dependabot
135
164
  def yarn_version
136
165
  return @yarn_version if defined?(@yarn_version)
137
166
 
138
- package = JSON.parse(package_json.content)
139
- if (package_manager = package.fetch("packageManager", nil))
140
- get_yarn_version_from_package_json(package_manager)
141
- elsif yarn_lock
142
- Helpers.yarn_version_numeric(yarn_lock)
143
- end
167
+ @yarn_version = package_manager.locked_version("yarn") || guess_yarn_version
168
+ end
169
+
170
+ def guess_yarn_version
171
+ return unless yarn_lock
172
+
173
+ Helpers.yarn_version_numeric(yarn_lock)
174
+ end
175
+
176
+ def pnpm_version
177
+ return @pnpm_version if defined?(@pnpm_version)
178
+
179
+ @pnpm_version = package_manager.locked_version("pnpm") || guess_pnpm_version
180
+ end
181
+
182
+ def guess_pnpm_version
183
+ return unless pnpm_lock
184
+
185
+ Helpers.pnpm_major_version
144
186
  end
145
187
 
146
- def get_yarn_version_from_package_json(package_manager)
147
- version_match = package_manager.match(/yarn@(?<version>\d+.\d+.\d+)/)
148
- version_match&.named_captures&.fetch("version", nil)
188
+ def package_manager
189
+ @package_manager ||= PackageManager.new(parsed_package_json)
149
190
  end
150
191
 
151
192
  def package_json
@@ -153,15 +194,27 @@ module Dependabot
153
194
  end
154
195
 
155
196
  def package_lock
156
- @package_lock ||= fetch_file_if_present("package-lock.json")
197
+ return @package_lock if defined?(@package_lock)
198
+
199
+ @package_lock = fetch_file_if_present("package-lock.json") unless skip_package_lock?
157
200
  end
158
201
 
159
202
  def yarn_lock
160
- @yarn_lock ||= fetch_file_if_present("yarn.lock")
203
+ return @yarn_lock if defined?(@yarn_lock)
204
+
205
+ @yarn_lock = fetch_file_if_present("yarn.lock")
206
+ end
207
+
208
+ def pnpm_lock
209
+ return @pnpm_lock if defined?(@pnpm_lock)
210
+
211
+ @pnpm_lock = fetch_file_if_present("pnpm-lock.yaml") unless skip_pnpm_lock?
161
212
  end
162
213
 
163
214
  def shrinkwrap
164
- @shrinkwrap ||= fetch_file_if_present("npm-shrinkwrap.json")
215
+ return @shrinkwrap if defined?(@shrinkwrap)
216
+
217
+ @shrinkwrap = fetch_file_if_present("npm-shrinkwrap.json")
165
218
  end
166
219
 
167
220
  def npmrc
@@ -207,6 +260,11 @@ module Dependabot
207
260
  tap { |f| f.support_file = true }
208
261
  end
209
262
 
263
+ def pnpm_workspace_yaml
264
+ @pnpm_workspace_yaml ||= fetch_file_if_present("pnpm-workspace.yaml")&.
265
+ tap { |f| f.support_file = true }
266
+ end
267
+
210
268
  def lerna_json
211
269
  @lerna_json ||= fetch_file_if_present("lerna.json")&.
212
270
  tap { |f| f.support_file = true }
@@ -220,6 +278,10 @@ module Dependabot
220
278
  @lerna_packages ||= fetch_lerna_packages
221
279
  end
222
280
 
281
+ def pnpm_workspace_package_jsons
282
+ @pnpm_workspace_package_jsons ||= fetch_pnpm_workspace_package_jsons
283
+ end
284
+
223
285
  # rubocop:disable Metrics/PerceivedComplexity
224
286
  def path_dependencies(fetched_files)
225
287
  package_json_files = []
@@ -344,50 +406,36 @@ module Dependabot
344
406
  def fetch_workspace_package_jsons
345
407
  return [] unless parsed_package_json["workspaces"]
346
408
 
347
- package_json_files = []
348
-
349
- workspace_paths(parsed_package_json["workspaces"]).each do |workspace|
350
- file = File.join(workspace, "package.json")
351
-
352
- begin
353
- package_json_files << fetch_file_from_host(file)
354
- rescue Dependabot::DependencyFileNotFound
355
- nil
356
- end
409
+ workspace_paths(parsed_package_json["workspaces"]).filter_map do |workspace|
410
+ fetch_package_json_if_present(workspace)
357
411
  end
358
-
359
- package_json_files
360
412
  end
361
413
 
362
414
  def fetch_lerna_packages
363
415
  return [] unless parsed_lerna_json["packages"]
364
416
 
365
- dependency_files = []
417
+ workspace_paths(parsed_lerna_json["packages"]).flat_map do |workspace|
418
+ fetch_lerna_packages_from_path(workspace)
419
+ end.compact
420
+ end
366
421
 
367
- workspace_paths(parsed_lerna_json["packages"]).each do |workspace|
368
- dependency_files += fetch_lerna_packages_from_path(workspace)
369
- end
422
+ def fetch_pnpm_workspace_package_jsons
423
+ return [] unless parsed_pnpm_workspace_yaml["packages"]
370
424
 
371
- dependency_files
425
+ workspace_paths(parsed_pnpm_workspace_yaml["packages"]).filter_map do |workspace|
426
+ fetch_package_json_if_present(workspace)
427
+ end
372
428
  end
373
429
 
374
430
  def fetch_lerna_packages_from_path(path)
375
- dependency_files = []
376
-
377
- package_json_path = File.join(path, "package.json")
378
-
379
- begin
380
- dependency_files << fetch_file_from_host(package_json_path)
381
- dependency_files += [
382
- fetch_file_if_present(File.join(path, "package-lock.json")),
383
- fetch_file_if_present(File.join(path, "yarn.lock")),
384
- fetch_file_if_present(File.join(path, "npm-shrinkwrap.json"))
385
- ].compact
386
- rescue Dependabot::DependencyFileNotFound
387
- nil
388
- end
431
+ package_json = fetch_package_json_if_present(path)
432
+ return unless package_json
389
433
 
390
- dependency_files
434
+ [package_json] + [
435
+ fetch_file_if_present(File.join(path, "package-lock.json")),
436
+ fetch_file_if_present(File.join(path, "yarn.lock")),
437
+ fetch_file_if_present(File.join(path, "npm-shrinkwrap.json"))
438
+ ]
391
439
  end
392
440
 
393
441
  def workspace_paths(workspace_object)
@@ -449,6 +497,18 @@ module Dependabot
449
497
  matching_paths(prefix + glob, paths)
450
498
  end
451
499
 
500
+ def fetch_package_json_if_present(workspace)
501
+ file = File.join(workspace, "package.json")
502
+
503
+ begin
504
+ fetch_file_from_host(file)
505
+ rescue Dependabot::DependencyFileNotFound
506
+ # Not all paths matched by a workspace glob may contain a package.json
507
+ # file. Ignore if that's the case
508
+ nil
509
+ end
510
+ end
511
+
452
512
  # The packages/!(not-this-package) syntax is unique to Yarn
453
513
  def yarn_ignored_glob(glob)
454
514
  glob.match?(/!\(.*?\)/) && glob.gsub(/(!\((.*?)\))/, '\2')
@@ -476,12 +536,26 @@ module Dependabot
476
536
  {}
477
537
  end
478
538
 
479
- def ignore_package_lock?
539
+ def parsed_pnpm_workspace_yaml
540
+ return {} unless pnpm_workspace_yaml
541
+
542
+ YAML.safe_load(pnpm_workspace_yaml.content)
543
+ rescue Pysch::SyntaxError
544
+ raise Dependabot::DependencyFileNotParseable, pnpm_workspace_yaml.path
545
+ end
546
+
547
+ def skip_package_lock?
480
548
  return false unless npmrc
481
549
 
482
550
  npmrc.content.match?(/^package-lock\s*=\s*false/)
483
551
  end
484
552
 
553
+ def skip_pnpm_lock?
554
+ return false unless npmrc
555
+
556
+ npmrc.content.match?(/^lockfile\s*=\s*false/)
557
+ end
558
+
485
559
  def build_unfetchable_deps(unfetchable_deps)
486
560
  return [] unless package_lock || yarn_lock
487
561
 
@@ -9,6 +9,7 @@ module Dependabot
9
9
  class FileParser
10
10
  class LockfileParser
11
11
  require "dependabot/npm_and_yarn/file_parser/yarn_lock"
12
+ require "dependabot/npm_and_yarn/file_parser/pnpm_lock"
12
13
  require "dependabot/npm_and_yarn/file_parser/json_lock"
13
14
 
14
15
  def initialize(dependency_files:)
@@ -22,7 +23,7 @@ module Dependabot
22
23
  # end up unique by name. That's not a perfect representation of
23
24
  # the nested nature of JS resolution, but it makes everything work
24
25
  # comparably to other flat-resolution strategies
25
- (yarn_locks + package_locks + shrinkwraps).each do |file|
26
+ (yarn_locks + pnpm_locks + package_locks + shrinkwraps).each do |file|
26
27
  dependency_set += lockfile_for(file).dependencies
27
28
  end
28
29
 
@@ -50,10 +51,10 @@ module Dependabot
50
51
  def potential_lockfiles_for_manifest(manifest_filename)
51
52
  dir_name = File.dirname(manifest_filename)
52
53
  possible_lockfile_names =
53
- %w(package-lock.json npm-shrinkwrap.json yarn.lock).map do |f|
54
+ %w(package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock).map do |f|
54
55
  Pathname.new(File.join(dir_name, f)).cleanpath.to_path
55
56
  end +
56
- %w(yarn.lock package-lock.json npm-shrinkwrap.json)
57
+ %w(yarn.lock pnpm-lock.yaml package-lock.json npm-shrinkwrap.json)
57
58
 
58
59
  possible_lockfile_names.uniq.
59
60
  filter_map { |nm| dependency_files.find { |f| f.name == nm } }
@@ -67,8 +68,10 @@ module Dependabot
67
68
  @lockfiles ||= {}
68
69
  @lockfiles[file.name] ||= if [*package_locks, *shrinkwraps].include?(file)
69
70
  JsonLock.new(file)
70
- else
71
+ elsif yarn_locks.include?(file)
71
72
  YarnLock.new(file)
73
+ else
74
+ PnpmLock.new(file)
72
75
  end
73
76
  end
74
77
 
@@ -78,6 +81,12 @@ module Dependabot
78
81
  select { |f| f.name.end_with?("package-lock.json") }
79
82
  end
80
83
 
84
+ def pnpm_locks
85
+ @pnpm_locks ||=
86
+ dependency_files.
87
+ select { |f| f.name.end_with?("pnpm-lock.yaml") }
88
+ end
89
+
81
90
  def yarn_locks
82
91
  @yarn_locks ||=
83
92
  dependency_files.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/errors"
4
+
5
+ module Dependabot
6
+ module NpmAndYarn
7
+ class FileParser
8
+ class PnpmLock
9
+ def initialize(dependency_file)
10
+ @dependency_file = dependency_file
11
+ end
12
+
13
+ def parsed
14
+ @parsed ||= SharedHelpers.in_a_temporary_directory do
15
+ File.write("pnpm-lock.yaml", @dependency_file.content)
16
+
17
+ SharedHelpers.run_helper_subprocess(
18
+ command: NativeHelpers.helper_path,
19
+ function: "pnpm:parseLockfile",
20
+ args: [Dir.pwd]
21
+ )
22
+ rescue SharedHelpers::HelperSubprocessFailed
23
+ raise Dependabot::DependencyFileNotParseable, @dependency_file.path
24
+ end
25
+ end
26
+
27
+ def dependencies
28
+ dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
29
+
30
+ parsed.each do |details|
31
+ next if details["aliased"]
32
+
33
+ name = details["name"]
34
+ version = details["version"]
35
+
36
+ dependency_args = {
37
+ name: name,
38
+ version: version,
39
+ package_manager: "npm_and_yarn",
40
+ requirements: []
41
+ }
42
+
43
+ if details["dev"]
44
+ dependency_args[:subdependency_metadata] =
45
+ [{ production: !details["dev"] }]
46
+ end
47
+
48
+ dependency_set << Dependency.new(**dependency_args)
49
+ end
50
+
51
+ dependency_set
52
+ end
53
+
54
+ def details(dependency_name, requirement, _manifest_name)
55
+ details_candidates = parsed.select { |info| info["name"] == dependency_name }
56
+
57
+ # If there's only one entry for this dependency, use it, even if
58
+ # the requirement in the lockfile doesn't match
59
+ if details_candidates.one?
60
+ details_candidates.first
61
+ else
62
+ details_candidates.find { |info| info["specifiers"]&.include?(requirement) }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -418,6 +418,12 @@ module Dependabot
418
418
  raise Dependabot::DependencyFileNotParseable, msg
419
419
  end
420
420
 
421
+ if error_message.include?("EBADENGINE")
422
+ msg = "Dependabot uses Node.js #{`node --version`} and NPM #{`npm --version`}. " \
423
+ "Due to the engine-strict setting, the update will not succeed."
424
+ raise Dependabot::DependencyFileNotResolvable, msg
425
+ end
426
+
421
427
  raise error
422
428
  end
423
429
  # rubocop:enable Metrics/AbcSize
@@ -81,7 +81,9 @@ module Dependabot
81
81
  next false if CENTRAL_REGISTRIES.include?(cred["registry"])
82
82
 
83
83
  # If all the URLs include this registry, it's global
84
- next true if dependency_urls.all? { |url| url.include?(cred["registry"]) }
84
+ next true if dependency_urls.size.positive? && dependency_urls.all? do |url|
85
+ url.include?(cred["registry"])
86
+ end
85
87
 
86
88
  # Check if this registry has already been defined in .npmrc as a scoped registry
87
89
  next false if npmrc_scoped_registries.any? { |sr| sr.include?(cred["registry"]) }
@@ -133,8 +135,8 @@ module Dependabot
133
135
  @dependency_urls = []
134
136
  if package_lock
135
137
  @dependency_urls +=
136
- parsed_package_lock.fetch("dependencies", {}).
137
- filter_map { |_, details| details["resolved"] }.
138
+ package_lock.content.scan(/"resolved"\s*:\s*"(.*)"/).
139
+ flatten.
138
140
  select { |url| url.is_a?(String) }.
139
141
  reject { |url| url.start_with?("git") }
140
142
  end
@@ -267,7 +269,7 @@ module Dependabot
267
269
 
268
270
  scopes = affected_urls.map do |url|
269
271
  url.split(/\%40|@/)[1]&.split(%r{\%2[fF]|/})&.first
270
- end
272
+ end.uniq
271
273
 
272
274
  # Registry used for unscoped packages
273
275
  return if scopes.include?(nil)
@@ -108,8 +108,9 @@ module Dependabot
108
108
  def update_package_json_resolutions(package_json_content:, new_req:,
109
109
  dependency:, old_req:)
110
110
  dep = dependency
111
+ parsed_json_content = JSON.parse(package_json_content)
111
112
  resolutions =
112
- JSON.parse(package_json_content).fetch("resolutions", {}).
113
+ parsed_json_content.fetch("resolutions", parsed_json_content.dig("pnpm", "overrides") || {}).
113
114
  reject { |_, v| v != old_req && v != dep.previous_version }.
114
115
  select { |k, _| k == dep.name || k.end_with?("/#{dep.name}") }
115
116
 
@@ -132,7 +133,7 @@ module Dependabot
132
133
  )
133
134
 
134
135
  content = update_package_json_sections(
135
- ["resolutions"], content, original_line, replacement_line
136
+ %w(resolutions overrides), content, original_line, replacement_line
136
137
  )
137
138
  end
138
139
  content