importmap-rails 2.1.0 → 2.2.1
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 +106 -11
- data/app/helpers/importmap/importmap_tags_helper.rb +14 -4
- data/lib/importmap/commands.rb +50 -32
- data/lib/importmap/map.rb +124 -19
- data/lib/importmap/npm.rb +57 -16
- data/lib/importmap/packager.rb +37 -14
- 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: c286a2385b19e8a170d991478ce228cfb1c0e27eb02fbda6349a889798caa85f
|
4
|
+
data.tar.gz: a40cea607756433bf36ad49e291e342f324a303c80e0286e980ee27c6c9001f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71fc6847c3860c86d611dad36554feab750059128eee5d72961da0cd6bc2ba6ec08dc96358bb7919f487243d7463ca304efbbbf68874d7b58c9b85580d652f75
|
7
|
+
data.tar.gz: 59b0d5ce601ef182abd5f1017f414eec8fea35d1e48e270b0edd10bcf0c59cd7c5e52c1af0dab4f98f0ea81972ce82004af3011d1f3399d152ce077fbbd7a7e1
|
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,9 +134,89 @@ 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
|
-
Unpinning and removing "object-assign"
|
123
137
|
```
|
124
138
|
|
139
|
+
## Subresource Integrity (SRI)
|
140
|
+
|
141
|
+
For enhanced security, importmap-rails supports [Subresource Integrity (SRI)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes for packages loaded from external CDNs.
|
142
|
+
|
143
|
+
### Automatic integrity for local assets
|
144
|
+
|
145
|
+
Starting with importmap-rails, **`integrity: true` is the default** for all pins. This automatically calculates integrity hashes for local assets served by the Rails asset pipeline:
|
146
|
+
|
147
|
+
```ruby
|
148
|
+
# config/importmap.rb
|
149
|
+
|
150
|
+
# These all use integrity: true by default
|
151
|
+
pin "application" # Auto-calculated integrity
|
152
|
+
pin "admin", to: "admin.js" # Auto-calculated integrity
|
153
|
+
pin_all_from "app/javascript/controllers", under: "controllers" # Auto-calculated integrity
|
154
|
+
|
155
|
+
# Mixed usage - explicitly controlling integrity
|
156
|
+
pin "cdn_package", integrity: "sha384-abc123..." # Pre-calculated hash
|
157
|
+
pin "no_integrity_package", integrity: false # Explicitly disable integrity
|
158
|
+
pin "nil_integrity_package", integrity: nil # Explicitly disable integrity
|
159
|
+
```
|
160
|
+
|
161
|
+
This is particularly useful for:
|
162
|
+
* **Local JavaScript files** managed by your Rails asset pipeline
|
163
|
+
* **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
|
164
|
+
* **Development workflow** where asset contents change frequently
|
165
|
+
|
166
|
+
This behavior can be disabled by setting `integrity: false` or `integrity: nil`
|
167
|
+
|
168
|
+
**Important for Propshaft users:** SRI support requires Propshaft 1.2+ and you must configure the integrity hash algorithm in your application:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# config/application.rb or config/environments/*.rb
|
172
|
+
config.assets.integrity_hash_algorithm = 'sha256' # or 'sha384', 'sha512'
|
173
|
+
```
|
174
|
+
|
175
|
+
Without this configuration, integrity will be disabled by default when using Propshaft. Sprockets includes integrity support out of the box.
|
176
|
+
|
177
|
+
**Example output with `integrity: true`:**
|
178
|
+
```json
|
179
|
+
{
|
180
|
+
"imports": {
|
181
|
+
"application": "/assets/application-abc123.js",
|
182
|
+
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
|
183
|
+
},
|
184
|
+
"integrity": {
|
185
|
+
"/assets/application-abc123.js": "sha256-xyz789...",
|
186
|
+
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
|
187
|
+
}
|
188
|
+
}
|
189
|
+
```
|
190
|
+
|
191
|
+
### How integrity works
|
192
|
+
|
193
|
+
The integrity hashes are automatically included in your import map and module preload tags:
|
194
|
+
|
195
|
+
**Import map JSON:**
|
196
|
+
```json
|
197
|
+
{
|
198
|
+
"imports": {
|
199
|
+
"lodash": "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js",
|
200
|
+
"application": "/assets/application-abc123.js",
|
201
|
+
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
|
202
|
+
},
|
203
|
+
"integrity": {
|
204
|
+
"https://ga.jspm.io/npm:lodash@4.17.21/lodash.js": "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF"
|
205
|
+
"/assets/application-abc123.js": "sha256-xyz789...",
|
206
|
+
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
|
207
|
+
}
|
208
|
+
}
|
209
|
+
```
|
210
|
+
|
211
|
+
**Module preload tags:**
|
212
|
+
```html
|
213
|
+
<link rel="modulepreload" href="https://ga.jspm.io/npm:lodash@4.17.21/lodash.js" integrity="sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF">
|
214
|
+
<link rel="modulepreload" href="/assets/application-abc123.js" integrity="sha256-xyz789...">
|
215
|
+
<link rel="modulepreload" href="/assets/controllers/hello_controller-def456.js" integrity="sha256-uvw012...">
|
216
|
+
```
|
217
|
+
|
218
|
+
Modern browsers will automatically validate these integrity hashes when loading the JavaScript modules, ensuring the files haven't been modified.
|
219
|
+
|
125
220
|
## Preloading pinned modules
|
126
221
|
|
127
222
|
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.
|
@@ -208,7 +303,7 @@ Pin your js file:
|
|
208
303
|
pin "checkout", preload: false
|
209
304
|
```
|
210
305
|
|
211
|
-
Import your module on the specific page. Note: you'll likely want to use a `content_for` block on the
|
306
|
+
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
307
|
|
213
308
|
```erb
|
214
309
|
<% 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,16 @@ 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"
|
15
16
|
def pin(*packages)
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
else
|
25
|
-
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
else
|
29
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
17
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
18
|
+
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
|
19
|
+
|
20
|
+
packager.download(package, url)
|
21
|
+
|
22
|
+
pin = packager.vendored_pin_for(package, url, options[:preload])
|
23
|
+
|
24
|
+
update_importmap_with_pin(package, pin)
|
30
25
|
end
|
31
26
|
end
|
32
27
|
|
@@ -34,15 +29,11 @@ class Importmap::Commands < Thor
|
|
34
29
|
option :env, type: :string, aliases: :e, default: "production"
|
35
30
|
option :from, type: :string, aliases: :f, default: "jspm"
|
36
31
|
def unpin(*packages)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
packager.remove(package)
|
42
|
-
end
|
32
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
33
|
+
if packager.packaged?(package)
|
34
|
+
puts %(Unpinning and removing "#{package}")
|
35
|
+
packager.remove(package)
|
43
36
|
end
|
44
|
-
else
|
45
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
46
37
|
end
|
47
38
|
end
|
48
39
|
|
@@ -50,17 +41,12 @@ class Importmap::Commands < Thor
|
|
50
41
|
option :env, type: :string, aliases: :e, default: "production"
|
51
42
|
option :from, type: :string, aliases: :f, default: "jspm"
|
52
43
|
def pristine
|
53
|
-
packages =
|
54
|
-
v.blank? ? p : [p, v].join("@")
|
55
|
-
end
|
44
|
+
packages = prepare_packages_with_versions
|
56
45
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
else
|
63
|
-
puts "Couldn't find any packages in #{packages.inspect} on #{options[:from]}"
|
46
|
+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
|
47
|
+
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
|
48
|
+
|
49
|
+
packager.download(package, url)
|
64
50
|
end
|
65
51
|
end
|
66
52
|
|
@@ -130,6 +116,18 @@ class Importmap::Commands < Thor
|
|
130
116
|
@npm ||= Importmap::Npm.new
|
131
117
|
end
|
132
118
|
|
119
|
+
def update_importmap_with_pin(package, pin)
|
120
|
+
if packager.packaged?(package)
|
121
|
+
gsub_file("config/importmap.rb", /^pin "#{package}".*$/, pin, verbose: false)
|
122
|
+
else
|
123
|
+
append_to_file("config/importmap.rb", "#{pin}\n", verbose: false)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def handle_package_not_found(packages, from)
|
128
|
+
puts "Couldn't find any packages in #{packages.inspect} on #{from}"
|
129
|
+
end
|
130
|
+
|
133
131
|
def remove_line_from_file(path, pattern)
|
134
132
|
path = File.expand_path(path, destination_root)
|
135
133
|
|
@@ -154,6 +152,26 @@ class Importmap::Commands < Thor
|
|
154
152
|
puts divider if row_number == 0
|
155
153
|
end
|
156
154
|
end
|
155
|
+
|
156
|
+
def prepare_packages_with_versions(packages = [])
|
157
|
+
if packages.empty?
|
158
|
+
npm.packages_with_versions.map do |p, v|
|
159
|
+
v.blank? ? p : [p, v].join("@")
|
160
|
+
end
|
161
|
+
else
|
162
|
+
packages
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def for_each_import(packages, **options, &block)
|
167
|
+
response = packager.import(*packages, **options)
|
168
|
+
|
169
|
+
if response
|
170
|
+
response[:imports].each(&block)
|
171
|
+
else
|
172
|
+
handle_package_not_found(packages, options[:from])
|
173
|
+
end
|
174
|
+
end
|
157
175
|
end
|
158
176
|
|
159
177
|
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: true)
|
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: true)
|
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
@@ -22,29 +22,32 @@ class Importmap::Packager
|
|
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
26
|
})
|
27
27
|
|
28
28
|
case response.code
|
29
|
-
when "200"
|
30
|
-
|
31
|
-
|
29
|
+
when "200"
|
30
|
+
extract_parsed_response(response)
|
31
|
+
when "404", "401"
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
handle_failure_response(response)
|
32
35
|
end
|
33
36
|
end
|
34
37
|
|
35
|
-
def pin_for(package, url)
|
36
|
-
%(
|
38
|
+
def pin_for(package, url = nil, preloads: nil)
|
39
|
+
to = url ? %(, to: "#{url}") : ""
|
40
|
+
preload_param = preload(preloads)
|
41
|
+
|
42
|
+
%(pin "#{package}") + to + preload_param
|
37
43
|
end
|
38
44
|
|
39
|
-
def vendored_pin_for(package, url)
|
45
|
+
def vendored_pin_for(package, url, preloads = nil)
|
40
46
|
filename = package_filename(package)
|
41
47
|
version = extract_package_version_from(url)
|
48
|
+
to = "#{package}.js" != filename ? filename : nil
|
42
49
|
|
43
|
-
|
44
|
-
%(pin "#{package}" # #{version})
|
45
|
-
else
|
46
|
-
%(pin "#{package}", to: "#{filename}" # #{version})
|
47
|
-
end
|
50
|
+
pin_for(package, to, preloads: preloads) + %( # #{version})
|
48
51
|
end
|
49
52
|
|
50
53
|
def packaged?(package)
|
@@ -63,6 +66,21 @@ class Importmap::Packager
|
|
63
66
|
end
|
64
67
|
|
65
68
|
private
|
69
|
+
def preload(preloads)
|
70
|
+
case Array(preloads)
|
71
|
+
in []
|
72
|
+
""
|
73
|
+
in ["true"]
|
74
|
+
%(, preload: true)
|
75
|
+
in ["false"]
|
76
|
+
%(, preload: false)
|
77
|
+
in [string]
|
78
|
+
%(, preload: "#{string}")
|
79
|
+
else
|
80
|
+
%(, preload: #{preloads})
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
66
84
|
def post_json(body)
|
67
85
|
Net::HTTP.post(self.class.endpoint, body.to_json, "Content-Type" => "application/json")
|
68
86
|
rescue => error
|
@@ -73,8 +91,13 @@ class Importmap::Packager
|
|
73
91
|
name.to_s == "jspm" ? "jspm.io" : name.to_s
|
74
92
|
end
|
75
93
|
|
76
|
-
def
|
77
|
-
JSON.parse(response.body)
|
94
|
+
def extract_parsed_response(response)
|
95
|
+
parsed = JSON.parse(response.body)
|
96
|
+
imports = parsed.dig("map", "imports")
|
97
|
+
|
98
|
+
{
|
99
|
+
imports: imports,
|
100
|
+
}
|
78
101
|
end
|
79
102
|
|
80
103
|
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.1
|
4
|
+
version: 2.2.1
|
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.
|