dependabot-npm_and_yarn 0.292.0 → 0.293.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e406eab7c13be2bea1200de0103017da062fcd4eda7b30652cc697cf2529c2de
4
- data.tar.gz: c41b184b80a82577f5ed87eb4df0c0c4bff862350afe5f992b75f04ac6e69f96
3
+ metadata.gz: 0d548c0891264c8b407f2b673e39266b3494683e431ac1e8b9ebc899319208f5
4
+ data.tar.gz: b43aeb89fb1a1c1e32a9d86eee5ccc188d8fc3da548ee4cbbcfbc4c76cfd59c7
5
5
  SHA512:
6
- metadata.gz: 535024739c08d5e33e7a53a300a75f16009c8227a27b27c8c758501b6328865db2ebeaaace0bc8ae94d5f199d93bd63f76f98164e1524df7896c22784aa04975
7
- data.tar.gz: e12a28a7d0933ad3fc4ccff35d36948e42b9ea9c884a132f7aed5bd9c33b67ad61037f0b29975c5fc04329a64ef7bcc8703ce3c684e4e278168706eecd1a37a7
6
+ metadata.gz: 95eef6390619686a8017fc949e5a5609e34f3675920fea726975fb42c55904e9901ef55b80df005ffa43bf74e87623ed00a9a287005af9446c93b78fcb0c010d
7
+ data.tar.gz: 9a104350702acef102f005bdc0c4649b7713288de3685992538f9e58384487802f48a1f1c6e57dd6ac244fb11e849fd89b72f2f3648355eb3115d1174846e21f
@@ -0,0 +1,46 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module NpmAndYarn
6
+ class BunPackageManager < Ecosystem::VersionManager
7
+ extend T::Sig
8
+ NAME = "bun"
9
+ LOCKFILE_NAME = "bun.lock"
10
+
11
+ # In Bun 1.1.39, the lockfile format was changed from a binary bun.lockb to a text-based bun.lock.
12
+ # https://bun.sh/blog/bun-lock-text-lockfile
13
+ MIN_SUPPORTED_VERSION = Version.new("1.1.39")
14
+ SUPPORTED_VERSIONS = T.let([MIN_SUPPORTED_VERSION].freeze, T::Array[Dependabot::Version])
15
+ DEPRECATED_VERSIONS = T.let([].freeze, T::Array[Dependabot::Version])
16
+
17
+ sig do
18
+ params(
19
+ detected_version: T.nilable(String),
20
+ raw_version: T.nilable(String),
21
+ requirement: T.nilable(Dependabot::NpmAndYarn::Requirement)
22
+ ).void
23
+ end
24
+ def initialize(detected_version: nil, raw_version: nil, requirement: nil)
25
+ super(
26
+ name: NAME,
27
+ detected_version: detected_version ? Version.new(detected_version) : nil,
28
+ version: raw_version ? Version.new(raw_version) : nil,
29
+ deprecated_versions: DEPRECATED_VERSIONS,
30
+ supported_versions: SUPPORTED_VERSIONS,
31
+ requirement: requirement
32
+ )
33
+ end
34
+
35
+ sig { override.returns(T::Boolean) }
36
+ def deprecated?
37
+ false
38
+ end
39
+
40
+ sig { override.returns(T::Boolean) }
41
+ def unsupported?
42
+ supported_versions.all? { |supported| supported > version }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -82,7 +82,7 @@ module Dependabot
82
82
 
83
83
  sig { params(lockfile: DependencyFile).returns(T::Boolean) }
84
84
  def workspaces_lockfile?(lockfile)
85
- return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"].include?(lockfile.name)
85
+ return false unless ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lock"].include?(lockfile.name)
86
86
 
87
87
  return false unless parsed_root_package_json["workspaces"] || dependency_files.any? do |file|
88
88
  file.name.end_with?("pnpm-workspace.yaml") && File.dirname(file.name) == File.dirname(lockfile.name)
@@ -148,6 +148,7 @@ module Dependabot
148
148
  "package-lock.json",
149
149
  "yarn.lock",
150
150
  "pnpm-lock.yaml",
