dependabot-npm_and_yarn 0.215.0 → 0.216.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/build +1 -1
  3. data/helpers/lib/yarn/subdependency-updater.js +15 -44
  4. data/helpers/package-lock.json +2584 -1559
  5. data/helpers/package.json +7 -8
  6. data/helpers/test/npm6/conflicting-dependency-parser.test.js +1 -2
  7. data/helpers/test/npm6/fixtures/conflicting-dependency-parser/deeply-nested/package-lock.json +3 -3
  8. data/helpers/test/npm6/updater.test.js +1 -2
  9. data/helpers/test/yarn/conflicting-dependency-parser.test.js +1 -2
  10. data/helpers/test/yarn/fixtures/conflicting-dependency-parser/deeply-nested/yarn.lock +3 -3
  11. data/helpers/test/yarn/updater.test.js +1 -2
  12. data/lib/dependabot/npm_and_yarn/file_fetcher.rb +26 -38
  13. data/lib/dependabot/npm_and_yarn/file_parser/json_lock.rb +86 -0
  14. data/lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb +21 -183
  15. data/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb +80 -0
  16. data/lib/dependabot/npm_and_yarn/file_parser.rb +23 -36
  17. data/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +55 -40
  18. data/lib/dependabot/npm_and_yarn/file_updater/yarn_lockfile_updater.rb +20 -1
  19. data/lib/dependabot/npm_and_yarn/helpers.rb +7 -1
  20. data/lib/dependabot/npm_and_yarn/update_checker/dependency_files_builder.rb +6 -0
  21. data/lib/dependabot/npm_and_yarn/update_checker/subdependency_version_resolver.rb +1 -1
  22. data/lib/dependabot/npm_and_yarn/update_checker/version_resolver.rb +20 -13
  23. data/lib/dependabot/npm_and_yarn/update_checker.rb +5 -0
  24. data/lib/dependabot/npm_and_yarn/version.rb +13 -2
  25. metadata +37 -32
data/helpers/package.json CHANGED
@@ -10,17 +10,16 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@dependabot/yarn-lib": "^1.22.19",
13
- "@npmcli/arborist": "^6.1.4",
13
+ "@npmcli/arborist": "^6.2.5",
14
14
  "detect-indent": "^6.1.0",
15
- "nock": "^13.2.9",
16
- "npm": "6.14.17",
15
+ "nock": "^13.3.0",
16
+ "npm": "6.14.18",
17
17
  "semver": "^7.3.8"
18
18
  },
19
19
  "devDependencies": {
20
- "eslint": "^8.29.0",
21
- "eslint-config-prettier": "^8.5.0",
22
- "jest": "^29.3.1",
23
- "prettier": "^2.8.0",
24
- "rimraf": "^3.0.2"
20
+ "eslint": "^8.38.0",
21
+ "eslint-config-prettier": "^8.8.0",
22
+ "jest": "^29.5.0",
23
+ "prettier": "^2.8.7"
25
24
  }
26
25
  }
@@ -1,7 +1,6 @@
1
1
  const path = require("path");
2
2
  const os = require("os");
3
3
  const fs = require("fs");
4
- const rimraf = require("rimraf");
5
4
  const {
6
5
  findConflictingDependencies,
7
6
  } = require("../../lib/npm/conflicting-dependency-parser");
