Skip to content

Commit b9343a9

Browse files
committed
Support calculating integrity hashes for local assets automatically
When `integrity: true` is used with `pin_all_from` or `pin`, the importmap will automatically calculate integrity hashes for local assets served by the Rails asset pipeline. This eliminates the need to manually manage integrity hashes for local files, enhancing security and simplifying development.
1 parent 7e2f7d7 commit b9343a9

File tree

12 files changed

+158
-26
lines changed

12 files changed

+158
-26
lines changed

Appraisals

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ end
1717

1818
appraise "rails_7.0_propshaft" do
1919
gem "rails", github: "rails/rails", branch: "7-0-stable"
20-
gem "propshaft"
2120
gem "sqlite3", "~> 1.4"
2221
end
2322

@@ -29,7 +28,6 @@ end
2928

3029
appraise "rails_7.1_propshaft" do
3130
gem "rails", "~> 7.1.0"
32-
gem "propshaft"
3331
end
3432

3533
appraise "rails_7.2_sprockets" do
@@ -40,7 +38,6 @@ end
4038

4139
appraise "rails_7.2_propshaft" do
4240
gem "rails", "~> 7.2.0"
43-
gem "propshaft"
4441
end
4542

4643
appraise "rails_8.0_sprockets" do
@@ -51,7 +48,6 @@ end
5148

5249
appraise "rails_8.0_propshaft" do
5350
gem "rails", "~> 8.0.0"
54-
gem "propshaft"
5551
end
5652

5753
appraise "rails_main_sprockets" do
@@ -62,5 +58,4 @@ end
6258

6359
appraise "rails_main_propshaft" do
6460
gem "rails", github: "rails/rails", branch: "main"
65-
gem "propshaft"
6661
end

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
55
gemspec
66

77
gem "rails"
8-
gem "propshaft"
8+
gem "propshaft", github: "rails/propshaft", branch: "main"
99

1010
gem "sqlite3"
1111

Gemfile.lock

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
GIT
2+
remote: https://github.com/rails/propshaft.git
3+
revision: 9bcad5fa48efac2f26568e5d019409a1784839d0
4+
branch: main
5+
specs:
6+
propshaft (1.1.0)
7+
actionpack (>= 7.0.0)
8+
activesupport (>= 7.0.0)
9+
rack
10+
111
PATH
212
remote: .
313
specs:
@@ -111,7 +121,7 @@ GEM
111121
activesupport (>= 6.1)
112122
i18n (1.14.7)
113123
concurrent-ruby (~> 1.0)
114-
io-console (0.8.0)
124+
io-console (0.8.1)
115125
irb (1.15.2)
116126
pp (>= 0.6.0)
117127
rdoc (>= 4.0.0)
@@ -150,11 +160,6 @@ GEM
150160
pp (0.6.2)
151161
prettyprint
152162
prettyprint (0.2.0)
153-
propshaft (1.1.0)
154-
actionpack (>= 7.0.0)
155-
activesupport (>= 7.0.0)
156-
rack
157-
railties (>= 7.0.0)
158163
psych (5.2.6)
159164
date
160165
stringio
@@ -257,7 +262,7 @@ DEPENDENCIES
257262
byebug
258263
capybara
259264
importmap-rails!
260-
propshaft
265+
propshaft!
261266
rails
262267
rexml
263268
selenium-webdriver

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ If you want to import local js module files from `app/javascript/src` or other s
7979
```rb
8080
# config/importmap.rb
8181
pin_all_from 'app/javascript/src', under: 'src', to: 'src'
82+
83+
# With automatic integrity calculation for enhanced security
84+
pin_all_from 'app/javascript/controllers', under: 'controllers', integrity: true
8285
```
8386

8487
The `:to` parameter is only required if you want to change the destination logical import name. If you drop the :to option, you must place the :under option directly after the first parameter.
8588

89+
The `integrity: true` option automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
90+
8691
Allows you to:
8792

