dependabot-npm_and_yarn 0.395.0 → 0.396.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 814f7f9eb89a5c4996711bb1ec4c541d2d7bbf0c7b3ec3767735fd859f3ea686
4
- data.tar.gz: 7be4b8ad726b88f8fb3491326a6cd32b27c3b30e71009aabdc58657156f7f4a9
3
+ metadata.gz: e7701a231519c944cf2fb1c6141f84c2ed5e391e5b5d29c72e42c2a85923947b
4
+ data.tar.gz: cdfe34310c136b297c99f715a53e665b53ebaa751022d29bb473c6e80ea82729
5
5
  SHA512:
6
- metadata.gz: '09a6318dab7354fa6bc0ba314ad763e6bfb2dd347a009fe13f957064a07d50fd25dd827d6a4ad414796133025a8705e78e1cdf9a975a7205e24f8abad9e57cec'
7
- data.tar.gz: 8b2ec8e2da37e4597cfc91ad544f52ea5a5f126ba3676f4bd0107abb076bceaff608b75fa898cd56ae8843511f209178130c0ad23889703d582abce0ae48b10d
6
+ metadata.gz: 29e772e38e68ffb7483a2e0315ae8ff49cf53e62a3d1c3133074641403329173052ae9a6d18bb04109bbb25065c150db195b86c83c81f792c77e654b6e353ecc
7
+ data.tar.gz: 2edf72ca8610f9145e2388871ae0b05cb952bf512cd9a9a974e643f0e0f73f98d7303388e631df6539db80c87f33e1461528d163d095e4fdbe19558b1d6965d8
@@ -4572,12 +4572,16 @@
4572
4572
  "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
4573
4573
  },
4574
4574
  "node_modules/baseline-browser-mapping": {
4575
- "version": "2.9.6",
4576
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
4577
- "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
4575
+ "version": "2.11.21",
4576
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
4577
+ "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
4578
4578
  "dev": true,
4579
+ "license": "Apache-2.0",
4579
4580
  "bin": {
4580
- "baseline-browser-mapping": "dist/cli.js"
4581
+ "baseline-browser-mapping": "dist/cli.cjs"
4582
+ },
4583
+ "engines": {
4584
+ "node": ">=6.0.0"
4581
4585
  }
4582
4586
  },
4583
4587
  "node_modules/bcrypt-pbkdf": {
@@ -20131,9 +20135,9 @@
20131
20135
  "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
20132
20136
  },
20133
20137
  "baseline-browser-mapping": {
20134
- "version": "2.9.6",
20135
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
20136
- "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
20138
+ "version": "2.11.21",
20139
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
20140
+ "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
20137
20141
  "dev": true
20138
20142
  },
20139
20143
  "bcrypt-pbkdf": {
@@ -1,7 +1,8 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/utils"
5
+ require "dependabot/package/npm_package_json"
5
6
  require "dependabot/npm_and_yarn/file_parser/lockfile_parser"
6
7
  require "sorbet-runtime"
7
8
 
@@ -88,7 +89,7 @@ module Dependabot
88
89
  return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock", "npm-shrinkwrap.json"]
89
90
  .include?(lockfile.name)
90
91
 
91
- return false unless parsed_root_package_json["workspaces"] || dependency_files.any? do |file|
92
+ return false unless root_package_json_document.workspaces? || dependency_files.any? do |file|
92
93
  file.name.end_with?("pnpm-workspace.yaml") && File.dirname(file.name) == File.dirname(lockfile.name)
93
94
  end
94
95
 
@@ -115,14 +116,14 @@ module Dependabot
115
116
  )
116
117
  end
117
118
 
