importmap-rails 0.8.0 → 2.2.3
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/MIT-LICENSE +1 -1
- data/README.md +241 -93
- data/Rakefile +0 -2
- data/app/controllers/importmap/freshness.rb +5 -0
- data/app/helpers/importmap/importmap_tags_helper.rb +21 -28
- data/lib/importmap/commands.rb +142 -35
- data/lib/importmap/engine.rb +37 -17
- data/lib/importmap/map.rb +211 -30
- data/lib/importmap/npm.rb +178 -0
- data/lib/importmap/packager.rb +97 -20
- data/lib/importmap/reloader.rb +4 -1
- data/lib/importmap/version.rb +1 -1
- data/lib/importmap-rails.rb +1 -1
- data/lib/install/config/importmap.rb +1 -1
- data/lib/install/install.rb +4 -0
- data/lib/tasks/importmap_tasks.rake +4 -1
- metadata +35 -12
- data/app/assets/javascripts/es-module-shims.js +0 -786
- data/app/assets/javascripts/es-module-shims.min.js +0 -1
- data/lib/shim.js +0 -1
data/lib/importmap/commands.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "thor"
|
|
2
2
|
require "importmap/packager"
|
|
3
|
+
require "importmap/npm"
|
|
3
4
|
|
|
4
5
|
class Importmap::Commands < Thor
|
|
5
6
|
include Thor::Actions
|
|
@@ -7,53 +8,39 @@ class Importmap::Commands < Thor
|
|
|
7
8
|
def self.exit_on_failure?
|
|
8
9
|
false
|
|
9
10
|
end
|
|
10
|
-
|
|
11
|
+
|
|
11
12
|
desc "pin [*PACKAGES]", "Pin new packages"
|
|
12
13
|
option :env, type: :string, aliases: :e, default: "production"
|
|
13
14
|
option :from, type: :string, aliases: :f, default: "jspm"
|
|
14
|
-
option :
|
|
15
|
+
option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
|
|
15
16
|
def pin(*packages)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if options[:download]
|
|
19
|
-
puts %(Pinning "#{package}" to vendor/#{package}.js via download from #{url})
|
|
20
|
-
packager.download(package, url)
|
|
21
|
-
pin = packager.vendored_pin_for(package, url)
|
|
22
|
-
else
|
|
23
|
-
puts %(Pinning "#{package}" to #{url})
|
|
24
|
-
pin = packager.pin_for(package, url)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
if packager.packaged?(package)
|
|
28
|
-
gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
|
|
29
|
-
else
|
|
30
|
-
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:provider]}"
|
|
17
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
18
|
+
pin_package(package, url, options[:preload])
|
|
35
19
|
end
|
|
36
20
|
end
|
|
37
21
|
|
|
38
22
|
desc "unpin [*PACKAGES]", "Unpin existing packages"
|
|
39
23
|
option :env, type: :string, aliases: :e, default: "production"
|
|
40
24
|
option :from, type: :string, aliases: :f, default: "jspm"
|
|
41
|
-
option :download, type: :boolean, aliases: :d, default: false
|
|
42
25
|
def unpin(*packages)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
puts %(Unpinning and removing "#{package}")
|
|
48
|
-
else
|
|
49
|
-
puts %(Unpinning "#{package}")
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
packager.remove(package)
|
|
53
|
-
end
|
|
26
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
27
|
+
if packager.packaged?(package)
|
|
28
|
+
puts %(Unpinning and removing "#{package}")
|
|
29
|
+
packager.remove(package)
|
|
54
30
|
end
|
|
55
|
-
|
|
56
|
-
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc "pristine", "Redownload all pinned packages"
|
|
35
|
+
option :env, type: :string, aliases: :e, default: "production"
|
|
36
|
+
option :from, type: :string, aliases: :f, default: "jspm"
|
|
37
|
+
def pristine
|
|
38
|
+
packages = prepare_packages_with_versions
|
|
39
|
+
|
|
40
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
41
|
+
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
|
|
42
|
+
|
|
43
|
+
packager.download(package, url)
|
|
57
44
|
end
|
|
58
45
|
end
|
|
59
46
|
|
|
@@ -63,11 +50,97 @@ class Importmap::Commands < Thor
|
|
|
63
50
|
puts Rails.application.importmap.to_json(resolver: ActionController::Base.helpers)
|
|
64
51
|
end
|
|
65
52
|
|
|
53
|
+
desc "audit", "Run a security audit"
|
|
54
|
+
def audit
|
|
55
|
+
vulnerable_packages = npm.vulnerable_packages
|
|
56
|
+
|
|
57
|
+
if vulnerable_packages.any?
|
|
58
|
+
table = [["Package", "Severity", "Vulnerable versions", "Vulnerability"]]
|
|
59
|
+
vulnerable_packages.each { |p| table << [p.name, p.severity, p.vulnerable_versions, p.vulnerability] }
|
|
60
|
+
|
|
61
|
+
puts_table(table)
|
|
62
|
+
vulnerabilities = 'vulnerability'.pluralize(vulnerable_packages.size)
|
|
63
|
+
severities = vulnerable_packages.map(&:severity).tally.sort_by(&:last).reverse
|
|
64
|
+
.map { |severity, count| "#{count} #{severity}" }
|
|
65
|
+
.join(", ")
|
|
66
|
+
puts " #{vulnerable_packages.size} #{vulnerabilities} found: #{severities}"
|
|
67
|
+
|
|
68
|
+
exit 1
|
|
69
|
+
else
|
|
70
|
+
puts "No vulnerable packages found"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc "outdated", "Check for outdated packages"
|
|
75
|
+
def outdated
|
|
76
|
+
if (outdated_packages = npm.outdated_packages).any?
|
|
77
|
+
table = [["Package", "Current", "Latest"]]
|
|
78
|
+
outdated_packages.each { |p| table << [p.name, p.current_version, p.latest_version || p.error] }
|
|
79
|
+
|
|
80
|
+
puts_table(table)
|
|
81
|
+
packages = 'package'.pluralize(outdated_packages.size)
|
|
82
|
+
puts " #{outdated_packages.size} outdated #{packages} found"
|
|
83
|
+
|
|
84
|
+
exit 1
|
|
85
|
+
else
|
|
86
|
+
puts "No outdated packages found"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc "update", "Update outdated package pins"
|
|
91
|
+
def update
|
|
92
|
+
if (outdated_packages = npm.outdated_packages).any?
|
|
93
|
+
package_names = outdated_packages.map(&:name)
|
|
94
|
+
packages_with_options = packager.extract_existing_pin_options(package_names)
|
|
95
|
+
|
|
96
|
+
for_each_import(package_names, env: "production", from: "jspm") do |package, url|
|
|
97
|
+
options = packages_with_options[package] || {}
|
|
98
|
+
|
|
99
|
+
pin_package(package, url, options[:preload])
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
puts "No outdated packages found"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
desc "packages", "Print out packages with version numbers"
|
|
107
|
+
def packages
|
|
108
|
+
puts npm.packages_with_versions.map { |x| x.join(' ') }
|
|
109
|
+
end
|
|
110
|
+
|
|
66
111
|
private
|
|
67
112
|
def packager
|
|
68
113
|
@packager ||= Importmap::Packager.new
|
|
69
114
|
end
|
|
70
115
|
|
|
116
|
+
def npm
|
|
117
|
+
@npm ||= Importmap::Npm.new
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def pin_package(package, url, preload)
|
|
121
|
+
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
|
|
122
|
+
|
|
123
|
+
packager.download(package, url)
|
|
124
|
+
|
|
125
|
+
pin = packager.vendored_pin_for(package, url, preload)
|
|
126
|
+
|
|
127
|
+
update_importmap_with_pin(package, pin)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def update_importmap_with_pin(package, pin)
|
|
131
|
+
new_pin = "#{pin}\n"
|
|
132
|
+
|
|
133
|
+
if packager.packaged?(package)
|
|
134
|
+
gsub_file("config/importmap.rb", Importmap::Map.pin_line_regexp_for(package), pin, verbose: false)
|
|
135
|
+
else
|
|
136
|
+
append_to_file("config/importmap.rb", new_pin, verbose: false)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_package_not_found(packages, from)
|
|
141
|
+
puts "Couldn't find any packages in #{packages.inspect} on #{from}"
|
|
142
|
+
end
|
|
143
|
+
|
|
71
144
|
def remove_line_from_file(path, pattern)
|
|
72
145
|
path = File.expand_path(path, destination_root)
|
|
73
146
|
|
|
@@ -78,6 +151,40 @@ class Importmap::Commands < Thor
|
|
|
78
151
|
with_lines_removed.each { |line| file.write(line) }
|
|
79
152
|
end
|
|
80
153
|
end
|
|
154
|
+
|
|
155
|
+
def puts_table(array)
|
|
156
|
+
column_sizes = array.reduce([]) do |lengths, row|
|
|
157
|
+
row.each_with_index.map{ |iterand, index| [lengths[index] || 0, iterand.to_s.length].max }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
divider = "|" + (column_sizes.map { |s| "-" * (s + 2) }.join('|')) + '|'
|
|
161
|
+
array.each_with_index do |row, row_number|
|
|
162
|
+
row = row.fill(nil, row.size..(column_sizes.size - 1))
|
|
163
|
+
row = row.each_with_index.map { |v, i| v.to_s + " " * (column_sizes[i] - v.to_s.length) }
|
|
164
|
+
puts "| " + row.join(" | ") + " |"
|
|
165
|
+
puts divider if row_number == 0
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def prepare_packages_with_versions(packages = [])
|
|
170
|
+
if packages.empty?
|
|
171
|
+
npm.packages_with_versions.map do |p, v|
|
|
172
|
+
v.blank? ? p : [p, v].join("@")
|
|
173
|
+
end
|
|
174
|
+
else
|
|
175
|
+
packages
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def for_each_import(packages, **options, &block)
|
|
180
|
+
response = packager.import(*packages, **options)
|
|
181
|
+
|
|
182
|
+
if response
|
|
183
|
+
response[:imports].each(&block)
|
|
184
|
+
else
|
|
185
|
+
handle_package_not_found(packages, options[:from])
|
|
186
|
+
end
|
|
187
|
+
end
|
|
81
188
|
end
|
|
82
189
|
|
|
83
190
|
Importmap::Commands.start(ARGV)
|
data/lib/importmap/engine.rb
CHANGED
|
@@ -6,29 +6,34 @@ Rails::Application.send(:attr_accessor, :importmap)
|
|
|
6
6
|
module Importmap
|
|
7
7
|
class Engine < ::Rails::Engine
|
|
8
8
|
config.importmap = ActiveSupport::OrderedOptions.new
|
|
9
|
+
config.importmap.paths = []
|
|
9
10
|
config.importmap.sweep_cache = Rails.env.development? || Rails.env.test?
|
|
11
|
+
config.importmap.cache_sweepers = []
|
|
12
|
+
config.importmap.rescuable_asset_errors = []
|
|
10
13
|
|
|
11
|
-
config.autoload_once_paths = %W( #{root}/app/helpers )
|
|
14
|
+
config.autoload_once_paths = %W( #{root}/app/helpers #{root}/app/controllers )
|
|
12
15
|
|
|
13
16
|
initializer "importmap" do |app|
|
|
14
|
-
app.importmap = Importmap::Map.new
|
|
17
|
+
app.importmap = Importmap::Map.new
|
|
18
|
+
app.config.importmap.paths << app.root.join("config/importmap.rb")
|
|
19
|
+
app.config.importmap.paths.each { |path| app.importmap.draw(path) }
|
|
15
20
|
end
|
|
16
21
|
|
|
17
22
|
initializer "importmap.reloader" do |app|
|
|
18
|
-
app.config.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
unless app.config.cache_classes
|
|
24
|
+
Importmap::Reloader.new.tap do |reloader|
|
|
25
|
+
reloader.execute
|
|
26
|
+
app.reloaders << reloader
|
|
27
|
+
app.reloader.to_run { reloader.execute }
|
|
28
|
+
end
|
|
24
29
|
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
initializer "importmap.cache_sweeper" do |app|
|
|
28
|
-
if app.config.importmap.sweep_cache
|
|
29
|
-
app.importmap.
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
if app.config.importmap.sweep_cache && !app.config.cache_classes
|
|
34
|
+
app.config.importmap.cache_sweepers << app.root.join("app/javascript")
|
|
35
|
+
app.config.importmap.cache_sweepers << app.root.join("vendor/javascript")
|
|
36
|
+
app.importmap.cache_sweeper(watches: app.config.importmap.cache_sweepers)
|
|
32
37
|
|
|
33
38
|
ActiveSupport.on_load(:action_controller_base) do
|
|
34
39
|
before_action { Rails.application.importmap.cache_sweeper.execute_if_updated }
|
|
@@ -36,11 +41,16 @@ module Importmap
|
|
|
36
41
|
end
|
|
37
42
|
end
|
|
38
43
|
|
|
39
|
-
initializer "importmap.assets" do
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
initializer "importmap.assets" do |app|
|
|
45
|
+
if app.config.respond_to?(:assets)
|
|
46
|
+
app.config.assets.paths << Rails.root.join("app/javascript")
|
|
47
|
+
app.config.assets.paths << Rails.root.join("vendor/javascript")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
initializer "importmap.concerns" do
|
|
52
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
53
|
+
extend Importmap::Freshness
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
|
|
@@ -49,5 +59,15 @@ module Importmap
|
|
|
49
59
|
helper Importmap::ImportmapTagsHelper
|
|
50
60
|
end
|
|
51
61
|
end
|
|
62
|
+
|
|
63
|
+
initializer "importmap.rescuable_asset_errors" do |app|
|
|
64
|
+
if defined?(Propshaft)
|
|
65
|
+
app.config.importmap.rescuable_asset_errors << Propshaft::MissingAssetError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if defined?(Sprockets::Rails)
|
|
69
|
+
app.config.importmap.rescuable_asset_errors << Sprockets::Rails::Helper::AssetNotFound
|
|
70
|
+
end
|
|
71
|
+
end
|
|
52
72
|
end
|
|
53
73
|
end
|
data/lib/importmap/map.rb
CHANGED
|
@@ -3,17 +3,27 @@ require "pathname"
|
|
|
3
3
|
class Importmap::Map
|
|
4
4
|
attr_reader :packages, :directories
|
|
5
5
|
|
|
6
|
+
PIN_REGEX = /^pin\s+["']([^"']+)["']/.freeze # :nodoc:
|
|
7
|
+
|
|
8
|
+
def self.pin_line_regexp_for(package) # :nodoc:
|
|
9
|
+
/^.*pin\s+["']#{Regexp.escape(package)}["'].*$/.freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class InvalidFile < StandardError; end
|
|
13
|
+
|
|
6
14
|
def initialize
|
|
15
|
+
@integrity = false
|
|
7
16
|
@packages, @directories = {}, {}
|
|
17
|
+
@cache = {}
|
|
8
18
|
end
|
|
9
19
|
|
|
10
20
|
def draw(path = nil, &block)
|
|
11
21
|
if path && File.exist?(path)
|
|
12
22
|
begin
|
|
13
23
|
instance_eval(File.read(path), path.to_s)
|
|
14
|
-
rescue
|
|
24
|
+
rescue StandardError => e
|
|
15
25
|
Rails.logger.error "Unable to parse import map from #{path}: #{e.message}"
|
|
16
|
-
raise "Unable to parse import map from #{path}: #{e.message}"
|
|
26
|
+
raise InvalidFile, "Unable to parse import map from #{path}: #{e.message}"
|
|
17
27
|
end
|
|
18
28
|
elsif block_given?
|
|
19
29
|
instance_eval(&block)
|
|
@@ -22,25 +32,138 @@ class Importmap::Map
|
|
|
22
32
|
self
|
|
23
33
|
end
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
# Enables automatic integrity hash calculation for all pinned modules.
|
|
36
|
+
#
|
|
37
|
+
# When enabled, integrity values are included in the importmap JSON for all
|
|
38
|
+
# pinned modules. For local assets served by the Rails asset pipeline,
|
|
39
|
+
# integrity hashes are automatically calculated when +integrity: true+ is
|
|
40
|
+
# specified. For modules with explicit integrity values, those values are
|
|
41
|
+
# included as provided. This provides Subresource Integrity (SRI) protection
|
|
42
|
+
# to ensure JavaScript modules haven't been tampered with.
|
|
43
|
+
#
|
|
44
|
+
# Clears the importmap cache when called to ensure fresh integrity hashes
|
|
45
|
+
# are generated.
|
|
46
|
+
#
|
|
47
|
+
# ==== Examples
|
|
48
|
+
#
|
|
49
|
+
# # config/importmap.rb
|
|
50
|
+
# enable_integrity!
|
|
51
|
+
#
|
|
52
|
+
# # These will now auto-calculate integrity hashes
|
|
53
|
+
# pin "application" # integrity: true by default
|
|
54
|
+
# pin "admin", to: "admin.js" # integrity: true by default
|
|
55
|
+
# pin_all_from "app/javascript/lib" # integrity: true by default
|
|
56
|
+
#
|
|
57
|
+
# # Manual control still works
|
|
58
|
+
# pin "no_integrity", integrity: false
|
|
59
|
+
# pin "custom_hash", integrity: "sha384-abc123..."
|
|
60
|
+
#
|
|
61
|
+
# ==== Notes
|
|
62
|
+
#
|
|
63
|
+
# * Integrity calculation is disabled by default and must be explicitly enabled
|
|
64
|
+
# * Requires asset pipeline support for integrity calculation (Sprockets or Propshaft 1.2+)
|
|
65
|
+
# * For Propshaft, you must configure +config.assets.integrity_hash_algorithm+
|
|
66
|
+
# * External CDN packages should provide their own integrity hashes
|
|
67
|
+
def enable_integrity!
|
|
26
68
|
clear_cache
|
|
27
|
-
@
|
|
69
|
+
@integrity = true
|
|
28
70
|
end
|
|
29
71
|
|
|
30
|
-
def
|
|
72
|
+
def pin(name, to: nil, preload: true, integrity: true)
|
|
31
73
|
clear_cache
|
|
32
|
-
@
|
|
74
|
+
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
|
|
33
75
|
end
|
|
34
76
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
77
|
+
def pin_all_from(dir, under: nil, to: nil, preload: true, integrity: true)
|
|
78
|
+
clear_cache
|
|
79
|
+
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload, integrity: integrity)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns an array of all the resolved module paths of the pinned packages. The `resolver` must respond to
|
|
83
|
+
# `path_to_asset`, such as `ActionController::Base.helpers` or `ApplicationController.helpers`. You'll want to use the
|
|
84
|
+
# resolver that has been configured for the `asset_host` you want these resolved paths to use. In case you need to
|
|
85
|
+
# resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for
|
|
86
|
+
# the different cases.
|
|
87
|
+
def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths)
|
|
88
|
+
preloaded_module_packages(resolver: resolver, entry_point: entry_point, cache_key: cache_key).keys
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns a hash of resolved module paths to their corresponding package objects for all pinned packages
|
|
92
|
+
# that are marked for preloading. The hash keys are the resolved asset paths, and the values are the
|
|
93
|
+
# +MappedFile+ objects containing package metadata including name, path, preload setting, and integrity.
|
|
94
|
+
#
|
|
95
|
+
# The +resolver+ must respond to +path_to_asset+, such as +ActionController::Base.helpers+ or
|
|
96
|
+
# +ApplicationController.helpers+. You'll want to use the resolver that has been configured for the
|
|
97
|
+
# +asset_host+ you want these resolved paths to use.
|
|
98
|
+
#
|
|
99
|
+
# ==== Parameters
|
|
100
|
+
#
|
|
101
|
+
# [+resolver+]
|
|
102
|
+
# An object that responds to +path_to_asset+ for resolving asset paths.
|
|
103
|
+
#
|
|
104
|
+
# [+entry_point+]
|
|
105
|
+
# The entry point name or array of entry point names to determine which packages should be preloaded.
|
|
106
|
+
# Defaults to +"application"+. Packages with +preload: true+ are always included regardless of entry point.
|
|
107
|
+
# Packages with specific entry point names (e.g., +preload: "admin"+) are only included when that entry
|
|
108
|
+
# point is specified.
|
|
109
|
+
#
|
|
110
|
+
# [+cache_key+]
|
|
111
|
+
# A custom cache key to vary the cache used by this method for different cases, such as resolving
|
|
112
|
+
# for different asset hosts. Defaults to +:preloaded_module_packages+.
|
|
113
|
+
#
|
|
114
|
+
# ==== Returns
|
|
115
|
+
#
|
|
116
|
+
# A hash where:
|
|
117
|
+
# * Keys are resolved asset paths (strings)
|
|
118
|
+
# * Values are +MappedFile+ objects with +name+, +path+, +preload+, and +integrity+ attributes
|
|
119
|
+
#
|
|
120
|
+
# Missing assets are gracefully handled and excluded from the returned hash.
|
|
121
|
+
#
|
|
122
|
+
# ==== Examples
|
|
123
|
+
#
|
|
124
|
+
# # Get all preloaded packages for the default "application" entry point
|
|
125
|
+
# packages = importmap.preloaded_module_packages(resolver: ApplicationController.helpers)
|
|
126
|
+
# # => { "/assets/application-abc123.js" => #<struct name="application", path="application.js", preload=true, integrity=nil>,
|
|
127
|
+
# # "https://cdn.skypack.dev/react" => #<struct name="react", path="https://cdn.skypack.dev/react", preload=true, integrity="sha384-..."> }
|
|
128
|
+
#
|
|
129
|
+
# # Get preloaded packages for a specific entry point
|
|
130
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: "admin")
|
|
131
|
+
#
|
|
132
|
+
# # Get preloaded packages for multiple entry points
|
|
133
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: ["application", "admin"])
|
|
134
|
+
#
|
|
135
|
+
# # Use a custom cache key for different asset hosts
|
|
136
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, cache_key: "cdn_host")
|
|
137
|
+
def preloaded_module_packages(resolver:, entry_point: "application", cache_key: :preloaded_module_packages)
|
|
138
|
+
cache_as(cache_key) do
|
|
139
|
+
expanded_preloading_packages_and_directories(entry_point:).filter_map do |_, package|
|
|
140
|
+
resolved_path = resolve_asset_path(package.path, resolver: resolver)
|
|
141
|
+
next unless resolved_path
|
|
142
|
+
|
|
143
|
+
resolved_integrity = resolve_integrity_value(package.integrity, package.path, resolver: resolver)
|
|
144
|
+
|
|
145
|
+
package = MappedFile.new(
|
|
146
|
+
name: package.name,
|
|
147
|
+
path: package.path,
|
|
148
|
+
preload: package.preload,
|
|
149
|
+
integrity: resolved_integrity
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
[resolved_path, package]
|
|
153
|
+
end.to_h
|
|
38
154
|
end
|
|
39
155
|
end
|
|
40
156
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
157
|
+
# Returns a JSON hash (as a string) of all the resolved module paths of the pinned packages in the import map format.
|
|
158
|
+
# The `resolver` must respond to `path_to_asset`, such as `ActionController::Base.helpers` or
|
|
159
|
+
# `ApplicationController.helpers`. You'll want to use the resolver that has been configured for the `asset_host` you
|
|
160
|
+
# want these resolved paths to use. In case you need to resolve for different asset hosts, you can pass in a custom
|
|
161
|
+
# `cache_key` to vary the cache used by this method for the different cases.
|
|
162
|
+
def to_json(resolver:, cache_key: :json)
|
|
163
|
+
cache_as(cache_key) do
|
|
164
|
+
packages = expanded_packages_and_directories
|
|
165
|
+
map = build_import_map(packages, resolver: resolver)
|
|
166
|
+
JSON.pretty_generate(map)
|
|
44
167
|
end
|
|
45
168
|
end
|
|
46
169
|
|
|
@@ -56,7 +179,7 @@ class Importmap::Map
|
|
|
56
179
|
Digest::SHA1.hexdigest(to_json(resolver: resolver).to_s)
|
|
57
180
|
end
|
|
58
181
|
|
|
59
|
-
# Returns an instance ActiveSupport::EventedFileUpdateChecker configured to clear the cache of the map
|
|
182
|
+
# Returns an instance of ActiveSupport::EventedFileUpdateChecker configured to clear the cache of the map
|
|
60
183
|
# when the directories passed on initialization via `watches:` have changes. This is used in development
|
|
61
184
|
# and test to ensure the map caches are reset when javascript files are changed.
|
|
62
185
|
def cache_sweeper(watches: nil)
|
|
@@ -71,35 +194,78 @@ class Importmap::Map
|
|
|
71
194
|
end
|
|
72
195
|
|
|
73
196
|
private
|
|
74
|
-
MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
|
|
75
|
-
MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
|
|
197
|
+
MappedDir = Struct.new(:dir, :path, :under, :preload, :integrity, keyword_init: true)
|
|
198
|
+
MappedFile = Struct.new(:name, :path, :preload, :integrity, keyword_init: true)
|
|
76
199
|
|
|
77
200
|
def cache_as(name)
|
|
78
|
-
if result =
|
|
201
|
+
if result = @cache[name.to_s]
|
|
79
202
|
result
|
|
80
203
|
else
|
|
81
|
-
|
|
204
|
+
@cache[name.to_s] = yield
|
|
82
205
|
end
|
|
83
206
|
end
|
|
84
207
|
|
|
85
208
|
def clear_cache
|
|
86
|
-
@
|
|
87
|
-
|
|
209
|
+
@cache.clear
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def rescuable_asset_error?(error)
|
|
213
|
+
Rails.application.config.importmap.rescuable_asset_errors.any? { |e| error.is_a?(e) }
|
|
88
214
|
end
|
|
89
215
|
|
|
90
216
|
def resolve_asset_paths(paths, resolver:)
|
|
91
217
|
paths.transform_values do |mapping|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
218
|
+
resolve_asset_path(mapping.path, resolver:)
|
|
219
|
+
end.compact
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def resolve_asset_path(path, resolver:)
|
|
223
|
+
begin
|
|
224
|
+
resolver.path_to_asset(path)
|
|
225
|
+
rescue => e
|
|
226
|
+
if rescuable_asset_error?(e)
|
|
227
|
+
Rails.logger.warn "Importmap skipped missing path: #{path}"
|
|
96
228
|
nil
|
|
229
|
+
else
|
|
230
|
+
raise e
|
|
97
231
|
end
|
|
98
|
-
end
|
|
232
|
+
end
|
|
99
233
|
end
|
|
100
234
|
|
|
101
|
-
def
|
|
102
|
-
|
|
235
|
+
def build_import_map(packages, resolver:)
|
|
236
|
+
map = { "imports" => resolve_asset_paths(packages, resolver: resolver) }
|
|
237
|
+
integrity = build_integrity_hash(packages, resolver: resolver)
|
|
238
|
+
map["integrity"] = integrity unless integrity.empty?
|
|
239
|
+
map
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def build_integrity_hash(packages, resolver:)
|
|
243
|
+
packages.filter_map do |name, mapping|
|
|
244
|
+
next unless mapping.integrity
|
|
245
|
+
|
|
246
|
+
resolved_path = resolve_asset_path(mapping.path, resolver: resolver)
|
|
247
|
+
next unless resolved_path
|
|
248
|
+
|
|
249
|
+
integrity_value = resolve_integrity_value(mapping.integrity, mapping.path, resolver: resolver)
|
|
250
|
+
next unless integrity_value
|
|
251
|
+
|
|
252
|
+
[resolved_path, integrity_value]
|
|
253
|
+
end.to_h
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def resolve_integrity_value(integrity, path, resolver:)
|
|
257
|
+
return unless @integrity
|
|
258
|
+
|
|
259
|
+
case integrity
|
|
260
|
+
when true
|
|
261
|
+
resolver.asset_integrity(path) if resolver.respond_to?(:asset_integrity)
|
|
262
|
+
when String
|
|
263
|
+
integrity
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def expanded_preloading_packages_and_directories(entry_point:)
|
|
268
|
+
expanded_packages_and_directories.select { |name, mapping| mapping.preload.in?([true, false]) ? mapping.preload : (Array(mapping.preload) & Array(entry_point)).any? }
|
|
103
269
|
end
|
|
104
270
|
|
|
105
271
|
def expanded_packages_and_directories
|
|
@@ -114,22 +280,37 @@ class Importmap::Map
|
|
|
114
280
|
module_name = module_name_from(module_filename, mapping)
|
|
115
281
|
module_path = module_path_from(module_filename, mapping)
|
|
116
282
|
|
|
117
|
-
paths[module_name] = MappedFile.new(
|
|
283
|
+
paths[module_name] = MappedFile.new(
|
|
284
|
+
name: module_name,
|
|
285
|
+
path: module_path,
|
|
286
|
+
preload: mapping.preload,
|
|
287
|
+
integrity: mapping.integrity
|
|
288
|
+
)
|
|
118
289
|
end
|
|
119
290
|
end
|
|
120
291
|
end
|
|
121
292
|
end
|
|
122
293
|
|
|
123
294
|
def module_name_from(filename, mapping)
|
|
124
|
-
|
|
295
|
+
# Regex explanation:
|
|
296
|
+
# (?:\/|^) # Matches either / OR the start of the string
|
|
297
|
+
# index # Matches the word index
|
|
298
|
+
# $ # Matches the end of the string
|
|
299
|
+
#
|
|
300
|
+
# Sample matches
|
|
301
|
+
# index
|
|
302
|
+
# folder/index
|
|
303
|
+
index_regex = /(?:\/|^)index$/
|
|
304
|
+
|
|
305
|
+
[ mapping.under, filename.to_s.chomp(filename.extname).remove(index_regex).presence ].compact.join("/")
|
|
125
306
|
end
|
|
126
307
|
|
|
127
308
|
def module_path_from(filename, mapping)
|
|
128
|
-
[ mapping.path || mapping.under, filename.to_s ].compact.join("/")
|
|
309
|
+
[ mapping.path || mapping.under, filename.to_s ].compact.reject(&:empty?).join("/")
|
|
129
310
|
end
|
|
130
311
|
|
|
131
312
|
def find_javascript_files_in_tree(path)
|
|
132
|
-
Dir[path.join("**/*.js{,m}")].collect { |file| Pathname.new(file) }.select(&:file?)
|
|
313
|
+
Dir[path.join("**/*.js{,m}")].sort.collect { |file| Pathname.new(file) }.select(&:file?)
|
|
133
314
|
end
|
|
134
315
|
|
|
135
316
|
def absolute_root_of(path)
|