Skip to content

Commit 03acd4c

Browse files
authored
Merge pull request #304 from rails/rm=sri
Implement SRI support in importmap-rails
2 parents 9cd897f + 1aecf66 commit 03acd4c

23 files changed

+940
-121
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737

3838
env:
3939
BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails-version }}_${{ matrix.assets-pipeline }}.gemfile
40+
ASSETS_PIPELINE: ${{ matrix.assets-pipeline }}
4041

4142
steps:
4243
- uses: actions/checkout@v4

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", ">= 1.2.0"
99

1010
gem "sqlite3"
1111

Gemfile.lock

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,13 @@ GEM
105105
crass (1.0.6)
106106
date (3.4.1)
107107
drb (2.2.3)
108-
erb (5.0.1)
108+
erb (5.0.2)
109109
erubi (1.13.1)
110110
globalid (1.2.1)
111111
activesupport (>= 6.1)
112112
i18n (1.14.7)
113113
concurrent-ruby (~> 1.0)
114-
io-console (0.8.0)
114+
io-console (0.8.1)
115115
irb (1.15.2)
116116
pp (>= 0.6.0)
117117
rdoc (>= 4.0.0)
@@ -150,11 +150,10 @@ GEM
150150
pp (0.6.2)
151151
prettyprint
152152
prettyprint (0.2.0)
153-
propshaft (1.1.0)
153+
propshaft (1.2.0)
154154
actionpack (>= 7.0.0)
155155
activesupport (>= 7.0.0)
156156
rack
157-
railties (>= 7.0.0)
158157
psych (5.2.6)
159158
date
160159
stringio
@@ -257,7 +256,7 @@ DEPENDENCIES
257256
byebug
258257
capybara
259258
importmap-rails!
260-
propshaft
259+
propshaft (>= 1.2.0)
261260
rails
262261
rexml
263262
selenium-webdriver

README.md

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import React from "./node_modules/react"
4444
import React from "https://ga.jspm.io/npm:[email protected]/index.js"
4545
```
4646

47-
Importmap-rails provides a clean API for mapping "bare module specifiers" like `"react"`
47+
Importmap-rails provides a clean API for mapping "bare module specifiers" like `"react"`
4848
to 1 of the 3 viable ways of loading ES Module javascript packages.
4949

5050
For example:
@@ -54,11 +54,11 @@ For example:
5454
pin "react", to: "https://ga.jspm.io/npm:[email protected]/index.js"
5555
```
5656

57-
means "everytime you see `import React from "react"`
57+
means "every time you see `import React from "react"`
5858
change it to `import React from "https://ga.jspm.io/npm:[email protected]/index.js"`"
5959

6060
```js
61-
import React from "react"
61+
import React from "react"
6262
// => import React from "https://ga.jspm.io/npm:[email protected]/index.js"
6363
```
6464

