importmap-plus 1.1.1 → 2.0.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/CHANGELOG.md +351 -0
- data/README.md +10 -2
- data/app/helpers/importmap/importmap_tags_helper.rb +3 -0
- data/lib/importmap/batch_resolver.rb +133 -0
- data/lib/importmap/commands.rb +333 -32
- data/lib/importmap/doctor.rb +331 -0
- data/lib/importmap/early_hints.rb +50 -0
- data/lib/importmap/engine.rb +3 -0
- data/lib/importmap/esm_run.rb +118 -0
- data/lib/importmap/graph.rb +126 -0
- data/lib/importmap/import_scanner.rb +53 -0
- data/lib/importmap/integrity.rb +25 -0
- data/lib/importmap/map.rb +38 -1
- data/lib/importmap/module_inspector.rb +200 -0
- data/lib/importmap/npm.rb +9 -0
- data/lib/importmap/package_graph.rb +299 -0
- data/lib/importmap/packager.rb +302 -110
- data/lib/importmap/provider_chain.rb +89 -0
- data/lib/importmap/vendored_graph.rb +214 -0
- data/lib/importmap/version.rb +1 -1
- metadata +12 -1
data/lib/importmap/commands.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
require "thor"
|
|
2
2
|
require "importmap/packager"
|
|
3
|
+
require "importmap/package_graph"
|
|
4
|
+
require "importmap/vendored_graph"
|
|
3
5
|
require "importmap/npm"
|
|
6
|
+
require "importmap/provider_chain"
|
|
7
|
+
require "importmap/batch_resolver"
|
|
8
|
+
require "importmap/integrity"
|
|
9
|
+
require "importmap/doctor"
|
|
4
10
|
|
|
5
11
|
class Importmap::Commands < Thor
|
|
6
12
|
include Thor::Actions
|
|
@@ -17,19 +23,24 @@ class Importmap::Commands < Thor
|
|
|
17
23
|
option :minify, type: :boolean, desc: "Minify the vendored download with bun, esbuild or terser"
|
|
18
24
|
option :lock, type: :boolean, desc: "Lock the pin at this version; update, pin and pristine leave it there until unlocked"
|
|
19
25
|
option :force, type: :boolean, default: false, desc: "Re-pin locked packages, keeping each lock at the new version"
|
|
26
|
+
option :vendor, type: :boolean, default: false, desc: "Vendor the download even when it looks like it needs sibling files; converts a pin kept remote back"
|
|
27
|
+
option :integrity, type: :boolean, default: true, desc: "Write a subresource-integrity hash on a pin kept remote; --no-integrity skips the fetch that computes it"
|
|
20
28
|
def pin(*packages)
|
|
21
|
-
packages = without_locked(packages, lock: options[:lock], force: options[:force])
|
|
29
|
+
packages = resolve_latest_versions(without_locked(packages, lock: options[:lock], force: options[:force]))
|
|
22
30
|
# jspm resolves a package together with its dependencies; --lock and
|
|
23
31
|
# --no-lock are about the packages that were asked for, not those.
|
|
24
32
|
requested = packages.map { |spec| packager.package_key_for(spec) }
|
|
25
33
|
|
|
26
|
-
for_each_import_grouped_by_provider(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
34
|
+
for_each_import_grouped_by_provider(packages, env: options[:env], from: options[:from], fallback: true) do |package, url|
|
|
27
35
|
next if keep_locked_dependency(package, requested)
|
|
28
36
|
|
|
29
37
|
pin_package(package, url, preload: options[:preload], remote: options[:remote], env: options[:env],
|
|
30
38
|
minify: options[:minify], from: options[:from],
|
|
31
|
-
lock: requested.include?(package) ? options[:lock] : nil
|
|
39
|
+
lock: requested.include?(package) ? options[:lock] : nil,
|
|
40
|
+
vendor: requested.include?(package) && options[:vendor])
|
|
32
41
|
end
|
|
42
|
+
|
|
43
|
+
exit 1 if skipped.any? || resolver.unresolved.any?
|
|
33
44
|
end
|
|
34
45
|
|
|
35
46
|
desc "lock [*PACKAGES]", "Lock packages at their pinned version"
|
|
@@ -60,6 +71,7 @@ class Importmap::Commands < Thor
|
|
|
60
71
|
option :minify, type: :boolean, desc: "Minify every download; defaults to what each vendored file already is"
|
|
61
72
|
def pristine
|
|
62
73
|
packages = prepare_packages_with_versions
|
|
74
|
+
unrestored = []
|
|
63
75
|
|
|
64
76
|
for_each_import_grouped_by_provider(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
65
77
|
if packager.remote_pin?(package)
|
|
@@ -67,14 +79,13 @@ class Importmap::Commands < Thor
|
|
|
67
79
|
elsif (resolved = version_drift_of_locked(package, url))
|
|
68
80
|
puts %(Skipping "#{package}" (locked at #{packager.pin_provenance(package)[:version]}, CDN resolved #{resolved}))
|
|
69
81
|
else
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url}#{" (minified)" if minify})
|
|
73
|
-
|
|
74
|
-
pin_esm_run_dependencies packager.download(package, url, minify: minify), minify: minify
|
|
75
|
-
record_provenance(package, url, minify) if provenance_changed?(package, url, minify)
|
|
82
|
+
unrestored << package unless restore_package(package, url)
|
|
76
83
|
end
|
|
77
84
|
end
|
|
85
|
+
|
|
86
|
+
# skipped: an esm.run bundle's dependency is pinned on the way, and one the
|
|
87
|
+
# CDN failed on is skipped the same way pin skips it.
|
|
88
|
+
exit 1 if unrestored.any? || skipped.any? || resolver.unresolved.any?
|
|
78
89
|
end
|
|
79
90
|
|
|
80
91
|
desc "json", "Show the full importmap in json"
|
|
@@ -149,14 +160,14 @@ class Importmap::Commands < Thor
|
|
|
149
160
|
else
|
|
150
161
|
keys = packages.any? ? requested_keys_for(packages, names) : outdated_keys_for(names)
|
|
151
162
|
|
|
152
|
-
for_each_import_grouped_by_provider(keys, env: "production") do |package, url|
|
|
163
|
+
for_each_import_grouped_by_provider(keys, env: "production", fallback: true) do |package, url|
|
|
153
164
|
next if keep_locked_dependency(package, keys)
|
|
154
165
|
|
|
155
166
|
pin_package(package, url)
|
|
156
167
|
end
|
|
157
168
|
end
|
|
158
169
|
|
|
159
|
-
exit 1 if unchecked_packages.any?
|
|
170
|
+
exit 1 if unchecked_packages.any? || skipped.any? || resolver.unresolved.any?
|
|
160
171
|
end
|
|
161
172
|
|
|
162
173
|
desc "packages", "Print out packages with version numbers"
|
|
@@ -164,7 +175,32 @@ class Importmap::Commands < Thor
|
|
|
164
175
|
puts npm.packages_with_versions.map { |x| x.join(' ') }
|
|
165
176
|
end
|
|
166
177
|
|
|
178
|
+
desc "doctor", "Check the importmap and the vendored files for problems"
|
|
179
|
+
option :online, type: :boolean, default: false, desc: "Also fetch every remote pin and check its integrity hash"
|
|
180
|
+
def doctor
|
|
181
|
+
require Rails.root.join("config/environment")
|
|
182
|
+
|
|
183
|
+
doctor = Importmap::Doctor.new(
|
|
184
|
+
importmap: Rails.application.importmap,
|
|
185
|
+
resolver: ActionController::Base.helpers,
|
|
186
|
+
root: Rails.root,
|
|
187
|
+
asset_paths: asset_paths,
|
|
188
|
+
online: options[:online]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
doctor.diagnose.each { |finding| puts finding }
|
|
192
|
+
puts doctor.summary
|
|
193
|
+
|
|
194
|
+
exit 1 if doctor.errors?
|
|
195
|
+
end
|
|
196
|
+
|
|
167
197
|
private
|
|
198
|
+
# Where a logical path is looked for on disk. Every engine adds its own, so
|
|
199
|
+
# a file a gem serves resolves here exactly as it does in a request.
|
|
200
|
+
def asset_paths
|
|
201
|
+
Rails.application.config.respond_to?(:assets) ? Rails.application.config.assets.paths : []
|
|
202
|
+
end
|
|
203
|
+
|
|
168
204
|
def packager
|
|
169
205
|
@packager ||= Importmap::Packager.new
|
|
170
206
|
end
|
|
@@ -175,35 +211,121 @@ class Importmap::Commands < Thor
|
|
|
175
211
|
|
|
176
212
|
# A lock outlives a rewrite unless the caller says otherwise, so update
|
|
177
213
|
# and --force re-lock at the version they move to.
|
|
178
|
-
def pin_package(package, url, preload: nil, remote: false, env: "production", minify: nil, from: nil, lock: nil)
|
|
214
|
+
def pin_package(package, url, preload: nil, remote: false, env: "production", minify: nil, from: nil, lock: nil, vendor: false)
|
|
179
215
|
existing_options = packager.extract_existing_pin_options(package)[package] || {}
|
|
180
216
|
preload = existing_options[:preload] if preload.nil?
|
|
181
217
|
integrity = existing_options[:integrity]
|
|
182
218
|
locked = lock.nil? ? packager.locked?(package) : lock
|
|
219
|
+
# A pin vendored on purpose stays vendored: without this an update would
|
|
220
|
+
# inspect the download again, refuse it again, and quietly undo --vendor.
|
|
221
|
+
vendor ||= packager.vendored?(package)
|
|
222
|
+
# A pin kept remote only because its file imports siblings is vendored
|
|
223
|
+
# again now that the whole graph can be. The reason its comment records
|
|
224
|
+
# is one this gem has since learned to answer, and without this an app
|
|
225
|
+
# that hit the check once would never see the fix arrive.
|
|
226
|
+
regraph = packager.remote_reason(package) == Importmap::PackageGraph::REASON
|
|
183
227
|
existing_url = existing_options[:to] if existing_options[:to].to_s.match?(Importmap::Packager::REMOTE_URL_REGEXP)
|
|
184
228
|
|
|
185
|
-
|
|
229
|
+
report_graph_shadowing(package)
|
|
230
|
+
|
|
231
|
+
if existing_url && !vendor && !regraph
|
|
186
232
|
repin_remote_package(package, url, existing_url, preload, env: env, from: from, integrity: integrity, locked: locked)
|
|
233
|
+
elsif existing_url && from.nil? && packager.provider_for_url(existing_url).nil?
|
|
234
|
+
keep_custom_url(package, existing_url)
|
|
187
235
|
elsif remote
|
|
188
236
|
pin_remote_package(package, url, preload, integrity: integrity, locked: locked)
|
|
189
237
|
else
|
|
190
|
-
pin_vendored_package(package, url, preload, minify: minify, integrity: integrity, locked: locked)
|
|
238
|
+
pin_vendored_package(package, url, preload, minify: minify, integrity: integrity, locked: locked, vendor: vendor)
|
|
191
239
|
end
|
|
192
240
|
end
|
|
193
241
|
|
|
194
|
-
|
|
242
|
+
# Nothing is said about a download until it has arrived and been found fit
|
|
243
|
+
# to serve on its own, so a package kept remote reports that and only that.
|
|
244
|
+
def pin_vendored_package(package, url, preload, minify: nil, integrity: nil, locked: false, vendor: false)
|
|
195
245
|
minify = vendored_minified?(package) if minify.nil?
|
|
196
246
|
|
|
197
|
-
|
|
247
|
+
begin
|
|
248
|
+
dependencies = packager.download(package, url, minify: minify, force: vendor, graph: !vendor)
|
|
249
|
+
rescue Importmap::Packager::Unvendorable, Importmap::Packager::NotAnEsModule => refusal
|
|
250
|
+
return pin_remote_package(package, url, preload, integrity: integrity, locked: locked, kept_remote: refusal)
|
|
251
|
+
rescue Importmap::VendoredGraph::Occupied => occupied
|
|
252
|
+
# Nothing is written, including the pin: the app is asked to move its
|
|
253
|
+
# own directory rather than told afterwards that this gem took it.
|
|
254
|
+
return skip(package, occupied.message)
|
|
255
|
+
rescue Importmap::Packager::Error => error
|
|
256
|
+
# The CDN couldn't be read once its retries were spent. The pin is left
|
|
257
|
+
# exactly as it was: a package that vendors today must not turn into a
|
|
258
|
+
# remote pin — losing the files that make it work — because of a 503.
|
|
259
|
+
return skip(package, error.message)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
graph = packager.last_graph
|
|
263
|
+
|
|
264
|
+
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url}#{" (minified)" if minify}#{graph_note(graph)})
|
|
198
265
|
|
|
199
|
-
|
|
266
|
+
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, preload, minify: minify, integrity: integrity, locked: locked, vendored: vendor))
|
|
267
|
+
|
|
268
|
+
# The line that maps the graph directory goes with the download: a
|
|
269
|
+
# package that stopped needing one takes its line and its files with it,
|
|
270
|
+
# or pin_all_from keeps mapping files nothing imports.
|
|
271
|
+
if graph
|
|
272
|
+
update_importmap_with_graph_pin(package, packager.graph_pin_for(package, url, preload))
|
|
273
|
+
else
|
|
274
|
+
packager.remove_graph(package)
|
|
275
|
+
end
|
|
200
276
|
|
|
201
|
-
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, preload, minify: minify, integrity: integrity, locked: locked))
|
|
202
277
|
report_lock(package) if locked
|
|
203
278
|
|
|
204
279
|
pin_esm_run_dependencies(dependencies, preload: preload, minify: minify)
|
|
205
280
|
end
|
|
206
281
|
|
|
282
|
+
# A package that couldn't be pinned has said why on stdout; this is what
|
|
283
|
+
# makes the exit code say so too, or `pin x && git commit` commits a tree
|
|
284
|
+
# with no pin in it. Kept on the command rather than returned up through
|
|
285
|
+
# pin_package, because an esm.run bundle's dependencies are pinned two
|
|
286
|
+
# calls down and are skipped the same way.
|
|
287
|
+
def skip(package, reason)
|
|
288
|
+
puts %(Skipping "#{package}": #{reason})
|
|
289
|
+
skipped << package
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def skipped
|
|
293
|
+
@skipped ||= []
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# pristine restores what each pin already says, so a package vendored
|
|
297
|
+
# before the single-file check existed — or on purpose with --vendor — is
|
|
298
|
+
# downloaded again rather than converted, and only a pin that already maps
|
|
299
|
+
# a graph directory has one crawled again. pin is where both are decided.
|
|
300
|
+
#
|
|
301
|
+
# A package the CDN can no longer serve the way its pin describes stops
|
|
302
|
+
# that one package being restored, not the run: pristine is the repair
|
|
303
|
+
# command, and the packages after it in the batch still need repairing.
|
|
304
|
+
def restore_package(package, url)
|
|
305
|
+
minify = options[:minify].nil? ? vendored_minified?(package) : options[:minify]
|
|
306
|
+
|
|
307
|
+
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url}#{" (minified)" if minify})
|
|
308
|
+
|
|
309
|
+
dependencies = packager.download(package, url, minify: minify, force: true,
|
|
310
|
+
graph: packager.vendored_graph(package).mapped?)
|
|
311
|
+
|
|
312
|
+
# A download that came back as one file — a --from that moved the package
|
|
313
|
+
# to a bundling CDN — takes its graph line with it, or pin_all_from is
|
|
314
|
+
# left mapping the directory that download just removed.
|
|
315
|
+
packager.remove_graph(package) unless packager.last_graph
|
|
316
|
+
|
|
317
|
+
pin_esm_run_dependencies dependencies, minify: minify
|
|
318
|
+
record_provenance(package, url, minify) if provenance_changed?(package, url, minify)
|
|
319
|
+
|
|
320
|
+
true
|
|
321
|
+
rescue Importmap::Packager::Unvendorable, Importmap::Packager::NotAnEsModule => refusal
|
|
322
|
+
puts %(Couldn't restore "#{package}": it #{refusal.message})
|
|
323
|
+
false
|
|
324
|
+
rescue Importmap::Packager::Error => error
|
|
325
|
+
puts %(Couldn't restore "#{package}": #{error.message})
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
|
|
207
329
|
def lock_package(spec)
|
|
208
330
|
package = packager.package_key_for(spec)
|
|
209
331
|
|
|
@@ -301,12 +423,20 @@ class Importmap::Commands < Thor
|
|
|
301
423
|
|
|
302
424
|
# The registry knows a package by name and the import map by key: a pin of
|
|
303
425
|
# "photoswipe/lightbox" is outdated when "photoswipe" is. A named update
|
|
304
|
-
# re-pins the keys that were asked for
|
|
426
|
+
# re-pins the keys that were asked for: a package name means every pin
|
|
427
|
+
# that carries it, the way outdated reports it and a bare update moves it,
|
|
428
|
+
# while "photoswipe/lightbox" means that one key.
|
|
305
429
|
def requested_keys_for(specs, outdated_names)
|
|
306
|
-
specs.
|
|
430
|
+
specs.flat_map { |spec| keys_for(packager.package_key_for(spec)) }
|
|
307
431
|
.select { |key| outdated_names.include?(packager.package_name_for(key)) }
|
|
308
432
|
end
|
|
309
433
|
|
|
434
|
+
def keys_for(key)
|
|
435
|
+
return [ key ] unless key == packager.package_name_for(key)
|
|
436
|
+
|
|
437
|
+
versioned_keys_by_package[key] || [ key ]
|
|
438
|
+
end
|
|
439
|
+
|
|
310
440
|
# An outdated package is outdated in every pin that carries it, so a bare
|
|
311
441
|
# update re-pins those keys: "photoswipe" moving updates the app's
|
|
312
442
|
# "photoswipe/lightbox" pin rather than appending a bare one beside it.
|
|
@@ -320,10 +450,12 @@ class Importmap::Commands < Thor
|
|
|
320
450
|
# doesn't match its key (pin "buffer", to: ".../npm:jspm-core@..."); it is
|
|
321
451
|
# still the only handle there is, so it goes through as itself.
|
|
322
452
|
def outdated_keys_for(names)
|
|
323
|
-
|
|
324
|
-
|
|
453
|
+
names.flat_map { |name| versioned_keys_by_package[name] || [ name ] }
|
|
454
|
+
end
|
|
325
455
|
|
|
326
|
-
|
|
456
|
+
def versioned_keys_by_package
|
|
457
|
+
packager.pinned_packages.select { |key| packager.pin_version(key) }
|
|
458
|
+
.group_by { |key| packager.package_name_for(key) }
|
|
327
459
|
end
|
|
328
460
|
|
|
329
461
|
def locked_pin_covering(name)
|
|
@@ -370,6 +502,7 @@ class Importmap::Commands < Thor
|
|
|
370
502
|
|
|
371
503
|
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, existing_options[:preload],
|
|
372
504
|
minify: minify, integrity: existing_options[:integrity],
|
|
505
|
+
vendored: packager.vendored?(package),
|
|
373
506
|
locked: packager.locked?(package)))
|
|
374
507
|
end
|
|
375
508
|
|
|
@@ -387,25 +520,119 @@ class Importmap::Commands < Thor
|
|
|
387
520
|
# A vendored package keeps coming from the CDN its pin comment names, the
|
|
388
521
|
# way a remote pin keeps its provider, unless --from says otherwise.
|
|
389
522
|
# Packages that aren't pinned yet resolve from jspm.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
523
|
+
#
|
|
524
|
+
# +fallback+ lets the group that named no CDN try the rest of the chain
|
|
525
|
+
# when jspm can't build it, which is what pin and update want. pristine
|
|
526
|
+
# doesn't: it restores what each pin already says, and a pin that moved to
|
|
527
|
+
# another CDN would be a rewrite, not a restore.
|
|
528
|
+
def for_each_import_grouped_by_provider(packages, env:, from: nil, fallback: false, &block)
|
|
529
|
+
packages.group_by { |spec| from || vendored_provider_for(spec) || Importmap::ProviderChain::DEFAULT }.each do |provider, group|
|
|
530
|
+
resolver.each_import(group, env: env, from: provider,
|
|
531
|
+
fallback: fallback && from.nil? && Importmap::ProviderChain.default?(provider), &block)
|
|
393
532
|
end
|
|
394
533
|
end
|
|
395
534
|
|
|
535
|
+
# Shared by every group of a single run, so the exit code can ask it once
|
|
536
|
+
# whether anything was left unresolved.
|
|
537
|
+
def resolver
|
|
538
|
+
@resolver ||= Importmap::BatchResolver.new(packager, on_miss: method(:handle_package_not_found))
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# A spec with no version means the latest, and the registry is what knows
|
|
542
|
+
# which that is: a CDN answers with the latest it has got round to indexing,
|
|
543
|
+
# which lags npm by anything from hours to a major release. Resolving it up
|
|
544
|
+
# front also pins the version for the whole chain, so falling back to
|
|
545
|
+
# another CDN can't quietly land the app on a different one. A registry
|
|
546
|
+
# that can't be reached leaves the spec alone, and the CDN chooses as before.
|
|
547
|
+
def resolve_latest_versions(specs)
|
|
548
|
+
specs.map do |spec|
|
|
549
|
+
name, version, _subpath = spec.to_s.match(Importmap::Packager::PACKAGE_SPEC_REGEXP)&.captures
|
|
550
|
+
next spec if version || name.nil?
|
|
551
|
+
|
|
552
|
+
if (latest = npm.latest_version(name))
|
|
553
|
+
puts %(Resolved "#{name}" to #{latest} from the npm registry)
|
|
554
|
+
packager.package_spec_for(spec, "@#{latest}")
|
|
555
|
+
else
|
|
556
|
+
spec
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# A subpath pin that names no CDN of its own comes from wherever its
|
|
562
|
+
# package's pin came from: pdfjs-dist and pdfjs-dist/build/pdf.worker.min.mjs
|
|
563
|
+
# were pinned together, and jspm, the default, can't resolve either.
|
|
564
|
+
# A pin answers for itself first and only then for its subpaths: the URL on
|
|
565
|
+
# pin "photoswipe/lightbox" says more about where that file comes from than
|
|
566
|
+
# its package's pin does. Asking the package first grouped the two together,
|
|
567
|
+
# and one spec the CDN can't answer for fails the whole batch.
|
|
396
568
|
def vendored_provider_for(spec)
|
|
397
|
-
packager.
|
|
569
|
+
key = packager.package_key_for(spec)
|
|
570
|
+
name = packager.package_name_for(key)
|
|
571
|
+
|
|
572
|
+
return packager.pin_provenance(key)&.dig(:provider) || kept_remote_provider(key) if key == name
|
|
573
|
+
|
|
574
|
+
provider_of_pin(key) || provider_of_pin(name)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# A pin the single-file check kept remote records its CDN only in its URL:
|
|
578
|
+
# the comment's provider slot is taken by the reason it was kept. Asking the
|
|
579
|
+
# URL keeps such a pin on its own CDN when it is re-pinned — and, now that a
|
|
580
|
+
# chunked package can be vendored whole, when it converts to a download.
|
|
581
|
+
# A pin the app made remote itself carries no reason and is left to
|
|
582
|
+
# repin_remote_package, which resolves it from its URL as it always did.
|
|
583
|
+
def kept_remote_provider(key)
|
|
584
|
+
return unless packager.remote_reason(key)
|
|
585
|
+
|
|
586
|
+
packager.provider_for_url(packager.extract_existing_pin_options(key).dig(key, :to))
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# A vendored pin records its CDN in the version comment; a remote one
|
|
590
|
+
# carries it in the URL and, unlocked, has no comment at all. Only a subpath
|
|
591
|
+
# asks this, and a bare package pin above stops at its comment: reading the
|
|
592
|
+
# URL of every pin would change where a plain remote pin resolves its
|
|
593
|
+
# dependencies from, which is not this lookup's business.
|
|
594
|
+
def provider_of_pin(package)
|
|
595
|
+
packager.pin_provenance(package)&.dig(:provider) ||
|
|
596
|
+
packager.provider_for_url(packager.extract_existing_pin_options(package).dig(package, :to))
|
|
398
597
|
end
|
|
399
598
|
|
|
400
|
-
|
|
401
|
-
|
|
599
|
+
# +kept_remote+ is the Packager's refusal to vendor a download it just
|
|
600
|
+
# fetched — its reasons, and the hash of the bytes it already has; without
|
|
601
|
+
# it the reason the pin already records is carried over, so update and a
|
|
602
|
+
# plain pin don't drop it.
|
|
603
|
+
def pin_remote_package(package, url, preload, integrity: nil, locked: false, kept_remote: nil)
|
|
604
|
+
if compute_integrity?
|
|
605
|
+
integrity = remote_integrity_for(url, integrity, kept_remote&.integrity)
|
|
606
|
+
elsif packager.integrity_hash?(package)
|
|
607
|
+
puts %(Dropping the integrity hash on "#{package}" (--no-integrity))
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
notes = +""
|
|
611
|
+
notes << %( (kept remote: #{kept_remote.reasons.to_sentence})) if kept_remote
|
|
612
|
+
notes << %( (integrity #{integrity})) if Importmap::Integrity.hash?(integrity)
|
|
613
|
+
|
|
614
|
+
puts %(Pinning "#{package}" to #{url}#{notes})
|
|
615
|
+
|
|
616
|
+
remote = kept_remote&.reasons&.first || packager.remote_reason(package)
|
|
402
617
|
|
|
403
618
|
packager.remove_existing_package_file(package)
|
|
619
|
+
packager.remove_graph(package)
|
|
404
620
|
|
|
405
|
-
update_importmap_with_pin(package, packager.pin_for(package, url, preloads: preload, integrity: integrity,
|
|
621
|
+
update_importmap_with_pin(package, packager.pin_for(package, url, preloads: preload, integrity: integrity,
|
|
622
|
+
locked: locked, remote: remote))
|
|
406
623
|
report_lock(package) if locked
|
|
407
624
|
end
|
|
408
625
|
|
|
626
|
+
# A URL on a host no provider answers for is the app's own choice, and the
|
|
627
|
+
# branch above skips it — but only while `vendor` is false. --vendor is
|
|
628
|
+
# about overriding the check that refuses a download, not about moving a
|
|
629
|
+
# pin to another source, so it lands here instead of vendoring whatever the
|
|
630
|
+
# spec happened to resolve to. Naming a CDN with --from is how a pin is
|
|
631
|
+
# moved on purpose, and that still goes through repin_remote_package.
|
|
632
|
+
def keep_custom_url(package, existing_url)
|
|
633
|
+
puts %(Skipping "#{package}" pinned to custom URL #{existing_url} (--vendor doesn't move a pin to another source; pass --from to choose one))
|
|
634
|
+
end
|
|
635
|
+
|
|
409
636
|
def repin_remote_package(package, url, existing_url, preload, env:, from: nil, integrity: nil, locked: false)
|
|
410
637
|
# `url` was already resolved from the requested CDN, so an explicit
|
|
411
638
|
# --from moves the pin instead of being overruled by its current one.
|
|
@@ -424,6 +651,31 @@ class Importmap::Commands < Thor
|
|
|
424
651
|
end
|
|
425
652
|
end
|
|
426
653
|
|
|
654
|
+
# Read from the command's own options rather than threaded through every
|
|
655
|
+
# pin path: update, pristine and an esm.run bundle's dependency pins all
|
|
656
|
+
# reach pin_remote_package too, and a keyword one of them forgets to pass
|
|
657
|
+
# silently hashes a pin the app asked not to hash. Only pin declares the
|
|
658
|
+
# flag, so anything else sees nil and hashes.
|
|
659
|
+
def compute_integrity?
|
|
660
|
+
options["integrity"] != false
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# A remote pin is the one place an app trusts a CDN at runtime, so the pin
|
|
664
|
+
# records the hash of what was resolved and the browser checks the bytes it
|
|
665
|
+
# gets against it. integrity: false is an app's decision and stays; a hash
|
|
666
|
+
# the line already carried was computed for the old URL and was dropped
|
|
667
|
+
# when the line was read, so nil here means "hash this". +computed+ is the
|
|
668
|
+
# hash a download that was just refused already carries — the file isn't
|
|
669
|
+
# fetched again for it.
|
|
670
|
+
def remote_integrity_for(url, integrity, computed = nil)
|
|
671
|
+
return integrity if integrity == false
|
|
672
|
+
|
|
673
|
+
computed || Importmap::Integrity.for(packager.fetch_remote(url))
|
|
674
|
+
rescue Importmap::Packager::Error => error
|
|
675
|
+
puts %(Couldn't hash #{url} (#{error.message}); pinning it without an integrity hash)
|
|
676
|
+
nil
|
|
677
|
+
end
|
|
678
|
+
|
|
427
679
|
def resolve_url_from_provider(package, reference_url, provider, env:)
|
|
428
680
|
version = packager.extract_package_version_from(reference_url)
|
|
429
681
|
response = packager.import(packager.package_spec_for(package, version), env: env, from: provider)
|
|
@@ -434,8 +686,52 @@ class Importmap::Commands < Thor
|
|
|
434
686
|
nil
|
|
435
687
|
end
|
|
436
688
|
|
|
689
|
+
# Importmap::Map expands a directory over the packages, so a key some other
|
|
690
|
+
# package's graph already maps resolves to that file whatever the pin about
|
|
691
|
+
# to be written says. Say so rather than write a line that does nothing.
|
|
692
|
+
def report_graph_shadowing(package)
|
|
693
|
+
mapped = Importmap::VendoredGraph.mapping_for(importmap_source, package)
|
|
694
|
+
|
|
695
|
+
puts %(Note: the graph of "#{mapped}" already maps "#{package}", and a mapped directory wins over a pin) if mapped
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Two directories mapping one package write their files under the same
|
|
699
|
+
# prefix, so at two versions the app gets one of them for every file the
|
|
700
|
+
# two share, and which one depends on the order of the lines.
|
|
701
|
+
def report_graph_conflict(pin)
|
|
702
|
+
directory, under, version = Importmap::VendoredGraph.mappings_in(pin).first
|
|
703
|
+
conflict = Importmap::VendoredGraph.conflict_in(importmap_source, under: under, version: version, except: directory)
|
|
704
|
+
|
|
705
|
+
puts %(Note: #{conflict.first} already maps "#{under}" at #{conflict.last}; a file both carry resolves to one of them) if conflict
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def importmap_source
|
|
709
|
+
File.exist?("config/importmap.rb") ? File.read("config/importmap.rb") : ""
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# How many files came down with the entry, so an app can see that a chunked
|
|
713
|
+
# package was vendored whole rather than as the one file that would 404.
|
|
714
|
+
def graph_note(graph)
|
|
715
|
+
%( (with #{graph.size} sibling #{"file".pluralize(graph.size)})) if graph && graph.size.positive?
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def update_importmap_with_graph_pin(package, pin)
|
|
719
|
+
report_graph_conflict(pin)
|
|
720
|
+
|
|
721
|
+
if packager.vendored_graph(package).mapped?
|
|
722
|
+
gsub_file("config/importmap.rb", packager.vendored_graph(package).line_regexp, pin, verbose: false)
|
|
723
|
+
else
|
|
724
|
+
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
packager.reload!
|
|
728
|
+
end
|
|
729
|
+
|
|
437
730
|
def update_importmap_with_pin(package, pin)
|
|
438
731
|
new_pin = "#{pin}\n"
|
|
732
|
+
# Two esm.run bundles can need one dependency: skipped on the first
|
|
733
|
+
# bundle's turn and pinned on the second's, it is in the tree after all.
|
|
734
|
+
skipped.delete(package)
|
|
439
735
|
|
|
440
736
|
if packager.packaged?(package)
|
|
441
737
|
gsub_file("config/importmap.rb", Importmap::Map.pin_line_regexp_for(package), pin, verbose: false)
|
|
@@ -446,8 +742,13 @@ class Importmap::Commands < Thor
|
|
|
446
742
|
packager.reload!
|
|
447
743
|
end
|
|
448
744
|
|
|
449
|
-
|
|
450
|
-
|
|
745
|
+
# jspm answers 401 with its generator's reason in the body — the subpath a
|
|
746
|
+
# dependency doesn't export, the module it couldn't find — and that is the
|
|
747
|
+
# one thing that tells an app developer whether to try another CDN.
|
|
748
|
+
def handle_package_not_found(packages, from, reason: packager.last_import_error)
|
|
749
|
+
detail = Importmap::ProviderChain.tidy_reason(reason)
|
|
750
|
+
|
|
751
|
+
puts "Couldn't find any packages in #{packages.inspect} on #{from}#{" (#{detail})" if detail}"
|
|
451
752
|
end
|
|
452
753
|
|
|
453
754
|
def remove_line_from_file(path, pattern)
|