dependabot-bun 0.400.0 → 0.402.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/bun/file_parser.rb +34 -3
- data/lib/dependabot/bun/file_updater/bun_lockfile_updater.rb +159 -9
- data/lib/dependabot/bun/file_updater/json_object_members.rb +127 -0
- data/lib/dependabot/bun/file_updater/package_json_updater.rb +84 -1
- data/lib/dependabot/bun/helpers.rb +8 -6
- data/lib/dependabot/bun/metadata_finder.rb +17 -10
- data/lib/dependabot/bun/package/registry_finder.rb +1 -1
- data/lib/dependabot/bun/update_checker/version_resolver.rb +111 -4
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4c9217ef3c6884ac153b6bb6e516709f0440e6d70a2c25e4f795aaf443adb47
|
|
4
|
+
data.tar.gz: 25430e72661535db6c374dee825b3dcac942f37e98853e47750061f326253602
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97eda92117fac8f2ebeb5883f865cff280b089d8430127b0dbb4da826f2ff24ddf11949181b45ca9f320a23ed3b8f8b4fbf5d2660946a5255f646b115dad5b76
|
|
7
|
+
data.tar.gz: 9b729c7238e1f784a09500789c26c4eec54c8afe891619600312345a7902cb15feb3ae0230e56744435876b963e4dc054f06eec94974353ef4d696f7642887c8
|
|
@@ -30,6 +30,7 @@ module Dependabot
|
|
|
30
30
|
require_relative "file_parser/lockfile_parser"
|
|
31
31
|
|
|
32
32
|
DEPENDENCY_TYPES = %w(dependencies devDependencies optionalDependencies).freeze
|
|
33
|
+
DEFAULT_CATALOG = "default"
|
|
33
34
|
ALIAS_PROTOCOL = "npm:"
|
|
34
35
|
GIT_URL_REGEX = %r{
|
|
35
36
|
(?<git_prefix>^|^git.*?|^github:|^bitbucket:|^gitlab:|github\.com/)
|
|
@@ -63,6 +64,7 @@ module Dependabot
|
|
|
63
64
|
dependency_set = DependencySet.new
|
|
64
65
|
dependency_set += manifest_dependencies
|
|
65
66
|
dependency_set += lockfile_dependencies
|
|
67
|
+
dependency_set += catalog_dependencies
|
|
66
68
|
|
|
67
69
|
dependencies = Helpers.dependencies_with_all_versions_metadata(dependency_set)
|
|
68
70
|
|
|
@@ -202,11 +204,34 @@ module Dependabot
|
|
|
202
204
|
lockfile_parser.parse_set
|
|
203
205
|
end
|
|
204
206
|
|
|
207
|
+
sig { returns(Dependabot::FileParsers::Base::DependencySet) }
|
|
208
|
+
def catalog_dependencies
|
|
209
|
+
dependency_set = DependencySet.new
|
|
210
|
+
document = package_json_document
|
|
211
|
+
catalogs = [[DEFAULT_CATALOG, document.catalog], *document.catalogs.to_a]
|
|
212
|
+
|
|
213
|
+
catalogs.each do |catalog_name, entries|
|
|
214
|
+
entries.each do |name, requirement|
|
|
215
|
+
# Catalog entries are production dependencies, as pnpm catalogs are in npm_and_yarn.
|
|
216
|
+
dep = build_dependency(
|
|
217
|
+
file: package_json,
|
|
218
|
+
type: "dependencies",
|
|
219
|
+
name: name,
|
|
220
|
+
requirement: requirement,
|
|
221
|
+
catalog: catalog_name
|
|
222
|
+
)
|
|
223
|
+
dependency_set << dep if dep
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
dependency_set
|
|
228
|
+
end
|
|
229
|
+
|
|
205
230
|
sig do
|
|
206
|
-
params(file: DependencyFile, type: String, name: String, requirement: String)
|
|
231
|
+
params(file: DependencyFile, type: String, name: String, requirement: String, catalog: T.nilable(String))
|
|
207
232
|
.returns(T.nilable(Dependency))
|
|
208
233
|
end
|
|
209
|
-
def build_dependency(file:, type:, name:, requirement:)
|
|
234
|
+
def build_dependency(file:, type:, name:, requirement:, catalog: nil)
|
|
210
235
|
lockfile_details = lockfile_parser.lockfile_details(
|
|
211
236
|
dependency_name: name,
|
|
212
237
|
requirement: requirement,
|
|
@@ -243,11 +268,17 @@ module Dependabot
|
|
|
243
268
|
requirement: requirement_for(requirement),
|
|
244
269
|
file: file.name,
|
|
245
270
|
groups: [type],
|
|
246
|
-
source: source_for(name, requirement, lockfile_details)
|
|
271
|
+
source: source_for(name, requirement, lockfile_details),
|
|
272
|
+
**catalog_metadata(catalog)
|
|
247
273
|
}]
|
|
248
274
|
)
|
|
249
275
|
end
|
|
250
276
|
|
|
277
|
+
sig { params(catalog: T.nilable(String)).returns(T::Hash[Symbol, T::Hash[Symbol, String]]) }
|
|
278
|
+
def catalog_metadata(catalog)
|
|
279
|
+
catalog ? { metadata: { catalog: catalog } } : {}
|
|
280
|
+
end
|
|
281
|
+
|
|
251
282
|
sig { override.void }
|
|
252
283
|
def check_required_files
|
|
253
284
|
return if get_original_file(MANIFEST_FILENAME)
|
|
@@ -6,6 +6,7 @@ require "sorbet-runtime"
|
|
|
6
6
|
require "dependabot/bun/helpers"
|
|
7
7
|
require "dependabot/bun/package/registry_finder"
|
|
8
8
|
require "dependabot/bun/registry_parser"
|
|
9
|
+
require "dependabot/package/npm_package_json"
|
|
9
10
|
require "dependabot/shared_helpers"
|
|
10
11
|
|
|
11
12
|
module Dependabot
|
|
@@ -69,6 +70,24 @@ module Dependabot
|
|
|
69
70
|
T::Hash[Regexp, Dependabot::DependabotError]
|
|
70
71
|
)
|
|
71
72
|
|
|
73
|
+
# A registry rejected the request for a package or its tarball. bun prints
|
|
74
|
+
# `error: failed to download <name>@<version>: <status> <reason>` with the tarball URL on the next line, or
|
|
75
|
+
# `error: GET <url> - <status>` when the request for the package metadata fails.
|
|
76
|
+
DOWNLOAD_FAILED =
|
|
77
|
+
%r{error: failed to download (?<spec>\S+): (?<status>\d{3})[^\n]*(?:\n[[:space:]]*(?<url>https?://\S+))?}
|
|
78
|
+
REQUEST_FAILED = %r{error: GET (?<url>https?://\S+) - (?<status>\d{3})\b}
|
|
79
|
+
ACCESS_DENIED_STATUSES = %w(401 403).freeze
|
|
80
|
+
NOT_FOUND_STATUS = "404"
|
|
81
|
+
|
|
82
|
+
# A workspace that bun.lock or package.json refers to is not part of the checkout
|
|
83
|
+
WORKSPACE_NOT_ON_DISK =
|
|
84
|
+
/
|
|
85
|
+
error:\s(?:the\sroot\spackage|workspace\s"[^"]+")\s
|
|
86
|
+
depends\son\sworkspace\s"(?<name>[^"]+)"\s\((?<path>[^)]*)\),\s
|
|
87
|
+
which\sis\slisted\sin\sbun\.lock\sbut\snot\son\sdisk
|
|
88
|
+
/x
|
|
89
|
+
WORKSPACE_NOT_FOUND = /error: Workspace not found "(?<path>[^"]+)"/
|
|
90
|
+
|
|
72
91
|
sig { params(bun_lock: Dependabot::DependencyFile).returns(String) }
|
|
73
92
|
def run_bun_update(bun_lock:)
|
|
74
93
|
SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
|
|
@@ -88,14 +107,83 @@ module Dependabot
|
|
|
88
107
|
|
|
89
108
|
sig { void }
|
|
90
109
|
def run_bun_updater
|
|
91
|
-
|
|
92
|
-
"#{d.name}@#{d.version}"
|
|
93
|
-
end.join(" ")
|
|
110
|
+
dependency_updates_by_target.each do |workspace_name, deps|
|
|
111
|
+
dependency_updates = deps.map { |d| "#{d.name}@#{d.version}" }
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
113
|
+
command = T.let(["install", *dependency_updates], T::Array[String])
|
|
114
|
+
command += ["--filter", workspace_name] if workspace_name
|
|
115
|
+
command += ["--save-text-lockfile", "--ignore-scripts"]
|
|
116
|
+
|
|
117
|
+
Helpers.run_bun_command(
|
|
118
|
+
command,
|
|
119
|
+
fingerprint: "install <dependency_updates> --save-text-lockfile --ignore-scripts"
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Maps each install target (nil for the repo root, or a workspace
|
|
125
|
+
# path) to the dependencies that need installing there. A dependency
|
|
126
|
+
# declared in multiple manifests must be installed in each one, since
|
|
127
|
+
# Bun merges same-name declarations into a single Dependency.
|
|
128
|
+
sig { returns(T::Hash[T.nilable(String), T::Array[Dependabot::Dependency]]) }
|
|
129
|
+
def dependency_updates_by_target
|
|
130
|
+
dependencies
|
|
131
|
+
.flat_map { |dependency| install_targets_for(dependency).map { |target| [target, dependency] } }
|
|
132
|
+
.group_by { |target, _dependency| target }
|
|
133
|
+
.transform_values { |pairs| pairs.map { |_target, dependency| dependency } }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns every install target (nil for the repo root, or a workspace
|
|
137
|
+
# path) that declares +dependency+. Falls back to an unfiltered root
|
|
138
|
+
# install if it isn't declared anywhere (e.g. a sub-dependency).
|
|
139
|
+
sig { params(dependency: Dependabot::Dependency).returns(T::Array[T.nilable(String)]) }
|
|
140
|
+
def install_targets_for(dependency)
|
|
141
|
+
root = root_package_json
|
|
142
|
+
|
|
143
|
+
workspace_targets = package_files.filter_map do |file|
|
|
144
|
+
next if file == root
|
|
145
|
+
next unless package_json_declares?(file, dependency)
|
|
146
|
+
|
|
147
|
+
workspace_filter(file)
|
|
148
|
+
end.uniq
|
|
149
|
+
|
|
150
|
+
declared_at_root = !root || package_json_declares?(root, dependency)
|
|
151
|
+
return workspace_targets if !declared_at_root && workspace_targets.any?
|
|
152
|
+
|
|
153
|
+
[nil, *workspace_targets]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Path-based --filter target for the workspace at +file+ (e.g.
|
|
157
|
+
# "./packages/app"). Bun's --filter also accepts a package's declared
|
|
158
|
+
# "name", but names can contain glob metacharacters that would match
|
|
159
|
+
# unrelated workspaces, so a path is used instead.
|
|
160
|
+
sig { params(file: Dependabot::DependencyFile).returns(String) }
|
|
161
|
+
def workspace_filter(file)
|
|
162
|
+
unless Dependabot::Package::NpmPackageJson.from_file(file).name
|
|
163
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
164
|
+
file.path,
|
|
165
|
+
"#{file.name} must declare a \"name\" to be targeted for a workspace-scoped update"
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
"./#{File.dirname(file.name)}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
sig do
|
|
173
|
+
params(package_json_file: Dependabot::DependencyFile, dependency: Dependabot::Dependency)
|
|
174
|
+
.returns(T::Boolean)
|
|
175
|
+
end
|
|
176
|
+
def package_json_declares?(package_json_file, dependency)
|
|
177
|
+
declared = T.let(false, T::Boolean)
|
|
178
|
+
Dependabot::Package::NpmPackageJson.from_file(package_json_file).each_dependency do |name, _req, _type|
|
|
179
|
+
declared ||= name == dependency.name
|
|
180
|
+
end
|
|
181
|
+
declared
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
sig { returns(T.nilable(Dependabot::DependencyFile)) }
|
|
185
|
+
def root_package_json
|
|
186
|
+
package_files.find { |f| f.name == "package.json" }
|
|
99
187
|
end
|
|
100
188
|
|
|
101
189
|
sig { void }
|
|
@@ -116,10 +204,13 @@ module Dependabot
|
|
|
116
204
|
).parse
|
|
117
205
|
end
|
|
118
206
|
|
|
119
|
-
sig { params(error: Dependabot::DependabotError,
|
|
120
|
-
def handle_bun_lock_updater_error(error,
|
|
207
|
+
sig { params(error: Dependabot::DependabotError, bun_lock: Dependabot::DependencyFile).returns(T.noreturn) }
|
|
208
|
+
def handle_bun_lock_updater_error(error, bun_lock)
|
|
121
209
|
error_message = error.message
|
|
122
210
|
|
|
211
|
+
raise_registry_error(error_message)
|
|
212
|
+
raise_workspace_error(error_message, bun_lock)
|
|
213
|
+
|
|
123
214
|
ERR_PATTERNS.each do |pattern, error_class|
|
|
124
215
|
raise error_class, error_message if error_message.match?(pattern)
|
|
125
216
|
end
|
|
@@ -127,6 +218,65 @@ module Dependabot
|
|
|
127
218
|
raise error
|
|
128
219
|
end
|
|
129
220
|
|
|
221
|
+
# Reports the first registry failure bun printed that the repository has to resolve: access that was denied,
|
|
222
|
+
# or a tarball the registry does not have. A failure with any other status is skipped, so it cannot hide a
|
|
223
|
+
# later one that can be classified, and is left to the generic handling when there is none.
|
|
224
|
+
sig { params(error_message: String).void }
|
|
225
|
+
def raise_registry_error(error_message)
|
|
226
|
+
registry_failures(error_message).each do |failure|
|
|
227
|
+
status = failure[:status]
|
|
228
|
+
url = failure[:url]
|
|
229
|
+
|
|
230
|
+
if ACCESS_DENIED_STATUSES.include?(status) && url
|
|
231
|
+
raise Dependabot::PrivateSourceAuthenticationFailure, registry_origin(url)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
next unless status == NOT_FOUND_STATUS && failure.names.include?("spec")
|
|
235
|
+
|
|
236
|
+
raise Dependabot::DependencyNotFound, package_name_from(T.must(failure[:spec]))
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Every registry failure in the output, in the order bun printed them
|
|
241
|
+
sig { params(error_message: String).returns(T::Array[MatchData]) }
|
|
242
|
+
def registry_failures(error_message)
|
|
243
|
+
[DOWNLOAD_FAILED, REQUEST_FAILED]
|
|
244
|
+
.flat_map { |pattern| error_message.to_enum(:scan, pattern).map { T.must(Regexp.last_match) } }
|
|
245
|
+
.sort_by { |match| match.begin(0) }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
sig { params(error_message: String, bun_lock: Dependabot::DependencyFile).void }
|
|
249
|
+
def raise_workspace_error(error_message, bun_lock)
|
|
250
|
+
if (match = error_message.match(WORKSPACE_NOT_ON_DISK))
|
|
251
|
+
raise Dependabot::DependencyFileNotResolvable,
|
|
252
|
+
"#{bun_lock.path} lists the workspace \"#{match[:name]}\" (#{match[:path]}), " \
|
|
253
|
+
"but it is not part of the repository. Every workspace in the lockfile must be present."
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
return unless (match = error_message.match(WORKSPACE_NOT_FOUND))
|
|
257
|
+
|
|
258
|
+
raise Dependabot::DependencyFileNotResolvable,
|
|
259
|
+
"package.json lists the workspace \"#{match[:path]}\", but it does not exist in the repository."
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# The scheme, host and any non-default port of the URL, so the error names the registry and not a single
|
|
263
|
+
# tarball. A registry on another port is a different endpoint.
|
|
264
|
+
sig { params(url: String).returns(String) }
|
|
265
|
+
def registry_origin(url)
|
|
266
|
+
uri = URI.parse(url)
|
|
267
|
+
default_port = T.cast(uri.default_port, T.nilable(Integer))
|
|
268
|
+
port = ":#{uri.port}" if uri.port && uri.port != default_port
|
|
269
|
+
"#{uri.scheme}://#{uri.host}#{port}"
|
|
270
|
+
rescue URI::InvalidURIError
|
|
271
|
+
url
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# "@scope/name@1.2.3" and "name@1.2.3" both end in the version
|
|
275
|
+
sig { params(spec: String).returns(String) }
|
|
276
|
+
def package_name_from(spec)
|
|
277
|
+
spec.sub(/@[^@]*\z/, "")
|
|
278
|
+
end
|
|
279
|
+
|
|
130
280
|
sig { void }
|
|
131
281
|
def write_final_package_json_files
|
|
132
282
|
package_files.each do |file|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# typed: strong
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "sorbet-runtime"
|
|
6
|
+
|
|
7
|
+
require "dependabot/bun/file_updater"
|
|
8
|
+
|
|
9
|
+
module Dependabot
|
|
10
|
+
module Bun
|
|
11
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
12
|
+
# Locates object members in JSON source text, so they can be edited in
|
|
13
|
+
# place without reformatting the document.
|
|
14
|
+
class JsonObjectMembers
|
|
15
|
+
extend T::Sig
|
|
16
|
+
|
|
17
|
+
# Maps each member of the JSON object opening at `start` whose value is
|
|
18
|
+
# an object to the range of that value in `content`.
|
|
19
|
+
sig { params(content: String, start: Integer).returns(T::Hash[String, T::Range[Integer]]) }
|
|
20
|
+
def self.objects(content, start)
|
|
21
|
+
new(content, start).objects
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Maps each member of the JSON object opening at `start` whose value is
|
|
25
|
+
# a string to the range of that value, quotes included, in `content`.
|
|
26
|
+
sig { params(content: String, start: Integer).returns(T::Hash[String, T::Range[Integer]]) }
|
|
27
|
+
def self.strings(content, start)
|
|
28
|
+
new(content, start).strings
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { returns(T::Hash[String, T::Range[Integer]]) }
|
|
32
|
+
attr_reader :objects
|
|
33
|
+
|
|
34
|
+
sig { returns(T::Hash[String, T::Range[Integer]]) }
|
|
35
|
+
attr_reader :strings
|
|
36
|
+
|
|
37
|
+
sig { params(content: String, start: Integer).void }
|
|
38
|
+
def initialize(content, start)
|
|
39
|
+
@objects = T.let({}, T::Hash[String, T::Range[Integer]])
|
|
40
|
+
@strings = T.let({}, T::Hash[String, T::Range[Integer]])
|
|
41
|
+
@depth = T.let(0, Integer)
|
|
42
|
+
@key = T.let(nil, T.nilable(String))
|
|
43
|
+
@value_start = T.let(nil, T.nilable(Integer))
|
|
44
|
+
@string = T.let(nil, T.nilable(String))
|
|
45
|
+
@last_string = T.let(nil, T.nilable(String))
|
|
46
|
+
@escaped = T.let(false, T::Boolean)
|
|
47
|
+
scan(content, start)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
sig { params(content: String, start: Integer).void }
|
|
53
|
+
def scan(content, start)
|
|
54
|
+
T.must(content[start..]).each_char.with_index(start) do |char, index|
|
|
55
|
+
if @string
|
|
56
|
+
read_string(@string, char, index)
|
|
57
|
+
else
|
|
58
|
+
read_structure(char, index)
|
|
59
|
+
break if @depth.zero?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig { params(string: String, char: String, index: Integer).void }
|
|
65
|
+
def read_string(string, char, index)
|
|
66
|
+
if @escaped
|
|
67
|
+
@escaped = false
|
|
68
|
+
elsif char == "\\"
|
|
69
|
+
@escaped = true
|
|
70
|
+
elsif char == "\""
|
|
71
|
+
close_string(string, index)
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
string << char
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sig { params(char: String, index: Integer).void }
|
|
79
|
+
def read_structure(char, index)
|
|
80
|
+
case char
|
|
81
|
+
when "\"" then open_string(index)
|
|
82
|
+
when ":" then @key = @last_string if @depth == 1
|
|
83
|
+
when "," then @key = nil if @depth == 1
|
|
84
|
+
when "{", "[" then enter(char, index)
|
|
85
|
+
when "}", "]" then leave(index)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { params(index: Integer).void }
|
|
90
|
+
def open_string(index)
|
|
91
|
+
@string = +""
|
|
92
|
+
@value_start = index if @depth == 1 && @key
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
sig { params(string: String, index: Integer).void }
|
|
96
|
+
def close_string(string, index)
|
|
97
|
+
value = T.cast(JSON.parse("\"#{string}\""), String)
|
|
98
|
+
@last_string = value
|
|
99
|
+
@string = nil
|
|
100
|
+
record(@strings, index) if @depth == 1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
sig { params(char: String, index: Integer).void }
|
|
104
|
+
def enter(char, index)
|
|
105
|
+
@value_start = index if @depth == 1 && char == "{" && @key
|
|
106
|
+
@depth += 1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(index: Integer).void }
|
|
110
|
+
def leave(index)
|
|
111
|
+
@depth -= 1
|
|
112
|
+
record(@objects, index) if @depth == 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
sig { params(members: T::Hash[String, T::Range[Integer]], index: Integer).void }
|
|
116
|
+
def record(members, index)
|
|
117
|
+
key = @key
|
|
118
|
+
value_start = @value_start
|
|
119
|
+
return unless key && value_start
|
|
120
|
+
|
|
121
|
+
members[key] = value_start..index
|
|
122
|
+
@value_start = nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
5
|
|
|
6
|
+
require "dependabot/bun/file_parser"
|
|
6
7
|
require "dependabot/bun/file_updater"
|
|
8
|
+
require "dependabot/bun/file_updater/json_object_members"
|
|
9
|
+
require "dependabot/package/npm_package_json"
|
|
7
10
|
|
|
8
11
|
module Dependabot
|
|
9
12
|
module Bun
|
|
@@ -93,7 +96,10 @@ module Dependabot
|
|
|
93
96
|
def old_requirement(dependency, new_requirement)
|
|
94
97
|
T.must(dependency.previous_requirements)
|
|
95
98
|
.select { |r| r.file == package_json.name }
|
|
96
|
-
.find
|
|
99
|
+
.find do |r|
|
|
100
|
+
r.groups == new_requirement.groups &&
|
|
101
|
+
r.metadata_string("catalog") == new_requirement.metadata_string("catalog")
|
|
102
|
+
end
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
sig { params(dependency: Dependabot::Dependency).returns(T::Array[Dependabot::DependencyRequirement]) }
|
|
@@ -131,6 +137,17 @@ module Dependabot
|
|
|
131
137
|
.returns(String)
|
|
132
138
|
end
|
|
133
139
|
def update_package_json_declaration(package_json_content:, new_req:, dependency_name:, old_req:)
|
|
140
|
+
catalog = new_req.metadata_string("catalog")
|
|
141
|
+
if catalog
|
|
142
|
+
return update_catalog_declaration(
|
|
143
|
+
catalog: catalog,
|
|
144
|
+
content: package_json_content,
|
|
145
|
+
dependency_name: dependency_name,
|
|
146
|
+
old_req: old_req,
|
|
147
|
+
new_req: new_req
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
134
151
|
original_line = declaration_line(
|
|
135
152
|
dependency_name: dependency_name,
|
|
136
153
|
dependency_req: old_req,
|
|
@@ -323,6 +340,72 @@ module Dependabot
|
|
|
323
340
|
end
|
|
324
341
|
end
|
|
325
342
|
|
|
343
|
+
# Bun reads catalogs only from `NpmPackageJson#catalog_container`, so
|
|
344
|
+
# the entry is looked up and replaced among those catalogs' members.
|
|
345
|
+
sig do
|
|
346
|
+
params(
|
|
347
|
+
catalog: String,
|
|
348
|
+
content: String,
|
|
349
|
+
dependency_name: String,
|
|
350
|
+
old_req: T.nilable(Dependabot::DependencyRequirement),
|
|
351
|
+
new_req: Dependabot::DependencyRequirement
|
|
352
|
+
)
|
|
353
|
+
.returns(String)
|
|
354
|
+
end
|
|
355
|
+
def update_catalog_declaration(catalog:, content:, dependency_name:, old_req:, new_req:)
|
|
356
|
+
value_ranges = catalog_ranges(catalog, content).filter_map do |range|
|
|
357
|
+
JsonObjectMembers.strings(content, range.begin)[dependency_name]
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Replacing from the end keeps the earlier ranges valid.
|
|
361
|
+
value_ranges.sort_by(&:begin).reverse.each_with_object(content.dup) do |range, updated|
|
|
362
|
+
value = updated_catalog_value(dependency_name, T.must(updated[range]), old_req, new_req)
|
|
363
|
+
updated[range] = value if value
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# The default catalog is `catalog` together with `catalogs.default`.
|
|
368
|
+
sig { params(catalog: String, content: String).returns(T::Array[T::Range[Integer]]) }
|
|
369
|
+
def catalog_ranges(catalog, content)
|
|
370
|
+
root_start = content.index("{")
|
|
371
|
+
return [] unless root_start
|
|
372
|
+
|
|
373
|
+
fields = JsonObjectMembers.objects(content, root_start)
|
|
374
|
+
document = Dependabot::Package::NpmPackageJson.new(JSON.parse(content), package_json.path)
|
|
375
|
+
container = document.catalog_container
|
|
376
|
+
if container
|
|
377
|
+
container_range = fields[container]
|
|
378
|
+
return [] unless container_range
|
|
379
|
+
|
|
380
|
+
fields = JsonObjectMembers.objects(content, container_range.begin)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
catalogs_range = fields["catalogs"]
|
|
384
|
+
[
|
|
385
|
+
(fields["catalog"] if catalog == FileParser::DEFAULT_CATALOG),
|
|
386
|
+
(JsonObjectMembers.objects(content, catalogs_range.begin)[catalog] if catalogs_range)
|
|
387
|
+
].compact
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# The value is matched and rewritten as a normalized declaration, so
|
|
391
|
+
# its source formatting never affects the match.
|
|
392
|
+
sig do
|
|
393
|
+
params(
|
|
394
|
+
dependency_name: String,
|
|
395
|
+
value: String,
|
|
396
|
+
old_req: T.nilable(Dependabot::DependencyRequirement),
|
|
397
|
+
new_req: Dependabot::DependencyRequirement
|
|
398
|
+
)
|
|
399
|
+
.returns(T.nilable(String))
|
|
400
|
+
end
|
|
401
|
+
def updated_catalog_value(dependency_name, value, old_req, new_req)
|
|
402
|
+
prefix = %("#{dependency_name}": )
|
|
403
|
+
line = prefix + JSON.generate(JSON.parse(value))
|
|
404
|
+
return if declaration_line(dependency_name: dependency_name, dependency_req: old_req, content: line).empty?
|
|
405
|
+
|
|
406
|
+
replacement_declaration_line(original_line: line, old_req: old_req, new_req: new_req).delete_prefix(prefix)
|
|
407
|
+
end
|
|
408
|
+
|
|
326
409
|
sig { params(string: String).returns(Integer) }
|
|
327
410
|
def closing_bracket_index(string)
|
|
328
411
|
closes_required = 1
|
|
@@ -63,21 +63,23 @@ module Dependabot
|
|
|
63
63
|
nil
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
sig { params(command: String, fingerprint: T.nilable(String)).returns(String) }
|
|
66
|
+
sig { params(command: T.any(String, T::Array[String]), fingerprint: T.nilable(String)).returns(String) }
|
|
67
67
|
def self.run_bun_command(command, fingerprint: nil)
|
|
68
|
-
full_command = "bun #{command}"
|
|
68
|
+
full_command = command.is_a?(Array) ? ["bun", *command] : "bun #{command}"
|
|
69
|
+
display_command = full_command.is_a?(Array) ? full_command.join(" ") : full_command
|
|
70
|
+
fingerprint_command = command.is_a?(Array) ? command.join(" ") : command
|
|
69
71
|
|
|
70
|
-
Dependabot.logger.info("Running bun command: #{
|
|
72
|
+
Dependabot.logger.info("Running bun command: #{display_command}")
|
|
71
73
|
|
|
72
74
|
result = Dependabot::SharedHelpers.run_shell_command(
|
|
73
75
|
full_command,
|
|
74
|
-
fingerprint: "bun #{fingerprint ||
|
|
76
|
+
fingerprint: "bun #{fingerprint || fingerprint_command}"
|
|
75
77
|
)
|
|
76
78
|
|
|
77
|
-
Dependabot.logger.info("Command executed successfully: #{
|
|
79
|
+
Dependabot.logger.info("Command executed successfully: #{display_command}")
|
|
78
80
|
result
|
|
79
81
|
rescue StandardError => e
|
|
80
|
-
Dependabot.logger.error("Error running bun command: #{
|
|
82
|
+
Dependabot.logger.error("Error running bun command: #{display_command}, Error: #{e.message}")
|
|
81
83
|
raise
|
|
82
84
|
end
|
|
83
85
|
|
|
@@ -178,21 +178,25 @@ module Dependabot
|
|
|
178
178
|
def latest_version_listing
|
|
179
179
|
return @latest_version_listing unless @latest_version_listing.nil?
|
|
180
180
|
|
|
181
|
+
@latest_version_listing = T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
182
|
+
|
|
181
183
|
response = Dependabot::RegistryClient.get(
|
|
182
184
|
url: "#{dependency_url}/latest",
|
|
183
185
|
headers: registry_auth_headers
|
|
184
186
|
)
|
|
185
|
-
return @latest_version_listing
|
|
187
|
+
return T.must(@latest_version_listing) if response.status >= 500
|
|
186
188
|
|
|
187
189
|
begin
|
|
188
|
-
|
|
190
|
+
# A registry can answer a missing package with a bare JSON string such as "Not Found"
|
|
191
|
+
parsed = JSON.parse(response.body)
|
|
192
|
+
@latest_version_listing = parsed if parsed.is_a?(Hash)
|
|
189
193
|
rescue JSON::ParserError
|
|
190
194
|
raise unless non_standard_registry?
|
|
191
|
-
|
|
192
|
-
@latest_version_listing = {}
|
|
193
195
|
end
|
|
196
|
+
|
|
197
|
+
T.must(@latest_version_listing)
|
|
194
198
|
rescue JSON::ParserError, Excon::Error::Timeout
|
|
195
|
-
|
|
199
|
+
T.must(@latest_version_listing)
|
|
196
200
|
end
|
|
197
201
|
|
|
198
202
|
sig { returns(T::Array[[String, T::Hash[String, T.untyped]]]) }
|
|
@@ -209,18 +213,21 @@ module Dependabot
|
|
|
209
213
|
def npm_listing
|
|
210
214
|
return @npm_listing unless @npm_listing.nil?
|
|
211
215
|
|
|
216
|
+
@npm_listing = T.let({}, T.nilable(T::Hash[String, T.untyped]))
|
|
217
|
+
|
|
212
218
|
response = Dependabot::RegistryClient.get(url: dependency_url, headers: registry_auth_headers)
|
|
213
|
-
return @npm_listing
|
|
219
|
+
return T.must(@npm_listing) if response.status >= 500
|
|
214
220
|
|
|
215
221
|
begin
|
|
216
|
-
|
|
222
|
+
parsed = JSON.parse(response.body)
|
|
223
|
+
@npm_listing = parsed if parsed.is_a?(Hash)
|
|
217
224
|
rescue JSON::ParserError
|
|
218
225
|
raise unless non_standard_registry?
|
|
219
|
-
|
|
220
|
-
@npm_listing = {}
|
|
221
226
|
end
|
|
227
|
+
|
|
228
|
+
T.must(@npm_listing)
|
|
222
229
|
rescue Excon::Error::Timeout
|
|
223
|
-
|
|
230
|
+
T.must(@npm_listing)
|
|
224
231
|
end
|
|
225
232
|
|
|
226
233
|
sig { returns(String) }
|
|
@@ -20,7 +20,7 @@ module Dependabot
|
|
|
20
20
|
https://registry.yarnpkg.com
|
|
21
21
|
http://registry.yarnpkg.com
|
|
22
22
|
).freeze
|
|
23
|
-
NPM_AUTH_TOKEN_REGEX = %r{
|
|
23
|
+
NPM_AUTH_TOKEN_REGEX = %r{^[ \t]*//(?<registry>.*)/:_authToken=(?<token>.*)$}
|
|
24
24
|
NPM_GLOBAL_REGISTRY_REGEX = /^registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
|
25
25
|
YARN_GLOBAL_REGISTRY_REGEX = /^(?:--)?registry\s+((['"](?<registry>.*)['"])|(?<registry>.*))/
|
|
26
26
|
NPM_SCOPED_REGISTRY_REGEX = /^(?<scope>@[^:]+)\s*:registry\s*=\s*['"]?(?<registry>.*?)['"]?$/
|
|
@@ -6,6 +6,7 @@ require "sorbet-runtime"
|
|
|
6
6
|
require "dependabot/errors"
|
|
7
7
|
require "dependabot/git_commit_checker"
|
|
8
8
|
require "dependabot/logger"
|
|
9
|
+
require "dependabot/package/npm_package_json"
|
|
9
10
|
require "dependabot/package/npm_registry_package"
|
|
10
11
|
require "dependabot/bun/dependency_files_filterer"
|
|
11
12
|
require "dependabot/bun/file_parser"
|
|
@@ -615,13 +616,119 @@ module Dependabot
|
|
|
615
616
|
def run_bun_checker(path:, version:)
|
|
616
617
|
SharedHelpers.with_git_configured(credentials: credentials) do
|
|
617
618
|
Dir.chdir(path) do
|
|
618
|
-
|
|
619
|
-
"update #{dependency.name}@#{version} --save-text-lockfile --ignore-scripts",
|
|
620
|
-
fingerprint: "update <dependency_name>@<version> --save-text-lockfile --ignore-scripts"
|
|
621
|
-
)
|
|
619
|
+
workspace_filters(path).flat_map { |filter| run_bun_update(filter: filter, version: version) }
|
|
622
620
|
end
|
|
623
621
|
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
sig do
|
|
625
|
+
params(
|
|
626
|
+
filter: T.nilable(String),
|
|
627
|
+
version: T.nilable(T.any(String, Gem::Version))
|
|
628
|
+
).returns(T::Array[PeerDependencyConflict])
|
|
629
|
+
end
|
|
630
|
+
def run_bun_update(filter:, version:)
|
|
631
|
+
command = T.let(["update", "#{dependency.name}@#{version}"], T::Array[String])
|
|
632
|
+
command += ["--filter", filter] if filter
|
|
633
|
+
command += ["--save-text-lockfile", "--ignore-scripts"]
|
|
634
|
+
|
|
635
|
+
Helpers.run_bun_command(
|
|
636
|
+
command,
|
|
637
|
+
fingerprint: "update <dependency_name>@<version> --save-text-lockfile --ignore-scripts"
|
|
638
|
+
)
|
|
624
639
|
[]
|
|
640
|
+
rescue SharedHelpers::HelperSubprocessFailed => e
|
|
641
|
+
handle_peer_dependency_errors(e.message)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Returns every workspace filter (nil for the +path+ we've already
|
|
645
|
+
# chdir'd into, or a workspace path relative to it) for manifests in
|
|
646
|
+
# the project rooted at +path+ that declare +dependency+. Manifests
|
|
647
|
+
# are matched by scanning their actual dependency fields (as
|
|
648
|
+
# BunLockfileUpdater does), rather than dependency.requirements,
|
|
649
|
+
# since Bun always records catalog entries against the root
|
|
650
|
+
# package.json even when a nested workspace is the only consumer
|
|
651
|
+
# (via "catalog:"). Falls back to an unfiltered check at +path+ if
|
|
652
|
+
# it isn't declared anywhere in this project (e.g. a
|
|
653
|
+
# sub-dependency).
|
|
654
|
+
sig { params(path: String).returns(T::Array[T.nilable(String)]) }
|
|
655
|
+
def workspace_filters(path)
|
|
656
|
+
manifests = package_jsons_in_project(path)
|
|
657
|
+
root_file = manifests.find { |file| File.dirname(file.name) == path }
|
|
658
|
+
|
|
659
|
+
workspace_filters = manifests.filter_map do |file|
|
|
660
|
+
next if file == root_file
|
|
661
|
+
next unless package_json_declares?(file)
|
|
662
|
+
|
|
663
|
+
workspace_filter(file, path: path)
|
|
664
|
+
end.uniq
|
|
665
|
+
|
|
666
|
+
declared_at_root = root_file && package_json_declares?(root_file)
|
|
667
|
+
return workspace_filters if !declared_at_root && workspace_filters.any?
|
|
668
|
+
|
|
669
|
+
[nil, *workspace_filters]
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Every package.json belonging to the same Bun project as +path+:
|
|
673
|
+
# its own manifest, plus any workspace members governed by the same
|
|
674
|
+
# lockfile. Manifests under a *nested* project with its own
|
|
675
|
+
# lockfile are excluded, so that sibling nested projects (each
|
|
676
|
+
# checked separately, chdir'd into their own path) don't leak their
|
|
677
|
+
# declarations into each other's filters.
|
|
678
|
+
sig { params(path: String).returns(T::Array[Dependabot::DependencyFile]) }
|
|
679
|
+
def package_jsons_in_project(path)
|
|
680
|
+
project_lockfile_dir = lockfile_dir_for(path)
|
|
681
|
+
|
|
682
|
+
dependency_files.select do |file|
|
|
683
|
+
next false unless file.name.end_with?("package.json")
|
|
684
|
+
|
|
685
|
+
dir = File.dirname(file.name)
|
|
686
|
+
next false unless path == "." || dir == path || dir.start_with?("#{path}/")
|
|
687
|
+
|
|
688
|
+
lockfile_dir_for(dir) == project_lockfile_dir
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# The directory of the bun.lock that governs +dir+: the lockfile
|
|
693
|
+
# whose own directory is the longest ancestor match, since a nested
|
|
694
|
+
# lockfile takes precedence over the root one for its own subtree.
|
|
695
|
+
sig { params(dir: String).returns(T.nilable(String)) }
|
|
696
|
+
def lockfile_dir_for(dir)
|
|
697
|
+
lockfile_dirs = dependency_files_builder.bun_locks.map { |lockfile| File.dirname(lockfile.name) }
|
|
698
|
+
lockfile_dirs.select { |lockfile_dir| lockfile_governs_dir?(lockfile_dir, dir) }.max_by(&:length)
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
sig { params(lockfile_dir: String, dir: String).returns(T::Boolean) }
|
|
702
|
+
def lockfile_governs_dir?(lockfile_dir, dir)
|
|
703
|
+
lockfile_dir == "." || dir == lockfile_dir || dir.start_with?("#{lockfile_dir}/")
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
sig { params(package_json_file: Dependabot::DependencyFile).returns(T::Boolean) }
|
|
707
|
+
def package_json_declares?(package_json_file)
|
|
708
|
+
declared = T.let(false, T::Boolean)
|
|
709
|
+
Dependabot::Package::NpmPackageJson.from_file(package_json_file).each_dependency do |name, _req, _type|
|
|
710
|
+
declared ||= name == dependency.name
|
|
711
|
+
end
|
|
712
|
+
declared
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Path-based --filter target, relative to +path+, for the workspace
|
|
716
|
+
# at +file+ (e.g. "./packages/app"). Bun's --filter also accepts a
|
|
717
|
+
# package's declared "name", but names can contain glob
|
|
718
|
+
# metacharacters that would match unrelated workspaces, so a path is
|
|
719
|
+
# used instead.
|
|
720
|
+
sig { params(file: Dependabot::DependencyFile, path: String).returns(String) }
|
|
721
|
+
def workspace_filter(file, path:)
|
|
722
|
+
unless Dependabot::Package::NpmPackageJson.from_file(file).name
|
|
723
|
+
raise Dependabot::DependencyFileNotParseable.new(
|
|
724
|
+
file.path,
|
|
725
|
+
"#{file.name} must declare a \"name\" to be targeted for a workspace-scoped check"
|
|
726
|
+
)
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
workspace_dir = File.dirname(file.name)
|
|
730
|
+
workspace_dir = workspace_dir.delete_prefix("#{path}/") unless path == "."
|
|
731
|
+
"./#{workspace_dir}"
|
|
625
732
|
end
|
|
626
733
|
|
|
627
734
|
sig do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dependabot-bun
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.402.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.
|
|
18
|
+
version: 0.402.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.
|
|
25
|
+
version: 0.402.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: debug
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -323,6 +323,7 @@ files:
|
|
|
323
323
|
- lib/dependabot/bun/file_parser/lockfile_parser.rb
|
|
324
324
|
- lib/dependabot/bun/file_updater.rb
|
|
325
325
|
- lib/dependabot/bun/file_updater/bun_lockfile_updater.rb
|
|
326
|
+
- lib/dependabot/bun/file_updater/json_object_members.rb
|
|
326
327
|
- lib/dependabot/bun/file_updater/npmrc_builder.rb
|
|
327
328
|
- lib/dependabot/bun/file_updater/package_json_preparer.rb
|
|
328
329
|
- lib/dependabot/bun/file_updater/package_json_updater.rb
|
|
@@ -355,7 +356,7 @@ licenses:
|
|
|
355
356
|
- MIT
|
|
356
357
|
metadata:
|
|
357
358
|
bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
|
|
358
|
-
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.
|
|
359
|
+
changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.402.0
|
|
359
360
|
rdoc_options: []
|
|
360
361
|
require_paths:
|
|
361
362
|
- lib
|