dependabot-npm_and_yarn 0.293.0 → 0.295.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/helpers/lib/npm/vulnerability-auditor.js +16 -16
- data/helpers/lib/npm6/updater.js +1 -1
- data/lib/dependabot/npm_and_yarn/bun_package_manager.rb +1 -1
- data/lib/dependabot/npm_and_yarn/constraint_helper.rb +306 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +12 -3
- data/lib/dependabot/npm_and_yarn/file_parser/bun_lock.rb +0 -1
- data/lib/dependabot/npm_and_yarn/file_parser.rb +47 -10
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +25 -2
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +81 -12
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_workspace_updater.rb +140 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +56 -1
- data/lib/dependabot/npm_and_yarn/helpers.rb +14 -2
- data/lib/dependabot/npm_and_yarn/npm_package_manager.rb +4 -10
- data/lib/dependabot/npm_and_yarn/package_manager.rb +59 -24
- data/lib/dependabot/npm_and_yarn/version.rb +4 -0
- data/lib/dependabot/npm_and_yarn/version_selector.rb +32 -7
- data/lib/dependabot/npm_and_yarn.rb +19 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14edb941111a95dc07d83e6449c01bd3b0f4b92c4e3168acd19d31ce1fa96183
|
4
|
+
data.tar.gz: 0ec9a05bb2ebda169ad58028bfc2c0465ef0bbf192265f317570842f5aaa81b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5822f02ef7a153c079c70f91724efe53250ea696af5c0a995cfb86537be568e155344eb9cbb1481c2f2be01c080d486a0b98300dd318aa72c6271b36bd0d906e
|
7
|
+
data.tar.gz: 85bf7be8bb171f92b38a67d7c0b12b6c0e68056a6cdf09a0b20d8c2054517276e614e1bc7fd01bc366a53e917ba54c032ee816c977d47ca4eb11c22afda6c2ee
|
@@ -97,9 +97,9 @@ async function findVulnerableDependencies(directory, advisories) {
|
|
97
97
|
|
98
98
|
for (const group of groupedFixUpdateChains.values()) {
|
99
99
|
const fixUpdateNode = group[0].nodes[0]
|
100
|
-
const groupTopLevelAncestors = group.reduce((
|
100
|
+
const groupTopLevelAncestors = group.reduce((ancestor, chain) => {
|
101
101
|
const topLevelNode = chain.nodes[chain.nodes.length - 1]
|
102
|
-
return
|
102
|
+
return ancestor.add(topLevelNode.name)
|
103
103
|
}, new Set())
|
104
104
|
|
105
105
|
// Add group's top-level ancestors to the set of all top-level ancestors of
|
@@ -269,23 +269,23 @@ const maybeReadFile = file => {
|
|
269
269
|
}
|
270
270
|
|
271
271
|
function loadCACerts(npmConfig) {
|
272
|
-
|
273
|
-
|
274
|
-
|
272
|
+
if (npmConfig.ca) {
|
273
|
+
return npmConfig.ca
|
274
|
+
}
|
275
275
|
|
276
|
-
|
277
|
-
|
278
|
-
|
276
|
+
if (!npmConfig.cafile) {
|
277
|
+
return
|
278
|
+
}
|
279
279
|
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
280
|
+
const raw = maybeReadFile(npmConfig.cafile)
|
281
|
+
if (!raw) {
|
282
|
+
return
|
283
|
+
}
|
284
284
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
285
|
+
const delim = '-----END CERTIFICATE-----'
|
286
|
+
return raw.replace(/\r\n/g, '\n').split(delim)
|
287
|
+
.filter(section => section.trim())
|
288
|
+
.map(section => section.trimStart() + delim)
|
289
289
|
}
|
290
290
|
|
291
291
|
module.exports = { findVulnerableDependencies }
|
data/helpers/lib/npm6/updater.js
CHANGED
@@ -0,0 +1,306 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module Dependabot
|
7
|
+
module NpmAndYarn
|
8
|
+
module ConstraintHelper
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
INVALID = "invalid" # Invalid constraint
|
12
|
+
# Regex Components for Semantic Versioning
|
13
|
+
DIGIT = "\\d+" # Matches a single number (e.g., "1")
|
14
|
+
PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
|
15
|
+
BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
|
16
|
+
DOT = "\\." # Matches a literal dot "."
|
17
|
+
|
18
|
+
# Matches semantic versions:
|
19
|
+
VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
|
20
|
+
|
21
|
+
VERSION_REGEX = T.let(/\A#{VERSION}\z/o, Regexp)
|
22
|
+
|
23
|
+
# SemVer regex: major.minor.patch[-prerelease][+build]
|
24
|
+
SEMVER_REGEX = /^(?<version>\d+\.\d+\.\d+)(?:-(?<prerelease>[a-zA-Z0-9.-]+))?(?:\+(?<build>[a-zA-Z0-9.-]+))?$/
|
25
|
+
|
26
|
+
# Constraint Types as Constants
|
27
|
+
CARET_CONSTRAINT_REGEX = T.let(/^\^(#{VERSION})$/, Regexp)
|
28
|
+
TILDE_CONSTRAINT_REGEX = T.let(/^~(#{VERSION})$/, Regexp)
|
29
|
+
EXACT_CONSTRAINT_REGEX = T.let(/^(#{VERSION})$/, Regexp)
|
30
|
+
GREATER_THAN_EQUAL_REGEX = T.let(/^>=(#{VERSION})$/, Regexp)
|
31
|
+
LESS_THAN_EQUAL_REGEX = T.let(/^<=(#{VERSION})$/, Regexp)
|
32
|
+
GREATER_THAN_REGEX = T.let(/^>(#{VERSION})$/, Regexp)
|
33
|
+
LESS_THAN_REGEX = T.let(/^<(#{VERSION})$/, Regexp)
|
34
|
+
WILDCARD_REGEX = T.let(/^\*$/, Regexp)
|
35
|
+
|
36
|
+
# Unified Regex for Valid Constraints
|
37
|
+
VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
|
38
|
+
CARET_CONSTRAINT_REGEX,
|
39
|
+
TILDE_CONSTRAINT_REGEX,
|
40
|
+
EXACT_CONSTRAINT_REGEX,
|
41
|
+
GREATER_THAN_EQUAL_REGEX,
|
42
|
+
LESS_THAN_EQUAL_REGEX,
|
43
|
+
GREATER_THAN_REGEX,
|
44
|
+
LESS_THAN_REGEX,
|
45
|
+
WILDCARD_REGEX
|
46
|
+
).freeze, Regexp)
|
47
|
+
|
48
|
+
# Validates if the provided semver constraint expression from a `package.json` is valid.
|
49
|
+
# A valid semver constraint expression in `package.json` can consist of multiple groups
|
50
|
+
# separated by logical OR (`||`). Within each group, space-separated constraints are treated
|
51
|
+
# as logical AND. Each individual constraint must conform to the semver rules defined in
|
52
|
+
# `VALID_CONSTRAINT_REGEX`.
|
53
|
+
#
|
54
|
+
# Example (valid `package.json` semver constraints):
|
55
|
+
# ">=1.2.3 <2.0.0 || ~3.4.5" → Valid (space-separated constraints are AND, `||` is OR)
|
56
|
+
# "^1.0.0 || >=2.0.0 <3.0.0" → Valid (caret and range constraints combined)
|
57
|
+
# "1.2.3" → Valid (exact version)
|
58
|
+
# "*" → Valid (wildcard allows any version)
|
59
|
+
#
|
60
|
+
# Example (invalid `package.json` semver constraints):
|
61
|
+
# ">=1.2.3 && <2.0.0" → Invalid (`&&` is not valid in semver)
|
62
|
+
# ">=x.y.z" → Invalid (non-numeric version parts are not valid)
|
63
|
+
# "1.2.3 ||" → Invalid (trailing OR operator)
|
64
|
+
#
|
65
|
+
# @param constraint_expression [String] The semver constraint expression from `package.json` to validate.
|
66
|
+
# @return [T::Boolean] Returns true if the constraint expression is valid semver, false otherwise.
|
67
|
+
sig { params(constraint_expression: T.nilable(String)).returns(T::Boolean) }
|
68
|
+
def self.valid_constraint_expression?(constraint_expression)
|
69
|
+
normalized_constraint = constraint_expression&.strip
|
70
|
+
|
71
|
+
# Treat nil or empty input as valid (no constraints)
|
72
|
+
return true if normalized_constraint.nil? || normalized_constraint.empty?
|
73
|
+
|
74
|
+
# Split the expression by logical OR (`||`) into groups
|
75
|
+
normalized_constraint.split("||").reject(&:empty?).all? do |or_group|
|
76
|
+
or_group.split(/\s+/).reject(&:empty?).all? do |and_constraint|
|
77
|
+
and_constraint.match?(VALID_CONSTRAINT_REGEX)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Extract unique constraints from the given constraint expression.
|
83
|
+
# @param constraint_expression [T.nilable(String)] The semver constraint expression.
|
84
|
+
# @return [T::Array[String]] The list of unique Ruby-compatible constraints.
|
85
|
+
sig do
|
86
|
+
params(
|
87
|
+
constraint_expression: T.nilable(String),
|
88
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
89
|
+
)
|
90
|
+
.returns(T.nilable(T::Array[String]))
|
91
|
+
end
|
92
|
+
def self.extract_constraints(constraint_expression, dependabot_versions = nil)
|
93
|
+
normalized_constraint = constraint_expression&.strip
|
94
|
+
return [] if normalized_constraint.nil? || normalized_constraint.empty?
|
95
|
+
|
96
|
+
parsed_constraints = parse_constraints(normalized_constraint, dependabot_versions)
|
97
|
+
|
98
|
+
return nil unless parsed_constraints
|
99
|
+
|
100
|
+
parsed_constraints.filter_map { |parsed| parsed[:constraint] }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Find the highest version from the given constraint expression.
|
104
|
+
# @param constraint_expression [T.nilable(String)] The semver constraint expression.
|
105
|
+
# @return [T.nilable(String)] The highest version, or nil if no versions are available.
|
106
|
+
sig do
|
107
|
+
params(
|
108
|
+
constraint_expression: T.nilable(String),
|
109
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
110
|
+
)
|
111
|
+
.returns(T.nilable(String))
|
112
|
+
end
|
113
|
+
def self.find_highest_version_from_constraint_expression(constraint_expression, dependabot_versions = nil)
|
114
|
+
normalized_constraint = constraint_expression&.strip
|
115
|
+
return nil if normalized_constraint.nil? || normalized_constraint.empty?
|
116
|
+
|
117
|
+
parsed_constraints = parse_constraints(normalized_constraint, dependabot_versions)
|
118
|
+
|
119
|
+
return nil unless parsed_constraints
|
120
|
+
|
121
|
+
parsed_constraints
|
122
|
+
.filter_map { |parsed| parsed[:version] } # Extract all versions
|
123
|
+
.max_by { |version| Version.new(version) }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Parse all constraints (split by logical OR `||`) and convert to Ruby-compatible constraints.
|
127
|
+
# Return:
|
128
|
+
# - `nil` if the constraint expression is invalid
|
129
|
+
# - `[]` if the constraint expression is valid but represents "no constraints"
|
130
|
+
# - An array of hashes for valid constraints with details about the constraint and version
|
131
|
+
sig do
|
132
|
+
params(
|
133
|
+
constraint_expression: T.nilable(String),
|
134
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
135
|
+
)
|
136
|
+
.returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
|
137
|
+
end
|
138
|
+
def self.parse_constraints(constraint_expression, dependabot_versions = nil)
|
139
|
+
normalized_constraint = constraint_expression&.strip
|
140
|
+
|
141
|
+
# Return an empty array for valid "no constraints" (nil or empty input)
|
142
|
+
return [] if normalized_constraint.nil? || normalized_constraint.empty?
|
143
|
+
|
144
|
+
# Return nil for invalid constraints
|
145
|
+
return nil unless valid_constraint_expression?(normalized_constraint)
|
146
|
+
|
147
|
+
# Parse valid constraints
|
148
|
+
constraints = normalized_constraint.split("||").flat_map do |or_group|
|
149
|
+
or_group.strip.split(/\s+/).map(&:strip)
|
150
|
+
end.then do |normalized_constraints| # rubocop:disable Style/MultilineBlockChain
|
151
|
+
to_ruby_constraints_with_versions(normalized_constraints, dependabot_versions)
|
152
|
+
end.uniq { |parsed| parsed[:constraint] } # Ensure uniqueness based on `:constraint` # rubocop:disable Style/MultilineBlockChain
|
153
|
+
constraints
|
154
|
+
end
|
155
|
+
|
156
|
+
sig do
|
157
|
+
params(
|
158
|
+
constraints: T::Array[String],
|
159
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
160
|
+
).returns(T::Array[T::Hash[Symbol, T.nilable(String)]])
|
161
|
+
end
|
162
|
+
def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
|
163
|
+
constraints.filter_map do |constraint|
|
164
|
+
parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
|
165
|
+
parsed if parsed && parsed[:constraint] # Only include valid constraints
|
166
|
+
end.uniq
|
167
|
+
end
|
168
|
+
|
169
|
+
# rubocop:disable Metrics/MethodLength
|
170
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
171
|
+
# rubocop:disable Metrics/AbcSize
|
172
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
173
|
+
# Converts a semver constraint to a Ruby-compatible constraint and extracts the version, if available.
|
174
|
+
# @param constraint [String] The semver constraint to parse.
|
175
|
+
# @return [T.nilable(T::Hash[Symbol, T.nilable(String)])] Returns the Ruby-compatible constraint and the version,
|
176
|
+
# if available, or nil if the constraint is invalid.
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# to_ruby_constraint_with_version("=1.2.3") # => { constraint: "=1.2.3", version: "1.2.3" }
|
180
|
+
# to_ruby_constraint_with_version("^1.2.3") # => { constraint: ">=1.2.3 <2.0.0", version: "1.2.3" }
|
181
|
+
# to_ruby_constraint_with_version("*") # => { constraint: nil, version: nil }
|
182
|
+
# to_ruby_constraint_with_version("invalid") # => nil
|
183
|
+
sig do
|
184
|
+
params(
|
185
|
+
constraint: String,
|
186
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
187
|
+
)
|
188
|
+
.returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
|
189
|
+
end
|
190
|
+
def self.to_ruby_constraint_with_version(constraint, dependabot_versions = [])
|
191
|
+
return nil if constraint.empty?
|
192
|
+
|
193
|
+
case constraint
|
194
|
+
when EXACT_CONSTRAINT_REGEX # Exact version, e.g., "1.2.3-alpha"
|
195
|
+
return unless Regexp.last_match
|
196
|
+
|
197
|
+
full_version = Regexp.last_match(1)
|
198
|
+
{ constraint: "=#{full_version}", version: full_version }
|
199
|
+
when CARET_CONSTRAINT_REGEX # Caret constraint, e.g., "^1.2.3"
|
200
|
+
return unless Regexp.last_match
|
201
|
+
|
202
|
+
full_version = Regexp.last_match(1)
|
203
|
+
_, major, minor = version_components(full_version)
|
204
|
+
return nil if major.nil?
|
205
|
+
|
206
|
+
ruby_constraint =
|
207
|
+
if major.to_i.zero?
|
208
|
+
minor.nil? ? ">=#{full_version} <1.0.0" : ">=#{full_version} <0.#{minor.to_i + 1}.0"
|
209
|
+
else
|
210
|
+
">=#{full_version} <#{major.to_i + 1}.0.0"
|
211
|
+
end
|
212
|
+
{ constraint: ruby_constraint, version: full_version }
|
213
|
+
when TILDE_CONSTRAINT_REGEX # Tilde constraint, e.g., "~1.2.3"
|
214
|
+
return unless Regexp.last_match
|
215
|
+
|
216
|
+
full_version = Regexp.last_match(1)
|
217
|
+
_, major, minor = version_components(full_version)
|
218
|
+
ruby_constraint =
|
219
|
+
if minor.nil?
|
220
|
+
">=#{full_version} <#{major.to_i + 1}.0.0"
|
221
|
+
else
|
222
|
+
">=#{full_version} <#{major}.#{minor.to_i + 1}.0"
|
223
|
+
end
|
224
|
+
{ constraint: ruby_constraint, version: full_version }
|
225
|
+
when GREATER_THAN_EQUAL_REGEX # Greater than or equal, e.g., ">=1.2.3"
|
226
|
+
|
227
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
228
|
+
|
229
|
+
found_version = highest_matching_version(
|
230
|
+
dependabot_versions,
|
231
|
+
T.must(Regexp.last_match(1))
|
232
|
+
) do |version, constraint_version|
|
233
|
+
version >= Version.new(constraint_version)
|
234
|
+
end
|
235
|
+
{ constraint: ">=#{Regexp.last_match(1)}", version: found_version&.to_s }
|
236
|
+
when LESS_THAN_EQUAL_REGEX # Less than or equal, e.g., "<=1.2.3"
|
237
|
+
return unless Regexp.last_match
|
238
|
+
|
239
|
+
full_version = Regexp.last_match(1)
|
240
|
+
{ constraint: "<=#{full_version}", version: full_version }
|
241
|
+
when GREATER_THAN_REGEX # Greater than, e.g., ">1.2.3"
|
242
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
243
|
+
|
244
|
+
found_version = highest_matching_version(
|
245
|
+
dependabot_versions,
|
246
|
+
T.must(Regexp.last_match(1))
|
247
|
+
) do |version, constraint_version|
|
248
|
+
version > Version.new(constraint_version)
|
249
|
+
end
|
250
|
+
{ constraint: ">#{Regexp.last_match(1)}", version: found_version&.to_s }
|
251
|
+
when LESS_THAN_REGEX # Less than, e.g., "<1.2.3"
|
252
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
253
|
+
|
254
|
+
found_version = highest_matching_version(
|
255
|
+
dependabot_versions,
|
256
|
+
T.must(Regexp.last_match(1))
|
257
|
+
) do |version, constraint_version|
|
258
|
+
version < Version.new(constraint_version)
|
259
|
+
end
|
260
|
+
{ constraint: "<#{Regexp.last_match(1)}", version: found_version&.to_s }
|
261
|
+
when WILDCARD_REGEX # Wildcard
|
262
|
+
{ constraint: nil, version: dependabot_versions&.max&.to_s } # Explicitly valid but no specific constraint
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
sig do
|
267
|
+
params(
|
268
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version]),
|
269
|
+
constraint_version: String,
|
270
|
+
condition: T.proc.params(version: Dependabot::Version, constraint: Dependabot::Version).returns(T::Boolean)
|
271
|
+
)
|
272
|
+
.returns(T.nilable(Dependabot::Version))
|
273
|
+
end
|
274
|
+
def self.highest_matching_version(dependabot_versions, constraint_version, &condition)
|
275
|
+
return unless dependabot_versions&.any?
|
276
|
+
|
277
|
+
# Returns the highest version that satisfies the condition, or nil if none.
|
278
|
+
dependabot_versions
|
279
|
+
.sort
|
280
|
+
.reverse
|
281
|
+
.find { |version| condition.call(version, Version.new(constraint_version)) } # rubocop:disable Performance/RedundantBlockCall
|
282
|
+
end
|
283
|
+
|
284
|
+
# rubocop:enable Metrics/MethodLength
|
285
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
286
|
+
# rubocop:enable Metrics/AbcSize
|
287
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
288
|
+
|
289
|
+
# Parses a semantic version string into its components as per the SemVer spec
|
290
|
+
# Example: "1.2.3-alpha+001" → ["1.2.3", "1", "2", "3", "alpha", "001"]
|
291
|
+
sig { params(full_version: T.nilable(String)).returns(T.nilable(T::Array[String])) }
|
292
|
+
def self.version_components(full_version)
|
293
|
+
return [] if full_version.nil?
|
294
|
+
|
295
|
+
match = full_version.match(SEMVER_REGEX)
|
296
|
+
return [] unless match
|
297
|
+
|
298
|
+
version = match[:version]
|
299
|
+
return [] unless version
|
300
|
+
|
301
|
+
major, minor, patch = version.split(".")
|
302
|
+
[version, major, minor, patch, match[:prerelease], match[:build]].compact
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
@@ -213,7 +213,7 @@ module Dependabot
|
|
213
213
|
|
214
214
|
sig { returns(T.nilable(T.any(Integer, String))) }
|
215
215
|
def bun_version
|
216
|
-
return @bun_version = nil unless
|
216
|
+
return @bun_version = nil unless allow_beta_ecosystems?
|
217
217
|
|
218
218
|
@bun_version ||= T.let(
|
219
219
|
package_manager_helper.setup(BunPackageManager::NAME),
|
@@ -453,6 +453,15 @@ module Dependabot
|
|
453
453
|
|
454
454
|
resolution_deps = resolution_objects.flat_map(&:to_a)
|
455
455
|
.map do |path, value|
|
456
|
+
# skip dependencies that contain invalid values such as inline comments, null, etc.
|
457
|
+
|
458
|
+
unless value.is_a?(String)
|
459
|
+
Dependabot.logger.warn("File fetcher: Skipping dependency \"#{path}\" " \
|
460
|
+
"with value: \"#{value}\"")
|
461
|
+
|
462
|
+
next
|
463
|
+
end
|
464
|
+
|
456
465
|
convert_dependency_path_to_name(path, value)
|
457
466
|
end
|
458
467
|
|
@@ -645,8 +654,8 @@ module Dependabot
|
|
645
654
|
def parsed_pnpm_workspace_yaml
|
646
655
|
return {} unless pnpm_workspace_yaml
|
647
656
|
|
648
|
-
YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content))
|
649
|
-
rescue Psych::SyntaxError
|
657
|
+
YAML.safe_load(T.must(T.must(pnpm_workspace_yaml).content), aliases: true)
|
658
|
+
rescue Psych::SyntaxError, Psych::BadAlias
|
650
659
|
raise Dependabot::DependencyFileNotParseable, T.must(pnpm_workspace_yaml).path
|
651
660
|
end
|
652
661
|
|
@@ -31,7 +31,6 @@ module Dependabot
|
|
31
31
|
version = content["lockfileVersion"]
|
32
32
|
raise_invalid!("expected 'lockfileVersion' to be an integer") unless version.is_a?(Integer)
|
33
33
|
raise_invalid!("expected 'lockfileVersion' to be >= 0") unless version >= 0
|
34
|
-
raise_invalid!("unsupported 'lockfileVersion' = #{version}") unless version.zero?
|
35
34
|
|
36
35
|
T.let(content, T.untyped)
|
37
36
|
end
|
@@ -55,10 +55,11 @@ module Dependabot
|
|
55
55
|
end
|
56
56
|
|
57
57
|
sig { override.returns(T::Array[Dependency]) }
|
58
|
-
def parse
|
58
|
+
def parse # rubocop:disable Metrics/PerceivedComplexity
|
59
59
|
dependency_set = DependencySet.new
|
60
60
|
dependency_set += manifest_dependencies
|
61
61
|
dependency_set += lockfile_dependencies
|
62
|
+
dependency_set += workspace_catalog_dependencies if enable_pnpm_workspace_catalog?
|
62
63
|
|
63
64
|
dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
|
64
65
|
|
@@ -93,6 +94,11 @@ module Dependabot
|
|
93
94
|
|
94
95
|
private
|
95
96
|
|
97
|
+
sig { returns(T.nilable(T::Boolean)) }
|
98
|
+
def enable_pnpm_workspace_catalog?
|
99
|
+
pnpm_workspace_yml && Dependabot::Experiments.enabled?(:enable_pnpm_workspace_catalog)
|
100
|
+
end
|
101
|
+
|
96
102
|
sig { returns(PackageManagerHelper) }
|
97
103
|
def package_manager_helper
|
98
104
|
@package_manager_helper ||= T.let(
|
@@ -143,56 +149,63 @@ module Dependabot
|
|
143
149
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
144
150
|
def shrinkwrap
|
145
151
|
@shrinkwrap ||= T.let(dependency_files.find do |f|
|
146
|
-
f.name
|
152
|
+
f.name.end_with?(NpmPackageManager::SHRINKWRAP_LOCKFILE_NAME)
|
147
153
|
end, T.nilable(Dependabot::DependencyFile))
|
148
154
|
end
|
149
155
|
|
150
156
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
151
157
|
def package_lock
|
152
158
|
@package_lock ||= T.let(dependency_files.find do |f|
|
153
|
-
f.name
|
159
|
+
f.name.end_with?(NpmPackageManager::LOCKFILE_NAME)
|
154
160
|
end, T.nilable(Dependabot::DependencyFile))
|
155
161
|
end
|
156
162
|
|
157
163
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
158
164
|
def yarn_lock
|
159
165
|
@yarn_lock ||= T.let(dependency_files.find do |f|
|
160
|
-
f.name
|
166
|
+
f.name.end_with?(YarnPackageManager::LOCKFILE_NAME)
|
161
167
|
end, T.nilable(Dependabot::DependencyFile))
|
162
168
|
end
|
163
169
|
|
164
170
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
165
171
|
def pnpm_lock
|
166
172
|
@pnpm_lock ||= T.let(dependency_files.find do |f|
|
167
|
-
f.name
|
173
|
+
f.name.end_with?(PNPMPackageManager::LOCKFILE_NAME)
|
174
|
+
end, T.nilable(Dependabot::DependencyFile))
|
175
|
+
end
|
176
|
+
|
177
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
178
|
+
def pnpm_workspace_yml
|
179
|
+
@pnpm_workspace_yml ||= T.let(dependency_files.find do |f|
|
180
|
+
f.name.end_with?(PNPMPackageManager::PNPM_WS_YML_FILENAME)
|
168
181
|
end, T.nilable(Dependabot::DependencyFile))
|
169
182
|
end
|
170
183
|
|
171
184
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
172
185
|
def bun_lock
|
173
186
|
@bun_lock ||= T.let(dependency_files.find do |f|
|
174
|
-
f.name
|
187
|
+
f.name.end_with?(BunPackageManager::LOCKFILE_NAME)
|
175
188
|
end, T.nilable(Dependabot::DependencyFile))
|
176
189
|
end
|
177
190
|
|
178
191
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
179
192
|
def npmrc
|
180
193
|
@npmrc ||= T.let(dependency_files.find do |f|
|
181
|
-
f.name
|
194
|
+
f.name.end_with?(NpmPackageManager::RC_FILENAME)
|
182
195
|
end, T.nilable(Dependabot::DependencyFile))
|
183
196
|
end
|
184
197
|
|
185
198
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
186
199
|
def yarnrc
|
187
200
|
@yarnrc ||= T.let(dependency_files.find do |f|
|
188
|
-
f.name
|
201
|
+
f.name.end_with?(YarnPackageManager::RC_FILENAME)
|
189
202
|
end, T.nilable(Dependabot::DependencyFile))
|
190
203
|
end
|
191
204
|
|
192
205
|
sig { returns(T.nilable(DependencyFile)) }
|
193
206
|
def yarnrc_yml
|
194
207
|
@yarnrc_yml ||= T.let(dependency_files.find do |f|
|
195
|
-
f.name
|
208
|
+
f.name.end_with?(YarnPackageManager::RC_YML_FILENAME)
|
196
209
|
end, T.nilable(Dependabot::DependencyFile))
|
197
210
|
end
|
198
211
|
|
@@ -212,7 +225,7 @@ module Dependabot
|
|
212
225
|
next unless requirement.is_a?(String)
|
213
226
|
|
214
227
|
# Skip dependencies using Yarn workspace cross-references as requirements
|
215
|
-
next if requirement.start_with?("workspace:")
|
228
|
+
next if requirement.start_with?("workspace:", "catalog:")
|
216
229
|
|
217
230
|
requirement = "*" if requirement == ""
|
218
231
|
dep = build_dependency(
|
@@ -225,6 +238,30 @@ module Dependabot
|
|
225
238
|
dependency_set
|
226
239
|
end
|
227
240
|
|
241
|
+
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
242
|
+
def workspace_catalog_dependencies
|
243
|
+
dependency_set = DependencySet.new
|
244
|
+
workspace_config = YAML.safe_load(T.must(pnpm_workspace_yml&.content), aliases: true)
|
245
|
+
|
246
|
+
workspace_config["catalog"]&.each do |name, version|
|
247
|
+
dep = build_dependency(
|
248
|
+
file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
|
249
|
+
)
|
250
|
+
dependency_set << dep if dep
|
251
|
+
end
|
252
|
+
|
253
|
+
workspace_config["catalogs"]&.each do |_, group_depenencies|
|
254
|
+
group_depenencies.each do |name, version|
|
255
|
+
dep = build_dependency(
|
256
|
+
file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
|
257
|
+
)
|
258
|
+
dependency_set << dep if dep
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
dependency_set
|
263
|
+
end
|
264
|
+
|
228
265
|
sig { returns(LockfileParser) }
|
229
266
|
def lockfile_parser
|
230
267
|
@lockfile_parser ||= T.let(LockfileParser.new(
|
@@ -37,8 +37,13 @@ module Dependabot
|
|
37
37
|
sig { returns(T::Array[Dependabot::Dependency]) }
|
38
38
|
attr_reader :dependencies
|
39
39
|
|
40
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
41
|
+
|
40
42
|
sig { returns(T.nilable(String)) }
|
41
43
|
def updated_package_json_content
|
44
|
+
# checks if we are updating single dependency in package.json
|
45
|
+
unique_deps_count = dependencies.map(&:name).to_a.uniq.compact.length
|
46
|
+
|
42
47
|
dependencies.reduce(package_json.content.dup) do |content, dep|
|
43
48
|
updated_requirements(dep)&.each do |new_req|
|
44
49
|
old_req = old_requirement(dep, new_req)
|
@@ -50,7 +55,25 @@ module Dependabot
|
|
50
55
|
new_req: new_req
|
51
56
|
)
|
52
57
|
|
53
|
-
|
58
|
+
if Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) &&
|
59
|
+
(content == new_content && unique_deps_count > 1)
|
60
|
+
|
61
|
+
# (we observed that) package.json does not always contains the same dependencies compared to
|
62
|
+
# "dependencies" list, for example, dependencies object can contain same name dependency "dep"=> "1.0.0"
|
63
|
+
# and "dev" => "1.0.1" while package.json can only contain "dep" => "1.0.0",the other dependency is
|
64
|
+
# not present in package.json so we don't have to update it, this is most likely (as observed)
|
65
|
+
# a transitive dependency which only needs update in lockfile, So we avoid throwing exception and let
|
66
|
+
# the update continue.
|
67
|
+
|
68
|
+
Dependabot.logger.info("experiment: avoid_duplicate_updates_package_json.
|
69
|
+
Updating package.json for #{dep.name} ")
|
70
|
+
|
71
|
+
raise "Expected content to change!"
|
72
|
+
end
|
73
|
+
|
74
|
+
if !Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) && (content == new_content)
|
75
|
+
raise "Expected content to change!"
|
76
|
+
end
|
54
77
|
|
55
78
|
content = new_content
|
56
79
|
end
|
@@ -69,7 +92,7 @@ module Dependabot
|
|
69
92
|
content
|
70
93
|
end
|
71
94
|
end
|
72
|
-
|
95
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
73
96
|
sig do
|
74
97
|
params(
|
75
98
|
dependency: Dependabot::Dependency,
|