@@ -12,7 +11,7 @@ describe("findConflictingDependencies", () => {
12
11
  beforeEach(() => {
13
12
  tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
14
13
  });
15
- afterEach(() => rimraf.sync(tempDir));
14
+ afterEach(() => fs.rmdir(tempDir, { recursive: true }, () => {}));
16
15
 
17
16
  it("finds conflicting dependencies", async () => {
18
17
  helpers.copyDependencies("conflicting-dependency-parser/simple", tempDir);
@@ -454,9 +454,9 @@
454
454
  }
455
455
  },
456
456
  "minimist": {
457
- "version": "1.2.5",
458
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
459
- "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
457
+ "version": "1.2.8",
458
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
459
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
460
460
  },
461
461
  "mkdirp": {
462
462
  "version": "0.5.5",
@@ -1,7 +1,6 @@
1
1
  const path = require("path");
2
2
  const os = require("os");
3
3
  const fs = require("fs");
4
- const rimraf = require("rimraf");
5
4
  const { updateDependencyFiles } = require("../../lib/npm6/updater");
6
5
  const helpers = require("./helpers");
7
6
 
@@ -10,7 +9,7 @@ describe("updater", () => {
10
9
  beforeEach(() => {
11
10
  tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
12
11
  });
13
- afterEach(() => rimraf.sync(tempDir));
12
+ afterEach(() => fs.rm(tempDir, { recursive: true }, () => {}));
14
13
 
15
14
  it("generates an updated package-lock.json", async () => {
16
15
  helpers.copyDependencies("updater/original", tempDir);
@@ -1,7 +1,6 @@
1
1
  const path = require("path");
2
2
  const os = require("os");
3
3
  const fs = require("fs");
4
- const rimraf = require("rimraf");
5
4
  const {
6
5
  findConflictingDependencies,
7
6
  } = require("../../lib/yarn/conflicting-dependency-parser");
@@ -12,7 +11,7 @@ fdescribe("findConflictingDependencies", () => {
12
11
  beforeEach(() => {
13
12
  tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
14
13
  });
15
- afterEach(() => rimraf.sync(tempDir));
14
+ afterEach(() => fs.rm(tempDir, { recursive: true }, () => {}));
16
15
 
17
16
  it("finds conflicting dependencies", async () => {
18
17
  helpers.copyDependencies("conflicting-dependency-parser/simple", tempDir);
@@ -399,9 +399,9 @@ minimatch@^3.0.4:
399
399
  brace-expansion "^1.1.7"
400
400
 
401
401
  minimist@^1.2.5:
402
- version "1.2.5"
403
- resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
404
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
402
+ version "1.2.8"
403
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
404
+ integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
405
405
 
406
406
  mkdirp@^0.5.1:
407
407
  version "0.5.5"
@@ -1,7 +1,6 @@
1
1
  const path = require("path");
2
2
  const os = require("os");
3
3
  const fs = require("fs");
4
- const rimraf = require("rimraf");
5
4
  const { updateDependencyFiles } = require("../../lib/yarn/updater");
6
5
  const helpers = require("./helpers");
7
6
 
@@ -10,7 +9,7 @@ describe("updater", () => {
10
9
  beforeEach(() => {
11
10
  tempDir = fs.mkdtempSync(os.tmpdir() + path.sep);
12
11
  });
13
- afterEach(() => rimraf.sync(tempDir));
12
+ afterEach(() => fs.rm(tempDir, { recursive: true }, () => {}));
14
13
 
15
14
  function copyDependencies(sourceDir, destDir) {
16
15
  const srcPackageJson = path.join(
@@ -10,7 +10,6 @@ require "dependabot/npm_and_yarn/file_parser/lockfile_parser"
10
10
 
11
11
  module Dependabot
12
12
  module NpmAndYarn
13
- # rubocop:disable Metrics/ClassLength
14
13
  class FileFetcher < Dependabot::FileFetchers::Base
15
14
  require_relative "file_fetcher/path_dependency_builder"
16
15
 
@@ -50,6 +49,19 @@ module Dependabot
50
49
  end
51
50
  end
52
51
 
52
+ def package_manager_version
53
+ package_managers = {}
54
+
55
+ package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
56
+ package_managers["yarn"] = yarn_version if yarn_version
57
+ package_managers["shrinkwrap"] = 1 if shrinkwrap
58
+
59
+ {
60
+ ecosystem: "npm",
61
+ package_managers: package_managers
62
+ }
63
+ end
64
+
53
65
  private
54
66
 
55
67
  def fetch_files
@@ -65,7 +77,6 @@ module Dependabot
65
77
  fetched_files += workspace_package_jsons
66
78
  fetched_files += lerna_packages
67
79
  fetched_files += path_dependencies(fetched_files)
68
- instrument_package_manager_version
69
80
 
70
81
  fetched_files << inferred_npmrc if inferred_npmrc
71
82
 
@@ -103,20 +114,6 @@ module Dependabot
103
114
  @inferred_npmrc = nil
104
115
  end
105
116
 
106
- def instrument_package_manager_version
107
- package_managers = {}
108
-
109
- package_managers["npm"] = Helpers.npm_version_numeric(package_lock.content) if package_lock
110
- package_managers["yarn"] = yarn_version if yarn_version
111
- package_managers["shrinkwrap"] = 1 if shrinkwrap
112
-
113
- Dependabot.instrument(
114
- Notifications::FILE_PARSER_PACKAGE_MANAGER_VERSION_PARSED,
115
- ecosystem: "npm",
116
- package_managers: package_managers
117
- )
118
- end
119
-
120
117
  def yarn_version
121
118
  return @yarn_version if defined?(@yarn_version)
122
119
 
@@ -275,6 +272,9 @@ module Dependabot
275
272
  current_dir = file.name.rpartition("/").first
276
273
  current_dir = nil if current_dir == ""
277
274
 
275
+ current_depth = File.join(directory, file.name).split("/").count { |path| !path.empty? }
276
+ path_to_directory = "../" * current_depth
277
+
278
278
  dep_types = NpmAndYarn::FileParser::DEPENDENCY_TYPES
279
279
  parsed_manifest = JSON.parse(file.content)
280
280
  dependency_objects = parsed_manifest.values_at(*dep_types).compact
@@ -294,7 +294,7 @@ module Dependabot
294
294
  select { |_, v| v.is_a?(String) && v.start_with?(*path_starts) }.
295
295
  map do |name, path|
296
296
  path = path.gsub(PATH_DEPENDENCY_CLEAN_REGEX, "")
297
- raise PathDependenciesNotReachable, "#{name} at #{path}" if path.start_with?("/")
297
+ raise PathDependenciesNotReachable, "#{name} at #{path}" if path.start_with?("/", "#{path_to_directory}..")
298
298
 
299
299
  path = File.join(current_dir, path) unless current_dir.nil?
300
300
  [name, Pathname.new(path).cleanpath.to_path]
@@ -353,7 +353,7 @@ module Dependabot
353
353
  dependency_files
354
354
  end
355
355
 
356
- def fetch_lerna_packages_from_path(path, nested = false)
356
+ def fetch_lerna_packages_from_path(path)
357
357
  dependency_files = []
358
358
 
359
359
  package_json_path = File.join(path, "package.json")
@@ -366,19 +366,7 @@ module Dependabot
366
366
  fetch_file_if_present(File.join(path, "npm-shrinkwrap.json"))
367
367
  ].compact
368
368
  rescue Dependabot::DependencyFileNotFound
369
- matches_double_glob =
370
- parsed_lerna_json["packages"].any? do |globbed_path|
371
- next false unless globbed_path.include?("**")
372
-
373
- File.fnmatch?(globbed_path, path)
374
- end
375
-
376
- if matches_double_glob && !nested
377
- dependency_files +=
378
- find_directories(File.join(path, "*")).flat_map do |nested_path|
379
- fetch_lerna_packages_from_path(nested_path, true)
380
- end
381
- end
369
+ nil
382
370
  end
383
371
 
384
372
  dependency_files
@@ -396,7 +384,6 @@ module Dependabot
396
384
  paths_array.flat_map { |path| recursive_find_directories(path) }
397
385
  end
398
386
 
399
- # Only expands globs one level deep, so path/**/* gets expanded to path/
400
387
  def find_directories(glob)
401
388
  return [glob] unless glob.include?("*") || yarn_ignored_glob(glob)
402
389
 
@@ -418,11 +405,12 @@ module Dependabot
418
405
  def matching_paths(glob, paths)
419
406
  ignored_glob = yarn_ignored_glob(glob)
420
407
  glob = glob.gsub(%r{^\./}, "").gsub(/!\(.*?\)/, "*")
408
+ glob = "#{glob}/*" if glob.end_with?("**")
421
409
 
422
- results = paths.select { |filename| File.fnmatch?(glob, filename) }
410
+ results = paths.select { |filename| File.fnmatch?(glob, filename, File::FNM_PATHNAME) }
423
411
  return results unless ignored_glob
424
412
 
425
- results.reject { |filename| File.fnmatch?(ignored_glob, filename) }
413
+ results.reject { |filename| File.fnmatch?(ignored_glob, filename, File::FNM_PATHNAME) }
426
414
  end
427
415
 
428
416
  def recursive_find_directories(glob, prefix = "")
@@ -431,11 +419,12 @@ module Dependabot
431
419
  glob = glob.gsub(%r{^\./}, "")
432
420
  glob_parts = glob.split("/")
433
421
 
434
- paths = find_directories(prefix + glob_parts.first)
435
- next_parts = glob_parts.drop(1)
422
+ current_glob = glob_parts.first
423
+ paths = find_directories(prefix + current_glob)
424
+ next_parts = current_glob == "**" ? glob_parts : glob_parts.drop(1)
436
425
  return paths if next_parts.empty?
437
426
 
438
- paths = paths.flat_map do |expanded_path|
427
+ paths += paths.flat_map do |expanded_path|
439
428
  recursive_find_directories(next_parts.join("/"), "#{expanded_path}/")
440
429
  end
441
430
 
@@ -497,7 +486,6 @@ module Dependabot
497
486
  raise Dependabot::DependencyFileNotParseable, lerna_json.path
498
487
  end
499
488
  end
500
- # rubocop:enable Metrics/ClassLength
501
489
  end
502
490
  end
503
491
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "dependabot/errors"
5
+ require "dependabot/npm_and_yarn/helpers"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ class FileParser
10
+ class JsonLock
11
+ def initialize(dependency_file)
12
+ @dependency_file = dependency_file
13
+ end
14
+
15
+ def parsed
16
+ @parsed ||= JSON.parse(@dependency_file.content)
17
+ rescue JSON::ParserError
18
+ raise Dependabot::DependencyFileNotParseable, @dependency_file.path
19
+ end
20
+
21
+ def dependencies
22
+ recursively_fetch_dependencies(parsed)
23
+ end
24
+
25
+ def details(dependency_name, _requirement, manifest_name)
26
+ if Helpers.npm_version(@dependency_file.content) == "npm8"
27
+ # NOTE: npm 8 sometimes doesn't install workspace dependencies in the
28
+ # workspace folder so we need to fallback to checking top-level
29
+ nested_details = parsed.dig("packages", node_modules_path(manifest_name, dependency_name))
30
+ details = nested_details || parsed.dig("packages", "node_modules/#{dependency_name}")
31
+ details&.slice("version", "resolved", "integrity", "dev")
32
+ else
33
+ parsed.dig("dependencies", dependency_name)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def recursively_fetch_dependencies(object_with_dependencies)
40
+ dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
41
+
42
+ dependencies = object_with_dependencies["dependencies"]
43
+ dependencies ||= object_with_dependencies.fetch("packages", {}).transform_keys do |name|
44
+ name.delete_prefix("node_modules/")
45
+ end
46
+
47
+ dependencies.each do |name, details|
48
+ next if name.empty? # v3 lockfiles include an empty key holding info of the current package
49
+
50
+ version = Version.semver_for(details["version"])
51
+ next unless version
52
+
53
+ dependency_args = {
54
+ name: name,
55
+ version: version,
56
+ package_manager: "npm_and_yarn",
57
+ requirements: []
58
+ }
59
+
60
+ if details["bundled"]
61
+ dependency_args[:subdependency_metadata] =
62
+ [{ npm_bundled: details["bundled"] }]
63
+ end
64
+
65
+ if details["dev"]
66
+ dependency_args[:subdependency_metadata] =
67
+ [{ production: !details["dev"] }]
68
+ end
69
+
70
+ dependency_set << Dependency.new(**dependency_args)
71
+ dependency_set += recursively_fetch_dependencies(details)
72
+ end
73
+
74
+ dependency_set
75
+ end
76
+
77
+ def node_modules_path(manifest_name, dependency_name)
78
+ return "node_modules/#{dependency_name}" if manifest_name == "package.json"
79
+
80
+ workspace_path = manifest_name.gsub("/package.json", "")
81
+ File.join(workspace_path, "node_modules", dependency_name)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -8,15 +8,23 @@ module Dependabot
8
8
  module NpmAndYarn
9
9
  class FileParser
10
10
  class LockfileParser
11
+ require "dependabot/npm_and_yarn/file_parser/yarn_lock"
12
+ require "dependabot/npm_and_yarn/file_parser/json_lock"
13
+
11
14
  def initialize(dependency_files:)
12
15
  @dependency_files = dependency_files
13
16
  end
14
17
 
15
18
  def parse_set
16
19
  dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
17
- dependency_set += yarn_lock_dependencies if yarn_locks.any?
18
- dependency_set += package_lock_dependencies if package_locks.any?
19
- dependency_set += shrinkwrap_dependencies if shrinkwraps.any?
20
+
21
+ # NOTE: The DependencySet will de-dupe our dependencies, so they
22
+ # end up unique by name. That's not a perfect representation of
23
+ # the nested nature of JS resolution, but it makes everything work
24
+ # comparably to other flat-resolution strategies
25
+ (yarn_locks + package_locks + shrinkwraps).each do |file|
26
+ dependency_set += lockfile_for(file).dependencies
27
+ end
20
28
 
21
29
  dependency_set
22
30
  end
@@ -27,12 +35,7 @@ module Dependabot
27
35
 
28
36
  def lockfile_details(dependency_name:, requirement:, manifest_name:)
29
37
  potential_lockfiles_for_manifest(manifest_name).each do |lockfile|
30
- details =
31
- if [*package_locks, *shrinkwraps].include?(lockfile)
32
- npm_lockfile_details(lockfile, dependency_name, manifest_name)
33
- else
34
- yarn_lockfile_details(lockfile, dependency_name, requirement, manifest_name)
35
- end
38
+ details = lockfile_for(lockfile).details(dependency_name, requirement, manifest_name)
36
39
 
37
40
  return details if details
38
41
  end
@@ -56,182 +59,17 @@ module Dependabot
56
59
  filter_map { |nm| dependency_files.find { |f| f.name == nm } }
57
60
  end
58
61
 
59
- def npm_lockfile_details(lockfile, dependency_name, manifest_name)
60
- parsed_lockfile = parse_package_lock(lockfile)
61
-
62
- if Helpers.npm_version(lockfile.content) == "npm8"
63
- # NOTE: npm 8 sometimes doesn't install workspace dependencies in the
64
- # workspace folder so we need to fallback to checking top-level
65
- nested_details = parsed_lockfile.dig("packages", node_modules_path(manifest_name, dependency_name))
66
- details = nested_details || parsed_lockfile.dig("packages", "node_modules/#{dependency_name}")
67
- details&.slice("version", "resolved", "integrity", "dev")
68
- else
69
- parsed_lockfile.dig("dependencies", dependency_name)
70
- end
71
- end
72
-
73
- def yarn_lockfile_details(lockfile, dependency_name, requirement, _manifest_name)
74
- parsed_yarn_lock = parse_yarn_lock(lockfile)
75
- details_candidates =
76
- parsed_yarn_lock.
77
- select { |k, _| k.split(/(?<=\w)\@/)[0] == dependency_name }
78
-
79
- # If there's only one entry for this dependency, use it, even if
80
- # the requirement in the lockfile doesn't match
81
- if details_candidates.one?
82
- details_candidates.first.last
83
- else
84
- details_candidates.find do |k, _|
85
- k.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
86
- end&.last
87
- end
88
- end
89
-
90
- def node_modules_path(manifest_name, dependency_name)
91
- return "node_modules/#{dependency_name}" if manifest_name == "package.json"
92
-
93
- workspace_path = manifest_name.gsub("/package.json", "")
94
- File.join(workspace_path, "node_modules", dependency_name)
95
- end
96
-
97
- def yarn_lock_dependencies
98
- dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
99
-
100
- yarn_locks.each do |yarn_lock|
101
- parse_yarn_lock(yarn_lock).each do |req, details|
102
- next unless semver_version_for(details["version"])
103
- next if alias_package?(req)
104
- next if workspace_package?(req)
105
- next if req == "__metadata"
106
-
107
- # NOTE: The DependencySet will de-dupe our dependencies, so they
108
- # end up unique by name. That's not a perfect representation of
109
- # the nested nature of JS resolution, but it makes everything work
110
- # comparably to other flat-resolution strategies
111
- dependency_set << Dependency.new(
112
- name: req.split(/(?<=\w)\@/).first,
113
- version: semver_version_for(details["version"]),
114
- package_manager: "npm_and_yarn",
115
- requirements: []
116
- )
117
- end
118
- end
119
-
120
- dependency_set
121
- end
122
-
123
- def package_lock_dependencies
124
- dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
125
-
126
- # NOTE: The DependencySet will de-dupe our dependencies, so they
127
- # end up unique by name. That's not a perfect representation of
128
- # the nested nature of JS resolution, but it makes everything work
129
- # comparably to other flat-resolution strategies
130
- package_locks.each do |package_lock|
131
- parsed_lockfile = parse_package_lock(package_lock)
132
- deps = recursively_fetch_npm_lock_dependencies(parsed_lockfile)
133
- dependency_set += deps
134
- end
135
-
136
- dependency_set
137
- end
138
-
139
- def shrinkwrap_dependencies
140
- dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
141
-
142
- # NOTE: The DependencySet will de-dupe our dependencies, so they
143
- # end up unique by name. That's not a perfect representation of
144
- # the nested nature of JS resolution, but it makes everything work
145
- # comparably to other flat-resolution strategies
146
- shrinkwraps.each do |shrinkwrap|
147
- parsed_lockfile = parse_shrinkwrap(shrinkwrap)
148
- deps = recursively_fetch_npm_lock_dependencies(parsed_lockfile)
149
- dependency_set += deps
150
- end
151
-
152
- dependency_set
153
- end
154
-
155
- def recursively_fetch_npm_lock_dependencies(object_with_dependencies)
156
- dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
157
-
158
- object_with_dependencies.
159
- fetch("dependencies", {}).each do |name, details|
160
- next unless semver_version_for(details["version"])
161
-
162
- dependency_args = {
163
- name: name,
164
- version: semver_version_for(details["version"]),
165
- package_manager: "npm_and_yarn",
166
- requirements: []
167
- }
168
-
169
- if details["bundled"]
170
- dependency_args[:subdependency_metadata] =
171
- [{ npm_bundled: details["bundled"] }]
172
- end
173
-
174
- if details["dev"]
175
- dependency_args[:subdependency_metadata] =
176
- [{ production: !details["dev"] }]
177
- end
178
-
179
- dependency_set << Dependency.new(**dependency_args)
180
- dependency_set += recursively_fetch_npm_lock_dependencies(details)
181
- end
182
-
183
- dependency_set
184
- end
185
-
186
- def semver_version_for(version_string)
187
- # The next two lines are to guard against improperly formatted
188
- # versions in a lockfile, such as an empty string or additional
189
- # characters. NPM/yarn fixes these when running an update, so we can
190
- # safely ignore these versions.
191
- return if version_string == ""
192
- return unless version_class.correct?(version_string)
193
-
194
- version_string
62
+ def parsed_lockfile(file)
63
+ lockfile_for(file).parsed
195
64
  end
196
65
 
197
- def alias_package?(requirement)
198
- requirement.match?(/@npm:(.+@(?!npm))/)
199
- end
200
-
201
- def workspace_package?(requirement)
202
- requirement.include?("@workspace:")
203
- end
204
-
205
- def parse_package_lock(package_lock)
206
- @parse_package_lock ||= {}
207
- @parse_package_lock[package_lock.name] ||=
208
- JSON.parse(package_lock.content)
209
- rescue JSON::ParserError
210
- raise Dependabot::DependencyFileNotParseable, package_lock.path
211
- end
212
-
213
- def parse_shrinkwrap(shrinkwrap)
214
- @parse_shrinkwrap ||= {}
215
- @parse_shrinkwrap[shrinkwrap.name] ||=
216
- JSON.parse(shrinkwrap.content)
217
- rescue JSON::ParserError
218
- raise Dependabot::DependencyFileNotParseable, shrinkwrap.path
219
- end
220
-
221
- def parse_yarn_lock(yarn_lock)
222
- @parsed_yarn_lock ||= {}
223
- @parsed_yarn_lock[yarn_lock.name] ||=
224
- SharedHelpers.in_a_temporary_directory do
225
- File.write("yarn.lock", yarn_lock.content)
226
-
227
- SharedHelpers.run_helper_subprocess(
228
- command: NativeHelpers.helper_path,
229
- function: "yarn:parseLockfile",
230
- args: [Dir.pwd]
231
- )
232
- rescue SharedHelpers::HelperSubprocessFailed
233
- raise Dependabot::DependencyFileNotParseable, yarn_lock.path
234
- end
66
+ def lockfile_for(file)
67
+ @lockfiles ||= {}
68
+ @lockfiles[file.name] ||= if [*package_locks, *shrinkwraps].include?(file)
69
+ JsonLock.new(file)
70
+ else
71
+ YarnLock.new(file)
72
+ end
235
73
  end
236
74
 
237
75
  def package_locks
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+ require "dependabot/errors"
5
+ require "dependabot/npm_and_yarn/native_helpers"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ class FileParser
10
+ class YarnLock
11
+ def initialize(dependency_file)
12
+ @dependency_file = dependency_file
13
+ end
14
+
15
+ def parsed
16
+ @parsed ||= SharedHelpers.in_a_temporary_directory do
17
+ File.write("yarn.lock", @dependency_file.content)
18
+
19
+ SharedHelpers.run_helper_subprocess(
20
+ command: NativeHelpers.helper_path,
21
+ function: "yarn:parseLockfile",
22
+ args: [Dir.pwd]
23
+ )
24
+ rescue SharedHelpers::HelperSubprocessFailed
25
+ raise Dependabot::DependencyFileNotParseable, @dependency_file.path
26
+ end
27
+ end
28
+
29
+ def dependencies
30
+ dependency_set = Dependabot::NpmAndYarn::FileParser::DependencySet.new
31
+
32
+ parsed.each do |reqs, details|
33
+ reqs.split(", ").each do |req|
34
+ version = Version.semver_for(details["version"])
35
+ next unless version
36
+ next if alias_package?(req)
37
+ next if workspace_package?(req)
38
+ next if req == "__metadata"
39
+
40
+ dependency_set << Dependency.new(
41
+ name: req.split(/(?<=\w)\@/).first,
42
+ version: version,
43
+ package_manager: "npm_and_yarn",
44
+ requirements: []
45
+ )
46
+ end
47
+ end
48
+
49
+ dependency_set
50
+ end
51
+
52
+ def details(dependency_name, requirement, _manifest_name)
53
+ details_candidates =
54
+ parsed.
55
+ select { |k, _| k.split(/(?<=\w)\@/)[0] == 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.last
61
+ else
62
+ details_candidates.find do |k, _|
63
+ k.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
64
+ end&.last
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def alias_package?(requirement)
71
+ requirement.match?(/@npm:(.+@(?!npm))/)
72
+ end
73
+
74
+ def workspace_package?(requirement)
75
+ requirement.include?("@workspace:")
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end