importmap-plus 1.1.1 → 2.0.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/CHANGELOG.md +351 -0
- data/README.md +10 -2
- data/app/helpers/importmap/importmap_tags_helper.rb +3 -0
- data/lib/importmap/batch_resolver.rb +133 -0
- data/lib/importmap/commands.rb +333 -32
- data/lib/importmap/doctor.rb +331 -0
- data/lib/importmap/early_hints.rb +50 -0
- data/lib/importmap/engine.rb +3 -0
- data/lib/importmap/esm_run.rb +118 -0
- data/lib/importmap/graph.rb +126 -0
- data/lib/importmap/import_scanner.rb +53 -0
- data/lib/importmap/integrity.rb +25 -0
- data/lib/importmap/map.rb +38 -1
- data/lib/importmap/module_inspector.rb +200 -0
- data/lib/importmap/npm.rb +9 -0
- data/lib/importmap/package_graph.rb +299 -0
- data/lib/importmap/packager.rb +302 -110
- data/lib/importmap/provider_chain.rb +89 -0
- data/lib/importmap/vendored_graph.rb +214 -0
- data/lib/importmap/version.rb +1 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a89ba9297f9e04e1f3a892ea319d509ce9300789f648327c3b4b1cf72c0951d7
|
|
4
|
+
data.tar.gz: c088e5ab5732c9f132bfa63a60d2a7d9243ac85690670e1a040e3658fc1fb091
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 81f2d28ee6c2d724917e338451d4a0f2baef86feb1bcc5477feca8114f13213787d9164b6befc8e7e02cf24e5ec0f810e13d05195be4e27c689bc70db5fd9311
|
|
7
|
+
data.tar.gz: 3cbd693b24887409203b8f75294798c3ee4b08dec82a90eb404de2a24e7ba7f273d4babd48a72c3a1608f8f263b4804b27dcfbf624edbb69252ea8ea6b346d63
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,356 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`pin` resolves the version on the npm registry, then falls back from jspm
|
|
8
|
+
to esm.run to jsDelivr.** jspm is the default CDN and was the only one `pin`
|
|
9
|
+
asked, so a package its generator can't build — `mermaid@10.6.0` fails on a
|
|
10
|
+
cytoscape subpath, `@mui/material@5.15.0` on a module it can't find — ended
|
|
11
|
+
in `Couldn't find any packages`, and a bare `pin foo` took whatever version
|
|
12
|
+
jspm had indexed, which lags npm. A package that names no CDN of its own is
|
|
13
|
+
now asked of each in turn, and its version is settled against the registry
|
|
14
|
+
before any of them is asked:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
$ bin/importmap pin mermaid
|
|
18
|
+
Resolved "mermaid" to 10.6.0 from the npm registry
|
|
19
|
+
jspm couldn't resolve "mermaid@10.6.0" (No './dist/cytoscape.umd.js' exports subpath defined in cytoscape@3.34.3); trying esm.run
|
|
20
|
+
Pinning "mermaid" to vendor/javascript/mermaid.js via download from https://cdn.jsdelivr.net/npm/mermaid@10.6.0/+esm
|
|
21
|
+
```
|
|
22
|
+
```ruby
|
|
23
|
+
pin "mermaid" # @10.6.0 (esm.run)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The pin comment already records a CDN that isn't jspm, so `update` and
|
|
27
|
+
`pristine` stay on esm.run from then on with no new state anywhere. An
|
|
28
|
+
explicit `--from`, and a provider a pin already names, are choices somebody
|
|
29
|
+
made: those are asked once and never fall back. When no CDN has the package,
|
|
30
|
+
each one's reason is printed and the summary names all three.
|
|
31
|
+
- **A download that isn't an ES module is kept remote instead of vendored.**
|
|
32
|
+
The CDNs that serve a package's own `dist` file hand back the UMD bundle
|
|
33
|
+
plenty of packages still publish; vendored into an import map it runs and
|
|
34
|
+
exports nothing, so `import x from "pkg"` fails to link in the browser and
|
|
35
|
+
nowhere else. `pin google-libphonenumber --from jsdelivr` now keeps the pin
|
|
36
|
+
remote and records `(remote: not an ES module)`. `--vendor` downloads it
|
|
37
|
+
anyway, and the default `pin` never sees it, since jspm converts the package.
|
|
38
|
+
- **The CDN's own reason reaches the terminal.** jspm answers 401 with its
|
|
39
|
+
generator's message in the body, which `pin` discarded: `Couldn't find any
|
|
40
|
+
packages in ["mermaid@10.6.0"] on jspm` said nothing about what went wrong.
|
|
41
|
+
That message is now printed with the sentence, whichever CDN was asked.
|
|
42
|
+
|
|
43
|
+
- **`pin` keeps a package remote when its file can't stand alone, and says
|
|
44
|
+
why.** A vendored package is one file served under a digested asset path,
|
|
45
|
+
but plenty of packages ship a file that imports a sibling by relative path,
|
|
46
|
+
spawns a `Worker`, reads `import.meta.url` or fetches a `.wasm` binary —
|
|
47
|
+
every one of those 404s in the browser, and importmap-rails vendors it
|
|
48
|
+
anyway. `pin` now reads the download before writing anything to
|
|
49
|
+
`vendor/javascript`; a file that needs more than itself is pinned to its CDN
|
|
50
|
+
URL and the reason goes on the pin:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
$ bin/importmap pin fflate@0.8.2
|
|
54
|
+
Pinning "fflate" to https://ga.jspm.io/npm:fflate@0.8.2/esm/browser.js (kept remote: workers)
|
|
55
|
+
```
|
|
56
|
+
```ruby
|
|
57
|
+
pin "fflate", to: "https://ga.jspm.io/npm:fflate@0.8.2/esm/browser.js" # @0.8.2 (remote: workers)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The pin then behaves like any other remote pin — `pin` and `update`
|
|
61
|
+
re-resolve it from the same CDN and keep the reason, `pristine` skips it.
|
|
62
|
+
`pin --vendor` downloads the package anyway and records `(vendored)` on the
|
|
63
|
+
pin, so a later `update` doesn't undo the override; it also converts a pin
|
|
64
|
+
that was kept remote back to a download. Nothing an app already vendored is
|
|
65
|
+
rewritten on its own — `pristine` redownloads those pins as it always has —
|
|
66
|
+
but the next `pin` or `update` that touches one re-resolves it, and a package
|
|
67
|
+
whose file can't stand alone converts to a remote pin then. That is the fix
|
|
68
|
+
arriving, not a surprise: the vendored file it replaces was already 404ing
|
|
69
|
+
for its siblings. pdf.js is the package most apps will see this on — see
|
|
70
|
+
the upgrading page for what to expect and how `--vendor` puts it back.
|
|
71
|
+
|
|
72
|
+
- **A pin that stays remote carries a subresource-integrity hash.** A vendored
|
|
73
|
+
file is served by the app; a remote pin is fetched from a CDN on every page
|
|
74
|
+
load with nothing checking the bytes, and importmap-rails never wrote an
|
|
75
|
+
`integrity:` value for one — the hash had to be computed by hand and redone
|
|
76
|
+
on every update. `pin --remote`, and a package [kept remote] because its file
|
|
77
|
+
can't stand alone, now fetch the URL they just resolved, hash it and write it
|
|
78
|
+
with the pin:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
$ bin/importmap pin md5@2.2.0 --remote
|
|
82
|
+
Pinning "md5" to https://ga.jspm.io/npm:md5@2.2.0/md5.js (integrity sha384-+wqk6m3DPZ6mVMgVZlXnGgDjDY2skGEZ3U9tBnRHiPJXLRLKBmYkKlX2urz9T61b)
|
|
83
|
+
```
|
|
84
|
+
```ruby
|
|
85
|
+
pin "md5", to: "https://ga.jspm.io/npm:md5@2.2.0/md5.js", integrity: "sha384-+wqk6m3DPZ6mVMgVZlXnGgDjDY2skGEZ3U9tBnRHiPJXLRLKBmYkKlX2urz9T61b"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The hash is `sha384` of the bytes the CDN served, computed here rather than
|
|
89
|
+
asked of any one CDN, so jspm, esm.run, jsDelivr, unpkg, esm.sh and skypack
|
|
90
|
+
are all covered by one code path. `update` and a later `pin` fetch the new
|
|
91
|
+
URL and rewrite the hash, so a pin never carries the hash of a file it no
|
|
92
|
+
longer points at — where before an explicit hash was simply dropped.
|
|
93
|
+
`enable_integrity!` in `config/importmap.rb` is still what puts the value in
|
|
94
|
+
the import map and on the preload link; `--no-integrity` skips the fetch for
|
|
95
|
+
a run, and `integrity: false` on a pin stays off for good. Vendored downloads
|
|
96
|
+
are unaffected: `integrity: true`, the default, already computes theirs
|
|
97
|
+
through the asset pipeline.
|
|
98
|
+
|
|
99
|
+
[kept remote]: https://importmap-plus.zoolutions.llc/docs/pinning
|
|
100
|
+
|
|
101
|
+
- **`pin` vendors a package's whole file graph, so a chunked package no longer
|
|
102
|
+
needs a CDN at runtime.** A package whose entry imports a sibling by relative
|
|
103
|
+
path — `@popperjs/core`, `date-fns`, `lodash-es`, and every package built by
|
|
104
|
+
a bundler that splits chunks — could not be vendored: the browser resolves
|
|
105
|
+
`./enums.js` against a digested asset path, and neither Propshaft nor
|
|
106
|
+
Sprockets rewrites `import` statements. Those packages were [kept remote].
|
|
107
|
+
`pin` now downloads the closed set of files the entry reaches, rewrites every
|
|
108
|
+
relative specifier to a bare key, and maps the directory with one
|
|
109
|
+
`pin_all_from` line:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
$ bin/importmap pin @popperjs/core@2.11.8
|
|
113
|
+
Pinning "@popperjs/core" to vendor/javascript/@popperjs/core.js via download from https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js (with 47 sibling files)
|
|
114
|
+
```
|
|
115
|
+
```ruby
|
|
116
|
+
pin "@popperjs/core", to: "@popperjs--core.js" # @2.11.8
|
|
117
|
+
pin_all_from "vendor/javascript/@popperjs--core", under: "@popperjs/core", to: "@popperjs--core" # @2.11.8 (graph of @popperjs/core)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The entry keeps the flat file and the plain comment it always had, so
|
|
121
|
+
`update`, `outdated`, `lock` and `pristine` read it exactly as before, and a
|
|
122
|
+
`config/importmap.rb` written this way still parses under importmap-rails.
|
|
123
|
+
Bare specifiers are untouched, and a file that is another pin's own entry is
|
|
124
|
+
rewritten to that pin's key rather than copied, so the browser evaluates each
|
|
125
|
+
module once. (Two pins of one package do each carry their own copy of a chunk
|
|
126
|
+
they share; both write it under the same key, so one of the copies is what
|
|
127
|
+
every importer gets and the other is dead weight.) `unpin` takes the directory and the line with the pin,
|
|
128
|
+
`pristine` rebuilds the directory, `pin --minify` minifies every file in it,
|
|
129
|
+
and `pin --vendor` downloads the entry on its own and drops the directory and
|
|
130
|
+
line it had. A directory the import map doesn't map as one of ours is the
|
|
131
|
+
app's: `pin` says so, writes nothing rather than renaming it away, and exits
|
|
132
|
+
non-zero — as it does when a CDN fails partway through a crawl, where the pin
|
|
133
|
+
and the files it had are left exactly as they were.
|
|
134
|
+
|
|
135
|
+
Only jspm, jsDelivr and unpkg are crawled — their URLs say where a package's
|
|
136
|
+
directory ends. A graph that can't be taken over whole (a relative path that
|
|
137
|
+
climbs out of the package, a sibling the CDN hasn't got, a sibling that isn't
|
|
138
|
+
JavaScript, two files that would collapse to one key) keeps the whole package
|
|
139
|
+
remote, as does a download that also spawns a worker, reads
|
|
140
|
+
`import.meta.url`, computes an `import()` or names a `.wasm` file. A pin
|
|
141
|
+
importmap-plus had kept remote for its relative imports is converted back to
|
|
142
|
+
a download by the next `pin` or `update`, on the CDN its URL names.
|
|
143
|
+
|
|
144
|
+
Two things `pin` says out loud rather than doing quietly: a key some other
|
|
145
|
+
package's graph already maps (a directory wins over a pin, so the pin would
|
|
146
|
+
do nothing), and a second directory mapping a package one already maps at
|
|
147
|
+
another version (a file they share resolves to one of them).
|
|
148
|
+
|
|
149
|
+
- **`bin/importmap doctor` checks an app's import map and vendored files,
|
|
150
|
+
offline.** `audit` and `outdated` ask the npm registry about your packages;
|
|
151
|
+
nothing asked whether the map still matched the files in the repo. A pin
|
|
152
|
+
whose file had gone missing, a vendored file importing a bare specifier
|
|
153
|
+
nobody pinned, a vendored file still importing the siblings it was downloaded
|
|
154
|
+
beside, a CommonJS bundle a CDN handed back — each of those said so in the
|
|
155
|
+
browser and nowhere else. `doctor` boots the app and reports them:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
$ bin/importmap doctor
|
|
159
|
+
error pin "not_there" → nowhere.js: no such asset
|
|
160
|
+
error vendor/javascript/shoelace.js imports "lit/decorators.js", which isn't pinned
|
|
161
|
+
error vendor/javascript/popper.js imports "./enums.js" by relative path — run bin/importmap pin @popperjs/core to vendor its files
|
|
162
|
+
warning vendor/javascript/old-lib.js isn't pinned by anything
|
|
163
|
+
warning "@hotwired/turbo" and "@hotwired/turbo-rails" both resolve to turbo.min.js
|
|
164
|
+
3 errors, 2 warnings
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Errors exit 1, so it belongs in CI beside `audit` and `outdated`; warnings —
|
|
168
|
+
a file nothing serves, including a `.mjs` that `pin_all_from` never picks up,
|
|
169
|
+
and two keys on one file or one package vendored at two versions — leave the
|
|
170
|
+
exit status alone. It reports and never edits: `pin`, `unpin` and `pristine`
|
|
171
|
+
are what fix what it finds.
|
|
172
|
+
|
|
173
|
+
`--online` adds the one check that needs the network, fetching every remote
|
|
174
|
+
pin to report one the CDN no longer serves and an `integrity:` hash that
|
|
175
|
+
doesn't match the bytes it does serve. Every other check reads only the map,
|
|
176
|
+
the asset paths and the files on disk, so the default is fast and can't fail
|
|
177
|
+
because a CDN is having a bad afternoon.
|
|
178
|
+
|
|
179
|
+
- **`config.importmap.preload_strategy = :reachable` preloads what the entry
|
|
180
|
+
point actually reaches.** Every pin with `preload: true` gets a modulepreload
|
|
181
|
+
link on every page, so a package the app only loads with `import()` is
|
|
182
|
+
fetched up front anyway unless someone writes `preload: false` on it — and on
|
|
183
|
+
everything it depends on, and keeps that list right as the package changes.
|
|
184
|
+
An app on this gem carried the comment "imported only by apexcharts.js, which
|
|
185
|
+
is itself lazy. Without it, 1.1 MB was preloaded on every page."
|
|
186
|
+
|
|
187
|
+
The import graph already knows. `app/javascript`, `vendor/javascript` and
|
|
188
|
+
every `pin_all_from` directory are files on disk, and their `import`
|
|
189
|
+
statements name pin keys:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
# config/application.rb
|
|
193
|
+
config.importmap.preload_strategy = :reachable
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`javascript_importmap_tags "application"` now emits a link only for the pins
|
|
197
|
+
`application` reaches through **static** imports, however deep. A dynamic
|
|
198
|
+
`import()` is the lazy boundary and contributes nothing — preloading its
|
|
199
|
+
target is the deferral the app asked for, undone. A pin naming an entry point
|
|
200
|
+
(`preload: "admin"`) is preloaded whether or not the graph reaches it, which
|
|
201
|
+
is the escape hatch for a specifier no regex can see; `preload: false` stays
|
|
202
|
+
off either way.
|
|
203
|
+
|
|
204
|
+
Nothing new happens on the request path: the files are the ones the asset
|
|
205
|
+
pipeline already serves, read once per import map cache generation and
|
|
206
|
+
dropped by the same sweeper that drops the rendered map when a `.js` file
|
|
207
|
+
changes. The default `:all` — upstream's behaviour, one link per
|
|
208
|
+
`preload: true` pin — is unchanged and never opens a file.
|
|
209
|
+
|
|
210
|
+
- **`javascript_importmap_tags` sends its modulepreload links as 103 Early
|
|
211
|
+
Hints.** A modulepreload link is markup, so the browser can only act on it
|
|
212
|
+
once the HTML has streamed far enough to be parsed — the whole module graph
|
|
213
|
+
waits on the response. The same list now goes out ahead of it as a `Link`
|
|
214
|
+
header, and the fetches start while the app is still rendering:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
HTTP/1.1 103 Early Hints
|
|
218
|
+
Link: </assets/application-abc.js>; rel=modulepreload, </assets/@hotwired--stimulus-def.js>; rel=modulepreload
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Exactly the modules the tags preload, so `preload: false`, an entry point's
|
|
222
|
+
own list and `preload_strategy = :reachable` all narrow the hinted set with
|
|
223
|
+
the tags. No `integrity` parameter: browsers don't honour one on a `Link`
|
|
224
|
+
header, and the tag in the body still carries it.
|
|
225
|
+
|
|
226
|
+
This is what Rails' own `javascript_include_tag` and `stylesheet_link_tag`
|
|
227
|
+
already do, and it needs what they need — a server that puts
|
|
228
|
+
`rack.early_hints` in the env, which Puma does with `early_hints true`. On a
|
|
229
|
+
server without it, `send_early_hints` is a no-op and nothing changes. Off
|
|
230
|
+
with `config.importmap.early_hints = false`.
|
|
231
|
+
|
|
232
|
+
### Changed
|
|
233
|
+
|
|
234
|
+
- **Everything this gem knows about esm.run lives in `Importmap::EsmRun`.**
|
|
235
|
+
The provider name, the bundle URL shapes, the rewrite that turns a bundle's
|
|
236
|
+
`/npm/dep@ver/+esm` imports into bare specifiers and the jsDelivr version
|
|
237
|
+
lookup were nine things in `Importmap::Packager`, which had grown to the
|
|
238
|
+
800-line ceiling with nowhere to put the next addition. Behaviour is
|
|
239
|
+
unchanged and no documented setting moved: `Importmap::Packager.esm_run_resolver`
|
|
240
|
+
still reads and writes the resolver, now on the new class. Only the `:nodoc:`
|
|
241
|
+
constants `Packager::ESM_RUN_*` are gone, as `Importmap::EsmRun::PROVIDER`,
|
|
242
|
+
`::CDN`, `::URL_REGEXP` and `::IMPORT_REGEXP`.
|
|
243
|
+
|
|
244
|
+
### Fixed
|
|
245
|
+
|
|
246
|
+
- **One package a CDN can't resolve no longer blocks — or silently moves — the
|
|
247
|
+
rest of the batch.** ([#32](https://github.com/zoolutions/importmap-plus/issues/32))
|
|
248
|
+
A CDN answers a batch of packages as a whole, so `bin/importmap update` with
|
|
249
|
+
three outdated packages reported `Couldn't find any packages in
|
|
250
|
+
["cheap-ruler", "mapbox-gl", "mermaid"] on jspm`, updated nothing and exited
|
|
251
|
+
0, because jspm's generator can't build `mermaid`. Commenting out the one
|
|
252
|
+
package let the other two through. A refused batch of more than one package
|
|
253
|
+
is now asked for one package at a time, along the path each would have taken
|
|
254
|
+
alone:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
$ bin/importmap pin md5@2.2.0 mermaid@10.6.0
|
|
258
|
+
jspm couldn't resolve "md5@2.2.0", "mermaid@10.6.0" (No './dist/cytoscape.umd.js' exports subpath defined in cytoscape@3.34.3); asking for each on its own
|
|
259
|
+
Pinning "md5" to vendor/javascript/md5.js via download from https://ga.jspm.io/npm:md5@2.2.0/md5.js
|
|
260
|
+
jspm couldn't resolve "mermaid@10.6.0" (No './dist/cytoscape.umd.js' exports subpath defined in cytoscape@3.34.3); trying esm.run
|
|
261
|
+
Pinning "mermaid" to vendor/javascript/mermaid.js via download from https://cdn.jsdelivr.net/npm/mermaid@10.6.0/+esm
|
|
262
|
+
```
|
|
263
|
+
```ruby
|
|
264
|
+
pin "md5" # @2.2.0
|
|
265
|
+
pin "mermaid" # @10.6.0 (esm.run)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Only the package the CDN refused travels the rest of the chain, so a healthy
|
|
269
|
+
package is never re-pinned from another CDN — and never has its provenance
|
|
270
|
+
comment rewritten — because a sibling failed. An explicit `--from`, or a
|
|
271
|
+
provider a pin records, still asks that one CDN and only that one, now once
|
|
272
|
+
per package. The batch stays the fast path: it is split only when it is
|
|
273
|
+
refused and holds more than one package.
|
|
274
|
+
|
|
275
|
+
`pin`, `update` and `pristine` now **exit 1** when a package was left
|
|
276
|
+
unresolved, so `bin/importmap update && git commit` can no longer commit an
|
|
277
|
+
import map that quietly missed a package. The packages that did resolve are
|
|
278
|
+
still written.
|
|
279
|
+
- **A CDN that fails mid-crawl leaves the pin alone.** Vendoring a graph makes
|
|
280
|
+
one request per file — 250 of them for `date-fns` — so a 503 that outlives
|
|
281
|
+
the retries is far likelier than it was for a single download. `pin` and
|
|
282
|
+
`pristine` report it (`Skipping "date-fns": Unexpected response code (503)`)
|
|
283
|
+
and change nothing, rather than taking it for "this package can't be
|
|
284
|
+
vendored" and converting a working pin to a remote one, which would delete
|
|
285
|
+
the very files that make it work.
|
|
286
|
+
- **`pristine` reports a package it can't restore and carries on.** It is the
|
|
287
|
+
repair command, and a pin whose graph the CDN no longer serves the way the
|
|
288
|
+
pin describes now raises where nothing used to — unrescued, that ended the
|
|
289
|
+
whole run with a backtrace and left every package after it unrestored. Each
|
|
290
|
+
one that fails is reported (`Couldn't restore "pdfjs-dist": it can't be
|
|
291
|
+
vendored as a single file (workers)`), the rest are restored, and the command
|
|
292
|
+
exits non-zero to say it didn't do all of it — as it does when a dependency
|
|
293
|
+
of an esm.run bundle, pinned on the way, had to be skipped.
|
|
294
|
+
- **A download the CDN encoded in a way Net::HTTP can't undo is fetched
|
|
295
|
+
again.** jspm answers some files with `content-encoding: br` whatever the
|
|
296
|
+
request advertises, and Net::HTTP decompresses gzip and deflate only:
|
|
297
|
+
`@popperjs/core@2.11.8/lib/utils/computeAutoPlacement.js` arrived as brotli
|
|
298
|
+
bytes, which read as invalid UTF-8 and took the source inspection down with
|
|
299
|
+
`ArgumentError: invalid byte sequence in UTF-8`. `pin` now repeats that one
|
|
300
|
+
request asking for an unencoded body. It doesn't ask up front: supplying an
|
|
301
|
+
`Accept-Encoding` at all stops Net::HTTP decoding the gzip it does
|
|
302
|
+
understand. Inherited from importmap-rails, which downloads the same way.
|
|
303
|
+
|
|
304
|
+
- **`Importmap::Packager::ServiceError` is a class again.** It was assigned
|
|
305
|
+
`Error.new(Error)` — an *instance* — so `rescue Packager::ServiceError`
|
|
306
|
+
raised `TypeError: class or module required for rescue clause`, and every
|
|
307
|
+
jspm service error arrived as a plain `Packager::Error`. Inherited from
|
|
308
|
+
importmap-rails, where it is still the case.
|
|
309
|
+
- **A failed download no longer deletes the file an app already has.**
|
|
310
|
+
`pin` and `pristine` removed `vendor/javascript/<package>.js` before
|
|
311
|
+
fetching, so a CDN that answered 500 — or, now, a file that can't be
|
|
312
|
+
vendored — left the app with no file at all. The existing file is replaced
|
|
313
|
+
only once the new one has arrived and been found fit to serve.
|
|
314
|
+
- **`update <package>` moves every pin of that package, not just the bare
|
|
315
|
+
one.** An app with `pin "pdfjs-dist"` and
|
|
316
|
+
`pin "pdfjs-dist/build/pdf.worker.min.mjs"` ran `update pdfjs-dist` and got
|
|
317
|
+
a new main file next to a worker still at the old version — the two are
|
|
318
|
+
built together and don't tolerate that. A package name now means every
|
|
319
|
+
key that carries it, the same set `outdated` reports and a bare `update`
|
|
320
|
+
moves; `update pdfjs-dist/build/pdf.worker.min.mjs` still means that one
|
|
321
|
+
key.
|
|
322
|
+
- **A subpath pin without a CDN in its comment resolves from the CDN its
|
|
323
|
+
package's pin names.** The worker pin above, written before provenance
|
|
324
|
+
existed, was sent to jspm — which can't resolve pdf.js at all — and
|
|
325
|
+
reported "Couldn't find any packages" on every update while the main pin
|
|
326
|
+
moved on from jsdelivr. Siblings pinned together come from the same place;
|
|
327
|
+
the next update records it on the pin. A pin answers for itself first — its
|
|
328
|
+
own comment, then its own `to:` URL — and only then does its package answer
|
|
329
|
+
for it, whether that pin is vendored, recording the CDN in its comment, or
|
|
330
|
+
remote, carrying it in the URL with no comment at all.
|
|
331
|
+
|
|
332
|
+
- **An array `preload:` survives a rewrite however it is quoted, and
|
|
333
|
+
`preload: []` stays `preload: []`.** The option was read back through
|
|
334
|
+
`JSON.parse`, so `pin 'md5', preload: ['admin']` — single quotes being a
|
|
335
|
+
supported pin shape everywhere else in this gem — took down every command
|
|
336
|
+
that reads a pin with `JSON::ParserError: unexpected character`. And
|
|
337
|
+
`preload: []` matched nothing at all, so `pin`, `update` and `pristine`
|
|
338
|
+
dropped it and quietly restored the `preload: true` default on a package the
|
|
339
|
+
app had asked to preload for no entry point. `config/importmap.rb` is Ruby,
|
|
340
|
+
not JSON: the entry points are scanned out of the literal now, and an empty
|
|
341
|
+
array is written back as one.
|
|
342
|
+
|
|
343
|
+
- **`pin --vendor` leaves a pin to a custom URL alone.** `--vendor` is meant to
|
|
344
|
+
override the check that refuses a download, not the URL an app chose to pin,
|
|
345
|
+
but it reached the vendoring path before the branch that skips custom URLs
|
|
346
|
+
could run: `bin/importmap pin md5 --vendor` against
|
|
347
|
+
`pin "md5", to: "https://cdn.example.com/md5.js"` downloaded md5 from
|
|
348
|
+
whichever CDN the spec resolved to and replaced the line with
|
|
349
|
+
`pin "md5" # @2.3.0 (vendored)`, without a word about the URL it had just
|
|
350
|
+
dropped. It now reports the skip the way a plain `pin` does and names the
|
|
351
|
+
flag in it. Moving such a pin on purpose is still `--from`, which re-resolves
|
|
352
|
+
it from the CDN you name.
|
|
353
|
+
|
|
3
354
|
## 1.1.0
|
|
4
355
|
|
|
5
356
|
### Added
|
data/README.md
CHANGED
|
@@ -25,8 +25,12 @@ An app without an import map yet:
|
|
|
25
25
|
| `--from esm.run` | Vendors jsDelivr's one-file bundle, rewrites its imports to bare specifiers, and pins the dependencies it needs. |
|
|
26
26
|
| `pin --lock`, `lock`, `unlock` | Holds a package at a version. `update`, `pristine` and `pin` leave it there until you unlock it or pass `--force`. |
|
|
27
27
|
| `update [PACKAGES] --all --force` | Update by name, or everything explicitly; `--force` moves locked packages and re-locks them. |
|
|
28
|
-
|
|
|
29
|
-
|
|
|
28
|
+
| Multi-file packages | A package whose entry imports siblings by relative path is vendored with its whole file graph, mapped by one `pin_all_from` line. One that needs more than files can give it — a worker, `import.meta.url`, a `.wasm` — stays on its CDN with the reason on the pin. |
|
|
29
|
+
| Registry-latest, then CDN fallback | A bare name is resolved on the npm registry, then asked of jspm, esm.run and jsDelivr in turn until one of them has it. `--from` disables the fallback. |
|
|
30
|
+
| `doctor` | Checks the import map against the files that are actually there — offline, reporting only, non-zero on an error — so CI catches a 404 that otherwise shows up on one page in the browser. |
|
|
31
|
+
| Provenance | The pin comment records the CDN, minification, lock and why a package was kept remote — `pin "luxon" # @3.7.2 (esm.run, minified, locked)` — so nothing silently drifts back to jspm. |
|
|
32
|
+
| Remote pins stay remote | A pin with a CDN URL is re-resolved from that CDN and carries a subresource-integrity hash of the bytes that were resolved; `preload:` and a boolean `integrity:` survive every rewrite; a custom URL is left alone. |
|
|
33
|
+
| Preload what the page reaches | `config.importmap.preload_strategy = :reachable` preloads what the entry point actually imports rather than every preloaded pin, and `config.importmap.early_hints` sends those links as a 103 Early Hints response. Both opt-in. |
|
|
30
34
|
| Requests retry | A reset connection, a timeout or a 429/5xx is tried three times with a growing pause before the command gives up. |
|
|
31
35
|
|
|
32
36
|
```bash
|
|
@@ -34,12 +38,16 @@ An app without an import map yet:
|
|
|
34
38
|
./bin/importmap pin @hotwired/stimulus@3.2.2 --lock
|
|
35
39
|
./bin/importmap update stimulus-use
|
|
36
40
|
./bin/importmap outdated
|
|
41
|
+
./bin/importmap doctor
|
|
37
42
|
```
|
|
38
43
|
|
|
39
44
|
```ruby
|
|
40
45
|
# config/importmap.rb
|
|
41
46
|
pin "luxon" # @3.7.2 (esm.run, minified)
|
|
42
47
|
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2 (locked)
|
|
48
|
+
pin "@popperjs/core", to: "@popperjs--core.js" # @2.11.8
|
|
49
|
+
pin_all_from "vendor/javascript/@popperjs--core", under: "@popperjs/core", to: "@popperjs--core" # @2.11.8 (graph of @popperjs/core)
|
|
50
|
+
pin "fflate", to: "https://ga.jspm.io/npm:fflate@0.8.2/esm/browser.js" # @0.8.2 (remote: workers)
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
The full guide — every command, option and behaviour, plus importmap-rails' own documentation for the parts that are unchanged — is at [importmap-plus.zoolutions.llc](https://importmap-plus.zoolutions.llc).
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module Importmap::ImportmapTagsHelper
|
|
2
2
|
# Setup all script tags needed to use an importmap-powered entrypoint (which defaults to application.js)
|
|
3
3
|
def javascript_importmap_tags(entry_point = "application", importmap: Rails.application.importmap)
|
|
4
|
+
preloaded = importmap.preloaded_module_packages(resolver: self, entry_point:, cache_key: entry_point)
|
|
5
|
+
Importmap::EarlyHints.send_modulepreload_links(self, preloaded)
|
|
6
|
+
|
|
4
7
|
safe_join [
|
|
5
8
|
javascript_inline_importmap_tag(importmap.to_json(resolver: self)),
|
|
6
9
|
javascript_importmap_module_preload_tags(importmap, entry_point:),
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
require "importmap/provider_chain"
|
|
2
|
+
|
|
3
|
+
# Resolves a group of specs against a CDN, batch first and then one at a time.
|
|
4
|
+
#
|
|
5
|
+
# The batch is the fast path and stays: one round trip, and jspm resolves the
|
|
6
|
+
# specs' shared dependencies as a single graph, which is what upstream does.
|
|
7
|
+
# But a CDN answers a batch as a whole — one spec its generator can't build is
|
|
8
|
+
# "no" for all of them — so a refused batch of more than one spec says nothing
|
|
9
|
+
# about the specs in it. Each is then asked on its own, along the path it would
|
|
10
|
+
# have taken alone: through the chain when the group named no CDN, of the same
|
|
11
|
+
# provider once when it did. A package that was always going to resolve is
|
|
12
|
+
# pinned from the CDN it was always going to be pinned from, whatever its
|
|
13
|
+
# siblings did.
|
|
14
|
+
#
|
|
15
|
+
# Specs nobody could resolve are collected rather than raised: each pin is
|
|
16
|
+
# independent of the others, so the rest still land and the exit code says the
|
|
17
|
+
# command didn't do all it was asked.
|
|
18
|
+
class Importmap::BatchResolver
|
|
19
|
+
# The specs no CDN answered for, in the order they were asked.
|
|
20
|
+
attr_reader :unresolved
|
|
21
|
+
|
|
22
|
+
# +on_miss+ is the command's own "couldn't find" reporter, so the sentence a
|
|
23
|
+
# single spec gets is written in exactly one place.
|
|
24
|
+
def initialize(packager, on_miss:, chain: Importmap::ProviderChain.new)
|
|
25
|
+
@packager = packager
|
|
26
|
+
@on_miss = on_miss
|
|
27
|
+
@chain = chain
|
|
28
|
+
@unresolved = []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Yields [ package, url ] for everything that resolved, as it resolves, so a
|
|
32
|
+
# spec that fails later can't discard the work of one that succeeded earlier.
|
|
33
|
+
def each_import(specs, env:, from:, fallback:, &block)
|
|
34
|
+
@named = specs.map { |spec| @packager.package_key_for(spec) }
|
|
35
|
+
@seen = {}
|
|
36
|
+
|
|
37
|
+
if specs.one?
|
|
38
|
+
resolve_one(specs.first, env: env, from: from, fallback: fallback, &block)
|
|
39
|
+
elsif (response = resolve_batch(specs, env: env, from: from, fallback: fallback))
|
|
40
|
+
emit(specs, response, &block)
|
|
41
|
+
else
|
|
42
|
+
specs.each { |spec| resolve_one(spec, env: env, from: from, fallback: fallback, &block) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
# The batch a group with no CDN of its own sends goes to jspm alone. Handing
|
|
48
|
+
# the whole list to the next CDN is what moved a healthy package's provenance
|
|
49
|
+
# when a sibling failed; the specs travel the rest of the chain separately.
|
|
50
|
+
def resolve_batch(specs, env:, from:, fallback:)
|
|
51
|
+
provider = fallback ? Importmap::ProviderChain::DEFAULT : from
|
|
52
|
+
error = nil
|
|
53
|
+
|
|
54
|
+
response =
|
|
55
|
+
begin
|
|
56
|
+
@packager.import(*specs, env: env, from: provider)
|
|
57
|
+
rescue Importmap::Packager::Error => raised
|
|
58
|
+
error = raised
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return response if response
|
|
63
|
+
|
|
64
|
+
report_split(specs, provider, error&.message || @packager.last_import_error, fallback)
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def report_split(specs, provider, reason, fallback)
|
|
69
|
+
detail = Importmap::ProviderChain.tidy_reason(reason)
|
|
70
|
+
miss = if fallback
|
|
71
|
+
%(#{provider} couldn't resolve #{quoted(specs)})
|
|
72
|
+
else
|
|
73
|
+
%(Couldn't find any packages in #{specs.inspect} on #{provider})
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts miss + (detail ? %( (#{detail})) : "") + "; asking for each on its own"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def resolve_one(spec, env:, from:, fallback:, &block)
|
|
80
|
+
source = fallback ? Importmap::ProviderChain.to_sentence : from
|
|
81
|
+
|
|
82
|
+
if (response = request_one(spec, env: env, from: from, fallback: fallback))
|
|
83
|
+
emit([ spec ], response, &block)
|
|
84
|
+
else
|
|
85
|
+
# A chain that came up empty has already said why each CDN couldn't.
|
|
86
|
+
@on_miss.call([ spec ], source, reason: fallback ? nil : @packager.last_import_error)
|
|
87
|
+
@unresolved << spec
|
|
88
|
+
end
|
|
89
|
+
rescue Importmap::Packager::Error => error
|
|
90
|
+
puts %(Couldn't resolve "#{spec}" from #{source}: #{error.message})
|
|
91
|
+
@unresolved << spec
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def request_one(spec, env:, from:, fallback:)
|
|
95
|
+
if fallback
|
|
96
|
+
@chain.resolve(@packager, [ spec ], env: env) { |_provider, response| response }
|
|
97
|
+
else
|
|
98
|
+
@packager.import(spec, env: env, from: from)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Split responses overlap: each carries the dependencies of its own spec.
|
|
103
|
+
# A spec the user named answers for itself, so a sibling's response naming
|
|
104
|
+
# it is a dependency edge and not the answer asked for. Anything else is
|
|
105
|
+
# taken from the first response that carried it, because rewriting it to a
|
|
106
|
+
# second URL would move a package the user never mentioned.
|
|
107
|
+
def emit(specs, response, &block)
|
|
108
|
+
asked_for = specs.map { |spec| @packager.package_key_for(spec) }
|
|
109
|
+
|
|
110
|
+
response[:imports].each do |package, url|
|
|
111
|
+
next if @named.include?(package) && !asked_for.include?(package)
|
|
112
|
+
|
|
113
|
+
if (kept = @seen[package])
|
|
114
|
+
report_kept(package, kept, url, specs.first) unless kept == url
|
|
115
|
+
else
|
|
116
|
+
@seen[package] = url
|
|
117
|
+
block.call(package, url)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def report_kept(package, kept, url, spec)
|
|
123
|
+
puts %(Keeping "#{package}" at #{version(kept)} ("#{spec}" resolved it to #{version(url)}))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def version(url)
|
|
127
|
+
@packager.extract_package_version_from(url) || url
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def quoted(specs)
|
|
131
|
+
specs.map { |spec| %("#{spec}") }.join(", ")
|
|
132
|
+
end
|
|
133
|
+
end
|