151
+ "bun.lock",
151
152
  "npm-shrinkwrap.json"
152
153
  )
153
154
  end
@@ -68,6 +68,7 @@ module Dependabot
68
68
  package_managers["npm"] = npm_version if npm_version
69
69
  package_managers["yarn"] = yarn_version if yarn_version
70
70
  package_managers["pnpm"] = pnpm_version if pnpm_version
71
+ package_managers["bun"] = bun_version if bun_version
71
72
  package_managers["unknown"] = 1 if package_managers.empty?
72
73
 
73
74
  {
@@ -83,6 +84,7 @@ module Dependabot
83
84
  fetched_files += npm_files if npm_version
84
85
  fetched_files += yarn_files if yarn_version
85
86
  fetched_files += pnpm_files if pnpm_version
87
+ fetched_files += bun_files if bun_version
86
88
  fetched_files += lerna_files
87
89
  fetched_files += workspace_package_jsons
88
90
  fetched_files += path_dependencies(fetched_files)
@@ -120,6 +122,13 @@ module Dependabot
120
122
  fetched_pnpm_files
121
123
  end
122
124
 
125
+ sig { returns(T::Array[DependencyFile]) }
126
+ def bun_files
127
+ fetched_bun_files = []
128
+ fetched_bun_files << bun_lock if bun_lock
129
+ fetched_bun_files
130
+ end
131
+
123
132
  sig { returns(T::Array[DependencyFile]) }
124
133
  def lerna_files
125
134
  fetched_lerna_files = []
@@ -202,6 +211,16 @@ module Dependabot
202
211
  )
203
212
  end
204
213
 
214
+ sig { returns(T.nilable(T.any(Integer, String))) }
215
+ def bun_version
216
+ return @bun_version = nil unless Experiments.enabled?(:bun_updates)
217
+
218
+ @bun_version ||= T.let(
219
+ package_manager_helper.setup(BunPackageManager::NAME),
220
+ T.nilable(T.any(Integer, String))
221
+ )
222
+ end
223
+
205
224
  sig { returns(PackageManagerHelper) }
206
225
  def package_manager_helper