@@ -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
@@ -131,6 +136,137 @@ If you later wish to remove a downloaded pin:
131136
Unpinning and removing "react"
132137
```
133138

139+
## Subresource Integrity (SRI)
140+
141+
For enhanced security, importmap-rails automatically includes [Subresource Integrity (SRI)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes by default when pinning packages. This ensures that JavaScript files loaded from CDNs haven't been tampered with.
142+
143+
### Default behavior with integrity
144+
145+
When you pin a package, integrity hashes are automatically included:
146+
147+
```bash
148+
./bin/importmap pin lodash
149+
Pinning "lodash" to vendor/javascript/lodash.js via download from https://ga.jspm.io/npm:[email protected]/lodash.js
150+
Using integrity: sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF
151+
```
152+
153+
This generates a pin in your `config/importmap.rb` with the integrity hash:
154+
155+
```ruby
156+
pin "lodash", integrity: "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF" # @4.17.21
157+
```
158+
159+
### Opting out of integrity
160+
161+
If you need to disable integrity checking (not recommended for security reasons), you can use the `--no-integrity` flag:
162+
163+
```bash
164+
./bin/importmap pin lodash --no-integrity
165+
Pinning "lodash" to vendor/javascript/lodash.js via download from https://ga.jspm.io/npm:[email protected]/lodash.js
166+
```
167+
168+
This generates a pin without integrity:
169+
170+
```ruby
171+
pin "lodash" # @4.17.21
172+
```
173+
174+
### Adding integrity to existing pins
175+
176+
If you have existing pins without integrity hashes, you can add them using the `integrity` command:
177+
178+
```bash
179+
# Add integrity to specific packages
180+
./bin/importmap integrity lodash react
181+
182+
# Add integrity to all pinned packages
183+
./bin/importmap integrity
184+
185+
# Update your importmap.rb file with integrity hashes
186+
./bin/importmap integrity --update
187+
```
188+
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+
235+
### How integrity works
236+
237+
The integrity hashes are automatically included in your import map and module preload tags:
238+
239+
**Import map JSON:**
240+
```json
241+
{
242+
"imports": {
243+
"lodash": "https://ga.jspm.io/npm:[email protected]/lodash.js"
244+
},
245+
"integrity": {
246+
"https://ga.jspm.io/npm:[email protected]/lodash.js": "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF"
247+
}
248+
}
249+
```
250+
251+
**Module preload tags:**
252+
```html
253+
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/lodash.js" integrity="sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF">
254+
```
255+
256+
Modern browsers will automatically validate these integrity hashes when loading the JavaScript modules, ensuring the files haven't been modified.
257+
258+
### Redownloading packages with integrity
259+
260+
The `pristine` command also includes integrity by default:
261+
262+
```bash
263+
# Redownload all packages with integrity (default)
264+
./bin/importmap pristine
265+
266+
# Redownload packages without integrity
267+
./bin/importmap pristine --no-integrity
268+
```
269+
134270
## Preloading pinned modules
135271

136272
To avoid the waterfall effect where the browser has to load one file after another before it can get to the deepest nested import, importmap-rails uses [modulepreload links](https://developers.google.com/web/updates/2017/12/modulepreload) by default. If you don't want to preload a dependency, because you want to load it on-demand for efficiency, append `preload: false` to the pin.
@@ -217,7 +353,7 @@ Pin your js file:
217353
pin "checkout", preload: false
218354
```
219355

220-
Import your module on the specific page. Note: you'll likely want to use a `content_for` block on the specifc page/partial, then yield it in your layout.
356+
Import your module on the specific page. Note: you'll likely want to use a `content_for` block on the specific page/partial, then yield it in your layout.
221357

222358
```erb
223359
<% content_for :head do %>

app/helpers/importmap/importmap_tags_helper.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,23 @@ def javascript_import_module_tag(*module_names)
2525
# (defaults to Rails.application.importmap), such that they'll be fetched
2626
# in advance by browsers supporting this link type (https://caniuse.com/?search=modulepreload).
2727
def javascript_importmap_module_preload_tags(importmap = Rails.application.importmap, entry_point: "application")
28-
javascript_module_preload_tag(*importmap.preloaded_module_paths(resolver: self, entry_point:, cache_key: entry_point))
28+
packages = importmap.preloaded_module_packages(resolver: self, entry_point:, cache_key: entry_point)
29+
30+
_generate_preload_tags(packages) { |path, package| [path, { integrity: package.integrity }] }
2931
end
3032

3133
# Link tag(s) for preloading the JavaScript module residing in `*paths`. Will return one link tag per path element.
3234
def javascript_module_preload_tag(*paths)
33-
safe_join(Array(paths).collect { |path|
34-
tag.link rel: "modulepreload", href: path, nonce: request&.content_security_policy_nonce
35-
}, "\n")
35+
_generate_preload_tags(paths) { |path| [path, {}] }
3636
end
37+
38+
private
39+
def _generate_preload_tags(items)
40+
content_security_policy_nonce = request&.content_security_policy_nonce
41+
42+
safe_join(Array(items).collect { |item|
43+
path, options = yield(item)
44+
tag.link rel: "modulepreload", href: path, nonce: content_security_policy_nonce, **options
45+
}, "\n")
46+
end
3747
end

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", ">= 1.2.0"
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", ">= 1.2.0"
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", ">= 1.2.0"
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", ">= 1.2.0"
77
gem "sqlite3"
88

99
group :development do

0 commit comments

Comments
 (0)