118
- sig { returns(T::Hash[String, T.untyped]) }
119
- def parsed_root_package_json
120
- @parsed_root_package_json ||= T.let(
119
+ sig { returns(Dependabot::Package::NpmPackageJson) }
120
+ def root_package_json_document
121
+ @root_package_json_document ||= T.let(
121
122
  begin
122
123
  package = T.must(dependency_files.find { |f| f.name == "package.json" })
123
- JSON.parse(T.must(package.content))
124
+ Dependabot::Package::NpmPackageJson.from_file(package)
124
125
  end,
125
- T.nilable(T::Hash[String, T.untyped])
126
+ T.nilable(Dependabot::Package::NpmPackageJson)
126
127
  )
127
128
  end
128
129
 
@@ -20,13 +20,11 @@ module Dependabot
20
20
  parsed = FileParser::YarnLock.new(@lockfile).parsed
21
21
 
22
22
  parsed.each_with_object({}) do |(req, details), rels|
23
- next unless details.is_a?(Hash)
24
-
25
- version = details["version"]
23
+ version = details.version
26
24
  parent_name = T.must(req.split(/(?<=\w)\@/).first)
27
- children = details.fetch("dependencies", {})
25
+ children = details.dependencies
28
26
 
29
- next if children.nil? || children.empty?
27
+ next if children.empty?
30
28
 
31
29
  key = "#{parent_name}@#{version}"
32
30
  resolved_children = resolve_children(children, parsed)
@@ -38,7 +36,10 @@ module Dependabot
38
36
 
39
37
  private
40
38
 
41
- sig { params(children: T::Hash[String, String], parsed: T::Hash[String, T.untyped]).returns(T::Array[String]) }
39
+ sig do
40
+ params(children: T::Hash[String, String], parsed: T::Hash[String, FileParser::YarnLock::Record])
41
+ .returns(T::Array[String])
42
+ end
42
43
  def resolve_children(children, parsed)
43
44
  children.filter_map do |child_name, child_req|
44
45
  version = resolve_child_version(child_name, child_req, parsed)
@@ -46,21 +47,25 @@ module Dependabot
46
47
  end
47
48
  end
48
49
 
49
- sig { params(child_name: String, child_req: String, parsed: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
50
+ sig do
51
+ params(child_name: String, child_req: String, parsed: T::Hash[String, FileParser::YarnLock::Record])
52
+ .returns(T.nilable(String))
53
+ end
50
54
  def resolve_child_version(child_name, child_req, parsed)
51
55
  # Try exact key first
52
56
  child_entry = parsed["#{child_name}@#{child_req}"]
53
- return child_entry["version"] if child_entry && child_entry["version"]
57
+ return child_entry.version if child_entry&.version
54
58
 
55
59
  # Yarn groups multiple requirements into single keys like "foo@^1.0.0, foo@^1.2.0"
56
60
  target_req = "#{child_name}@#{child_req}"
57
61
  grouped_match = parsed.find { |k, _| k.split(", ").include?(target_req) }
58
- return grouped_match.last["version"] if grouped_match && grouped_match.last["version"]
62
+ grouped_version = grouped_match&.last&.version
63
+ return grouped_version if grouped_version
59
64
 
60
65
  # Fallback: find by name only if there's exactly one candidate
61
66
  candidates = parsed.select { |k, _| k.split(/(?<=\w)\@/).first == child_name }
62
67
  candidate = candidates.first
63
- candidate.last["version"] if candidates.size == 1 && candidate
68
+ candidate.last.version if candidates.size == 1 && candidate
64
69
  end
65
70
  end
66
71
  end
@@ -179,17 +179,9 @@ module Dependabot
179
179
  end
180
180
 
181
181
  known_registries = []
182
- FileParser::JsonLock.new(T.must(package_lock)).parsed.fetch(
183
- "dependencies",
184
- {}
185
- ).each do |dependency_name, details|
186
- resolved = details.fetch("resolved", DEFAULT_NPM_REGISTRY)
187
-
188
- begin
189
- uri = URI.parse(resolved)
190
- rescue URI::InvalidURIError
191
- next
192
- end
182
+ FileParser::JsonLock.new(T.must(package_lock)).legacy_dependencies.each do |dependency_name, details|
183
+ uri = details.registry_uri(DEFAULT_NPM_REGISTRY)
184
+ next unless uri
193
185
 
194
186
  next unless uri.scheme && uri.host
195
187
 
@@ -0,0 +1,155 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "uri"
5
+ require "sorbet-runtime"
6
+ require "dependabot/npm_and_yarn/file_parser/json_lock"
7
+
8
+ module Dependabot
9
+ module NpmAndYarn
10
+ class FileParser < Dependabot::FileParsers::Base
11
+ class JsonLock
12
+ class Record
13
+ extend T::Sig
14
+
15
+ ObjectHash = T.type_alias { T::Hash[String, Object] }
16
+
17
+ sig { params(data: Object, path: String, context: String).void }
18
+ def initialize(data, path:, context:)
19
+ @data = data
20
+ @path = path
21
+ @context = context
22
+ @object = T.let(nil, T.nilable(ObjectHash))
23
+ end
24
+
25
+ sig { returns(T.nilable(String)) }
26
+ def version
27
+ optional_string("version")
28
+ end
29
+
30
+ sig { returns(T.nilable(String)) }
31
+ def name
32
+ optional_string("name")
33
+ end
34
+
35
+ sig { returns(T.nilable(String)) }
36
+ def resolved
37
+ optional_string("resolved")
38
+ end
39
+
40
+ sig { returns(T.nilable(String)) }
41
+ def resolution
42
+ optional_string("resolution")
43
+ end
44
+
45
+ sig { returns(T::Boolean) }
46
+ def dev?
47
+ boolean("dev")
48
+ end
49
+
50
+ sig { returns(T::Boolean) }
51
+ def bundled?
52
+ boolean("bundled")
53
+ end
54
+
55
+ sig { returns(T::Hash[String, Record]) }
56
+ def dependency_entries
57
+ field = object["dependencies"] ? "dependencies" : "packages"
58
+ context = "#{@context}.#{field}"
59
+ mapping = object_value(object.fetch(field, {}), context)
60
+
61
+ # Modern package records contain requirement strings rather than nested lock entries.
62
+ mapping.reject { |_, value| value.is_a?(String) }.to_h do |key, data|
63
+ [key, self.class.new(data, path: @path, context: "#{context}.#{key}")]
64
+ end
65
+ end
66
+
67
+ sig { returns(T::Hash[String, Record]) }
68
+ def legacy_entries
69
+ entries(object.fetch("dependencies", {}), "#{@context}.dependencies")
70
+ end
71
+
72
+ sig { params(name: String).returns(T.nilable(Record)) }
73
+ def legacy_entry(name)
74
+ dependencies = object["dependencies"]
75
+ return if dependencies.nil?
76
+
77
+ data = object_value(dependencies, "#{@context}.dependencies")[name]
78
+ self.class.new(data, path: @path, context: "dependencies.#{name}") unless data.nil?
79
+ end
80
+
81
+ sig { params(primary: String, fallback: String).returns(T.nilable(Record)) }
82
+ def package_entry(primary, fallback)
83
+ packages = object["packages"]
84
+ return if packages.nil?
85
+
86
+ mapping = object_value(packages, "#{@context}.packages")
87
+ key = mapping[primary] ? primary : fallback
88
+ data = mapping[key]
89
+ self.class.new(data, path: @path, context: "packages.#{key}") unless data.nil?
90
+ end
91
+
92
+ sig { params(default_registry: String).returns(T.nilable(URI::Generic)) }
93
+ def registry_uri(default_registry)
94
+ value = object.fetch("resolved", default_registry)
95
+ # Registry inference filters invalid URIs, including non-string values.
96
+ return unless value.is_a?(String)
97
+
98
+ URI.parse(value)
99
+ rescue URI::InvalidURIError
100
+ nil
101
+ end
102
+
103
+ private
104
+
105
+ sig { returns(ObjectHash) }
106
+ def object
107
+ @object ||= object_value(@data, @context)
108
+ end
109
+
110
+ sig { params(value: Object, context: String).returns(ObjectHash) }
111
+ def object_value(value, context)
112
+ invalid!("#{context} must be an object") unless value.is_a?(Hash)
113
+
114
+ value.to_h do |raw_key, raw_value|
115
+ key = T.cast(raw_key, Object)
116
+ invalid!("#{context} keys must be strings") unless key.is_a?(String)
117
+
118
+ [key, T.cast(raw_value, Object)]
119
+ end
120
+ end
121
+
122
+ sig { params(value: Object, context: String).returns(T::Hash[String, Record]) }
123
+ def entries(value, context)
124
+ object_value(value, context).to_h do |key, data|
125
+ [key, self.class.new(data, path: @path, context: "#{context}.#{key}")]
126
+ end
127
+ end
128
+
129
+ sig { params(key: String).returns(T.nilable(String)) }
130
+ def optional_string(key)
131
+ value = object[key]
132
+ return if value.nil?
133
+ return value if value.is_a?(String)
134
+
135
+ invalid!("#{@context}.#{key} must be a string or nil")
136
+ end
137
+
138
+ sig { params(key: String).returns(T::Boolean) }
139
+ def boolean(key)
140
+ value = object[key]
141
+ return false if value.nil? || value == false
142
+ return true if value == true
143
+
144
+ invalid!("#{@context}.#{key} must be a boolean or nil")
145
+ end
146
+
147
+ sig { params(message: String).returns(T.noreturn) }
148
+ def invalid!(message)
149
+ raise DependencyFileNotParseable.new(@path, message)
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -1,9 +1,11 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "json"
5
5
  require "dependabot/errors"
6
+ require "dependabot/npm_and_yarn/file_parser"
6
7
  require "dependabot/npm_and_yarn/helpers"
8
+ require "dependabot/package/npm_lockfile_details"
7
9
  require "sorbet-runtime"
8
10
 
9
11
  module Dependabot
@@ -12,16 +14,24 @@ module Dependabot
12
14
  class JsonLock
13
15
  extend T::Sig
14
16
 
17
+ require_relative "json_lock/record"
18
+
15
19
  sig { params(dependency_file: DependencyFile, dealias_packages: T::Boolean).void }
16
20
  def initialize(dependency_file, dealias_packages: false)
17
21
  @dependency_file = dependency_file
18
22
  @dealias_packages = dealias_packages
19
23
  end
20
24
 
21
- sig { returns(T::Hash[String, T.untyped]) }
25
+ sig { returns(Record) }
22
26
  def parsed
23
- json_obj = JSON.parse(T.must(@dependency_file.content))
24
- @parsed ||= T.let(json_obj, T.nilable(T::Hash[String, T.untyped]))
27
+ @parsed ||= T.let(
28
+ Record.new(
29
+ T.cast(JSON.parse(T.must(@dependency_file.content)), Object),
30
+ path: @dependency_file.path,
31
+ context: "lockfile"
32
+ ),
33
+ T.nilable(Record)
34
+ )
25
35
  rescue JSON::ParserError
26
36
  raise Dependabot::DependencyFileNotParseable, @dependency_file.path
27
37
  end
@@ -31,90 +41,91 @@ module Dependabot
31
41
  recursively_fetch_dependencies(parsed)
32
42
  end
33
43
 
44
+ sig { returns(T::Hash[String, Record]) }
45
+ def legacy_dependencies
46
+ parsed.legacy_entries
47
+ end
48
+
34
49
  sig do
35
- params(dependency_name: String, _requirement: T.untyped, manifest_name: String)
36
- .returns(T.nilable(T::Hash[String, T.untyped]))
50
+ params(dependency_name: String, _requirement: T.nilable(String), manifest_name: String)
51
+ .returns(T.nilable(Dependabot::Package::NpmLockfileDetails))
37
52
  end
38
53
  def details(dependency_name, _requirement, manifest_name)
39
- if Helpers.parse_npm8?(@dependency_file)
40
- # NOTE: npm 8 sometimes doesn't install workspace dependencies in the
41
- # workspace folder so we need to fallback to checking top-level
42
- nested_details = parsed.dig("packages", node_modules_path(manifest_name, dependency_name))
43
- details = nested_details || parsed.dig("packages", "node_modules/#{dependency_name}")
44
- details&.slice("version", "resolved", "integrity", "dev")
45
- else
46
- parsed.dig("dependencies", dependency_name)
47
- end
54
+ modern = Helpers.parse_npm8?(@dependency_file)
55
+ details = if modern
56
+ # NOTE: npm 8 sometimes doesn't install workspace dependencies in the
57
+ # workspace folder so we need to fallback to checking top-level
58
+ parsed.package_entry(
59
+ node_modules_path(manifest_name, dependency_name),
60
+ "node_modules/#{dependency_name}"
61
+ )
62
+ else
63
+ parsed.legacy_entry(dependency_name)
64
+ end
65
+ return if details.nil?
66
+
67
+ Dependabot::Package::NpmLockfileDetails.new(
68
+ version: details.version,
69
+ resolved: details.resolved,
70
+ resolution: modern ? nil : details.resolution
71
+ )
48
72
  end
49
73
 
50
74
  private
51
75
 
52
76
  sig do
53
- params(object_with_dependencies: T::Hash[String, T.untyped])
77
+ params(object_with_dependencies: Record)
54
78
  .returns(Dependabot::FileParsers::Base::DependencySet)
55
79
  end
56
80
  def recursively_fetch_dependencies(object_with_dependencies)
57
81
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
58
82
 
59
- dependencies = object_with_dependencies["dependencies"]
60
- dependencies ||= object_with_dependencies.fetch("packages", {})
61
-
62
- dependencies.each do |name, details|
83
+ object_with_dependencies.dependency_entries.each do |name, details|
63
84
  next if name.empty? # v3 lockfiles include an empty key holding info of the current package
64
85
 
65
- version = Version.semver_for(details["version"])
86
+ version = Version.semver_for(details.version)
66
87
  next unless version
67
88
 
68
89
  package_name = package_name_for(name, details)
69
90
  version = version.to_s
70
91
 
71
- dependency_args = {
92
+ metadata = aliased_package?(name, details) ? { alias: name.split("node_modules/").last } : nil
93
+ subdependency_metadata = T.let(nil, T.nilable(T::Array[T::Hash[Symbol, Object]]))
94
+ subdependency_metadata = [{ npm_bundled: true }] if details.bundled?
95
+ subdependency_metadata = [{ production: false }] if details.dev?
96
+
97
+ dependency_set << Dependency.new(
72
98
  name: package_name,
73
99
  version: version,
74
100
  package_manager: "npm_and_yarn",
75
- requirements: []
76
- }
77
-
78
- if aliased_package?(name, details)
79
- alias_name = name.split("node_modules/").last
80
- dependency_args[:metadata] = { alias: alias_name }
81
- end
82
-
83
- if details["bundled"]
84
- dependency_args[:subdependency_metadata] =
85
- [{ npm_bundled: details["bundled"] }]
86
- end
87
-
88
- if details["dev"]
89
- dependency_args[:subdependency_metadata] =
90
- [{ production: !details["dev"] }]
91
- end
92
-
93
- dependency_set << Dependency.new(**dependency_args)
101
+ requirements: [],
102
+ metadata: metadata,
103
+ subdependency_metadata: subdependency_metadata
104
+ )
94
105
  dependency_set += recursively_fetch_dependencies(details)
95
106
  end
96
107
 
97
108
  dependency_set
98
109
  end
99
110
 
100
- sig { params(package_path: String, details: T::Hash[String, T.untyped]).returns(String) }
111
+ sig { params(package_path: String, details: Record).returns(String) }
101
112
  def package_name_for(package_path, details)
102
113
  package_name = T.must(package_path.split("node_modules/").last)
103
114
  return package_name unless dealias_packages?
104
115
 
105
- real_package_name = details["name"]
106
- return package_name unless real_package_name.is_a?(String)
116
+ real_package_name = details.name
117
+ return package_name if real_package_name.nil?
107
118
  return package_name if real_package_name == package_name
108
119
 
109
120
  real_package_name
110
121
  end
111
122
 
112
- sig { params(package_path: String, details: T::Hash[String, T.untyped]).returns(T::Boolean) }
123
+ sig { params(package_path: String, details: Record).returns(T::Boolean) }
113
124
  def aliased_package?(package_path, details)
114
125
  return false unless dealias_packages?
115
126
 
116
- real_package_name = details["name"]
117
- return false unless real_package_name.is_a?(String)
127
+ real_package_name = details.name
128
+ return false if real_package_name.nil?
118
129
 
119
130
  package_name = T.must(package_path.split("node_modules/").last)
120
131
  real_package_name != package_name
@@ -4,6 +4,7 @@
4
4
  require "dependabot/dependency_file"
5
5
  require "dependabot/npm_and_yarn/file_parser"
6
6
  require "dependabot/npm_and_yarn/helpers"
7
+ require "dependabot/package/npm_lockfile_details"
7
8
  require "sorbet-runtime"
8
9
 
9
10
  module Dependabot
@@ -48,10 +49,10 @@ module Dependabot
48
49
 
49
50
  sig do
50
51
  params(dependency_name: String, requirement: T.nilable(String), manifest_name: String)
51
- .returns(T.nilable(T::Hash[String, T.untyped]))
52
+ .returns(T.nilable(Dependabot::Package::NpmLockfileDetails))
52
53
  end
53
54
  def lockfile_details(dependency_name:, requirement:, manifest_name:)
54
- details = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
55
+ details = T.let(nil, T.nilable(Dependabot::Package::NpmLockfileDetails))
55
56
  potential_lockfiles_for_manifest(manifest_name).each do |lockfile|
56
57
  details = lockfile_for(lockfile).details(dependency_name, requirement, manifest_name)
57
58
 
@@ -1,12 +1,13 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
5
 
6
+ require "dependabot/npm_and_yarn/file_parser"
6
7
  require "dependabot/errors"
7
8
  require "dependabot/dependency"
8
- require "dependabot/file_parsers/base"
9
9
  require "dependabot/npm_and_yarn/native_helpers"
10
+ require "dependabot/package/npm_lockfile_details"
10
11
  require "dependabot/shared_helpers"
11
12
 
12
13
  module Dependabot
@@ -15,99 +16,112 @@ module Dependabot
15
16
  class PnpmLock
16
17
  extend T::Sig
17
18
 
19
+ class Record < T::ImmutableStruct
20
+ extend T::Sig
21
+
22
+ const :name, String
23
+ const :version, String
24
+ const :resolved, T.nilable(String)
25
+ const :dev, T::Boolean
26
+ const :specifiers, T::Array[String]
27
+ const :aliased, T::Boolean
28
+
29
+ sig { params(value: Object, path: String, index: Integer).returns(Record) }
30
+ def self.from_object(value, path:, index:)
31
+ context = "pnpm helper result[#{index}]"
32
+ raise DependencyFileNotParseable.new(path, "#{context} must be an object") unless value.is_a?(Hash)
33
+
34
+ resolved = string(T.cast(value["resolved"], Object), path, "#{context}.resolved") if value.key?("resolved")
35
+
36
+ new(
37
+ name: string(T.cast(value["name"], Object), path, "#{context}.name"),
38
+ version: string(T.cast(value["version"], Object), path, "#{context}.version"),
39
+ resolved: resolved,
40
+ dev: boolean(T.cast(value["dev"], Object), path, "#{context}.dev"),
41
+ specifiers: strings(T.cast(value["specifiers"], Object), path, "#{context}.specifiers"),
42
+ aliased: boolean(T.cast(value["aliased"], Object), path, "#{context}.aliased")
43
+ )
44
+ end
45
+
46
+ sig { params(value: Object, path: String, field: String).returns(String) }
47
+ def self.string(value, path, field)
48
+ return value if value.is_a?(String)
49
+
50
+ raise DependencyFileNotParseable.new(path, "#{field} must be a string")
51
+ end
52
+ private_class_method :string
53
+
54
+ sig { params(value: Object, path: String, field: String).returns(T::Boolean) }
55
+ def self.boolean(value, path, field)
56
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
57
+
58
+ raise DependencyFileNotParseable.new(path, "#{field} must be a boolean")
59
+ end
60
+ private_class_method :boolean
61
+
62
+ sig { params(value: Object, path: String, field: String).returns(T::Array[String]) }
63
+ def self.strings(value, path, field)
64
+ raise DependencyFileNotParseable.new(path, "#{field} must be an array") unless value.is_a?(Array)
65
+
66
+ value.each_with_index.map do |specifier, index|
67
+ string(T.cast(specifier, Object), path, "#{field}[#{index}]")
68
+ end
69
+ end
70
+ private_class_method :strings
71
+ end
72
+
18
73
  sig { params(dependency_file: Dependabot::DependencyFile, dealias_packages: T::Boolean).void }
19
74
  def initialize(dependency_file, dealias_packages: false)
20
75
  @dependency_file = dependency_file
21
76
  @dealias_packages = dealias_packages
77
+ @parsed = T.let(nil, T.nilable(T::Array[Record]))
22
78
  end
23
79
 
24
- sig { returns(T::Array[T::Hash[String, T.untyped]]) }
80
+ sig { returns(T::Array[Record]) }
25
81
  def parsed
26
- @parsed ||= T.let(
27
- T.cast(
28
- SharedHelpers.in_a_temporary_directory do
29
- File.write("pnpm-lock.yaml", @dependency_file.content)
30
-
31
- SharedHelpers.run_helper_subprocess(
32
- command: NativeHelpers.helper_path,
33
- function: "pnpm:parseLockfile",
34
- args: [Dir.pwd]
35
- )
36
- rescue SharedHelpers::HelperSubprocessFailed
37
- raise Dependabot::DependencyFileNotParseable, @dependency_file.path
38
- end,
39
- T::Array[T::Hash[String, T.untyped]]
40
- ),
41
- T.nilable(T::Array[T::Hash[String, T.untyped]])
42
- )
43
- end
44
-
45
- # rubocop:disable Metrics/MethodLength
46
- # rubocop:disable Metrics/AbcSize
47
- # rubocop:disable Metrics/PerceivedComplexity
48
- sig { returns(Dependabot::FileParsers::Base::DependencySet) }
49
- def dependencies
50
- dependency_set = Dependabot::FileParsers::Base::DependencySet.new
51
-
52
- # Separate dependencies into two categories: with specifiers and without specifiers.
53
- dependencies_with_specifiers = T.let([], T::Array[T::Hash[Symbol, T.untyped]])
54
- dependencies_without_specifiers = T.let([], T::Array[T::Hash[Symbol, T.untyped]])
55
-
56
- parsed.each do |details|
57
- next if details["aliased"] && !dealias_packages?
58
-
59
- name = T.cast(details["name"], String)
60
- version = T.cast(details["version"], T.nilable(String))
61
-
62
- dependency_args = {
63
- name: name,
64
- version: version,
65
- package_manager: "npm_and_yarn",
66
- requirements: []
67
- }
82
+ return @parsed if @parsed
68
83
 
69
- # Tag aliased packages with metadata so the grapher can identify them as direct
70
- dependency_args[:metadata] = { alias: name } if details["aliased"]
84
+ result = SharedHelpers.in_a_temporary_directory do
85
+ File.write("pnpm-lock.yaml", @dependency_file.content)
71
86
 
72
- # Add metadata for subdependencies if marked as a dev dependency.
73
- dependency_args[:subdependency_metadata] = [{ production: !details["dev"] }] if details["dev"]
87
+ SharedHelpers.run_helper_subprocess(
88
+ command: NativeHelpers.helper_path,
89
+ function: "pnpm:parseLockfile",
90
+ args: [Dir.pwd]
91
+ )
92
+ rescue SharedHelpers::HelperSubprocessFailed
93
+ raise Dependabot::DependencyFileNotParseable, @dependency_file.path
94
+ end
74
95
 
75
- specifiers = details["specifiers"]
76
- if specifiers&.any?
77
- dependencies_with_specifiers << dependency_args
78
- else
79
- dependencies_without_specifiers << dependency_args
80
- end
96
+ unless result.is_a?(Array)
97
+ raise DependencyFileNotParseable.new(@dependency_file.path, "pnpm helper result must be an array")
81
98
  end
82
99
 
83
- # Add prioritized dependencies to the dependency set.
84
- dependencies_with_specifiers.each do |dependency_args|
85
- dependency_set << Dependency.new(
86
- name: dependency_args[:name],
87
- version: dependency_args[:version],
88
- package_manager: dependency_args[:package_manager],
89
- requirements: dependency_args[:requirements],
90
- subdependency_metadata: dependency_args[:subdependency_metadata],
91
- metadata: dependency_args[:metadata]
92
- )
100
+ @parsed = result.each_with_index.map do |record, index|
101
+ Record.from_object(T.cast(record, Object), path: @dependency_file.path, index: index)
93
102
  end
103
+ end
104
+
105
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
106
+ def dependencies
107
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
108
+ with_specifiers, without_specifiers = parsed.partition { |record| record.specifiers.any? }
109
+
110
+ (with_specifiers + without_specifiers).each do |record|
111
+ next if record.aliased && !dealias_packages?
94
112
 
95
- dependencies_without_specifiers.each do |dependency_args|
96
113
  dependency_set << Dependency.new(
97
- name: dependency_args[:name],
98
- version: dependency_args[:version],
99
- package_manager: dependency_args[:package_manager],
100
- requirements: dependency_args[:requirements],
101
- subdependency_metadata: dependency_args[:subdependency_metadata],
102
- metadata: dependency_args[:metadata]
114
+ name: record.name,
115
+ version: record.version,
116
+ package_manager: "npm_and_yarn",
117
+ requirements: [],
118
+ subdependency_metadata: record.dev ? [{ production: false }] : nil,
119
+ metadata: record.aliased ? { alias: record.name } : nil
103
120
  )
104
121
  end
105
122
 
106
123
  dependency_set
107
124
  end
108
- # rubocop:enable Metrics/PerceivedComplexity
109
- # rubocop:enable Metrics/AbcSize
110
- # rubocop:enable Metrics/MethodLength
111
125
 
112
126
  sig do
113
127
  params(
@@ -115,18 +129,21 @@ module Dependabot
115
129
  requirement: T.nilable(String),
116
130
  _manifest_name: T.nilable(String)
117
131
  )
118
- .returns(T.nilable(T::Hash[String, T.untyped]))
132
+ .returns(T.nilable(Dependabot::Package::NpmLockfileDetails))
119
133
  end
120
134
  def details(dependency_name, requirement, _manifest_name)
121
- details_candidates = parsed.select { |info| info["name"] == dependency_name }
135
+ details_candidates = parsed.select { |record| record.name == dependency_name }
122
136
 
123
137
  # If there's only one entry for this dependency, use it, even if
124
138
  # the requirement in the lockfile doesn't match
125
- if details_candidates.one?
126
- details_candidates.first
127
- else
128
- details_candidates.find { |info| info["specifiers"]&.include?(requirement) }
129
- end
139
+ details = if details_candidates.one?
140
+ details_candidates.first
141
+ elsif requirement
142
+ details_candidates.find { |record| record.specifiers.include?(requirement) }
143
+ end
144
+ return if details.nil?
145
+
146
+ Dependabot::Package::NpmLockfileDetails.new(version: details.version, resolved: details.resolved)
130
147
  end
131
148
 
132
149
  private
@@ -0,0 +1,79 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "dependabot/npm_and_yarn/file_parser/yarn_lock"
6
+
7
+ module Dependabot
8
+ module NpmAndYarn
9
+ class FileParser < Dependabot::FileParsers::Base
10
+ class YarnLock
11
+ class Record
12
+ extend T::Sig
13
+
14
+ ObjectHash = T.type_alias { T::Hash[String, Object] }
15
+
16
+ sig { params(data: Object, path: String, context: String).void }
17
+ def initialize(data, path:, context:)
18
+ @data = data
19
+ @path = path
20
+ @context = context
21
+ @object = T.let(nil, T.nilable(ObjectHash))
22
+ end
23
+
24
+ sig { returns(T.nilable(String)) }
25
+ def version
26
+ value = object["version"]
27
+ return if value.nil?
28
+ return value if value.is_a?(String)
29
+
30
+ invalid!("#{@context}.version must be a string or nil")
31
+ end
32
+
33
+ sig { returns(Dependabot::Package::NpmLockfileDetails) }
34
+ def lookup_details
35
+ Dependabot::Package::NpmLockfileDetails.from_object(object, path: @path, context: @context)
36
+ end
37
+
38
+ sig { returns(T::Hash[String, String]) }
39
+ def dependencies
40
+ value = object["dependencies"]
41
+ return {} if value.nil?
42
+
43
+ invalid!("#{@context}.dependencies must be an object") unless value.is_a?(Hash)
44
+ value.to_h do |raw_name, raw_requirement|
45
+ name = T.cast(raw_name, Object)
46
+ requirement = T.cast(raw_requirement, Object)
47
+ invalid!("#{@context}.dependencies keys must be strings") unless name.is_a?(String)
48
+ invalid!("#{@context}.dependencies.#{name} must be a string") unless requirement.is_a?(String)
49
+
50
+ [name, requirement]
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ sig { returns(ObjectHash) }
57
+ def object
58
+ return @object if @object
59
+
60
+ data = @data
61
+ invalid!("#{@context} must be an object") unless data.is_a?(Hash)
62
+
63
+ @object = data.to_h do |raw_key, raw_value|
64
+ key = T.cast(raw_key, Object)
65
+ invalid!("#{@context} keys must be strings") unless key.is_a?(String)
66
+
67
+ [key, T.cast(raw_value, Object)]
68
+ end
69
+ end
70
+
71
+ sig { params(message: String).returns(T.noreturn) }
72
+ def invalid!(message)
73
+ raise DependencyFileNotParseable.new(@path, message)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -3,7 +3,9 @@
3
3
 
4
4
  require "dependabot/shared_helpers"
5
5
  require "dependabot/errors"
6
+ require "dependabot/npm_and_yarn/file_parser"
6
7
  require "dependabot/npm_and_yarn/native_helpers"
8
+ require "dependabot/package/npm_lockfile_details"
7
9
  require "sorbet-runtime"
8
10
 
9
11
  module Dependabot
@@ -12,35 +14,36 @@ module Dependabot
12
14
  class YarnLock
13
15
  extend T::Sig
14
16
 
17
+ require_relative "yarn_lock/record"
18
+
15
19
  sig { params(dependency_file: Dependabot::DependencyFile, dealias_packages: T::Boolean).void }
16
20
  def initialize(dependency_file, dealias_packages: false)
17
21
  @dependency_file = dependency_file
18
22
  @dealias_packages = dealias_packages
23
+ @parsed = T.let(nil, T.nilable(T::Hash[String, Record]))
19
24
  end
20
25
 
21
- sig { returns(T::Hash[String, T::Hash[String, T.untyped]]) }
26
+ sig { returns(T::Hash[String, Record]) }
22
27
  def parsed
23
- @parsed ||= T.let(
24
- T.cast(
25
- SharedHelpers.in_a_temporary_directory do
26
- File.write("yarn.lock", @dependency_file.content)
27
-
28
- SharedHelpers.run_helper_subprocess(
29
- command: NativeHelpers.helper_path,
30
- function: "yarn:parseLockfile",
31
- args: [Dir.pwd]
32
- )
33
- rescue SharedHelpers::HelperSubprocessFailed => e
34
- raise Dependabot::OutOfDisk, e.message if e.message.end_with?("No space left on device")
35
- raise Dependabot::OutOfDisk, e.message if e.message.end_with?("Out of diskspace")
36
- raise Dependabot::OutOfMemory, e.message if e.message.end_with?("MemoryError")
37
-
38
- raise Dependabot::DependencyFileNotParseable, @dependency_file.path
39
- end,
40
- T::Hash[String, T::Hash[String, T.untyped]]
41
- ),
42
- T.nilable(T::Hash[String, T::Hash[String, T.untyped]])
43
- )
28
+ return @parsed if @parsed
29
+
30
+ result = SharedHelpers.in_a_temporary_directory do
31
+ File.write("yarn.lock", @dependency_file.content)
32
+
33
+ SharedHelpers.run_helper_subprocess(
34
+ command: NativeHelpers.helper_path,
35
+ function: "yarn:parseLockfile",
36
+ args: [Dir.pwd]
37
+ )
38
+ rescue SharedHelpers::HelperSubprocessFailed => e
39
+ raise Dependabot::OutOfDisk, e.message if e.message.end_with?("No space left on device")
40
+ raise Dependabot::OutOfDisk, e.message if e.message.end_with?("Out of diskspace")
41
+ raise Dependabot::OutOfMemory, e.message if e.message.end_with?("MemoryError")
42
+
43
+ raise Dependabot::DependencyFileNotParseable, @dependency_file.path
44
+ end
45
+
46
+ @parsed = parse_records(result)
44
47
  end
45
48
 
46
49
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
@@ -49,11 +52,12 @@ module Dependabot
49
52
 
50
53
  parsed.each do |reqs, details|
51
54
  reqs.split(", ").each do |req|
52
- version = Version.semver_for(details["version"])
53
- next unless version
54
55
  next if workspace_package?(req)
55
56
  next if req == "__metadata"
56
57
 
58
+ version = Version.semver_for(details.version)
59
+ next unless version
60
+
57
61
  if alias_package?(req)
58
62
  # Skip unless we are dealiasing packages
59
63
  next unless dealias_packages?
@@ -88,9 +92,9 @@ module Dependabot
88
92
  params(
89
93
  dependency_name: String,
90
94
  requirement: T.nilable(String),
91
- _manifest_name: T.untyped
95
+ _manifest_name: String
92
96
  )
93
- .returns(T.nilable(T::Hash[String, T.untyped]))
97
+ .returns(T.nilable(Dependabot::Package::NpmLockfileDetails))
94
98
  end
95
99
  def details(dependency_name, requirement, _manifest_name)
96
100
  details_candidates =
@@ -99,17 +103,43 @@ module Dependabot
99
103
 
100
104
  # If there's only one entry for this dependency, use it, even if
101
105
  # the requirement in the lockfile doesn't match
102
- if details_candidates.one?
103
- T.must(details_candidates.first).last
104
- else
105
- details_candidates.find do |k, _|
106
- k.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
107
- end&.last
108
- end
106
+ details = if details_candidates.one?
107
+ T.must(details_candidates.first).last
108
+ else
109
+ details_candidates.find do |k, _|
110
+ k.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
111
+ end&.last
112
+ end
113
+ return if details.nil?
114
+
115
+ details.lookup_details
109
116
  end
110
117
 
111
118
  private
112
119
 
120
+ sig { params(value: Object).returns(T::Hash[String, Record]) }
121
+ def parse_records(value)
122
+ unless value.is_a?(Hash)
123
+ raise DependencyFileNotParseable.new(@dependency_file.path, "yarn helper result must be an object")
124
+ end
125
+
126
+ records = T.let({}, T::Hash[String, Record])
127
+ value.each_with_index do |(raw_key, data), index|
128
+ key = T.cast(raw_key, Object)
129
+ unless key.is_a?(String)
130
+ raise DependencyFileNotParseable.new(@dependency_file.path, "yarn helper descriptors must be strings")
131
+ end
132
+ next if key == "__metadata"
133
+
134
+ records[key] = Record.new(
135
+ T.cast(data, Object),
136
+ path: @dependency_file.path,
137
+ context: "yarn helper result[#{index}]"
138
+ )
139
+ end
140
+ records
141
+ end
142
+
113
143
  sig { returns(T::Boolean) }
114
144
  def dealias_packages?
115
145
  @dealias_packages
@@ -5,6 +5,8 @@
5
5
 
6
6
  require "cgi/escape"
7
7
  require "dependabot/dependency"
8
+ require "dependabot/package/npm_lockfile_details"
9
+ require "dependabot/package/npm_package_json"
8
10
  require "dependabot/file_parsers"
9
11
  require "dependabot/file_parsers/base"
10
12
  require "dependabot/shared_helpers"
@@ -99,7 +101,7 @@ module Dependabot
99
101
  def package_manager_helper
100
102
  @package_manager_helper ||= T.let(
101
103
  PackageManagerHelper.new(
102
- Dependabot::Package::NpmPackageManagerConfig.from_package_json(parsed_package_json),
104
+ package_json_document.package_manager_config,
103
105
  lockfiles,
104
106
  registry_config_files,
105
107
  credentials
@@ -126,9 +128,9 @@ module Dependabot
126
128
  }
127
129
  end
128
130
 
129
- sig { returns(T.untyped) }
130
- def parsed_package_json
131
- JSON.parse(T.must(package_json.content))
131
+ sig { returns(Dependabot::Package::NpmPackageJson) }
132
+ def package_json_document
133
+ Dependabot::Package::NpmPackageJson.from_file(package_json)
132
134
  rescue JSON::ParserError
133
135
  raise Dependabot::DependencyFileNotParseable, package_json.path
134
136
  end
@@ -227,16 +229,14 @@ module Dependabot
227
229
  dependency_set = DependencySet.new
228
230
 
229
231
  package_files.each do |file|
230
- json = JSON.parse(T.must(file.content))
232
+ manifest = Dependabot::Package::NpmPackageJson.from_file(file)
231
233
 
232
234
  # TODO: Currently, Dependabot can't handle flat dependency files
233
235
  # (and will error at the FileUpdater stage, because the
234
236
  # UpdateChecker doesn't take account of flat resolution).
235
- next if json["flat"]
236
-
237
- self.class.each_dependency(json) do |name, requirement, type|
238
- next unless requirement.is_a?(String)
237
+ next if manifest.flat?
239
238
 
239
+ manifest.each_dependency do |name, requirement, type|
240
240
  # Skip dependencies using Yarn workspace cross-references as requirements
241
241
  next if requirement.start_with?("workspace:", "catalog:")
242
242
 
@@ -295,7 +295,7 @@ module Dependabot
295
295
  end
296
296
 
297
297
  sig do
298
- params(file: DependencyFile, type: T.untyped, name: String, requirement: String)
298
+ params(file: DependencyFile, type: String, name: String, requirement: String)
299
299
  .returns(T.nilable(Dependency))
300
300
  end
301
301
  def build_dependency(file:, type:, name:, requirement:)
@@ -454,14 +454,14 @@ module Dependabot
454
454
  def workspace_package_names
455
455
  @workspace_package_names ||= T.let(
456
456
  package_files.filter_map do |f|
457
- JSON.parse(T.must(f.content))["name"]
457
+ Dependabot::Package::NpmPackageJson.from_file(f).name
458
458
  end,
459
459
  T.nilable(T::Array[String])
460
460
  )
461
461
  end
462
462
 
463
463
  sig do
464
- params(requirement: String, lockfile_details: T.nilable(T::Hash[String, T.untyped]))
464
+ params(requirement: String, lockfile_details: T.nilable(Dependabot::Package::NpmLockfileDetails))
465
465
  .returns(T.nilable(T.any(String, Integer, Gem::Version)))
466
466
  end
467
467
  def version_for(requirement, lockfile_details)
@@ -483,10 +483,10 @@ module Dependabot
483
483
  end
484
484
  end
485
485
 
486
- sig { params(lockfile_details: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(String)) }
486
+ sig { params(lockfile_details: T.nilable(Dependabot::Package::NpmLockfileDetails)).returns(T.nilable(String)) }
487
487
  def git_revision_for(lockfile_details)
488
- version = T.cast(lockfile_details&.fetch("version", nil), T.nilable(String))
489
- resolved = T.cast(lockfile_details&.fetch("resolved", nil), T.nilable(String))
488
+ version = lockfile_details&.version
489
+ resolved = lockfile_details&.resolved
490
490
  [
491
491
  version&.split("#")&.last,
492
492
  resolved&.split("#")&.last,
@@ -526,11 +526,11 @@ module Dependabot
526
526
  end
527
527
 
528
528
  sig do
529
- params(lockfile_details: T.nilable(T::Hash[String, T.untyped]))
529
+ params(lockfile_details: T.nilable(Dependabot::Package::NpmLockfileDetails))
530
530
  .returns(T.nilable(T.any(String, Integer, Gem::Version)))
531
531
  end
532
532
  def lockfile_version_for(lockfile_details)
533
- semver_version_for(lockfile_details&.fetch("version", ""))
533
+ semver_version_for(lockfile_details&.version)
534
534
  end
535
535
 
536
536
  sig { params(version: T.nilable(String)).returns(T.nilable(T.any(String, Integer, Gem::Version))) }
@@ -549,18 +549,18 @@ module Dependabot
549
549
  end
550
550
 
551
551
  sig do
552
- params(name: String, requirement: String, lockfile_details: T.nilable(T::Hash[String, T.untyped]))
552
+ params(name: String, requirement: String, lockfile_details: T.nilable(Dependabot::Package::NpmLockfileDetails))
553
553
  .returns(T.nilable(T::Hash[Symbol, T.untyped]))
554
554
  end
555
555
  def source_for(name, requirement, lockfile_details)
556
556
  return git_source_for(requirement) if git_url?(requirement)
557
557
  return jsr_registry_source if requirement.start_with?("jsr:")
558
558
 
559
- resolved_url = lockfile_details&.fetch("resolved", nil)
559
+ resolved_url = lockfile_details&.resolved
560
560
 
561
- resolution = lockfile_details&.fetch("resolution", nil)
562
- package_match = resolution&.match(/__archiveUrl=(?<package_url>.+)/)
563
- resolved_url = CGI.unescape(package_match.named_captures.fetch("package_url", "")) if package_match
561
+ resolution = lockfile_details&.resolution
562
+ package_url = resolution&.[](/__archiveUrl=(.+)/, 1)
563
+ resolved_url = CGI.unescape(package_url) if package_url
564
564
 
565
565
  return unless resolved_url
566
566
  return unless resolved_url.start_with?("http")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-npm_and_yarn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.395.0
4
+ version: 0.396.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.395.0
18
+ version: 0.396.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.395.0
25
+ version: 0.396.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: debug
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -346,9 +346,11 @@ files:
346
346
  - lib/dependabot/npm_and_yarn/file_fetcher/path_dependency_builder.rb
347
347
  - lib/dependabot/npm_and_yarn/file_parser.rb
348
348
  - lib/dependabot/npm_and_yarn/file_parser/json_lock.rb
349
+ - lib/dependabot/npm_and_yarn/file_parser/json_lock/record.rb
349
350
  - lib/dependabot/npm_and_yarn/file_parser/lockfile_parser.rb
350
351
  - lib/dependabot/npm_and_yarn/file_parser/pnpm_lock.rb
351
352
  - lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb
353
+ - lib/dependabot/npm_and_yarn/file_parser/yarn_lock/record.rb
352
354
  - lib/dependabot/npm_and_yarn/file_updater.rb
353
355
  - lib/dependabot/npm_and_yarn/file_updater/berry_lockfile_handler.rb
354
356
  - lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb
@@ -390,7 +392,7 @@ licenses:
390
392
  - MIT
391
393
  metadata:
392
394
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
393
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.395.0
395
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.396.0
394
396
  rdoc_options: []
395
397
  require_paths:
396
398
  - lib