207
226
  @package_manager_helper ||= T.let(
@@ -219,7 +238,8 @@ module Dependabot
219
238
  {
220
239
  npm: package_lock || shrinkwrap,
221
240
  yarn: yarn_lock,
222
- pnpm: pnpm_lock
241
+ pnpm: pnpm_lock,
242
+ bun: bun_lock
223
243
  }
224
244
  end
225
245
 
@@ -261,17 +281,18 @@ module Dependabot
261
281
 
262
282
  return @pnpm_lock if @pnpm_lock || directory == "/"
263
283
 
264
- # Loop through parent directories looking for a pnpm-lock
265
- (1..directory.split("/").count).each do |i|
266
- @pnpm_lock = fetch_file_from_host(("../" * i) + PNPMPackageManager::LOCKFILE_NAME)
267
- .tap { |f| f.support_file = true }
268
- break if @pnpm_lock
269
- rescue Dependabot::DependencyFileNotFound
270
- # Ignore errors (pnpm_lock.yaml may not be present)
271
- nil
272
- end
284
+ @pnpm_lock = fetch_file_from_parent_directories(PNPMPackageManager::LOCKFILE_NAME)
285
+ end
286
+
287
+ sig { returns(T.nilable(DependencyFile)) }
288
+ def bun_lock
289
+ return @bun_lock if defined?(@bun_lock)
290
+
291
+ @bun_lock ||= T.let(fetch_file_if_present(BunPackageManager::LOCKFILE_NAME), T.nilable(DependencyFile))
273
292
 
274
- @pnpm_lock
293
+ return @bun_lock if @bun_lock || directory == "/"
294
+
295
+ @bun_lock = fetch_file_from_parent_directories(BunPackageManager::LOCKFILE_NAME)
275
296
  end
276
297
 
277
298
  sig { returns(T.nilable(DependencyFile)) }
@@ -294,17 +315,7 @@ module Dependabot
294
315
 
295
316
  return @npmrc if @npmrc || directory == "/"
296
317
 
297
- # Loop through parent directories looking for an npmrc
298
- (1..directory.split("/").count).each do |i|
299
- @npmrc = fetch_file_from_host(("../" * i) + NpmPackageManager::RC_FILENAME)
300
- .tap { |f| f.support_file = true }
301
- break if @npmrc
302
- rescue Dependabot::DependencyFileNotFound
303
- # Ignore errors (.npmrc may not be present)
304
- nil
305
- end
306
-
307
- @npmrc
318
+ @npmrc = fetch_file_from_parent_directories(NpmPackageManager::RC_FILENAME)
308
319
  end
309
320
 
310
321
  sig { returns(T.nilable(DependencyFile)) }
@@ -315,17 +326,7 @@ module Dependabot
315
326
 
316
327
  return @yarnrc if @yarnrc || directory == "/"
317
328
 
318
- # Loop through parent directories looking for an yarnrc
319
- (1..directory.split("/").count).each do |i|
320
- @yarnrc = fetch_file_from_host(("../" * i) + YarnPackageManager::RC_FILENAME)
321
- .tap { |f| f.support_file = true }
322
- break if @yarnrc
323
- rescue Dependabot::DependencyFileNotFound
324
- # Ignore errors (.yarnrc may not be present)
325
- nil
326
- end
327
-
328
- @yarnrc
329
+ @yarnrc = fetch_file_from_parent_directories(YarnPackageManager::RC_FILENAME)
329
330
  end
330
331
 
331
332
  sig { returns(T.nilable(DependencyFile)) }
@@ -699,6 +700,22 @@ module Dependabot
699
700
  Dependabot.logger.info("Repository contents path does not exist")
700
701
  end
701
702
  end
703
+
704
+ sig { params(filename: String).returns(T.nilable(DependencyFile)) }
705
+ def fetch_file_with_support(filename)
706
+ fetch_file_from_host(filename).tap { |f| f.support_file = true }
707
+ rescue Dependabot::DependencyFileNotFound
708
+ nil
709
+ end
710
+
711
+ sig { params(filename: String).returns(T.nilable(DependencyFile)) }
712
+ def fetch_file_from_parent_directories(filename)
713
+ (1..directory.split("/").count).each do |i|
714
+ file = fetch_file_with_support(("../" * i) + filename)
715
+ return file if file
716
+ end
717
+ nil
718
+ end
702
719
  end
703
720
  end
704
721
  end
@@ -0,0 +1,141 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "yaml"
5
+ require "dependabot/errors"
6
+ require "dependabot/npm_and_yarn/helpers"
7
+ require "sorbet-runtime"
8
+
9
+ module Dependabot
10
+ module NpmAndYarn
11
+ class FileParser < Dependabot::FileParsers::Base
12
+ class BunLock
13
+ extend T::Sig
14
+
15
+ sig { params(dependency_file: DependencyFile).void }
16
+ def initialize(dependency_file)
17
+ @dependency_file = dependency_file
18
+ end
19
+
20
+ sig { returns(T::Hash[String, T.untyped]) }
21
+ def parsed
22
+ @parsed ||= begin
23
+ content = begin
24
+ # Since bun.lock is a JSONC file, which is a subset of YAML, we can use YAML to parse it
25
+ YAML.load(T.must(@dependency_file.content))
26
+ rescue Psych::SyntaxError => e
27
+ raise_invalid!("malformed JSONC at line #{e.line}, column #{e.column}")
28
+ end
29
+ raise_invalid!("expected to be an object") unless content.is_a?(Hash)
30
+
31
+ version = content["lockfileVersion"]
32
+ raise_invalid!("expected 'lockfileVersion' to be an integer") unless version.is_a?(Integer)
33
+ raise_invalid!("expected 'lockfileVersion' to be >= 0") unless version >= 0
34
+ raise_invalid!("unsupported 'lockfileVersion' = #{version}") unless version.zero?
35
+
36
+ T.let(content, T.untyped)
37
+ end
38
+ end
39
+
40
+ sig { returns(Dependabot::FileParsers::Base::DependencySet) }
41
+ def dependencies
42
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
43
+
44
+ # bun.lock v0 format:
45
+ # https://github.com/oven-sh/bun/blob/c130df6c589fdf28f9f3c7f23ed9901140bc9349/src/install/bun.lock.zig#L595-L605
46
+
47
+ packages = parsed["packages"]
48
+ raise_invalid!("expected 'packages' to be an object") unless packages.is_a?(Hash)
49
+
50
+ packages.each do |key, details|
51
+ raise_invalid!("expected 'packages.#{key}' to be an array") unless details.is_a?(Array)
52
+
53
+ resolution = details.first
54
+ raise_invalid!("expected 'packages.#{key}[0]' to be a string") unless resolution.is_a?(String)
55
+
56
+ name, version = resolution.split(/(?<=\w)\@/)
57
+ next if name.empty?
58
+
59
+ semver = Version.semver_for(version)
60
+ next unless semver
61
+
62
+ dependency_set << Dependency.new(
63
+ name: name,
64
+ version: semver.to_s,
65
+ package_manager: "npm_and_yarn",
66
+ requirements: []
67
+ )
68
+ end
69
+
70
+ dependency_set
71
+ end
72
+
73
+ sig do
74
+ params(dependency_name: String, requirement: T.untyped, _manifest_name: String)
75
+ .returns(T.nilable(T::Hash[String, T.untyped]))
76
+ end
77
+ def details(dependency_name, requirement, _manifest_name)
78
+ packages = parsed["packages"]
79
+ return unless packages.is_a?(Hash)
80
+
81
+ candidates =
82
+ packages
83
+ .select { |name, _| name == dependency_name }
84
+ .values
85
+
86
+ # If there's only one entry for this dependency, use it, even if
87
+ # the requirement in the lockfile doesn't match
88
+ if candidates.one?
89
+ parse_details(candidates.first)
90
+ else
91
+ candidate = candidates.find do |label, _|
92
+ label.scan(/(?<=\w)\@(?:npm:)?([^\s,]+)/).flatten.include?(requirement)
93
+ end&.last
94
+ parse_details(candidate)
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ sig { params(message: String).void }
101
+ def raise_invalid!(message)
102
+ raise Dependabot::DependencyFileNotParseable.new(@dependency_file.path, "Invalid bun.lock file: #{message}")
103
+ end
104
+
105
+ sig do
106
+ params(entry: T.nilable(T::Array[T.untyped])).returns(T.nilable(T::Hash[String, T.untyped]))
107
+ end
108
+ def parse_details(entry)
109
+ return unless entry.is_a?(Array)
110
+
111
+ # Either:
112
+ # - "{name}@{version}", registry, details, integrity
113
+ # - "{name}@{resolution}", details
114
+ resolution = entry.first
115
+ return unless resolution.is_a?(String)
116
+
117
+ name, version = resolution.split(/(?<=\w)\@/)
118
+ semver = Version.semver_for(version)
119
+
120
+ if semver
121
+ registry, details, integrity = entry[1..3]
122
+ {
123
+ "name" => name,
124
+ "version" => semver.to_s,
125
+ "registry" => registry,
126
+ "details" => details,
127
+ "integrity" => integrity
128
+ }
129
+ else
130
+ details = entry[1]
131
+ {
132
+ "name" => name,
133
+ "resolution" => version,
134
+ "details" => details
135
+ }
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -15,6 +15,11 @@ module Dependabot
15
15
  require "dependabot/npm_and_yarn/file_parser/yarn_lock"
16
16
  require "dependabot/npm_and_yarn/file_parser/pnpm_lock"
17
17
  require "dependabot/npm_and_yarn/file_parser/json_lock"
18
+ require "dependabot/npm_and_yarn/file_parser/bun_lock"
19
+
20
+ DEFAULT_LOCKFILES = %w(package-lock.json yarn.lock pnpm-lock.yaml bun.lock npm-shrinkwrap.json).freeze
21
+
22
+ LockFile = T.type_alias { T.any(JsonLock, YarnLock, PnpmLock, BunLock) }
18
23
 
19
24
  sig { params(dependency_files: T::Array[DependencyFile]).void }
20
25
  def initialize(dependency_files:)
@@ -29,7 +34,7 @@ module Dependabot
29
34
  # end up unique by name. That's not a perfect representation of
30
35
  # the nested nature of JS resolution, but it makes everything work
31
36
  # comparably to other flat-resolution strategies
32
- (yarn_locks + pnpm_locks + package_locks + shrinkwraps).each do |file|
37
+ (yarn_locks + pnpm_locks + package_locks + bun_locks + shrinkwraps).each do |file|
33
38
  dependency_set += lockfile_for(file).dependencies
34
39
  end
35
40
 
@@ -64,58 +69,59 @@ module Dependabot
64
69
  sig { params(manifest_filename: String).returns(T::Array[DependencyFile]) }
65
70
  def potential_lockfiles_for_manifest(manifest_filename)
66
71
  dir_name = File.dirname(manifest_filename)
67
- possible_lockfile_names =
68
- %w(package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock).map do |f|
69
- Pathname.new(File.join(dir_name, f)).cleanpath.to_path
70
- end +
71
- %w(yarn.lock pnpm-lock.yaml package-lock.json npm-shrinkwrap.json)
72
+ possible_lockfile_names = DEFAULT_LOCKFILES.map do |f|
73
+ Pathname.new(File.join(dir_name, f)).cleanpath.to_path
74
+ end + DEFAULT_LOCKFILES
72
75
 
73
76
  possible_lockfile_names.uniq
74
77
  .filter_map { |nm| dependency_files.find { |f| f.name == nm } }
75
78
  end
76
79
 
77
- sig { params(file: DependencyFile).returns(T.any(JsonLock, YarnLock, PnpmLock)) }
80
+ sig { params(file: DependencyFile).returns(LockFile) }
78
81
  def lockfile_for(file)
79
- @lockfiles ||= T.let({}, T.nilable(T::Hash[String, T.any(JsonLock, YarnLock, PnpmLock)]))
80
- @lockfiles[file.name] ||= if [*package_locks, *shrinkwraps].include?(file)
82
+ @lockfiles ||= T.let({}, T.nilable(T::Hash[String, LockFile]))
83
+ @lockfiles[file.name] ||= case file.name
84
+ when *package_locks.map(&:name), *shrinkwraps.map(&:name)
81
85
  JsonLock.new(file)
82
- elsif yarn_locks.include?(file)
86
+ when *yarn_locks.map(&:name)
83
87
  YarnLock.new(file)
84
- else
88
+ when *pnpm_locks.map(&:name)
85
89
  PnpmLock.new(file)
90
+ when *bun_locks.map(&:name)
91
+ BunLock.new(file)
92
+ else
93
+ raise "Unexpected lockfile: #{file.name}"
86
94
  end
87
95
  end
88
96
 
97
+ sig { params(extension: String).returns(T::Array[DependencyFile]) }
98
+ def select_files_by_extension(extension)
99
+ dependency_files.select { |f| f.name.end_with?(extension) }
100
+ end
101
+
89
102
  sig { returns(T::Array[DependencyFile]) }
90
103
  def package_locks
91
- @package_locks ||= T.let(
92
- dependency_files
93
- .select { |f| f.name.end_with?("package-lock.json") }, T.nilable(T::Array[DependencyFile])
94
- )
104
+ @package_locks ||= T.let(select_files_by_extension("package-lock.json"), T.nilable(T::Array[DependencyFile]))
95
105
  end
96
106
 
97
107
  sig { returns(T::Array[DependencyFile]) }
98
108
  def pnpm_locks
99
- @pnpm_locks ||= T.let(
100
- dependency_files
101
- .select { |f| f.name.end_with?("pnpm-lock.yaml") }, T.nilable(T::Array[DependencyFile])
102
- )
109
+ @pnpm_locks ||= T.let(select_files_by_extension("pnpm-lock.yaml"), T.nilable(T::Array[DependencyFile]))
110
+ end
111
+
112
+ sig { returns(T::Array[DependencyFile]) }
113
+ def bun_locks
114
+ @bun_locks ||= T.let(select_files_by_extension("bun.lock"), T.nilable(T::Array[DependencyFile]))
103
115
  end
104
116
 
105
117
  sig { returns(T::Array[DependencyFile]) }
106
118
  def yarn_locks
107
- @yarn_locks ||= T.let(
108
- dependency_files
109
- .select { |f| f.name.end_with?("yarn.lock") }, T.nilable(T::Array[DependencyFile])
110
- )
119
+ @yarn_locks ||= T.let(select_files_by_extension("yarn.lock"), T.nilable(T::Array[DependencyFile]))
111
120
  end
112
121
 
113
122
  sig { returns(T::Array[DependencyFile]) }
114
123
  def shrinkwraps
115
- @shrinkwraps ||= T.let(
116
- dependency_files
117
- .select { |f| f.name.end_with?("npm-shrinkwrap.json") }, T.nilable(T::Array[DependencyFile])
118
- )
124
+ @shrinkwraps ||= T.let(select_files_by_extension("npm-shrinkwrap.json"), T.nilable(T::Array[DependencyFile]))
119
125
  end
120
126
 
121
127
  sig { returns(T.class_of(Dependabot::NpmAndYarn::Version)) }
@@ -26,6 +26,10 @@ module Dependabot
26
26
  end
27
27
 
28
28
  def dependencies
29
+ if Dependabot::Experiments.enabled?(:enable_fix_for_pnpm_no_change_error)
30
+ return dependencies_with_prioritization
31
+ end
32
+
29
33
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
30
34
 
31
35
  parsed.each do |details|
@@ -52,6 +56,49 @@ module Dependabot
52
56
  dependency_set
53
57
  end
54
58
 
59
+ def dependencies_with_prioritization
60
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
61
+
62
+ # Separate dependencies into two categories: with specifiers and without specifiers.
63
+ dependencies_with_specifiers = [] # Main dependencies with specifiers.
64
+ dependencies_without_specifiers = [] # Subdependencies without specifiers.
65
+
66
+ parsed.each do |details|
67
+ next if details["aliased"]
68
+
69
+ name = details["name"]
70
+ version = details["version"]
71
+
72
+ dependency_args = {
73
+ name: name,
74
+ version: version,
75
+ package_manager: "npm_and_yarn",
76
+ requirements: []
77
+ }
78
+
79
+ # Add metadata for subdependencies if marked as a dev dependency.
80
+ dependency_args[:subdependency_metadata] = [{ production: !details["dev"] }] if details["dev"]
81
+
82
+ specifiers = details["specifiers"]
83
+ if specifiers&.any?
84
+ dependencies_with_specifiers << dependency_args
85
+ else
86
+ dependencies_without_specifiers << dependency_args
87
+ end
88
+ end
89
+
90
+ # Add prioritized dependencies to the dependency set.
91
+ dependencies_with_specifiers.each do |dependency_args|
92
+ dependency_set << Dependency.new(**dependency_args)
93
+ end
94
+
95
+ dependencies_without_specifiers.each do |dependency_args|
96
+ dependency_set << Dependency.new(**dependency_args)
97
+ end
98
+
99
+ dependency_set
100
+ end
101
+
55
102
  def details(dependency_name, requirement, _manifest_name)
56
103
  details_candidates = parsed.select { |info| info["name"] == dependency_name }
57
104
 
@@ -110,7 +110,8 @@ module Dependabot
110
110
  {
111
111
  npm: package_lock || shrinkwrap,
112
112
  yarn: yarn_lock,
113
- pnpm: pnpm_lock
113
+ pnpm: pnpm_lock,
114
+ bun: bun_lock
114
115
  }
115
116
  end
116
117
 
@@ -167,6 +168,13 @@ module Dependabot
167
168
  end, T.nilable(Dependabot::DependencyFile))
168
169
  end
169
170
 
171
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
172
+ def bun_lock
173
+ @bun_lock ||= T.let(dependency_files.find do |f|
174
+ f.name == BunPackageManager::LOCKFILE_NAME
175
+ end, T.nilable(Dependabot::DependencyFile))
176
+ end
177
+
170
178
  sig { returns(T.nilable(Dependabot::DependencyFile)) }
171
179
  def npmrc
172
180
  @npmrc ||= T.let(dependency_files.find do |f|