proscenium 0.25.3 → 0.26.1
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/README.md +170 -23
- data/exe/proscenium +19 -0
- data/exe/psm +5 -0
- data/lib/proscenium/app_package.rb +90 -0
- data/lib/proscenium/builder.rb +221 -20
- data/lib/proscenium/bundled_gems.rb +103 -0
- data/lib/proscenium/cli/brackets.rb +66 -0
- data/lib/proscenium/cli/bun_linker.rb +79 -0
- data/lib/proscenium/cli/capabilities.json +29 -0
- data/lib/proscenium/cli/collisions.rb +94 -0
- data/lib/proscenium/cli/contexts.rb +133 -0
- data/lib/proscenium/cli/diff.rb +80 -0
- data/lib/proscenium/cli/error.rb +291 -0
- data/lib/proscenium/cli/gem_archive.rb +44 -0
- data/lib/proscenium/cli/gem_check.rb +150 -0
- data/lib/proscenium/cli/gitignore.rb +63 -0
- data/lib/proscenium/cli/inspect.rb +257 -0
- data/lib/proscenium/cli/install.rb +378 -0
- data/lib/proscenium/cli/manager.rb +323 -0
- data/lib/proscenium/cli/platform_variants.rb +82 -0
- data/lib/proscenium/cli/pnpm_approvals.rb +109 -0
- data/lib/proscenium/cli/pnpm_workspace.rb +149 -0
- data/lib/proscenium/cli/project_lock.rb +106 -0
- data/lib/proscenium/cli/registration.rb +140 -0
- data/lib/proscenium/cli/reporter.rb +103 -0
- data/lib/proscenium/cli/rules.rb +169 -0
- data/lib/proscenium/cli/runner.rb +142 -0
- data/lib/proscenium/cli/verify.rb +107 -0
- data/lib/proscenium/cli.rb +122 -0
- data/lib/proscenium/context_map.rb +181 -0
- data/lib/proscenium/css_module/transformer.rb +18 -3
- data/lib/proscenium/css_module.rb +5 -6
- data/lib/proscenium/dependency_context.rb +74 -0
- data/lib/proscenium/error.rb +10 -0
- data/lib/proscenium/helper.rb +38 -4
- data/lib/proscenium/importer.rb +53 -42
- data/lib/proscenium/manifest.rb +18 -23
- data/lib/proscenium/mapping_generation.rb +64 -0
- data/lib/proscenium/middleware/base.rb +5 -10
- data/lib/proscenium/middleware/chunks.rb +5 -0
- data/lib/proscenium/monkey.rb +51 -69
- data/lib/proscenium/railtie.rb +16 -0
- data/lib/proscenium/railties/assets.rake +2 -1
- data/lib/proscenium/resolver.rb +43 -11
- data/lib/proscenium/runtime/bootstrap.js +26 -6
- data/lib/proscenium/runtime/server.rb +57 -36
- data/lib/proscenium/side_load.rb +75 -18
- data/lib/proscenium/stale_contexts.rb +118 -0
- data/lib/proscenium/templates/rescues/build_error.html.erb +9 -7
- data/lib/proscenium/utils.rb +55 -0
- data/lib/proscenium/version.rb +1 -1
- data/lib/proscenium/yaml_tree.rb +48 -0
- data/lib/proscenium.rb +7 -4
- metadata +42 -13
- data/lib/proscenium/ext/proscenium +0 -0
- data/lib/proscenium/ext/proscenium.h +0 -113
- data/lib/proscenium/react-manager/index.jsx +0 -102
- data/lib/proscenium/react-manager/react.js +0 -2
- data/lib/proscenium/react_componentable.rb +0 -94
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'bundler'
|
|
5
|
+
require_relative '../bundled_gems'
|
|
6
|
+
require_relative '../dependency_context'
|
|
7
|
+
require_relative '../stale_contexts'
|
|
8
|
+
require_relative 'collisions'
|
|
9
|
+
require_relative 'contexts'
|
|
10
|
+
require_relative 'diff'
|
|
11
|
+
require_relative 'manager'
|
|
12
|
+
require_relative 'platform_variants'
|
|
13
|
+
require_relative 'project_lock'
|
|
14
|
+
require_relative 'registration'
|
|
15
|
+
require_relative 'runner'
|
|
16
|
+
require_relative 'verify'
|
|
17
|
+
|
|
18
|
+
module Proscenium
|
|
19
|
+
module CLI
|
|
20
|
+
# `proscenium install` and `install --frozen` (#154): the plan's installation algorithm.
|
|
21
|
+
class Install
|
|
22
|
+
# What a context directory without its package.json may hold and still be install's to remove.
|
|
23
|
+
OURS = %w[node_modules package.json.tmp .DS_Store].freeze
|
|
24
|
+
|
|
25
|
+
# Options: frozen, production, offline, manager (requested), experimental, js_args.
|
|
26
|
+
def initialize(root, reporter, **options)
|
|
27
|
+
@root = root
|
|
28
|
+
@reporter = reporter
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call
|
|
33
|
+
@timings = {}
|
|
34
|
+
if defined?(PROSCENIUM_STARTED)
|
|
35
|
+
@timings[:load] = ms(Process.clock_gettime(Process::CLOCK_MONOTONIC) - PROSCENIUM_STARTED)
|
|
36
|
+
end
|
|
37
|
+
timed(:manager_check) do
|
|
38
|
+
@manager = Manager.select(@root, requested: @options[:manager])
|
|
39
|
+
@manager.check_version!(experimental: @options[:experimental], frozen: frozen?)
|
|
40
|
+
@manager.check_project!(frozen: frozen?)
|
|
41
|
+
end
|
|
42
|
+
if (warning = @manager.end_of_life_warning)
|
|
43
|
+
@reporter.warning(warning, phase: 'manager')
|
|
44
|
+
end
|
|
45
|
+
read_bundle
|
|
46
|
+
frozen? ? check_frozen : install
|
|
47
|
+
0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The gems whose contexts declare a package the manager's failure output names, as
|
|
51
|
+
# [gem, package] pairs: an age-gate refusal or a failed fetch names only the package.
|
|
52
|
+
def self.blame(contexts, output)
|
|
53
|
+
return [] unless output.match?(/ERR_|error:/i)
|
|
54
|
+
|
|
55
|
+
contexts.flat_map do |gem, json|
|
|
56
|
+
context = JSON.parse(json)
|
|
57
|
+
Rules::DEPENDENCY_FIELDS.flat_map { (context[it] || {}).keys }
|
|
58
|
+
.reject { it.start_with?('@rubygems/') }
|
|
59
|
+
.select { output.match?(named(it)) }
|
|
60
|
+
.map { [gem, it] }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `name` as a whole package name, not a part of another.
|
|
65
|
+
def self.named(name) = %r{(?<![\w@/.-])#{Regexp.escape(name)}(?![\w/-])}
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def frozen? = @options[:frozen]
|
|
70
|
+
|
|
71
|
+
# Milliseconds each phase took, reported with the result so an install's cost can be split
|
|
72
|
+
# between Proscenium and the manager (C40).
|
|
73
|
+
def timed(phase)
|
|
74
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
|
+
yield
|
|
76
|
+
ensure
|
|
77
|
+
@timings[phase] = ms(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def ms(seconds) = (seconds * 1000).round
|
|
81
|
+
|
|
82
|
+
def count(...) = Reporter.count(...)
|
|
83
|
+
|
|
84
|
+
def read_bundle
|
|
85
|
+
overrides = BundledGems.overrides(@root)
|
|
86
|
+
specs = timed(:bundle) { BundledGems.installed_specs }
|
|
87
|
+
@excluded = BundledGems.excluded_names(specs)
|
|
88
|
+
@contexts = timed(:projection) do
|
|
89
|
+
Contexts.new(@root, BundledGems.participating(specs, overrides:))
|
|
90
|
+
end
|
|
91
|
+
@committed = Contexts.committed(@root)
|
|
92
|
+
check_owned_directory
|
|
93
|
+
report_problems
|
|
94
|
+
check_platform_variants(specs, overrides)
|
|
95
|
+
check_trusted_sources
|
|
96
|
+
check_collisions
|
|
97
|
+
rescue BundledGems::ConfigError => e
|
|
98
|
+
raise Error.new('PSM-E-CONFIG', detail: e.message)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def report_problems
|
|
102
|
+
@contexts.warnings.each do |code, args|
|
|
103
|
+
@reporter.warning(Error.new(code, **args), phase: 'validate')
|
|
104
|
+
end
|
|
105
|
+
return if @contexts.ok?
|
|
106
|
+
|
|
107
|
+
@contexts.problems.each do |code, args|
|
|
108
|
+
@reporter.error(Error.new(code, **args), phase: 'validate', manager: @manager.name)
|
|
109
|
+
end
|
|
110
|
+
raise Error.new('PSM-E-PROBLEMS', count: @contexts.problems.size)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `.proscenium/packages/` is Proscenium's: every entry is a generated context. A package.json
|
|
114
|
+
# there without the generated `proscenium` block, or a folder with no package.json that holds
|
|
115
|
+
# more than a node_modules, is someone else's, and install refuses rather than delete it as an
|
|
116
|
+
# orphan.
|
|
117
|
+
def check_owned_directory
|
|
118
|
+
check_owned_links
|
|
119
|
+
foreign = @committed.select do |gem, path|
|
|
120
|
+
# Without a context, only an old context's node_modules, a write that never finished, or
|
|
121
|
+
# macOS's .DS_Store is ours to remove; anything else, a .git included, is the user's.
|
|
122
|
+
next (Dir.children(File.dirname(path)) - OURS).any? unless File.exist?(path)
|
|
123
|
+
|
|
124
|
+
!generated?(gem, ContextMap.read_json(path))
|
|
125
|
+
rescue JSON::ParserError
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
entries = foreign.keys + strays
|
|
129
|
+
return if entries.empty?
|
|
130
|
+
|
|
131
|
+
raise Error.new('PSM-E-OWNED-DIR', entries: entries.join(', '))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Whether `package` is a context install wrote for `gem`: its generated name, and a
|
|
135
|
+
# `proscenium` block naming a projection. A `proscenium` key alone is not enough, or install
|
|
136
|
+
# would remove someone else's package as an orphan.
|
|
137
|
+
def generated?(gem, package)
|
|
138
|
+
package.is_a?(Hash) && package['name'] == DependencyContext.name_for(gem) &&
|
|
139
|
+
package.dig('proscenium', 'projection').is_a?(String)
|
|
140
|
+
rescue TypeError
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# A file, or a link leading nowhere, where only context directories belong.
|
|
145
|
+
def strays
|
|
146
|
+
dir = File.join(@root, Contexts::DIR)
|
|
147
|
+
return [] unless Dir.exist?(dir)
|
|
148
|
+
|
|
149
|
+
Dir.children(dir).reject { File.directory?(File.join(dir, it)) } - OURS
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Everything install writes is under .proscenium/ (C29). A link anywhere on the way, or in
|
|
153
|
+
# place of a context, would send those writes outside the app.
|
|
154
|
+
def check_owned_links
|
|
155
|
+
owned = ['.proscenium', Contexts::DIR] + @committed.flat_map do |gem, path|
|
|
156
|
+
[File.join(Contexts::DIR, gem), relative(path)]
|
|
157
|
+
end
|
|
158
|
+
links = owned.select { File.symlink?(File.join(@root, it)) }
|
|
159
|
+
raise Error.new('PSM-E-OWNED-LINK', paths: links.join(', ')) if links.any?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def check_platform_variants(specs, overrides)
|
|
163
|
+
found = PlatformVariants.differences(Bundler.app_cache.to_s, specs, overrides:)
|
|
164
|
+
return if found.empty?
|
|
165
|
+
|
|
166
|
+
raise Error.new('PSM-E-PLATFORM-VARIANT',
|
|
167
|
+
variants: found.map { |gem, file| "#{gem} (#{file})" }.join(', '))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def check_trusted_sources
|
|
171
|
+
manifests = @contexts.contexts.transform_values { JSON.parse(it.json) }
|
|
172
|
+
# Bun's trust is read only when a gem reaches some package another way.
|
|
173
|
+
return if Rules.substitutions(manifests).empty?
|
|
174
|
+
|
|
175
|
+
found = Rules.trusted_substitutions(@manager.trusted_names, manifests)
|
|
176
|
+
return if found.empty?
|
|
177
|
+
|
|
178
|
+
raise Error.new('PSM-E-TRUSTED-SOURCE', list: found.map { " - #{it}" }.join("\n"))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def check_collisions
|
|
182
|
+
found = Collisions.find(@root, @manager.name, @contexts.contexts.keys)
|
|
183
|
+
return if found.empty?
|
|
184
|
+
|
|
185
|
+
raise Error.new('PSM-E-COLLISION', count: found.size,
|
|
186
|
+
list: found.map { " - #{it}" }.join("\n"))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Committed contexts no participating gem owns. A locked gem that is not installed (another
|
|
190
|
+
# group or platform) keeps its context: the production rule trusts it.
|
|
191
|
+
def orphans = @committed.keys - @contexts.contexts.keys - @excluded
|
|
192
|
+
|
|
193
|
+
def changed
|
|
194
|
+
@contexts.contexts.values.reject do |context|
|
|
195
|
+
path = @committed[context.gem]
|
|
196
|
+
path && File.exist?(path) && File.read(path) == context.json
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def check_frozen
|
|
201
|
+
# The lock, not the marker: a frozen install writes nothing of Proscenium's, but its manager
|
|
202
|
+
# writes node_modules, so it must not run alongside another install. Drift is read and the
|
|
203
|
+
# tree verified under it too, so another install cannot change what is being compared.
|
|
204
|
+
ProjectLock.new(@root).synchronize('proscenium install --frozen') do |lock|
|
|
205
|
+
check_drift
|
|
206
|
+
timed(:manager) { run_manager(lock) }
|
|
207
|
+
timed(:verify) { Verify.new(@root, @manager, @contexts.contexts.keys).call }
|
|
208
|
+
end
|
|
209
|
+
headline = "Everything is up to date for #{count(@contexts.contexts.size, 'gem')}."
|
|
210
|
+
lines = [@reporter.paint(headline, :bold, :green), *kept]
|
|
211
|
+
lines.unshift('') unless @reporter.json?
|
|
212
|
+
@reporter.info(lines.join("\n"), event: 'frozen', timings: @timings)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def check_drift
|
|
216
|
+
@committed = Contexts.committed(@root)
|
|
217
|
+
drift = []
|
|
218
|
+
registered = Registration.registered?(@root, @manager.name)
|
|
219
|
+
drift << 'the gem contexts are not registered' unless registered
|
|
220
|
+
drift << "#{lockfile} is missing" unless File.exist?(File.join(@root, lockfile))
|
|
221
|
+
changed.each { drift << describe_change(it) }
|
|
222
|
+
orphans.each { drift << format(StaleContexts::ORPHAN, gem: it) }
|
|
223
|
+
return if drift.empty?
|
|
224
|
+
|
|
225
|
+
list = drift.map { " - #{it}" }.join("\n")
|
|
226
|
+
raise Error.new('PSM-E-DRIFT', count: drift.size, list:)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# A line for each committed context kept for a gem that is not installed.
|
|
230
|
+
def kept
|
|
231
|
+
(@excluded & @committed.keys).map do |gem|
|
|
232
|
+
" #{gem} is not installed here, so its committed dependency context was kept as it is."
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def describe_change(context)
|
|
237
|
+
path = @committed[context.gem]
|
|
238
|
+
unless path && File.exist?(path)
|
|
239
|
+
return "#{context.gem}: its dependency context hasn't been written yet"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
before = File.read(path)
|
|
243
|
+
diff = Diff.unified(relative(path), before, context.json)
|
|
244
|
+
committed = JSON.parse(before)['proscenium'] || {}
|
|
245
|
+
current = JSON.parse(context.json)['proscenium']
|
|
246
|
+
cause = if committed['projection'] != current['projection']
|
|
247
|
+
'its dependency context was written by a different version of Proscenium'
|
|
248
|
+
elsif committed['projectionSha256'] == current['projectionSha256']
|
|
249
|
+
'its dependency context was edited by hand'
|
|
250
|
+
else
|
|
251
|
+
'its NPM dependencies changed since its dependency context was written'
|
|
252
|
+
end
|
|
253
|
+
"#{context.gem}: #{cause}\n#{diff}"
|
|
254
|
+
rescue JSON::ParserError
|
|
255
|
+
"#{context.gem}: its dependency context is not valid JSON"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def install
|
|
259
|
+
lock = ProjectLock.new(@root)
|
|
260
|
+
lock.synchronize('proscenium install') do
|
|
261
|
+
if lock.interrupted?
|
|
262
|
+
@reporter.info('Finishing an install that was stopped before it finished.',
|
|
263
|
+
event: 'recover')
|
|
264
|
+
end
|
|
265
|
+
# Computed before the marker: a registration it refuses has changed nothing, and must not
|
|
266
|
+
# leave the engine refusing every build.
|
|
267
|
+
edits = Registration.edits(@root, @manager.name)
|
|
268
|
+
lock.mark!
|
|
269
|
+
@written = register(edits) + write_contexts + remove_orphans
|
|
270
|
+
timed(:manager) { run_manager(lock) }
|
|
271
|
+
timed(:verify) { Verify.new(@root, @manager, @contexts.contexts.keys).call }
|
|
272
|
+
lock.unmark!
|
|
273
|
+
end
|
|
274
|
+
summarize
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The one-time registration, printed as a diff before it is written.
|
|
278
|
+
def register(edits)
|
|
279
|
+
edits.map do |path, before, after|
|
|
280
|
+
diff = Diff.unified(relative(path), before, after)
|
|
281
|
+
message = if @reporter.json? then diff
|
|
282
|
+
else "Updating #{relative(path)}:\n#{@reporter.paint_diff(diff)}"
|
|
283
|
+
end
|
|
284
|
+
@reporter.info(message, event: 'registration', file: relative(path))
|
|
285
|
+
File.write(path, after)
|
|
286
|
+
relative(path)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Each changed context, written atomically; an unchanged one is left alone, so its native
|
|
291
|
+
# links survive.
|
|
292
|
+
def write_contexts
|
|
293
|
+
changed.map do |context|
|
|
294
|
+
dir = File.join(@root, Contexts::DIR, context.gem)
|
|
295
|
+
FileUtils.mkdir_p(dir)
|
|
296
|
+
path = File.join(dir, 'package.json')
|
|
297
|
+
ProjectLock.write("#{path}.tmp", context.json)
|
|
298
|
+
File.rename("#{path}.tmp", path)
|
|
299
|
+
relative(path)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def remove_orphans
|
|
304
|
+
orphans.map do |gem|
|
|
305
|
+
FileUtils.rm_rf(File.join(@root, Contexts::DIR, gem))
|
|
306
|
+
"#{Contexts::DIR}/#{gem} (removed)"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def run_manager(lock = nil)
|
|
311
|
+
unless @reporter.json?
|
|
312
|
+
@reporter.info(@reporter.paint("Running #{@manager.name} #{manager_args.join(' ')}...",
|
|
313
|
+
:dim))
|
|
314
|
+
end
|
|
315
|
+
Runner.run(@manager.executable, manager_args, root: @root,
|
|
316
|
+
on_spawn: lock&.method(:manager_started),
|
|
317
|
+
manager: @manager.name)
|
|
318
|
+
rescue Error => e
|
|
319
|
+
raise unless e.code == 'PSM-E-NATIVE'
|
|
320
|
+
|
|
321
|
+
blamed = self.class.blame(@contexts.contexts.transform_values(&:json), e.output.to_s)
|
|
322
|
+
raise if blamed.empty?
|
|
323
|
+
|
|
324
|
+
involved = blamed.map { |gem, package| "#{package}, which #{gem} brings in" }.join('; ')
|
|
325
|
+
error = Error.new('PSM-E-NATIVE', command: "#{@manager.name} #{manager_args.join(' ')}",
|
|
326
|
+
status: e.details[:exitStatus], details: e.details,
|
|
327
|
+
escape: "It involves #{involved}.")
|
|
328
|
+
raise error
|
|
329
|
+
ensure
|
|
330
|
+
lock&.manager_finished
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def manager_args
|
|
334
|
+
args = ['install']
|
|
335
|
+
args << '--frozen-lockfile' if frozen?
|
|
336
|
+
args << (@manager.name == 'bun' ? '--production' : '--prod') if @options[:production]
|
|
337
|
+
args << '--offline' if @options[:offline]
|
|
338
|
+
# A gem only in an excluded group has no source to read; its context is trusted and its
|
|
339
|
+
# dependencies left out of a production install.
|
|
340
|
+
if @options[:production]
|
|
341
|
+
(@excluded & @committed.keys).each do |gem|
|
|
342
|
+
args << "--filter=!#{DependencyContext.name_for(gem)}"
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
args + Array(@options[:js_args])
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def lockfile = Manager::LOCKFILES.key(@manager.name)
|
|
349
|
+
|
|
350
|
+
def relative(path) = path.delete_prefix("#{@root}/")
|
|
351
|
+
|
|
352
|
+
def summarize
|
|
353
|
+
contexts = @contexts.contexts.values
|
|
354
|
+
headline = "Installed NPM dependencies for #{count(contexts.size, 'gem')} with " \
|
|
355
|
+
"#{@manager.name} #{@manager.version}."
|
|
356
|
+
width = contexts.map { it.gem.size }.max.to_i
|
|
357
|
+
lines = [@reporter.paint(headline, :bold, :green)]
|
|
358
|
+
contexts.each do |context|
|
|
359
|
+
total = JSON.parse(context.json).values_at('dependencies', 'peerDependencies',
|
|
360
|
+
'optionalDependencies').compact.sum(&:size)
|
|
361
|
+
lines << " #{context.gem.ljust(width)} #{count(total, 'dependency', 'dependencies')}"
|
|
362
|
+
end
|
|
363
|
+
introduced = contexts.flat_map(&:git_and_url)
|
|
364
|
+
if introduced.any?
|
|
365
|
+
lines << '' << 'Gems bring in these Git and URL dependencies:'
|
|
366
|
+
lines.concat(introduced.map { " #{it}" })
|
|
367
|
+
end
|
|
368
|
+
lines.concat(kept)
|
|
369
|
+
commit = (@written + [lockfile]).uniq
|
|
370
|
+
lines << '' << @reporter.paint('Commit these files:', :bold)
|
|
371
|
+
lines.concat(commit.map { " #{it}" })
|
|
372
|
+
lines.unshift('') unless @reporter.json?
|
|
373
|
+
@reporter.info(lines.join("\n"), event: 'installed', manager: @manager.name,
|
|
374
|
+
gems: contexts.map(&:gem), commit:, timings: @timings)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|