Skip to content

Commit e8dd767

Browse files
committed
Add support to specify the integrity hash of a asset when pinning
1 parent 7fefb0d commit e8dd767

File tree

5 files changed

+81
-22
lines changed

5 files changed

+81
-22
lines changed

lib/importmap/map.rb

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ def draw(path = nil, &block)
2525
self
2626
end
2727

28-
def pin(name, to: nil, preload: true)
28+
def pin(name, to: nil, preload: true, integrity: nil)
2929
clear_cache
30-
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload)
30+
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
3131
end
3232

3333
def pin_all_from(dir, under: nil, to: nil, preload: true)
@@ -53,7 +53,9 @@ def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :pr
5353
# `cache_key` to vary the cache used by this method for the different cases.
5454
def to_json(resolver:, cache_key: :json)
5555
cache_as(cache_key) do
56-
JSON.pretty_generate({ "imports" => resolve_asset_paths(expanded_packages_and_directories, resolver: resolver) })
56+
packages = expanded_packages_and_directories
57+
map = build_import_map(packages, resolver: resolver)
58+
JSON.pretty_generate(map)
5759
end
5860
end
5961

@@ -85,7 +87,7 @@ def cache_sweeper(watches: nil)
8587

8688
private
8789
MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
88-
MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
90+
MappedFile = Struct.new(:name, :path, :preload, :integrity, keyword_init: true)
8991

9092
def cache_as(name)
9193
if result = @cache[name.to_s]
@@ -105,19 +107,41 @@ def rescuable_asset_error?(error)
105107

106108
def resolve_asset_paths(paths, resolver:)
107109
paths.transform_values do |mapping|
108-
begin
109-
resolver.path_to_asset(mapping.path)
110-
rescue => e
111-
if rescuable_asset_error?(e)
112-
Rails.logger.warn "Importmap skipped missing path: #{mapping.path}"
113-
nil
114-
else
115-
raise e
116-
end
117-
end
110+
resolve_asset_path(mapping.path, resolver:)
118111
end.compact
119112
end
120113

114+
def resolve_asset_path(path, resolver:)
115+
begin
116+
resolver.path_to_asset(path)
117+
rescue => e
118+
if rescuable_asset_error?(e)
119+
Rails.logger.warn "Importmap skipped missing path: #{path}"
120+
nil
121+
else
122+
raise e
123+
end
124+
end
125+
end
126+
127+
def build_import_map(packages, resolver:)
128+
map = { "imports" => resolve_asset_paths(packages, resolver: resolver) }
129+
integrity = build_integrity_hash(packages, resolver: resolver)
130+
map["integrity"] = integrity unless integrity.empty?
131+
map
132+
end
133+
134+
def build_integrity_hash(packages, resolver:)
135+
packages.filter_map do |name, mapping|
136+
next unless mapping.integrity
137+
138+
resolved_path = resolve_asset_path(mapping.path, resolver: resolver)
139+
next unless resolved_path
140+
141+
[resolved_path, mapping.integrity]
142+
end.to_h
143+
end
144+
121145
def expanded_preloading_packages_and_directories(entry_point:)
122146
expanded_packages_and_directories.select { |name, mapping| mapping.preload.in?([true, false]) ? mapping.preload : (Array(mapping.preload) & Array(entry_point)).any? }
123147
end

test/dummy/config/importmap.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
pin "md5", to: "https://cdn.skypack.dev/md5", preload: true
44
pin "not_there", to: "nowhere.js", preload: false
5+
pin "rich_text", preload: true, integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"

test/importmap_tags_helper_test.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,33 @@ def content_security_policy_nonce
2020
end
2121

