importmap-rails 2.1.0 → 2.2.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/README.md +156 -11
- data/app/helpers/importmap/importmap_tags_helper.rb +14 -4
- data/lib/importmap/commands.rb +87 -20
- data/lib/importmap/map.rb +124 -19
- data/lib/importmap/npm.rb +57 -16
- data/lib/importmap/packager.rb +42 -15
- data/lib/importmap/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '079361e5c1e2b84206422fb3f2a5de6f6969c07a29e73d40843986d1549410ce'
|
4
|
+
data.tar.gz: 61e335f52eacd06ffb635e4400ac68701e58cb57a9edfd7d38cc53a1a173c7a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c92850ba94cc450f18b1554675737f94eff235e5b51d2922cbb1433a9cd6f3be065a839a72b93661feb7b1f346177b43fbf558a6b9f30445e232f7446e2071a
|
7
|
+
data.tar.gz: 0be5aa11b95db77526ef6d933eeca0197346ba69ce5ef6a5148fb871eb7f986c050399c177cce0ccd8178dcb05ccd69a6d673b8efabadc382fea79284df7105c
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ import React from "./node_modules/react"
|
|
44
44
|
import React from "https://ga.jspm.io/npm:react@17.0.1/index.js"
|
45
45
|
```
|
46
46
|
|
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"`
|
48
48
|
to 1 of the 3 viable ways of loading ES Module javascript packages.
|
49
49
|
|
50
50
|
For example:
|
@@ -54,11 +54,11 @@ For example:
|
|
54
54
|
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"
|
55
55
|
```
|
56
56
|
|
57
|
-
means "
|
57
|
+
means "every time you see `import React from "react"`
|
58
58
|
change it to `import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"`"
|
59
59
|
|
60
60
|
```js
|
61
|
-
import React from "react"
|
61
|
+
import React from "react"
|
62
62
|
// => import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"
|
63
63
|
```
|
64
64
|
|
@@ -79,10 +79,15 @@ If you want to import local js module files from `app/javascript/src` or other s
|
|
79
79
|
```rb
|
80
80
|
# config/importmap.rb
|
81
81
|
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
|
82
85
|
```
|
83
86
|
|
84
87
|
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.
|
85
88
|
|
89
|
+
The `integrity: true` option automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
|
90
|
+
|
86
91
|
Allows you to:
|
87
92
|
|
88
93
|
```js
|
@@ -97,19 +102,29 @@ Note: Sprockets used to serve assets (albeit without filename digests) it couldn
|
|
97
102
|
|
98
103
|
Importmap for Rails downloads and vendors your npm package dependencies via JavaScript CDNs that provide pre-compiled distribution versions.
|
99
104
|
|
100
|
-
You can use the `./bin/importmap` command that's added as part of the install to pin, unpin, or update npm packages in your import map.
|
105
|
+
You can use the `./bin/importmap` command that's added as part of the install to pin, unpin, or update npm packages in your import map. By default this command uses an API from [JSPM.org](https://jspm.org) to resolve your package dependencies efficiently, and then add the pins to your `config/importmap.rb` file.
|
101
106
|
|
102
107
|
```bash
|
103
108
|
./bin/importmap pin react
|
104
|
-
Pinning "react" to vendor/react.js via download from https://ga.jspm.io/npm:react@
|
105
|
-
Pinning "object-assign" to vendor/object-assign.js via download from https://ga.jspm.io/npm:object-assign@4.1.1/index.js
|
109
|
+
Pinning "react" to vendor/javascript/react.js via download from https://ga.jspm.io/npm:react@19.1.0/index.js
|
106
110
|
```
|
107
111
|
|
108
|
-
This will produce
|
112
|
+
This will produce a pin in your `config/importmap.rb` like so:
|
109
113
|
|
110
114
|
```ruby
|
111
|
-
pin "react" #
|
112
|
-
|
115
|
+
pin "react" # @19.1.0
|
116
|
+
```
|
117
|
+
|
118
|
+
Other CDNs like [unpkg.com](https://unpkg.com) and [jsdelivr.com](https://www.jsdelivr.com) can be specified with `--from`:
|
119
|
+
|
120
|
+
```bash
|
121
|
+
./bin/importmap pin react --from unpkg
|
122
|
+
Pinning "react" to vendor/javascript/react.js via download from https://unpkg.com/react@19.1.0/index.js
|
123
|
+
```
|
124
|
+
|
125
|
+
```bash
|
126
|
+
./bin/importmap pin react --from jsdelivr
|
127
|
+
Pinning "react" to vendor/javascript/react.js via download from https://cdn.jsdelivr.net/npm/react@19.1.0/index.js
|
113
128
|
```
|
114
129
|
|
115
130
|
The packages are downloaded to `vendor/javascript`, which you can check into your source control, and they'll be available through your application's own asset pipeline serving.
|
@@ -119,7 +134,137 @@ If you later wish to remove a downloaded pin:
|
|
119
134
|
```bash
|
120
135
|
./bin/importmap unpin react
|
121
136
|
Unpinning and removing "react"
|
122
|
-
|
137
|
+
```
|
138
|
+
|
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:lodash@4.17.21/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:lodash@4.17.21/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:lodash@4.17.21/lodash.js"
|
244
|
+
},
|
245
|
+
"integrity": {
|
246
|
+
"https://ga.jspm.io/npm:lodash@4.17.21/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:lodash@4.17.21/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
|
123
268
|
```
|
124
269
|
|
125
270
|
## Preloading pinned modules
|
@@ -208,7 +353,7 @@ Pin your js file:
|
|
208
353
|
pin "checkout", preload: false
|
209
354
|
```
|
210
355
|
|
211
|
-
Import your module on the specific page. Note: you'll likely want to use a `content_for` block on the
|
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.
|
212
357
|
|
213
358
|
```erb
|
214
359
|
<% content_for :head do %>
|
@@ -25,13 +25,23 @@ module Importmap::ImportmapTagsHelper
|
|
25
25
|
# (defaults to Rails.application.importmap), such that they'll be fetched
|
26
26
|
# in advance by browsers supporting this link type (https://caniuse.com/?search=modulepreload).
|
27
27
|
def javascript_importmap_module_preload_tags(importmap = Rails.application.importmap, entry_point: "application")
|
28
|
-
|
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 }] }
|
29
31
|
end
|
30
32
|
|
31
33
|
# Link tag(s) for preloading the JavaScript module residing in `*paths`. Will return one link tag per path element.
|
32
34
|
def javascript_module_preload_tag(*paths)
|
33
|
-
|
34
|
-
tag.link rel: "modulepreload", href: path, nonce: request&.content_security_policy_nonce
|
35
|
-
}, "\n")
|
35
|
+
_generate_preload_tags(paths) { |path| [path, {}] }
|
36
36
|
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
|
37
47
|
end
|
data/lib/importmap/commands.rb
CHANGED
@@ -12,21 +12,20 @@ class Importmap::Commands < Thor
|
|
12
12
|
desc "pin [*PACKAGES]", "Pin new packages"
|
13
13
|
option :env, type: :string, aliases: :e, default: "production"
|
14
14
|
option :from, type: :string, aliases: :f, default: "jspm"
|
15
|
+
option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
|
16
|
+
option :integrity, type: :boolean, aliases: :i, default: true, desc: "Include integrity hash from JSPM"
|
15
17
|
def pin(*packages)
|
16
|
-
|
17
|
-
imports
|
18
|
+
with_import_response(packages, env: options[:env], from: options[:from], integrity: options[:integrity]) do |imports, integrity_hashes|
|
19
|
+
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
|
18
20
|
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
|
21
|
+
|
19
22
|
packager.download(package, url)
|
20
|
-
pin = packager.vendored_pin_for(package, url)
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
24
|
+
pin = packager.vendored_pin_for(package, url, options[:preload], integrity: integrity_hash)
|
25
|
+
|
26
|
+
log_integrity_usage(integrity_hash)
|
27
|
+
update_importmap_with_pin(package, pin)
|
27
28
|
end
|
28
|
-
else
|
29
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
@@ -34,33 +33,31 @@ class Importmap::Commands < Thor
|
|
34
33
|
option :env, type: :string, aliases: :e, default: "production"
|
35
34
|
option :from, type: :string, aliases: :f, default: "jspm"
|
36
35
|
def unpin(*packages)
|
37
|
-
|
36
|
+
with_import_response(packages, env: options[:env], from: options[:from]) do |imports, _integrity_hashes|
|
38
37
|
imports.each do |package, url|
|
39
38
|
if packager.packaged?(package)
|
40
39
|
puts %(Unpinning and removing "#{package}")
|
41
40
|
packager.remove(package)
|
42
41
|
end
|
43
42
|
end
|
44
|
-
else
|
45
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
46
43
|
end
|
47
44
|
end
|
48
45
|
|
49
46
|
desc "pristine", "Redownload all pinned packages"
|
50
47
|
option :env, type: :string, aliases: :e, default: "production"
|
51
48
|
option :from, type: :string, aliases: :f, default: "jspm"
|
49
|
+
option :integrity, type: :boolean, aliases: :i, default: true, desc: "Include integrity hash from JSPM"
|
52
50
|
def pristine
|
53
|
-
packages =
|
54
|
-
v.blank? ? p : [p, v].join("@")
|
55
|
-
end
|
51
|
+
packages = prepare_packages_with_versions
|
56
52
|
|
57
|
-
|
58
|
-
imports
|
53
|
+
with_import_response(packages, env: options[:env], from: options[:from], integrity: options[:integrity]) do |imports, integrity_hashes|
|
54
|
+
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
|
59
55
|
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
|
56
|
+
|
60
57
|
packager.download(package, url)
|
58
|
+
|
59
|
+
log_integrity_usage(integrity_hash)
|
61
60
|
end
|
62
|
-
else
|
63
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
64
61
|
end
|
65
62
|
end
|
66
63
|
|
@@ -121,6 +118,33 @@ class Importmap::Commands < Thor
|
|
121
118
|
puts npm.packages_with_versions.map { |x| x.join(' ') }
|
122
119
|
end
|
123
120
|
|
121
|
+
desc "integrity [*PACKAGES]", "Download and add integrity hashes for packages"
|
122
|
+
option :env, type: :string, aliases: :e, default: "production"
|
123
|
+
option :from, type: :string, aliases: :f, default: "jspm"
|
124
|
+
option :update, type: :boolean, aliases: :u, default: false, desc: "Update importmap.rb with integrity hashes"
|
125
|
+
def integrity(*packages)
|
126
|
+
packages = prepare_packages_with_versions(packages)
|
127
|
+
|
128
|
+
with_import_response(packages, env: options[:env], from: options[:from], integrity: true) do |imports, integrity_hashes|
|
129
|
+
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
|
130
|
+
puts %(Getting integrity for "#{package}" from #{url})
|
131
|
+
|
132
|
+
if integrity_hash
|
133
|
+
puts %( #{package}: #{integrity_hash})
|
134
|
+
|
135
|
+
if options[:update]
|
136
|
+
pin_with_integrity = packager.pin_for(package, url, integrity: integrity_hash)
|
137
|
+
|
138
|
+
update_importmap_with_pin(package, pin_with_integrity)
|
139
|
+
puts %( Updated importmap.rb with integrity for "#{package}")
|
140
|
+
end
|
141
|
+
else
|
142
|
+
puts %( No integrity hash available for "#{package}")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
124
148
|
private
|
125
149
|
def packager
|
126
150
|
@packager ||= Importmap::Packager.new
|
@@ -130,6 +154,22 @@ class Importmap::Commands < Thor
|
|
130
154
|
@npm ||= Importmap::Npm.new
|
131
155
|
end
|
132
156
|
|
157
|
+
def update_importmap_with_pin(package, pin)
|
158
|
+
if packager.packaged?(package)
|
159
|
+
gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
|
160
|
+
else
|
161
|
+
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def log_integrity_usage(integrity_hash)
|
166
|
+
puts %( Using integrity: #{integrity_hash}) if integrity_hash
|
167
|
+
end
|
168
|
+
|
169
|
+
def handle_package_not_found(packages, from)
|
170
|
+
puts "Couldn't find any packages in #{packages.inspect} on #{from}"
|
171
|
+
end
|
172
|
+
|
133
173
|
def remove_line_from_file(path, pattern)
|
134
174
|
path = File.expand_path(path, destination_root)
|
135
175
|
|
@@ -154,6 +194,33 @@ class Importmap::Commands < Thor
|
|
154
194
|
puts divider if row_number == 0
|
155
195
|
end
|
156
196
|
end
|
197
|
+
|
198
|
+
def prepare_packages_with_versions(packages = [])
|
199
|
+
if packages.empty?
|
200
|
+
npm.packages_with_versions.map do |p, v|
|
201
|
+
v.blank? ? p : [p, v].join("@")
|
202
|
+
end
|
203
|
+
else
|
204
|
+
packages
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def process_imports(imports, integrity_hashes, &block)
|
209
|
+
imports.each do |package, url|
|
210
|
+
integrity_hash = integrity_hashes[url]
|
211
|
+
block.call(package, url, integrity_hash)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def with_import_response(packages, **options)
|
216
|
+
response = packager.import(*packages, **options)
|
217
|
+
|
218
|
+
if response
|
219
|
+
yield response[:imports], response[:integrity]
|
220
|
+
else
|
221
|
+
handle_package_not_found(packages, options[:from])
|
222
|
+
end
|
223
|
+
end
|
157
224
|
end
|
158
225
|
|
159
226
|
Importmap::Commands.start(ARGV)
|
data/lib/importmap/map.rb
CHANGED
@@ -25,14 +25,14 @@ class Importmap::Map
|
|
25
25
|
self
|
26
26
|
end
|
27
27
|
|
28
|
-
def pin(name, to: nil, preload: true)
|
28
|
+
def pin(name, to: nil, preload: true, integrity: nil)
|
29
29
|
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)
|
31
31
|
end
|
32
32
|
|
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)
|
34
34
|
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)
|
36
36
|
end
|
37
37
|
|
38
38
|
# Returns an array of all the resolved module paths of the pinned packages. The `resolver` must respond to
|
@@ -41,8 +41,72 @@ class Importmap::Map
|
|
41
41
|
# resolve for different asset hosts, you can pass in a custom `cache_key` to vary the cache used by this method for
|
42
42
|
# the different cases.
|
43
43
|
def preloaded_module_paths(resolver:, entry_point: "application", cache_key: :preloaded_module_paths)
|
44
|
+
preloaded_module_packages(resolver: resolver, entry_point: entry_point, cache_key: cache_key).keys
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a hash of resolved module paths to their corresponding package objects for all pinned packages
|
48
|
+
# that are marked for preloading. The hash keys are the resolved asset paths, and the values are the
|
49
|
+
# +MappedFile+ objects containing package metadata including name, path, preload setting, and integrity.
|
50
|
+
#
|
51
|
+
# The +resolver+ must respond to +path_to_asset+, such as +ActionController::Base.helpers+ or
|
52
|
+
# +ApplicationController.helpers+. You'll want to use the resolver that has been configured for the
|
53
|
+
# +asset_host+ you want these resolved paths to use.
|
54
|
+
#
|
55
|
+
# ==== Parameters
|
56
|
+
#
|
57
|
+
# [+resolver+]
|
58
|
+
# An object that responds to +path_to_asset+ for resolving asset paths.
|
59
|
+
#
|
60
|
+
# [+entry_point+]
|
61
|
+
# The entry point name or array of entry point names to determine which packages should be preloaded.
|
62
|
+
# Defaults to +"application"+. Packages with +preload: true+ are always included regardless of entry point.
|
63
|
+
# Packages with specific entry point names (e.g., +preload: "admin"+) are only included when that entry
|
64
|
+
# point is specified.
|
65
|
+
#
|
66
|
+
# [+cache_key+]
|
67
|
+
# A custom cache key to vary the cache used by this method for different cases, such as resolving
|
68
|
+
# for different asset hosts. Defaults to +:preloaded_module_packages+.
|
69
|
+
#
|
70
|
+
# ==== Returns
|
71
|
+
#
|
72
|
+
# A hash where:
|
73
|
+
# * Keys are resolved asset paths (strings)
|
74
|
+
# * Values are +MappedFile+ objects with +name+, +path+, +preload+, and +integrity+ attributes
|
75
|
+
#
|
76
|
+
# Missing assets are gracefully handled and excluded from the returned hash.
|
77
|
+
#
|
78
|
+
# ==== Examples
|
79
|
+
#
|
80
|
+
# # Get all preloaded packages for the default "application" entry point
|
81
|
+
# packages = importmap.preloaded_module_packages(resolver: ApplicationController.helpers)
|
82
|
+
# # => { "/assets/application-abc123.js" => #<struct name="application", path="application.js", preload=true, integrity=nil>,
|
83
|
+
# # "https://cdn.skypack.dev/react" => #<struct name="react", path="https://cdn.skypack.dev/react", preload=true, integrity="sha384-..."> }
|
84
|
+
#
|
85
|
+
# # Get preloaded packages for a specific entry point
|
86
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: "admin")
|
87
|
+
#
|
88
|
+
# # Get preloaded packages for multiple entry points
|
89
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, entry_point: ["application", "admin"])
|
90
|
+
#
|
91
|
+
# # Use a custom cache key for different asset hosts
|
92
|
+
# packages = importmap.preloaded_module_packages(resolver: helpers, cache_key: "cdn_host")
|
93
|
+
def preloaded_module_packages(resolver:, entry_point: "application", cache_key: :preloaded_module_packages)
|
44
94
|
cache_as(cache_key) do
|
45
|
-
|
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
|
46
110
|
end
|
47
111
|
end
|
48
112
|
|
@@ -53,7 +117,9 @@ class Importmap::Map
|
|
53
117
|
# `cache_key` to vary the cache used by this method for the different cases.
|
54
118
|
def to_json(resolver:, cache_key: :json)
|
55
119
|
cache_as(cache_key) do
|
56
|
-
|
120
|
+
packages = expanded_packages_and_directories
|
121
|
+
map = build_import_map(packages, resolver: resolver)
|
122
|
+
JSON.pretty_generate(map)
|
57
123
|
end
|
58
124
|
end
|
59
125
|
|
@@ -84,8 +150,8 @@ class Importmap::Map
|
|
84
150
|
end
|
85
151
|
|
86
152
|
private
|
87
|
-
MappedDir = Struct.new(:dir, :path, :under, :preload, keyword_init: true)
|
88
|
-
MappedFile = Struct.new(:name, :path, :preload, keyword_init: true)
|
153
|
+
MappedDir = Struct.new(:dir, :path, :under, :preload, :integrity, keyword_init: true)
|
154
|
+
MappedFile = Struct.new(:name, :path, :preload, :integrity, keyword_init: true)
|
89
155
|
|
90
156
|
def cache_as(name)
|
91
157
|
if result = @cache[name.to_s]
|
@@ -105,19 +171,53 @@ class Importmap::Map
|
|
105
171
|
|
106
172
|
def resolve_asset_paths(paths, resolver:)
|
107
173
|
paths.transform_values do |mapping|
|
108
|
-
|
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
|
174
|
+
resolve_asset_path(mapping.path, resolver:)
|
118
175
|
end.compact
|
119
176
|
end
|
120
177
|
|
178
|
+
def resolve_asset_path(path, resolver:)
|
179
|
+
begin
|
180
|
+
resolver.path_to_asset(path)
|
181
|
+
rescue => e
|
182
|
+
if rescuable_asset_error?(e)
|
183
|
+
Rails.logger.warn "Importmap skipped missing path: #{path}"
|
184
|
+
nil
|
185
|
+
else
|
186
|
+
raise e
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def build_import_map(packages, resolver:)
|
192
|
+
map = { "imports" => resolve_asset_paths(packages, resolver: resolver) }
|
193
|
+
integrity = build_integrity_hash(packages, resolver: resolver)
|
194
|
+
map["integrity"] = integrity unless integrity.empty?
|
195
|
+
map
|
196
|
+
end
|
197
|
+
|
198
|
+
def build_integrity_hash(packages, resolver:)
|
199
|
+
packages.filter_map do |name, mapping|
|
200
|
+
next unless mapping.integrity
|
201
|
+
|
202
|
+
resolved_path = resolve_asset_path(mapping.path, resolver: resolver)
|
203
|
+
next unless resolved_path
|
204
|
+
|
205
|
+
integrity_value = resolve_integrity_value(mapping.integrity, mapping.path, resolver: resolver)
|
206
|
+
next unless integrity_value
|
207
|
+
|
208
|
+
[resolved_path, integrity_value]
|
209
|
+
end.to_h
|
210
|
+
end
|
211
|
+
|
212
|
+
def resolve_integrity_value(integrity, path, resolver:)
|
213
|
+
case integrity
|
214
|
+
when true
|
215
|
+
resolver.asset_integrity(path) if resolver.respond_to?(:asset_integrity)
|
216
|
+
when String
|
217
|
+
integrity
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
121
221
|
def expanded_preloading_packages_and_directories(entry_point:)
|
122
222
|
expanded_packages_and_directories.select { |name, mapping| mapping.preload.in?([true, false]) ? mapping.preload : (Array(mapping.preload) & Array(entry_point)).any? }
|
123
223
|
end
|
@@ -134,7 +234,12 @@ class Importmap::Map
|
|
134
234
|
module_name = module_name_from(module_filename, mapping)
|
135
235
|
module_path = module_path_from(module_filename, mapping)
|
136
236
|
|
137
|
-
paths[module_name] = MappedFile.new(
|
237
|
+
paths[module_name] = MappedFile.new(
|
238
|
+
name: module_name,
|
239
|
+
path: module_path,
|
240
|
+
preload: mapping.preload,
|
241
|
+
integrity: mapping.integrity
|
242
|
+
)
|
138
243
|
end
|
139
244
|
end
|
140
245
|
end
|
data/lib/importmap/npm.rb
CHANGED
@@ -3,20 +3,22 @@ require "uri"
|
|
3
3
|
require "json"
|
4
4
|
|
5
5
|
class Importmap::Npm
|
6
|
+
PIN_REGEX = /^pin ["']([^["']]*)["'].*/
|
7
|
+
|
6
8
|
Error = Class.new(StandardError)
|
7
9
|
HTTPError = Class.new(Error)
|
8
10
|
|
9
11
|
singleton_class.attr_accessor :base_uri
|
10
12
|
self.base_uri = URI("https://registry.npmjs.org")
|
11
13
|
|
12
|
-
def initialize(importmap_path = "config/importmap.rb")
|
14
|
+
def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript")
|
13
15
|
@importmap_path = Pathname.new(importmap_path)
|
16
|
+
@vendor_path = Pathname.new(vendor_path)
|
14
17
|
end
|
15
18
|
|
16
19
|
def outdated_packages
|
17
20
|
packages_with_versions.each.with_object([]) do |(package, current_version), outdated_packages|
|
18
|
-
outdated_package = OutdatedPackage.new(name: package,
|
19
|
-
current_version: current_version)
|
21
|
+
outdated_package = OutdatedPackage.new(name: package, current_version: current_version)
|
20
22
|
|
21
23
|
if !(response = get_package(package))
|
22
24
|
outdated_package.error = 'Response error'
|
@@ -36,10 +38,12 @@ class Importmap::Npm
|
|
36
38
|
def vulnerable_packages
|
37
39
|
get_audit.flat_map do |package, vulnerabilities|
|
38
40
|
vulnerabilities.map do |vulnerability|
|
39
|
-
VulnerablePackage.new(
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
VulnerablePackage.new(
|
42
|
+
name: package,
|
43
|
+
severity: vulnerability['severity'],
|
44
|
+
vulnerable_versions: vulnerability['vulnerable_versions'],
|
45
|
+
vulnerability: vulnerability['title']
|
46
|
+
)
|
43
47
|
end
|
44
48
|
end.sort_by { |p| [p.name, p.severity] }
|
45
49
|
end
|
@@ -47,17 +51,20 @@ class Importmap::Npm
|
|
47
51
|
def packages_with_versions
|
48
52
|
# We cannot use the name after "pin" because some dependencies are loaded from inside packages
|
49
53
|
# Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js"
|
54
|
+
with_versions = importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/)(.*)(?=@\d+\.\d+\.\d+)@(\d+\.\d+\.\d+(?:[^\/\s["']]*)).*$/) |
|
55
|
+
importmap.scan(/#{PIN_REGEX} #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
|
56
|
+
|
57
|
+
vendored_packages_without_version(with_versions).each do |package, path|
|
58
|
+
$stdout.puts "Ignoring #{package} (#{path}) since no version is specified in the importmap"
|
59
|
+
end
|
50
60
|
|
51
|
-
|
52
|
-
importmap.scan(/^pin ["']([^["']]*)["'].* #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
|
61
|
+
with_versions
|
53
62
|
end
|
54
63
|
|
55
64
|
private
|
56
65
|
OutdatedPackage = Struct.new(:name, :current_version, :latest_version, :error, keyword_init: true)
|
57
66
|
VulnerablePackage = Struct.new(:name, :severity, :vulnerable_versions, :vulnerability, keyword_init: true)
|
58
67
|
|
59
|
-
|
60
|
-
|
61
68
|
def importmap
|
62
69
|
@importmap ||= File.read(@importmap_path)
|
63
70
|
end
|
@@ -76,13 +83,19 @@ class Importmap::Npm
|
|
76
83
|
request = Net::HTTP::Get.new(uri)
|
77
84
|
request["Content-Type"] = "application/json"
|
78
85
|
|
79
|
-
response =
|
80
|
-
|
81
|
-
|
86
|
+
response = begin
|
87
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http|
|
88
|
+
http.request(request)
|
89
|
+
}
|
90
|
+
rescue => error
|
91
|
+
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
92
|
+
end
|
93
|
+
|
94
|
+
unless response.code.to_i < 300
|
95
|
+
raise HTTPError, "Unexpected error response #{response.code}: #{response.body}"
|
96
|
+
end
|
82
97
|
|
83
98
|
response.body
|
84
|
-
rescue => error
|
85
|
-
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
86
99
|
end
|
87
100
|
|
88
101
|
def find_latest_version(response)
|
@@ -111,6 +124,11 @@ class Importmap::Npm
|
|
111
124
|
return {} if body.empty?
|
112
125
|
|
113
126
|
response = post_json(uri, body)
|
127
|
+
|
128
|
+
unless response.code.to_i < 300
|
129
|
+
raise HTTPError, "Unexpected error response #{response.code}: #{response.body}"
|
130
|
+
end
|
131
|
+
|
114
132
|
JSON.parse(response.body)
|
115
133
|
end
|
116
134
|
|
@@ -119,4 +137,27 @@ class Importmap::Npm
|
|
119
137
|
rescue => error
|
120
138
|
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
121
139
|
end
|
140
|
+
|
141
|
+
def vendored_packages_without_version(packages_with_versions)
|
142
|
+
versioned_packages = packages_with_versions.map(&:first).to_set
|
143
|
+
|
144
|
+
importmap
|
145
|
+
.lines
|
146
|
+
.filter_map { |line| find_unversioned_vendored_package(line, versioned_packages) }
|
147
|
+
end
|
148
|
+
|
149
|
+
def find_unversioned_vendored_package(line, versioned_packages)
|
150
|
+
regexp = line.include?("to:")? /#{PIN_REGEX}to: ["']([^["']]*)["'].*/ : PIN_REGEX
|
151
|
+
match = line.match(regexp)
|
152
|
+
|
153
|
+
return unless match
|
154
|
+
|
155
|
+
package, filename = match.captures
|
156
|
+
filename ||= "#{package}.js"
|
157
|
+
|
158
|
+
return if versioned_packages.include?(package)
|
159
|
+
|
160
|
+
path = File.join(@vendor_path, filename)
|
161
|
+
[package, path] if File.exist?(path)
|
162
|
+
end
|
122
163
|
end
|
data/lib/importmap/packager.rb
CHANGED
@@ -17,34 +17,39 @@ class Importmap::Packager
|
|
17
17
|
@vendor_path = Pathname.new(vendor_path)
|
18
18
|
end
|
19
19
|
|
20
|
-
def import(*packages, env: "production", from: "jspm")
|
20
|
+
def import(*packages, env: "production", from: "jspm", integrity: false)
|
21
21
|
response = post_json({
|
22
22
|
"install" => Array(packages),
|
23
23
|
"flattenScope" => true,
|
24
24
|
"env" => [ "browser", "module", env ],
|
25
|
-
"provider" => normalize_provider(from)
|
25
|
+
"provider" => normalize_provider(from),
|
26
|
+
"integrity" => integrity
|
26
27
|
})
|
27
28
|
|
28
29
|
case response.code
|
29
|
-
when "200"
|
30
|
-
|
31
|
-
|
30
|
+
when "200"
|
31
|
+
extract_parsed_response(response)
|
32
|
+
when "404", "401"
|
33
|
+
nil
|
34
|
+
else
|
35
|
+
handle_failure_response(response)
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
35
|
-
def pin_for(package, url)
|
36
|
-
%(
|
39
|
+
def pin_for(package, url = nil, preloads: nil, integrity: nil)
|
40
|
+
to = url ? %(, to: "#{url}") : ""
|
41
|
+
preload_param = preload(preloads)
|
42
|
+
integrity_param = integrity ? %(, integrity: "#{integrity}") : ""
|
43
|
+
|
44
|
+
%(pin "#{package}") + to + preload_param + integrity_param
|
37
45
|
end
|
38
46
|
|
39
|
-
def vendored_pin_for(package, url)
|
47
|
+
def vendored_pin_for(package, url, preloads = nil, integrity: nil)
|
40
48
|
filename = package_filename(package)
|
41
49
|
version = extract_package_version_from(url)
|
50
|
+
to = "#{package}.js" != filename ? filename : nil
|
42
51
|
|
43
|
-
|
44
|
-
%(pin "#{package}" # #{version})
|
45
|
-
else
|
46
|
-
%(pin "#{package}", to: "#{filename}" # #{version})
|
47
|
-
end
|
52
|
+
pin_for(package, to, preloads: preloads, integrity: integrity) + %( # #{version})
|
48
53
|
end
|
49
54
|
|
50
55
|
def packaged?(package)
|
@@ -63,6 +68,21 @@ class Importmap::Packager
|
|
63
68
|
end
|
64
69
|
|
65
70
|
private
|
71
|
+
def preload(preloads)
|
72
|
+
case Array(preloads)
|
73
|
+
in []
|
74
|
+
""
|
75
|
+
in ["true"]
|
76
|
+
%(, preload: true)
|
77
|
+
in ["false"]
|
78
|
+
%(, preload: false)
|
79
|
+
in [string]
|
80
|
+
%(, preload: "#{string}")
|
81
|
+
else
|
82
|
+
%(, preload: #{preloads})
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
66
86
|
def post_json(body)
|
67
87
|
Net::HTTP.post(self.class.endpoint, body.to_json, "Content-Type" => "application/json")
|
68
88
|
rescue => error
|
@@ -73,8 +93,15 @@ class Importmap::Packager
|
|
73
93
|
name.to_s == "jspm" ? "jspm.io" : name.to_s
|
74
94
|
end
|
75
95
|
|
76
|
-
def
|
77
|
-
JSON.parse(response.body)
|
96
|
+
def extract_parsed_response(response)
|
97
|
+
parsed = JSON.parse(response.body)
|
98
|
+
imports = parsed.dig("map", "imports")
|
99
|
+
integrity = parsed.dig("map", "integrity") || {}
|
100
|
+
|
101
|
+
{
|
102
|
+
imports: imports,
|
103
|
+
integrity: integrity
|
104
|
+
}
|
78
105
|
end
|
79
106
|
|
80
107
|
def handle_failure_response(response)
|
data/lib/importmap/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: importmap-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Heinemeier Hansson
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: railties
|
@@ -52,7 +51,6 @@ dependencies:
|
|
52
51
|
- - ">="
|
53
52
|
- !ruby/object:Gem::Version
|
54
53
|
version: 6.0.0
|
55
|
-
description:
|
56
54
|
email: david@loudthinking.com
|
57
55
|
executables: []
|
58
56
|
extensions: []
|
@@ -81,7 +79,6 @@ licenses:
|
|
81
79
|
metadata:
|
82
80
|
homepage_uri: https://github.com/rails/importmap-rails
|
83
81
|
source_code_uri: https://github.com/rails/importmap-rails
|
84
|
-
post_install_message:
|
85
82
|
rdoc_options: []
|
86
83
|
require_paths:
|
87
84
|
- lib
|
@@ -96,8 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
93
|
- !ruby/object:Gem::Version
|
97
94
|
version: '0'
|
98
95
|
requirements: []
|
99
|
-
rubygems_version: 3.
|
100
|
-
signing_key:
|
96
|
+
rubygems_version: 3.6.7
|
101
97
|
specification_version: 4
|
102
98
|
summary: Use ESM with importmap to manage modern JavaScript in Rails without transpiling
|
103
99
|
or bundling.
|