8893
```js
@@ -181,6 +186,52 @@ If you have existing pins without integrity hashes, you can add them using the `
181186
./bin/importmap integrity --update
182187
```
183188

189+
### Automatic integrity for local assets
190+
191+
For local assets served by the Rails asset pipeline (like those created with `pin` or `pin_all_from`), you can use `integrity: true` to automatically calculate integrity hashes from the compiled assets:
192+
193+
```ruby
194+
# config/importmap.rb
195+
196+
# Automatically calculate integrity from asset pipeline
197+
pin "application", integrity: true
198+
pin "admin", to: "admin.js", integrity: true
199+
200+
# Works with pin_all_from too
201+
pin_all_from "app/javascript/controllers", under: "controllers", integrity: true
202+
pin_all_from "app/javascript/lib", under: "lib", integrity: true
203+
204+
# Mixed usage
205+
pin "local_module", integrity: true # Auto-calculated
206+
pin "cdn_package", integrity: "sha384-abc123..." # Pre-calculated
207+
pin "no_integrity_package" # No integrity (default)
208+
```
209+
210+
This is particularly useful for:
211+
* **Local JavaScript files** managed by your Rails asset pipeline
212+
* **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
213+
* **Development workflow** where asset contents change frequently
214+
215+
The `integrity: true` option:
216+
* Uses the Rails asset pipeline's built-in integrity calculation
217+
* Works with both Sprockets and Propshaft
218+
* Automatically updates when assets are recompiled
219+
* Gracefully handles missing assets (returns `nil` for non-existent files)
220+
221+
**Example output with `integrity: true`:**
222+
```json
223+
{
224+
"imports": {
225+
"application": "/assets/application-abc123.js",
226+
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
227+
},
228+
"integrity": {
229+
"/assets/application-abc123.js": "sha256-xyz789...",
230+
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
231+
}
232+
}
233+
```
234+
184235
### How integrity works
185236

186237
The integrity hashes are automatically included in your import map and module preload tags:

gemfiles/rails_7.0_propshaft.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source "https://rubygems.org"
44

55
gem "rails", branch: "7-0-stable", git: "https://github.com/rails/rails.git"
6-
gem "propshaft"
6+
gem "propshaft", branch: "main", git: "https://github.com/rails/propshaft.git"
77
gem "sqlite3", "~> 1.4"
88

99
group :development do

gemfiles/rails_7.1_propshaft.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source "https://rubygems.org"
44

55
gem "rails", "~> 7.1.0"
6-
gem "propshaft"
6+
gem "propshaft", branch: "main", git: "https://github.com/rails/propshaft.git"
77
gem "sqlite3"
88

99
group :development do

gemfiles/rails_7.2_propshaft.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source "https://rubygems.org"
44

55
gem "rails", "~> 7.2.0"
6-
gem "propshaft"
6+
gem "propshaft", branch: "main", git: "https://github.com/rails/propshaft.git"
77
gem "sqlite3"
88

99
group :development do

gemfiles/rails_8.0_propshaft.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source "https://rubygems.org"
44

55
gem "rails", "~> 8.0.0"
6-
gem "propshaft"
6+
gem "propshaft", branch: "main", git: "https://github.com/rails/propshaft.git"
77
gem "sqlite3"
88

99
group :development do

gemfiles/rails_main_propshaft.gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
source "https://rubygems.org"
44

55
gem "rails", branch: "main", git: "https://github.com/rails/rails.git"
6-
gem "propshaft"
6+
gem "propshaft", branch: "main", git: "https://github.com/rails/propshaft.git"
77
gem "sqlite3"
88

99
group :development do

lib/importmap/map.rb

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ def pin(name, to: nil, preload: true, integrity: nil)
3030
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
3131
end
3232

33-
def pin_all_from(dir, under: nil, to: nil, preload: true)
33+
def pin_all_from(dir, under: nil, to: nil, preload: true, integrity: nil)
3434
clear_cache
35-
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload)
35+
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload, integrity: integrity)
3636
end
3737

3838
# Returns an array of all the resolved module paths of the pinned packages. The `resolver` must respond to
@@ -92,9 +92,21 @@ def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :pr
9292
# packages = importmap.preloaded_module_packages(resolver: helpers, cache_key: "cdn_host")
9393
def preloaded_module_packages(resolver:, entry_point: "application", cache_key: :preloaded_module_packages)
9494
cache_as(cache_key) do
95-
expanded_preloading_packages_and_directories(entry_point:).to_h do |_, package|
96-
[resolve_asset_path(package.path, resolver: resolver), package]
97-
end.delete_if { |key| key.nil? }
95+
expanded_preloading_packages_and_directories(entry_point:).filter_map do |_, package|
96+
resolved_path = resolve_asset_path(package.path, resolver: resolver)
97+
next unless resolved_path
98+
99+
resolved_integrity = resolve_integrity_value(package.integrity, package.path, resolver: resolver)
100+
101+
package = MappedFile.new(
102+
name: package.name,
103+
path: package.path,
104+
preload: package.preload,
105+
integrity: resolved_integrity
106+
)
107+
108+
[resolved_path, package]
109+
end.to_h
98110
end
99111
end
100112

@@ -138,7 +150,7 @@ def cache_sweeper(watches: nil)
138150
end
139151

140152
private
141-
MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
153+
MappedDir = Struct.new(:dir, :path, :under, :preload, :integrity, keyword_init: true)
142154
MappedFile = Struct.new(:name, :path, :preload, :integrity, keyword_init: true)
143155

144156
def cache_as(name)
@@ -190,10 +202,22 @@ def build_integrity_hash(packages, resolver:)
190202
resolved_path = resolve_asset_path(mapping.path, resolver: resolver)
191203
next unless resolved_path
192204

193-
[resolved_path, mapping.integrity]
205+
integrity_value = resolve_integrity_value(mapping.integrity, mapping.path, resolver: resolver)
206+
next unless integrity_value
207+
208+
[resolved_path, integrity_value]
194209
end.to_h
195210
end
196211

212+
def resolve_integrity_value(integrity, path, resolver:)
213+
case integrity
214+
when true
215+
resolver.asset_integrity(path)
216+
when String
217+
integrity
218+
end
219+
end
220+
197221
def expanded_preloading_packages_and_directories(entry_point:)
198222
expanded_packages_and_directories.select { |name, mapping| mapping.preload.in?([true, false]) ? mapping.preload : (Array(mapping.preload) & Array(entry_point)).any? }
199223
end
@@ -210,7 +234,12 @@ def expand_directories_into(paths)
210234
module_name = module_name_from(module_filename, mapping)
211235
module_path = module_path_from(module_filename, mapping)
212236

213-
paths[module_name] = MappedFile.new(name: module_name, path: module_path, preload: mapping.preload)
237+
paths[module_name] = MappedFile.new(
238+
name: module_name,
239+
path: module_path,
240+
preload: mapping.preload,
241+
integrity: mapping.integrity
242+
)
214243
end
215244
end
216245
end

0 commit comments

Comments
 (0)