2222
test "javascript_inline_importmap_tag" do
23-
assert_match \
24-
%r{<script type="importmap" data-turbo-track="reload">{\n \"imports\": {\n \"md5\": \"https://cdn.skypack.dev/md5\",\n \"not_there\": \"/nowhere.js\"\n }\n}</script>},
23+
assert_dom_equal(
24+
%(
25+
<script type="importmap" data-turbo-track="reload">
26+
{
27+
"imports": {
28+
"md5": "https://cdn.skypack.dev/md5",
29+
"not_there": "/nowhere.js",
30+
"rich_text": "/rich_text.js"
31+
},
32+
"integrity": {
33+
"/rich_text.js": "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"
34+
}
35+
}
36+
</script>
37+
),
2538
javascript_inline_importmap_tag
39+
)
2640
end
2741

2842
test "javascript_importmap_module_preload_tags" do
29-
assert_dom_equal \
30-
%(<link rel="modulepreload" href="https://cdn.skypack.dev/md5">),
43+
assert_dom_equal(
44+
%(
45+
<link rel="modulepreload" href="https://cdn.skypack.dev/md5">
46+
<link rel="modulepreload" href="/rich_text.js">
47+
),
3148
javascript_importmap_module_preload_tags
49+
)
3250
end
3351

3452
test "tags have no nonce if CSP is not configured" do

test/importmap_test.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ def setup
55
@importmap = Importmap::Map.new.tap do |map|
66
map.draw do
77
pin "application", preload: false
8-
pin "editor", to: "rich_text.js", preload: false
9-
pin "not_there", to: "nowhere.js", preload: false
8+
pin "editor", to: "rich_text.js", preload: false, integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"
9+
pin "not_there", to: "nowhere.js", preload: false, integrity: "sha384-somefakehash"
1010
pin "md5", to: "https://cdn.skypack.dev/md5", preload: true
1111
pin "leaflet", to: "https://cdn.skypack.dev/leaflet", preload: 'application'
1212
pin "chartkick", to: "https://cdn.skypack.dev/chartkick", preload: ['application', 'alternate']
@@ -30,6 +30,22 @@ def setup
3030
assert_match %r|assets/rich_text-.*\.js|, generate_importmap_json["imports"]["editor"]
3131
end
3232

33+
test "local pin with integrity" do
34+
editor_path = generate_importmap_json["imports"]["editor"]
35+
assert_match %r|assets/rich_text-.*\.js|, editor_path
36+
assert_equal "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb", generate_importmap_json["integrity"][editor_path]
37+
assert_nil generate_importmap_json["imports"]["not_there"]
38+
assert_not_includes generate_importmap_json["integrity"].values, "sha384-somefakehash"
39+
end
40+
41+
test "integrity is not present if there is no integrity set in the map" do
42+
@importmap = Importmap::Map.new.tap do |map|
43+
map.pin "application", preload: false
44+
end
45+
46+
assert_not generate_importmap_json.key?("integrity")
47+
end
48+
3349
test "local pin missing is removed from generated importmap" do
3450
assert_nil generate_importmap_json["imports"]["not_there"]
3551
end
@@ -138,6 +154,6 @@ def setup
138154

139155
private
140156
def generate_importmap_json
141-
JSON.parse @importmap.to_json(resolver: ApplicationController.helpers)
157+
@generate_importmap_json ||= JSON.parse @importmap.to_json(resolver: ApplicationController.helpers)
142158
end
143159
end

test/reloader_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class ReloaderTest < ActiveSupport::TestCase
1616
Rails.application.importmap = Importmap::Map.new.draw { pin "md5", to: "https://cdn.skypack.dev/md5" }
1717
assert_not_predicate @reloader, :updated?
1818

19-
assert_changes -> { Rails.application.importmap.packages.keys }, from: %w[ md5 ], to: %w[ md5 not_there ] do
19+
assert_changes -> { Rails.application.importmap.packages.keys }, from: %w[ md5 ], to: %w[ md5 not_there rich_text ] do
2020
touch_config
2121
assert @reloader.execute_if_updated
2222
end

0 commit comments

Comments
 (0)