dependabot-npm_and_yarn 0.294.0 → 0.296.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 +4 -4
- data/lib/dependabot/npm_and_yarn/constraint_helper.rb +359 -0
- data/lib/dependabot/npm_and_yarn/file_fetcher.rb +5 -0
- data/lib/dependabot/npm_and_yarn/file_parser/bun_lock.rb +0 -1
- data/lib/dependabot/npm_and_yarn/file_parser.rb +33 -1
- data/lib/dependabot/npm_and_yarn/file_updater/package_json_updater.rb +57 -3
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +17 -7
- data/lib/dependabot/npm_and_yarn/file_updater/pnpm_workspace_updater.rb +140 -0
- data/lib/dependabot/npm_and_yarn/file_updater.rb +126 -32
- data/lib/dependabot/npm_and_yarn/helpers.rb +7 -0
- data/lib/dependabot/npm_and_yarn/npm_package_manager.rb +4 -10
- data/lib/dependabot/npm_and_yarn/package_manager.rb +70 -27
- 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: 40c0445c84d264374459bc6e6d333d051a116ec598ae0e208536123242a2a56b
|
4
|
+
data.tar.gz: 208a49c91628dda0ada48108de1ef09b07b8ca7b4f12df008906689e5ad173f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1a2bc9cfa98144e171f653efedd490ccf0eb02176c29496a5ee288d83a54b83f8390e3a7d67b98fa544ef3f5e4074080101d8115414fdc34cb4c3ec075a2f25
|
7
|
+
data.tar.gz: da594bed80a0ad0c2e0fef360bb026f176020c0edc1c665e4e4823f882edd72e85e07a52b5a2036c78620f0889a0e050d0342462b21f6671a8d6daaa5976cbb3
|
@@ -0,0 +1,359 @@
|
|
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
|
+
# Regex Components for Semantic Versioning
|
12
|
+
DIGIT = "\\d+" # Matches a single number (e.g., "1")
|
13
|
+
PRERELEASE = "(?:-[a-zA-Z0-9.-]+)?" # Matches optional pre-release tag (e.g., "-alpha")
|
14
|
+
BUILD_METADATA = "(?:\\+[a-zA-Z0-9.-]+)?" # Matches optional build metadata (e.g., "+001")
|
15
|
+
|
16
|
+
# Matches semantic versions:
|
17
|
+
VERSION = T.let("#{DIGIT}(?:\\.#{DIGIT}){0,2}#{PRERELEASE}#{BUILD_METADATA}".freeze, String)
|
18
|
+
|
19
|
+
VERSION_REGEX = T.let(/^#{VERSION}$/, Regexp)
|
20
|
+
|
21
|
+
# Base regex for SemVer (major.minor.patch[-prerelease][+build])
|
22
|
+
# This pattern extracts valid semantic versioning strings based on the SemVer 2.0 specification.
|
23
|
+
SEMVER_REGEX = T.let(/
|
24
|
+
(?<version>\d+\.\d+\.\d+) # Match major.minor.patch (e.g., 1.2.3)
|
25
|
+
(?:-(?<prerelease>[a-zA-Z0-9.-]+))? # Optional prerelease (e.g., -alpha.1, -rc.1, -beta.5)
|
26
|
+
(?:\+(?<build>[a-zA-Z0-9.-]+))? # Optional build metadata (e.g., +build.20231101, +exp.sha.5114f85)
|
27
|
+
/x, Regexp)
|
28
|
+
|
29
|
+
# Full SemVer validation regex (ensures the entire string is a valid SemVer)
|
30
|
+
# This ensures the entire input strictly follows SemVer, without extra characters before/after.
|
31
|
+
SEMVER_VALIDATION_REGEX = T.let(/^#{SEMVER_REGEX}$/, Regexp)
|
32
|
+
|
33
|
+
# SemVer constraint regex (supports package.json version constraints)
|
34
|
+
# This pattern ensures proper parsing of SemVer versions with optional operators.
|
35
|
+
SEMVER_CONSTRAINT_REGEX = T.let(/
|
36
|
+
(?: (>=|<=|>|<|=|~|\^)\s*)? # Make operators optional (e.g., >=, ^, ~)
|
37
|
+
(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?) # Match full SemVer versions
|
38
|
+
| (\*|latest) # Match wildcard (*) or 'latest'
|
39
|
+
/x, Regexp)
|
40
|
+
|
41
|
+
# /(>=|<=|>|<|=|~|\^)\s*(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?)|(\*|latest)/
|
42
|
+
|
43
|
+
SEMVER_OPERATOR_REGEX = /^(>=|<=|>|<|~|\^|=)$/
|
44
|
+
|
45
|
+
# Constraint Types as Constants
|
46
|
+
CARET_CONSTRAINT_REGEX = T.let(/^\^\s*(#{VERSION})$/, Regexp)
|
47
|
+
TILDE_CONSTRAINT_REGEX = T.let(/^~\s*(#{VERSION})$/, Regexp)
|
48
|
+
EXACT_CONSTRAINT_REGEX = T.let(/^\s*(#{VERSION})$/, Regexp)
|
49
|
+
GREATER_THAN_EQUAL_REGEX = T.let(/^>=\s*(#{VERSION})$/, Regexp)
|
50
|
+
LESS_THAN_EQUAL_REGEX = T.let(/^<=\s*(#{VERSION})$/, Regexp)
|
51
|
+
GREATER_THAN_REGEX = T.let(/^>\s*(#{VERSION})$/, Regexp)
|
52
|
+
LESS_THAN_REGEX = T.let(/^<\s*(#{VERSION})$/, Regexp)
|
53
|
+
WILDCARD_REGEX = T.let(/^\*$/, Regexp)
|
54
|
+
LATEST_REGEX = T.let(/^latest$/, Regexp)
|
55
|
+
SEMVER_CONSTANTS = ["*", "latest"].freeze
|
56
|
+
|
57
|
+
# Unified Regex for Valid Constraints
|
58
|
+
VALID_CONSTRAINT_REGEX = T.let(Regexp.union(
|
59
|
+
CARET_CONSTRAINT_REGEX,
|
60
|
+
TILDE_CONSTRAINT_REGEX,
|
61
|
+
EXACT_CONSTRAINT_REGEX,
|
62
|
+
GREATER_THAN_EQUAL_REGEX,
|
63
|
+
LESS_THAN_EQUAL_REGEX,
|
64
|
+
GREATER_THAN_REGEX,
|
65
|
+
LESS_THAN_REGEX,
|
66
|
+
WILDCARD_REGEX,
|
67
|
+
LATEST_REGEX
|
68
|
+
).freeze, Regexp)
|
69
|
+
|
70
|
+
# Extract unique constraints from the given constraint expression.
|
71
|
+
# @param constraint_expression [T.nilable(String)] The semver constraint expression.
|
72
|
+
# @return [T::Array[String]] The list of unique Ruby-compatible constraints.
|
73
|
+
sig do
|
74
|
+
params(
|
75
|
+
constraint_expression: T.nilable(String),
|
76
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
77
|
+
)
|
78
|
+
.returns(T.nilable(T::Array[String]))
|
79
|
+
end
|
80
|
+
def self.extract_ruby_constraints(constraint_expression, dependabot_versions = nil)
|
81
|
+
parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
|
82
|
+
|
83
|
+
return nil unless parsed_constraints
|
84
|
+
|
85
|
+
parsed_constraints.filter_map { |parsed| parsed[:constraint] }
|
86
|
+
end
|
87
|
+
|
88
|
+
# rubocop:disable Metrics/AbcSize
|
89
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
90
|
+
# rubocop:disable Metrics/MethodLength
|
91
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
92
|
+
sig do
|
93
|
+
params(constraint_expression: T.nilable(String))
|
94
|
+
.returns(T.nilable(T::Array[String]))
|
95
|
+
end
|
96
|
+
def self.split_constraints(constraint_expression)
|
97
|
+
normalized_constraint = constraint_expression&.strip
|
98
|
+
return [] if normalized_constraint.nil? || normalized_constraint.empty?
|
99
|
+
|
100
|
+
# Split constraints by logical OR (`||`)
|
101
|
+
constraint_groups = normalized_constraint.split("||")
|
102
|
+
|
103
|
+
# Split constraints by logical AND (`,`)
|
104
|
+
constraint_groups = constraint_groups.map do |or_constraint|
|
105
|
+
or_constraint.split(",").map(&:strip)
|
106
|
+
end.flatten
|
107
|
+
|
108
|
+
constraint_groups = constraint_groups.map do |constraint|
|
109
|
+
tokens = constraint.split(/\s+/).map(&:strip)
|
110
|
+
|
111
|
+
and_constraints = []
|
112
|
+
|
113
|
+
previous = T.let(nil, T.nilable(String))
|
114
|
+
operator = T.let(false, T.nilable(T::Boolean))
|
115
|
+
wildcard = T.let(false, T::Boolean)
|
116
|
+
|
117
|
+
tokens.each do |token|
|
118
|
+
token = token.strip
|
119
|
+
next if token.empty?
|
120
|
+
|
121
|
+
# Invalid constraint if wildcard and anything else
|
122
|
+
return nil if wildcard
|
123
|
+
|
124
|
+
# If token is one of the operators (>=, <=, >, <, ~, ^, =)
|
125
|
+
if token.match?(SEMVER_OPERATOR_REGEX)
|
126
|
+
wildcard = false
|
127
|
+
operator = true
|
128
|
+
# If token is wildcard or latest
|
129
|
+
elsif token.match?(/(\*|latest)/)
|
130
|
+
and_constraints << token
|
131
|
+
wildcard = true
|
132
|
+
operator = false
|
133
|
+
# If token is exact version (e.g., "1.2.3")
|
134
|
+
elsif token.match(VERSION_REGEX)
|
135
|
+
and_constraints << if operator
|
136
|
+
"#{previous}#{token}"
|
137
|
+
else
|
138
|
+
token
|
139
|
+
end
|
140
|
+
wildcard = false
|
141
|
+
operator = false
|
142
|
+
# If token is a valid constraint (e.g., ">=1.2.3", "<=2.0.0")
|
143
|
+
elsif token.match(VALID_CONSTRAINT_REGEX)
|
144
|
+
return nil if operator
|
145
|
+
|
146
|
+
and_constraints << token
|
147
|
+
|
148
|
+
wildcard = false
|
149
|
+
operator = false
|
150
|
+
else
|
151
|
+
# invalid constraint
|
152
|
+
return nil
|
153
|
+
end
|
154
|
+
previous = token
|
155
|
+
end
|
156
|
+
and_constraints.uniq
|
157
|
+
end.flatten
|
158
|
+
constraint_groups if constraint_groups.any?
|
159
|
+
end
|
160
|
+
|
161
|
+
# rubocop:enable Metrics/AbcSize
|
162
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
163
|
+
# rubocop:enable Metrics/MethodLength
|
164
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
165
|
+
|
166
|
+
# Find the highest version from the given constraint expression.
|
167
|
+
# @param constraint_expression [T.nilable(String)] The semver constraint expression.
|
168
|
+
# @return [T.nilable(String)] The highest version, or nil if no versions are available.
|
169
|
+
sig do
|
170
|
+
params(
|
171
|
+
constraint_expression: T.nilable(String),
|
172
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
173
|
+
)
|
174
|
+
.returns(T.nilable(String))
|
175
|
+
end
|
176
|
+
def self.find_highest_version_from_constraint_expression(constraint_expression, dependabot_versions = nil)
|
177
|
+
parsed_constraints = parse_constraints(constraint_expression, dependabot_versions)
|
178
|
+
|
179
|
+
return nil unless parsed_constraints
|
180
|
+
|
181
|
+
parsed_constraints
|
182
|
+
.filter_map { |parsed| parsed[:version] } # Extract all versions
|
183
|
+
.max_by { |version| Version.new(version) }
|
184
|
+
end
|
185
|
+
|
186
|
+
# Parse all constraints (split by logical OR `||`) and convert to Ruby-compatible constraints.
|
187
|
+
# Return:
|
188
|
+
# - `nil` if the constraint expression is invalid
|
189
|
+
# - `[]` if the constraint expression is valid but represents "no constraints"
|
190
|
+
# - An array of hashes for valid constraints with details about the constraint and version
|
191
|
+
sig do
|
192
|
+
params(
|
193
|
+
constraint_expression: T.nilable(String),
|
194
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
195
|
+
)
|
196
|
+
.returns(T.nilable(T::Array[T::Hash[Symbol, T.nilable(String)]]))
|
197
|
+
end
|
198
|
+
def self.parse_constraints(constraint_expression, dependabot_versions = nil)
|
199
|
+
splitted_constraints = split_constraints(constraint_expression)
|
200
|
+
|
201
|
+
return unless splitted_constraints
|
202
|
+
|
203
|
+
constraints = to_ruby_constraints_with_versions(splitted_constraints, dependabot_versions)
|
204
|
+
constraints
|
205
|
+
end
|
206
|
+
|
207
|
+
sig do
|
208
|
+
params(
|
209
|
+
constraints: T::Array[String],
|
210
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
211
|
+
).returns(T::Array[T::Hash[Symbol, T.nilable(String)]])
|
212
|
+
end
|
213
|
+
def self.to_ruby_constraints_with_versions(constraints, dependabot_versions = [])
|
214
|
+
constraints.filter_map do |constraint|
|
215
|
+
parsed = to_ruby_constraint_with_version(constraint, dependabot_versions)
|
216
|
+
parsed if parsed
|
217
|
+
end.uniq
|
218
|
+
end
|
219
|
+
|
220
|
+
# rubocop:disable Metrics/MethodLength
|
221
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
222
|
+
# rubocop:disable Metrics/AbcSize
|
223
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
224
|
+
# Converts a semver constraint to a Ruby-compatible constraint and extracts the version, if available.
|
225
|
+
# @param constraint [String] The semver constraint to parse.
|
226
|
+
# @return [T.nilable(T::Hash[Symbol, T.nilable(String)])] Returns the Ruby-compatible constraint and the version,
|
227
|
+
# if available, or nil if the constraint is invalid.
|
228
|
+
#
|
229
|
+
# @example
|
230
|
+
# to_ruby_constraint_with_version("=1.2.3") # => { constraint: "=1.2.3", version: "1.2.3" }
|
231
|
+
# to_ruby_constraint_with_version("^1.2.3") # => { constraint: ">=1.2.3 <2.0.0", version: "1.2.3" }
|
232
|
+
# to_ruby_constraint_with_version("*") # => { constraint: nil, version: nil }
|
233
|
+
# to_ruby_constraint_with_version("invalid") # => nil
|
234
|
+
sig do
|
235
|
+
params(
|
236
|
+
constraint: String,
|
237
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version])
|
238
|
+
)
|
239
|
+
.returns(T.nilable(T::Hash[Symbol, T.nilable(String)]))
|
240
|
+
end
|
241
|
+
def self.to_ruby_constraint_with_version(constraint, dependabot_versions = [])
|
242
|
+
return nil if constraint.empty?
|
243
|
+
|
244
|
+
case constraint
|
245
|
+
when EXACT_CONSTRAINT_REGEX # Exact version, e.g., "1.2.3-alpha"
|
246
|
+
return unless Regexp.last_match
|
247
|
+
|
248
|
+
full_version = Regexp.last_match(1)
|
249
|
+
{ constraint: "=#{full_version}", version: full_version }
|
250
|
+
when CARET_CONSTRAINT_REGEX # Caret constraint, e.g., "^1.2.3"
|
251
|
+
return unless Regexp.last_match
|
252
|
+
|
253
|
+
full_version = Regexp.last_match(1)
|
254
|
+
_, major, minor = version_components(full_version)
|
255
|
+
return nil if major.nil?
|
256
|
+
|
257
|
+
ruby_constraint =
|
258
|
+
if major.to_i.zero?
|
259
|
+
minor.nil? ? ">=#{full_version} <1.0.0" : ">=#{full_version} <0.#{minor.to_i + 1}.0"
|
260
|
+
else
|
261
|
+
">=#{full_version} <#{major.to_i + 1}.0.0"
|
262
|
+
end
|
263
|
+
{ constraint: ruby_constraint, version: full_version }
|
264
|
+
when TILDE_CONSTRAINT_REGEX # Tilde constraint, e.g., "~1.2.3"
|
265
|
+
return unless Regexp.last_match
|
266
|
+
|
267
|
+
full_version = Regexp.last_match(1)
|
268
|
+
_, major, minor = version_components(full_version)
|
269
|
+
ruby_constraint =
|
270
|
+
if minor.nil?
|
271
|
+
">=#{full_version} <#{major.to_i + 1}.0.0"
|
272
|
+
else
|
273
|
+
">=#{full_version} <#{major}.#{minor.to_i + 1}.0"
|
274
|
+
end
|
275
|
+
{ constraint: ruby_constraint, version: full_version }
|
276
|
+
when GREATER_THAN_EQUAL_REGEX # Greater than or equal, e.g., ">=1.2.3"
|
277
|
+
|
278
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
279
|
+
|
280
|
+
found_version = highest_matching_version(
|
281
|
+
dependabot_versions,
|
282
|
+
T.must(Regexp.last_match(1))
|
283
|
+
) do |version, constraint_version|
|
284
|
+
version >= Version.new(constraint_version)
|
285
|
+
end
|
286
|
+
{ constraint: ">=#{Regexp.last_match(1)}", version: found_version&.to_s }
|
287
|
+
when LESS_THAN_EQUAL_REGEX # Less than or equal, e.g., "<=1.2.3"
|
288
|
+
return unless Regexp.last_match
|
289
|
+
|
290
|
+
full_version = Regexp.last_match(1)
|
291
|
+
{ constraint: "<=#{full_version}", version: full_version }
|
292
|
+
when GREATER_THAN_REGEX # Greater than, e.g., ">1.2.3"
|
293
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
294
|
+
|
295
|
+
found_version = highest_matching_version(
|
296
|
+
dependabot_versions,
|
297
|
+
T.must(Regexp.last_match(1))
|
298
|
+
) do |version, constraint_version|
|
299
|
+
version > Version.new(constraint_version)
|
300
|
+
end
|
301
|
+
{ constraint: ">#{Regexp.last_match(1)}", version: found_version&.to_s }
|
302
|
+
when LESS_THAN_REGEX # Less than, e.g., "<1.2.3"
|
303
|
+
return unless Regexp.last_match && Regexp.last_match(1)
|
304
|
+
|
305
|
+
found_version = highest_matching_version(
|
306
|
+
dependabot_versions,
|
307
|
+
T.must(Regexp.last_match(1))
|
308
|
+
) do |version, constraint_version|
|
309
|
+
version < Version.new(constraint_version)
|
310
|
+
end
|
311
|
+
{ constraint: "<#{Regexp.last_match(1)}", version: found_version&.to_s }
|
312
|
+
when WILDCARD_REGEX # No specific constraint, resolves to the highest available version
|
313
|
+
{ constraint: nil, version: dependabot_versions&.max&.to_s }
|
314
|
+
when LATEST_REGEX
|
315
|
+
{ constraint: nil, version: dependabot_versions&.max&.to_s } # Resolves to the latest available version
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
sig do
|
320
|
+
params(
|
321
|
+
dependabot_versions: T.nilable(T::Array[Dependabot::Version]),
|
322
|
+
constraint_version: String,
|
323
|
+
condition: T.proc.params(version: Dependabot::Version, constraint: Dependabot::Version).returns(T::Boolean)
|
324
|
+
)
|
325
|
+
.returns(T.nilable(Dependabot::Version))
|
326
|
+
end
|
327
|
+
def self.highest_matching_version(dependabot_versions, constraint_version, &condition)
|
328
|
+
return unless dependabot_versions&.any?
|
329
|
+
|
330
|
+
# Returns the highest version that satisfies the condition, or nil if none.
|
331
|
+
dependabot_versions
|
332
|
+
.sort
|
333
|
+
.reverse
|
334
|
+
.find { |version| condition.call(version, Version.new(constraint_version)) } # rubocop:disable Performance/RedundantBlockCall
|
335
|
+
end
|
336
|
+
|
337
|
+
# rubocop:enable Metrics/MethodLength
|
338
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
339
|
+
# rubocop:enable Metrics/AbcSize
|
340
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
341
|
+
|
342
|
+
# Parses a semantic version string into its components as per the SemVer spec
|
343
|
+
# Example: "1.2.3-alpha+001" → ["1.2.3", "1", "2", "3", "alpha", "001"]
|
344
|
+
sig { params(full_version: T.nilable(String)).returns(T.nilable(T::Array[String])) }
|
345
|
+
def self.version_components(full_version)
|
346
|
+
return [] if full_version.nil?
|
347
|
+
|
348
|
+
match = full_version.match(SEMVER_VALIDATION_REGEX)
|
349
|
+
return [] unless match
|
350
|
+
|
351
|
+
version = match[:version]
|
352
|
+
return [] unless version
|
353
|
+
|
354
|
+
major, minor, patch = version.split(".")
|
355
|
+
[version, major, minor, patch, match[:prerelease], match[:build]].compact
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
@@ -342,6 +342,11 @@ module Dependabot
|
|
342
342
|
fetch_support_file(PNPMPackageManager::PNPM_WS_YML_FILENAME),
|
343
343
|
T.nilable(DependencyFile)
|
344
344
|
)
|
345
|
+
|
346
|
+
# Only fetch from parent directories if the file wasn't found initially
|
347
|
+
@pnpm_workspace_yaml ||= fetch_file_from_parent_directories(PNPMPackageManager::PNPM_WS_YML_FILENAME)
|
348
|
+
|
349
|
+
@pnpm_workspace_yaml
|
345
350
|
end
|
346
351
|
|
347
352
|
sig { returns(T.nilable(DependencyFile)) }
|
@@ -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 pnpm_workspace_yml
|
62
63
|
|
63
64
|
dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
|
64
65
|
|
@@ -168,6 +169,13 @@ module Dependabot
|
|
168
169
|
end, T.nilable(Dependabot::DependencyFile))
|
169
170
|
end
|
170
171
|
|
172
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
173
|
+
def pnpm_workspace_yml
|
174
|
+
@pnpm_workspace_yml ||= T.let(dependency_files.find do |f|
|
175
|
+
f.name.end_with?(PNPMPackageManager::PNPM_WS_YML_FILENAME)
|
176
|
+
end, T.nilable(Dependabot::DependencyFile))
|
177
|
+
end
|
178
|
+
|
171
179
|
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
172
180
|
def bun_lock
|
173
181
|
@bun_lock ||= T.let(dependency_files.find do |f|
|
@@ -225,6 +233,30 @@ module Dependabot
|
|
225
233
|
dependency_set
|
226
234
|
end
|
227
235
|
|
236
|
+
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
237
|
+
def workspace_catalog_dependencies
|
238
|
+
dependency_set = DependencySet.new
|
239
|
+
workspace_config = YAML.safe_load(T.must(pnpm_workspace_yml&.content), aliases: true)
|
240
|
+
|
241
|
+
workspace_config["catalog"]&.each do |name, version|
|
242
|
+
dep = build_dependency(
|
243
|
+
file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
|
244
|
+
)
|
245
|
+
dependency_set << dep if dep
|
246
|
+
end
|
247
|
+
|
248
|
+
workspace_config["catalogs"]&.each do |_, group_depenencies|
|
249
|
+
group_depenencies.each do |name, version|
|
250
|
+
dep = build_dependency(
|
251
|
+
file: T.must(pnpm_workspace_yml), type: "dependencies", name: name, requirement: version
|
252
|
+
)
|
253
|
+
dependency_set << dep if dep
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
dependency_set
|
258
|
+
end
|
259
|
+
|
228
260
|
sig { returns(LockfileParser) }
|
229
261
|
def lockfile_parser
|
230
262
|
@lockfile_parser ||= T.let(LockfileParser.new(
|
@@ -11,11 +11,15 @@ module Dependabot
|
|
11
11
|
class PackageJsonUpdater
|
12
12
|
extend T::Sig
|
13
13
|
|
14
|
+
LOCAL_PACKAGE = T.let([/portal:/, /file:/].freeze, T::Array[Regexp])
|
15
|
+
|
16
|
+
PATCH_PACKAGE = T.let([/patch:/].freeze, T::Array[Regexp])
|
17
|
+
|
14
18
|
sig do
|
15
19
|
params(
|
16
20
|
package_json: Dependabot::DependencyFile,
|
17
21
|
dependencies: T::Array[Dependabot::Dependency]
|
18
|
-
)
|
22
|
+
).void
|
19
23
|
end
|
20
24
|
def initialize(package_json:, dependencies:)
|
21
25
|
@package_json = package_json
|
@@ -37,8 +41,13 @@ module Dependabot
|
|
37
41
|
sig { returns(T::Array[Dependabot::Dependency]) }
|
38
42
|
attr_reader :dependencies
|
39
43
|
|
44
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
45
|
+
|
40
46
|
sig { returns(T.nilable(String)) }
|
41
47
|
def updated_package_json_content
|
48
|
+
# checks if we are updating single dependency in package.json
|
49
|
+
unique_deps_count = dependencies.map(&:name).to_a.uniq.compact.length
|
50
|
+
|
42
51
|
dependencies.reduce(package_json.content.dup) do |content, dep|
|
43
52
|
updated_requirements(dep)&.each do |new_req|
|
44
53
|
old_req = old_requirement(dep, new_req)
|
@@ -50,7 +59,25 @@ module Dependabot
|
|
50
59
|
new_req: new_req
|
51
60
|
)
|
52
61
|
|
53
|
-
|
62
|
+
if Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) &&
|
63
|
+
(content == new_content && unique_deps_count > 1)
|
64
|
+
|
65
|
+
# (we observed that) package.json does not always contains the same dependencies compared to
|
66
|
+
# "dependencies" list, for example, dependencies object can contain same name dependency "dep"=> "1.0.0"
|
67
|
+
# and "dev" => "1.0.1" while package.json can only contain "dep" => "1.0.0",the other dependency is
|
68
|
+
# not present in package.json so we don't have to update it, this is most likely (as observed)
|
69
|
+
# a transitive dependency which only needs update in lockfile, So we avoid throwing exception and let
|
70
|
+
# the update continue.
|
71
|
+
|
72
|
+
Dependabot.logger.info("experiment: avoid_duplicate_updates_package_json.
|
73
|
+
Updating package.json for #{dep.name} ")
|
74
|
+
|
75
|
+
raise "Expected content to change!"
|
76
|
+
end
|
77
|
+
|
78
|
+
if !Dependabot::Experiments.enabled?(:avoid_duplicate_updates_package_json) && (content == new_content)
|
79
|
+
raise "Expected content to change!"
|
80
|
+
end
|
54
81
|
|
55
82
|
content = new_content
|
56
83
|
end
|
@@ -69,7 +96,7 @@ module Dependabot
|
|
69
96
|
content
|
70
97
|
end
|
71
98
|
end
|
72
|
-
|
99
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
73
100
|
sig do
|
74
101
|
params(
|
75
102
|
dependency: Dependabot::Dependency,
|
@@ -92,6 +119,8 @@ module Dependabot
|
|
92
119
|
def updated_requirements(dependency)
|
93
120
|
return unless dependency.previous_requirements
|
94
121
|
|
122
|
+
preliminary_check_for_update(dependency)
|
123
|
+
|
95
124
|
updated_requirement_pairs =
|
96
125
|
dependency.requirements.zip(T.must(dependency.previous_requirements))
|
97
126
|
.reject do |new_req, old_req|
|
@@ -318,6 +347,31 @@ module Dependabot
|
|
318
347
|
|
319
348
|
0
|
320
349
|
end
|
350
|
+
|
351
|
+
sig { params(dependency: Dependabot::Dependency).void }
|
352
|
+
def preliminary_check_for_update(dependency)
|
353
|
+
T.must(dependency.previous_requirements).each do |req, _dep|
|
354
|
+
next if req.fetch(:requirement).nil?
|
355
|
+
|
356
|
+
# some deps are patched with local patches, we don't need to update them
|
357
|
+
if req.fetch(:requirement).match?(Regexp.union(PATCH_PACKAGE))
|
358
|
+
Dependabot.logger.info("Func: updated_requirements. dependency patched #{dependency.name}," \
|
359
|
+
" Requirement: '#{req.fetch(:requirement)}'")
|
360
|
+
|
361
|
+
raise DependencyFileNotResolvable,
|
362
|
+
"Dependency is patched locally, Update not required."
|
363
|
+
end
|
364
|
+
|
365
|
+
# some deps are added as local packages, we don't need to update them as they are referred to a local path
|
366
|
+
next unless req.fetch(:requirement).match?(Regexp.union(LOCAL_PACKAGE))
|
367
|
+
|
368
|
+
Dependabot.logger.info("Func: updated_requirements. local package #{dependency.name}," \
|
369
|
+
" Requirement: '#{req.fetch(:requirement)}'")
|
370
|
+
|
371
|
+
raise DependencyFileNotResolvable,
|
372
|
+
"Local package, Update not required."
|
373
|
+
end
|
374
|
+
end
|
321
375
|
end
|
322
376
|
end
|
323
377
|
end
|
@@ -24,11 +24,14 @@ module Dependabot
|
|
24
24
|
)
|
25
25
|
end
|
26
26
|
|
27
|
-
def updated_pnpm_lock_content(pnpm_lock)
|
27
|
+
def updated_pnpm_lock_content(pnpm_lock, updated_pnpm_workspace_content: nil)
|
28
28
|
@updated_pnpm_lock_content ||= {}
|
29
29
|
return @updated_pnpm_lock_content[pnpm_lock.name] if @updated_pnpm_lock_content[pnpm_lock.name]
|
30
30
|
|
31
|
-
new_content = run_pnpm_update(
|
31
|
+
new_content = run_pnpm_update(
|
32
|
+
pnpm_lock: pnpm_lock,
|
33
|
+
updated_pnpm_workspace_content: updated_pnpm_workspace_content
|
34
|
+
)
|
32
35
|
@updated_pnpm_lock_content[pnpm_lock.name] = new_content
|
33
36
|
rescue SharedHelpers::HelperSubprocessFailed => e
|
34
37
|
handle_pnpm_lock_updater_error(e, pnpm_lock)
|
@@ -100,14 +103,17 @@ module Dependabot
|
|
100
103
|
# Peer dependencies configuration error
|
101
104
|
ERR_PNPM_PEER_DEP_ISSUES = /ERR_PNPM_PEER_DEP_ISSUES/
|
102
105
|
|
103
|
-
def run_pnpm_update(pnpm_lock:)
|
106
|
+
def run_pnpm_update(pnpm_lock:, updated_pnpm_workspace_content: nil)
|
104
107
|
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
|
105
108
|
File.write(".npmrc", npmrc_content(pnpm_lock))
|
106
109
|
|
107
110
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
+
if updated_pnpm_workspace_content
|
112
|
+
File.write("pnpm-workspace.yaml", updated_pnpm_workspace_content["pnpm-workspace.yaml"])
|
113
|
+
else
|
114
|
+
run_pnpm_update_packages
|
115
|
+
write_final_package_json_files
|
116
|
+
end
|
111
117
|
|
112
118
|
run_pnpm_install
|
113
119
|
|
@@ -140,11 +146,15 @@ module Dependabot
|
|
140
146
|
)
|
141
147
|
end
|
142
148
|
|
149
|
+
def workspace_files
|
150
|
+
@workspace_files ||= dependency_files.select { |f| f.name.end_with?("pnpm-workspace.yaml") }
|
151
|
+
end
|
152
|
+
|
143
153
|
def lockfile_dependencies(lockfile)
|
144
154
|
@lockfile_dependencies ||= {}
|
145
155
|
@lockfile_dependencies[lockfile.name] ||=
|
146
156
|
NpmAndYarn::FileParser.new(
|
147
|
-
dependency_files: [lockfile, *package_files],
|
157
|
+
dependency_files: [lockfile, *package_files, *workspace_files],
|
148
158
|
source: nil,
|
149
159
|
credentials: credentials
|
150
160
|
).parse
|