importmap-plus 1.0.0 → 1.1.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/CHANGELOG.md +69 -0
- data/README.md +33 -427
- data/lib/importmap/commands.rb +228 -26
- data/lib/importmap/npm.rb +28 -14
- data/lib/importmap/packager.rb +142 -19
- data/lib/importmap/version.rb +1 -1
- metadata +7 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c325b5de05deaf38ca9ed4f671d5c676b36f726ab4e3079e616dc964bdac00b8
|
|
4
|
+
data.tar.gz: c11ca96640558bf7e4f3e294139ed80b9a893239811e4afa879024268401c90d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a69a963a3506522598a3b0cecb6c8b95c7e4dd275a2fe1cf748f7f5b181bbae62f490f22dc066b42adaf0071dc54e24d940309b2b31ee4b971365aec89ff8a68
|
|
7
|
+
data.tar.gz: 3ff45f8c8e2ca16ef2f26d4d18438a870bcf50a12c63f29b44a58bf334a9404e3009f4544201f81d1fe7e2f862cad5e15858af5b05a14d7fb97894f2ef972638
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Version locks.** `bin/importmap pin luxon@3.7.2 --lock`, or
|
|
8
|
+
`bin/importmap lock luxon` for a package already pinned, records the lock
|
|
9
|
+
in the version comment — `pin "luxon" # @3.7.2 (locked)` — and `update`
|
|
10
|
+
and a plain `pin` skip the package from then on, saying so. A remote pin
|
|
11
|
+
gains the comment too, carrying the version from its URL. `pin --force`
|
|
12
|
+
moves a locked package and keeps the lock at the new version; `--no-lock`
|
|
13
|
+
drops it; `bin/importmap unlock luxon` removes it without touching the
|
|
14
|
+
file. `pristine` still redownloads a locked vendored package, at the
|
|
15
|
+
locked version; remote pins are skipped as always. Only the packages
|
|
16
|
+
named on the command line are locked, never the dependencies a CDN
|
|
17
|
+
resolves with them, and a locked dependency stays where it is when the
|
|
18
|
+
package that needs it is pinned or updated.
|
|
19
|
+
- **`update` takes package names, `--all` and `--force`.**
|
|
20
|
+
`bin/importmap update luxon stimulus-use` re-pins just those, asking the
|
|
21
|
+
registry about them alone; `update --all` says explicitly what a bare
|
|
22
|
+
`update` has always done. A named package that is up to date, or has no
|
|
23
|
+
version to compare, is reported; a name with no pin stops the command
|
|
24
|
+
before anything is touched. `--force` updates locked packages too and
|
|
25
|
+
keeps each lock at the new version.
|
|
26
|
+
- **`outdated` shows locks.** A new Locked column marks packages held at
|
|
27
|
+
their version, and the command exits 1 only when an unlocked package is
|
|
28
|
+
outdated, so CI stays green for the versions the app chose.
|
|
29
|
+
- **`integrity: true` and `integrity: false` survive a rewrite.** An
|
|
30
|
+
`update`, `pristine` or `pin` used to drop the option; only an integrity
|
|
31
|
+
hash, which belongs to the old file, is still removed when the URL changes.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`update` no longer re-pins a package the registry couldn't be checked
|
|
36
|
+
for.** A package whose registry lookup came back unusable has no latest
|
|
37
|
+
version, so nothing established that it moved — but `update` re-pinned it
|
|
38
|
+
anyway, letting a bad answer re-resolve the pin against the CDN and carry
|
|
39
|
+
it to a version nobody asked for. Those packages are now reported —
|
|
40
|
+
`Couldn't check "md5": Unexpected error response 500: …` — and left where
|
|
41
|
+
they are; every other package still updates, and the command exits 1.
|
|
42
|
+
Inherited from importmap-rails, where a bare `update` has always behaved
|
|
43
|
+
this way.
|
|
44
|
+
- **`update` re-pins a subpath pin instead of appending a bare one.** The
|
|
45
|
+
registry answers about `photoswipe`, the import map pins
|
|
46
|
+
`photoswipe/lightbox`, and a bare `update` or `update --all` used to re-pin
|
|
47
|
+
the name it was answered with: the subpath pin stayed at its old version and
|
|
48
|
+
a `pin "photoswipe"` was appended beside it — vendored, since a fresh pin
|
|
49
|
+
has no provenance to say otherwise. Every key carrying an outdated package
|
|
50
|
+
is now re-pinned, and a package pinned under several keys moves all of them.
|
|
51
|
+
Only pins that declare a version take part, the same ones `outdated`
|
|
52
|
+
reports on, so an app file pinned under a package's namespace —
|
|
53
|
+
`pin "md5/helpers", to: "md5/helpers.js"` — is left alone. Inherited from
|
|
54
|
+
importmap-rails; `update photoswipe/lightbox` was fixed for the named form
|
|
55
|
+
in 1.1.0.
|
|
56
|
+
- **A remote subpath pin is re-resolved from the CDN it is on.** Moving
|
|
57
|
+
`pin "photoswipe/lightbox", to: "https://cdn.jsdelivr.net/npm/photoswipe@5.3.0/…"`
|
|
58
|
+
back onto jsDelivr asked for `photoswipe/lightbox@5.4.4`, a path no CDN
|
|
59
|
+
has, so `update` gave up with `Keeping "photoswipe/lightbox" pinned to …
|
|
60
|
+
(couldn't resolve it from jsdelivr)` and the pin never moved. The version
|
|
61
|
+
now goes where a CDN expects it, ahead of the subpath —
|
|
62
|
+
`photoswipe@5.4.4/lightbox`.
|
|
63
|
+
- **A registry that won't answer for one package no longer ends the run.**
|
|
64
|
+
A 404, a 5xx or a connection that kept resetting used to escape
|
|
65
|
+
`outdated_packages` once the retries were spent, so `outdated` and
|
|
66
|
+
`update` died with a backtrace and checked nothing else. The failure is
|
|
67
|
+
now recorded against that package alone — `outdated` prints the reason in
|
|
68
|
+
its Latest column, which is what the column was always for — and every
|
|
69
|
+
other package is still checked. `audit` is unchanged: a registry it can't
|
|
70
|
+
reach still fails the command outright.
|
|
71
|
+
|
|
3
72
|
## 1.0.0
|
|
4
73
|
|
|
5
74
|
First release of importmap-plus, a drop-in replacement for
|
data/README.md
CHANGED
|
@@ -1,457 +1,63 @@
|
|
|
1
1
|
# importmap-plus
|
|
2
2
|
|
|
3
|
-
A drop-in replacement for [importmap-rails](https://github.com/rails/importmap-rails) with
|
|
3
|
+
A drop-in replacement for [importmap-rails](https://github.com/rails/importmap-rails) with vendoring that keeps its promises: `bin/importmap pin --minify`, `--from esm.run` for jsDelivr's bundled builds, `--lock` to hold a package at a version, `update` by package name, and a pin comment that remembers where each package came from so later updates respect it. Everything else is importmap-rails, constants included, so an app switches by changing one line in its `Gemfile`.
|
|
4
4
|
|
|
5
|
-
**
|
|
6
|
-
|
|
7
|
-
See [what this adds](#loading-bundles-from-esmrun) below; the rest of this README is importmap-rails' own, which this gem tracks at the version in `Importmap::UPSTREAM_VERSION`.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
[Import maps](https://github.com/WICG/import-maps) let you import JavaScript modules using logical names that map to versioned/digested files – directly from the browser. So you can [build modern JavaScript applications using JavaScript libraries made for ES modules (ESM) without the need for transpiling or bundling](https://world.hey.com/dhh/modern-web-apps-without-javascript-bundling-or-transpiling-a20f2755). This frees you from needing Webpack, Yarn, npm, or any other part of the JavaScript toolchain. All you need is the asset pipeline that's already included in Rails.
|
|
12
|
-
|
|
13
|
-
With this approach you'll ship many small JavaScript files instead of one big JavaScript file. Thanks to HTTP/2 that no longer carries a material performance penalty during the initial transport, and in fact offers substantial benefits over the long run due to better caching dynamics. Whereas before any change to any JavaScript file included in your big bundle would invalidate the cache for the whole bundle, now only the cache for that single file is invalidated.
|
|
14
|
-
|
|
15
|
-
[Import maps are supported natively in all major, modern browsers](https://caniuse.com/?search=importmap). If you need to work with legacy browsers without native support, you can explore using [the shim available](https://github.com/guybedford/es-module-shims).
|
|
5
|
+
**Documentation: [importmap-plus.zoolutions.llc](https://importmap-plus.zoolutions.llc)** — also as [llms.txt](https://importmap-plus.zoolutions.llc/llms.txt) and a read-only MCP endpoint at `https://importmap-plus.zoolutions.llc/mcp`.
|
|
16
6
|
|
|
7
|
+
**Install this gem or importmap-rails, never both** — they define the same `Importmap::` constants and the same engine.
|
|
17
8
|
|
|
18
9
|
## Installation
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
1. Replace `gem "importmap-rails"` with `gem "importmap-plus"` in your `Gemfile`, or run `./bin/bundle add importmap-plus` in an app that has neither
|
|
23
|
-
2. Run `./bin/rails importmap:install` if the app has no `config/importmap.rb` yet
|
|
24
|
-
|
|
25
|
-
An app already on importmap-rails needs nothing else: the pins, the `config/importmap.rb`, the helpers and the `Importmap::` constants are unchanged.
|
|
26
|
-
|
|
27
|
-
Note: In order to use JavaScript from Rails frameworks like Action Cable, Action Text, and Active Storage, you must be running Rails 7.0+. This was the first version that shipped with ESM compatible builds of these libraries.
|
|
28
|
-
|
|
29
|
-
You can pin those libraries manually by relying on the compiled versions included in Rails like this:
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
pin "@rails/actioncable", to: "actioncable.esm.js"
|
|
33
|
-
pin "@rails/activestorage", to: "activestorage.esm.js"
|
|
34
|
-
pin "@rails/actiontext", to: "actiontext.esm.js"
|
|
35
|
-
pin "trix"
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## How do importmaps work?
|
|
39
|
-
|
|
40
|
-
At their core, importmaps are essentially a string substitution for what are referred to as "bare module specifiers". A "bare module specifier" looks like this: `import React from "react"`. This is not compatible with the ES Module loader spec. Instead, to be ESM compatible, you must provide 1 of the 3 following types of specifiers:
|
|
41
|
-
|
|
42
|
-
- Absolute path:
|
|
43
|
-
```js
|
|
44
|
-
import React from "/Users/DHH/projects/basecamp/node_modules/react"
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
- Relative path:
|
|
48
|
-
```js
|
|
49
|
-
import React from "./node_modules/react"
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
- HTTP path:
|
|
53
|
-
```js
|
|
54
|
-
import React from "https://ga.jspm.io/npm:react@17.0.1/index.js"
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Importmap-rails provides a clean API for mapping "bare module specifiers" like `"react"`
|
|
58
|
-
to 1 of the 3 viable ways of loading ES Module javascript packages.
|
|
59
|
-
|
|
60
|
-
For example:
|
|
61
|
-
|
|
62
|
-
```rb
|
|
63
|
-
# config/importmap.rb
|
|
64
|
-
pin "react", to: "https://ga.jspm.io/npm:react@17.0.2/index.js"
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
means "every time you see `import React from "react"`
|
|
68
|
-
change it to `import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"`"
|
|
69
|
-
|
|
70
|
-
```js
|
|
71
|
-
import React from "react"
|
|
72
|
-
// => import React from "https://ga.jspm.io/npm:react@17.0.2/index.js"
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Usage
|
|
76
|
-
|
|
77
|
-
The import map is setup through `Rails.application.importmap` via the configuration in `config/importmap.rb`. This file is automatically reloaded in development upon changes, but note that you must restart the server if you remove pins and need them gone from the rendered importmap or list of preloads.
|
|
78
|
-
|
|
79
|
-
This import map is inlined in the `<head>` of your application layout using `<%= javascript_importmap_tags %>`, which will setup the JSON configuration inside a `<script type="importmap">` tag. Then the application entrypoint is imported via `<script type="module">import "application"</script>`. That logical entrypoint, `application`, is mapped in the importmap script tag to the file `app/javascript/application.js`.
|
|
80
|
-
|
|
81
|
-
It's in `app/javascript/application.js` you setup your application by importing any of the modules that have been defined in the import map. You can use the full ESM functionality of importing any particular export of the modules or everything.
|
|
82
|
-
|
|
83
|
-
It makes sense to use logical names that match the package names used by npm, such that if you later want to start transpiling or bundling your code, you won't have to change any module imports.
|
|
84
|
-
|
|
85
|
-
### Local modules
|
|
86
|
-
|
|
87
|
-
If you want to import local js module files from `app/javascript/src` or other sub-folders of `app/javascript` (such as `channels`), you must pin these to be able to import them. You can use `pin_all_from` to pick all files in a specific folder, so you don't have to `pin` each module individually.
|
|
88
|
-
|
|
89
|
-
```rb
|
|
90
|
-
# config/importmap.rb
|
|
91
|
-
pin_all_from 'app/javascript/src', under: 'src', to: 'src'
|
|
92
|
-
|
|
93
|
-
# With automatic integrity calculation for enhanced security
|
|
94
|
-
enable_integrity!
|
|
95
|
-
pin_all_from 'app/javascript/controllers', under: 'controllers', integrity: true
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
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.
|
|
99
|
-
|
|
100
|
-
The `enable_integrity!` call enables integrity calculation globally, and `integrity: true` automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
|
|
101
|
-
|
|
102
|
-
Allows you to:
|
|
103
|
-
|
|
104
|
-
```js
|
|
105
|
-
// app/javascript/application.js
|
|
106
|
-
import { ExampleFunction } from 'src/example_function'
|
|
107
|
-
```
|
|
108
|
-
Which imports the function from `app/javascript/src/example_function.js`.
|
|
109
|
-
|
|
110
|
-
Note: Sprockets used to serve assets (albeit without filename digests) it couldn't find from the `app/javascripts` folder with logical relative paths, meaning pinning local files wasn't needed. Propshaft doesn't have this fallback, so when you use Propshaft you have to pin your local modules.
|
|
111
|
-
|
|
112
|
-
## Using npm packages via JavaScript CDNs
|
|
113
|
-
|
|
114
|
-
Importmap for Rails downloads and vendors your npm package dependencies via JavaScript CDNs that provide pre-compiled distribution versions.
|
|
115
|
-
|
|
116
|
-
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.
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
./bin/importmap pin react
|
|
120
|
-
Pinning "react" to vendor/javascript/react.js via download from https://ga.jspm.io/npm:react@19.1.0/index.js
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
This will produce a pin in your `config/importmap.rb` like so:
|
|
124
|
-
|
|
125
|
-
```ruby
|
|
126
|
-
pin "react" # @19.1.0
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Other CDNs like [unpkg.com](https://unpkg.com) and [jsdelivr.com](https://www.jsdelivr.com) can be specified with `--from`:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
./bin/importmap pin react --from unpkg
|
|
133
|
-
Pinning "react" to vendor/javascript/react.js via download from https://unpkg.com/react@19.1.0/index.js
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
```bash
|
|
137
|
-
./bin/importmap pin react --from jsdelivr
|
|
138
|
-
Pinning "react" to vendor/javascript/react.js via download from https://cdn.jsdelivr.net/npm/react@19.1.0/index.js
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
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.
|
|
142
|
-
|
|
143
|
-
If you later wish to remove a downloaded pin:
|
|
144
|
-
|
|
145
|
-
```bash
|
|
146
|
-
./bin/importmap unpin react
|
|
147
|
-
Unpinning and removing "react"
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Pinning to remote CDN URLs
|
|
151
|
-
|
|
152
|
-
If you'd rather load a package straight from the CDN instead of vendoring a download, pass `--remote`:
|
|
153
|
-
|
|
154
|
-
```bash
|
|
155
|
-
./bin/importmap pin react --remote
|
|
156
|
-
Pinning "react" to https://ga.jspm.io/npm:react@19.1.0/index.js
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
This will produce a pin in your `config/importmap.rb` like so:
|
|
160
|
-
|
|
161
|
-
```ruby
|
|
162
|
-
pin "react", to: "https://ga.jspm.io/npm:react@19.1.0/index.js"
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
Remote pins are respected from then on — no `--remote` flag needed. When a remote-pinned package is pinned again or picked up by `./bin/importmap update` (whether directly or as a dependency of another package), the pin stays remote: the URL is re-resolved from the same CDN provider it already points to (`ga.jspm.io`, `unpkg.com`, `cdn.jsdelivr.net`, `cdn.skypack.dev`, or `esm.sh`) instead of being replaced with a download. Pins pointing at any other host are left completely untouched and reported as skipped, and `./bin/importmap pristine` skips remote pins since there is nothing to redownload.
|
|
166
|
-
|
|
167
|
-
Options on existing pins, like `preload: false`, are preserved when a pin is rewritten. An explicit `integrity:` value is dropped when the URL changes, since the old hash would no longer match — see the SRI section below for pinning fresh integrity hashes.
|
|
168
|
-
|
|
169
|
-
### Loading bundles from esm.run
|
|
11
|
+
Replace `gem "importmap-rails"` with `gem "importmap-plus"` in your `Gemfile` and run `bundle install`. An app already on importmap-rails needs nothing else: the pins, `config/importmap.rb`, the helpers and the `Importmap::` constants are unchanged.
|
|
170
12
|
|
|
171
|
-
|
|
13
|
+
An app without an import map yet:
|
|
172
14
|
|
|
173
15
|
```bash
|
|
174
|
-
./bin/
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
A bundle references the packages it depends on as absolute `/npm/dep@1.2.3/+esm` imports, which only resolve on jsDelivr. When the bundle is vendored, those imports are rewritten to bare specifiers (`import { Controller } from "@hotwired/stimulus"`) so they resolve through your import map, and every dependency without a pin is pinned the same way — vendored from esm.run, at the version the bundle was built against:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
./bin/importmap pin stimulus-use --from esm.run
|
|
182
|
-
Pinning "stimulus-use" to vendor/javascript/stimulus-use.js via download from https://cdn.jsdelivr.net/npm/stimulus-use@0.53.1/+esm
|
|
183
|
-
Keeping existing pin for "@hotwired/stimulus" (bundle was built against @3.2.2)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
A dependency you already pin is left alone: the bundle then resolves to whatever your import map says, exactly like a jspm download would. With `--remote`, the pin points at the bundle URL and its imports load from jsDelivr as-is.
|
|
187
|
-
|
|
188
|
-
Versions are resolved through jsDelivr's data API, so `pin luxon@3 --from esm.run` and subpaths like `pin apexcharts/core --from esm.run` work the way they do on npm.
|
|
189
|
-
|
|
190
|
-
### Where a package came from is remembered
|
|
191
|
-
|
|
192
|
-
The version comment on a vendored pin also names the CDN when it isn't jspm:
|
|
193
|
-
|
|
194
|
-
```ruby
|
|
195
|
-
pin "luxon" # @3.7.2 (esm.run)
|
|
196
|
-
pin "react" # @19.1.0 (unpkg)
|
|
16
|
+
./bin/bundle add importmap-plus
|
|
17
|
+
./bin/rails importmap:install
|
|
197
18
|
```
|
|
198
19
|
|
|
199
|
-
|
|
20
|
+
## What it adds
|
|
200
21
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
22
|
+
| | |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `pin --minify` | Runs a download through bun, esbuild or terser before it lands in `vendor/javascript`; later updates keep minifying it. |
|
|
25
|
+
| `--from esm.run` | Vendors jsDelivr's one-file bundle, rewrites its imports to bare specifiers, and pins the dependencies it needs. |
|
|
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
|
+
| `update [PACKAGES] --all --force` | Update by name, or everything explicitly; `--force` moves locked packages and re-locks them. |
|
|
28
|
+
| Provenance | The pin comment records the CDN, minification and lock — `pin "luxon" # @3.7.2 (esm.run, minified, locked)` — so nothing silently drifts back to jspm. |
|
|
29
|
+
| Remote pins stay remote | A pin with a CDN URL is re-resolved from that CDN; `preload:` and a boolean `integrity:` survive every rewrite; a custom URL is left alone. |
|
|
30
|
+
| Requests retry | A reset connection, a timeout or a 429/5xx is tried three times with a growing pause before the command gives up. |
|
|
204
31
|
|
|
205
32
|
```bash
|
|
206
|
-
./bin/importmap pin luxon --minify
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
The first of [bun](https://bun.sh), [esbuild](https://esbuild.github.io) or [terser](https://terser.org) found in `node_modules/.bin` or on your `PATH` is used, always in transform-only mode so bare import specifiers are left exactly as the CDN resolved them. The pin records it (`pin "luxon" # @3.7.2 (minified)`, or `(esm.run, minified)` for a bundle), and from then on `update`, `pristine` and a plain `pin` keep minifying that package; `--no-minify` turns it off again, and `./bin/importmap pristine --minify` minifies everything you have vendored in one go.
|
|
211
|
-
|
|
212
|
-
To use a different minifier, assign anything that responds to `call(source)` and returns the minified source. `bin/importmap` loads `config/application.rb` but not your initializers, so the assignment has to happen there (or in a file it requires):
|
|
213
|
-
|
|
214
|
-
```ruby
|
|
215
|
-
# config/application.rb
|
|
216
|
-
require "importmap/packager"
|
|
217
|
-
|
|
218
|
-
Importmap::Packager.minifier = ->(source) { MyMinifier.minify(source) }
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
## Subresource Integrity (SRI)
|
|
222
|
-
|
|
223
|
-
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.
|
|
224
|
-
|
|
225
|
-
### Automatic integrity for local assets
|
|
226
|
-
|
|
227
|
-
To enable automatic integrity calculation for local assets served by the Rails asset pipeline, you must first call `enable_integrity!` in your importmap configuration:
|
|
228
|
-
|
|
229
|
-
```ruby
|
|
230
|
-
# config/importmap.rb
|
|
231
|
-
|
|
232
|
-
# Enable integrity calculation globally
|
|
233
|
-
enable_integrity!
|
|
234
|
-
|
|
235
|
-
# With integrity enabled, these will auto-calculate integrity hashes
|
|
236
|
-
pin "application" # Auto-calculated integrity
|
|
237
|
-
pin "admin", to: "admin.js" # Auto-calculated integrity
|
|
238
|
-
pin_all_from "app/javascript/controllers", under: "controllers" # Auto-calculated integrity
|
|
239
|
-
|
|
240
|
-
# Mixed usage - explicitly controlling integrity
|
|
241
|
-
pin "cdn_package", integrity: "sha384-abc123..." # Pre-calculated hash
|
|
242
|
-
pin "no_integrity_package", integrity: false # Explicitly disable integrity
|
|
243
|
-
pin "nil_integrity_package", integrity: nil # Explicitly disable integrity
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
This is particularly useful for:
|
|
247
|
-
* **Local JavaScript files** managed by your Rails asset pipeline
|
|
248
|
-
* **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
|
|
249
|
-
* **Development workflow** where asset contents change frequently
|
|
250
|
-
|
|
251
|
-
**Note:** Integrity calculation is opt-in and must be enabled with `enable_integrity!`. This behavior can be further controlled by setting `integrity: false` or `integrity: nil` on individual pins.
|
|
252
|
-
|
|
253
|
-
**Important for Propshaft users:** SRI support requires Propshaft 1.2+ and you must configure the integrity hash algorithm in your application:
|
|
254
|
-
|
|
255
|
-
```ruby
|
|
256
|
-
# config/application.rb or config/environments/*.rb
|
|
257
|
-
config.assets.integrity_hash_algorithm = 'sha256' # or 'sha384', 'sha512'
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
Without this configuration, integrity will be disabled by default when using Propshaft. Sprockets includes integrity support out of the box.
|
|
261
|
-
|
|
262
|
-
**Example output with `enable_integrity!` and `integrity: true`:**
|
|
263
|
-
```json
|
|
264
|
-
{
|
|
265
|
-
"imports": {
|
|
266
|
-
"application": "/assets/application-abc123.js",
|
|
267
|
-
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
|
|
268
|
-
},
|
|
269
|
-
"integrity": {
|
|
270
|
-
"/assets/application-abc123.js": "sha256-xyz789...",
|
|
271
|
-
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### How integrity works
|
|
277
|
-
|
|
278
|
-
The integrity hashes are automatically included in your import map and module preload tags:
|
|
279
|
-
|
|
280
|
-
**Import map JSON:**
|
|
281
|
-
```json
|
|
282
|
-
{
|
|
283
|
-
"imports": {
|
|
284
|
-
"lodash": "https://ga.jspm.io/npm:lodash@4.17.21/lodash.js",
|
|
285
|
-
"application": "/assets/application-abc123.js",
|
|
286
|
-
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
|
|
287
|
-
},
|
|
288
|
-
"integrity": {
|
|
289
|
-
"https://ga.jspm.io/npm:lodash@4.17.21/lodash.js": "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF"
|
|
290
|
-
"/assets/application-abc123.js": "sha256-xyz789...",
|
|
291
|
-
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Module preload tags:**
|
|
297
|
-
```html
|
|
298
|
-
<link rel="modulepreload" href="https://ga.jspm.io/npm:lodash@4.17.21/lodash.js" integrity="sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF">
|
|
299
|
-
<link rel="modulepreload" href="/assets/application-abc123.js" integrity="sha256-xyz789...">
|
|
300
|
-
<link rel="modulepreload" href="/assets/controllers/hello_controller-def456.js" integrity="sha256-uvw012...">
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Modern browsers will automatically validate these integrity hashes when loading the JavaScript modules, ensuring the files haven't been modified.
|
|
304
|
-
|
|
305
|
-
## Preloading pinned modules
|
|
306
|
-
|
|
307
|
-
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.
|
|
308
|
-
|
|
309
|
-
Example:
|
|
310
|
-
|
|
311
|
-
```ruby
|
|
312
|
-
# config/importmap.rb
|
|
313
|
-
pin "@github/hotkey", to: "@github--hotkey.js" # file lives in vendor/javascript/@github--hotkey.js
|
|
314
|
-
pin "md5", preload: false # file lives in vendor/javascript/md5.js
|
|
315
|
-
|
|
316
|
-
# app/views/layouts/application.html.erb
|
|
317
|
-
<%= javascript_importmap_tags %>
|
|
318
|
-
|
|
319
|
-
# will include the following link before the importmap is setup:
|
|
320
|
-
<link rel="modulepreload" href="/assets/javascript/@github--hotkey.js">
|
|
321
|
-
...
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
You can also specify which entry points to preload a particular dependency in by providing `preload:` a string or array of strings.
|
|
325
|
-
|
|
326
|
-
Example:
|
|
327
|
-
|
|
328
|
-
```ruby
|
|
329
|
-
# config/importmap.rb
|
|
330
|
-
pin "@github/hotkey", to: "@github--hotkey.js", preload: 'application'
|
|
331
|
-
pin "md5", preload: ['application', 'alternate']
|
|
332
|
-
|
|
333
|
-
# app/views/layouts/application.html.erb
|
|
334
|
-
<%= javascript_importmap_tags 'alternate' %>
|
|
335
|
-
|
|
336
|
-
# will include the following link before the importmap is setup:
|
|
337
|
-
<link rel="modulepreload" href="/assets/javascript/md5.js">
|
|
338
|
-
...
|
|
33
|
+
./bin/importmap pin luxon --from esm.run --minify
|
|
34
|
+
./bin/importmap pin @hotwired/stimulus@3.2.2 --lock
|
|
35
|
+
./bin/importmap update stimulus-use
|
|
36
|
+
./bin/importmap outdated
|
|
339
37
|
```
|
|
340
38
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
## Composing import maps
|
|
344
|
-
|
|
345
|
-
By default, Rails loads import map definition from the application's `config/importmap.rb` to the `Importmap::Map` object available at `Rails.application.importmap`.
|
|
346
|
-
|
|
347
|
-
You can combine multiple import maps by adding paths to additional import map configs to `Rails.application.config.importmap.paths`. For example, appending import maps defined in Rails engines:
|
|
348
|
-
|
|
349
39
|
```ruby
|
|
350
|
-
# my_engine/lib/my_engine/engine.rb
|
|
351
|
-
|
|
352
|
-
module MyEngine
|
|
353
|
-
class Engine < ::Rails::Engine
|
|
354
|
-
# ...
|
|
355
|
-
initializer "my-engine.importmap", before: "importmap" do |app|
|
|
356
|
-
app.config.importmap.paths << Engine.root.join("config/importmap.rb")
|
|
357
|
-
# ...
|
|
358
|
-
end
|
|
359
|
-
end
|
|
360
|
-
end
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
And pinning JavaScript modules from the engine:
|
|
364
|
-
|
|
365
|
-
```ruby
|
|
366
|
-
# my_engine/config/importmap.rb
|
|
367
|
-
|
|
368
|
-
pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
## Selectively importing modules
|
|
373
|
-
|
|
374
|
-
You can selectively import your javascript modules on specific pages.
|
|
375
|
-
|
|
376
|
-
Create your javascript in `app/javascript`:
|
|
377
|
-
|
|
378
|
-
```js
|
|
379
|
-
// /app/javascript/checkout.js
|
|
380
|
-
// some checkout specific js
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
Pin your js file:
|
|
384
|
-
|
|
385
|
-
```rb
|
|
386
40
|
# config/importmap.rb
|
|
387
|
-
#
|
|
388
|
-
pin "
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
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.
|
|
392
|
-
|
|
393
|
-
```erb
|
|
394
|
-
<% content_for :head do %>
|
|
395
|
-
<%= javascript_import_module_tag "checkout" %>
|
|
396
|
-
<% end %>
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
**Important**: The `javascript_import_module_tag` should come after your `javascript_importmap_tags`
|
|
400
|
-
|
|
401
|
-
```erb
|
|
402
|
-
<%= javascript_importmap_tags %>
|
|
403
|
-
<%= yield(:head) %>
|
|
41
|
+
pin "luxon" # @3.7.2 (esm.run, minified)
|
|
42
|
+
pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2 (locked)
|
|
404
43
|
```
|
|
405
44
|
|
|
45
|
+
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).
|
|
406
46
|
|
|
407
|
-
##
|
|
408
|
-
|
|
409
|
-
If you're using [ETags](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) generated by Rails helpers like `stale?` or `fresh_when`, you need to include the digest of the import map into this calculation. Otherwise your application will return [304](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) cache responses even when your JavaScript assets have changed. You can avoid this using the `stale_when_importmap_changes` method:
|
|
410
|
-
|
|
411
|
-
```ruby
|
|
412
|
-
class ApplicationController < ActionController::Base
|
|
413
|
-
stale_when_importmap_changes
|
|
414
|
-
end
|
|
415
|
-
```
|
|
47
|
+
## Tracking importmap-rails
|
|
416
48
|
|
|
417
|
-
This
|
|
49
|
+
This gem tracks importmap-rails at the release named in `Importmap::UPSTREAM_VERSION`; its own version is `Importmap::VERSION`. Upstream changes are merged as they land, and the parts of the documentation that describe upstream behaviour are kept in step. See [CHANGELOG.md](CHANGELOG.md) for what each release adds.
|
|
418
50
|
|
|
51
|
+
## Development
|
|
419
52
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
If you're pinning local files from outside of `app/javascript`, you'll need to add them to the cache sweeper configuration or restart your development server upon changes to those external files. For example, here's how you can do it for Rails engine:
|
|
425
|
-
|
|
426
|
-
```ruby
|
|
427
|
-
# my_engine/lib/my_engine/engine.rb
|
|
428
|
-
|
|
429
|
-
module MyEngine
|
|
430
|
-
class Engine < ::Rails::Engine
|
|
431
|
-
# ...
|
|
432
|
-
initializer "my-engine.importmap", before: "importmap" do |app|
|
|
433
|
-
# ...
|
|
434
|
-
app.config.importmap.cache_sweepers << Engine.root.join("app/assets/javascripts")
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
end
|
|
53
|
+
```bash
|
|
54
|
+
bundle install
|
|
55
|
+
bundle exec rake test # the command tests talk to live CDNs (jspm, jsDelivr)
|
|
56
|
+
cd docs && bin/dev # the docs site, a docs-kit app at docs/
|
|
438
57
|
```
|
|
439
58
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Importmap for Rails provides two commands to check your pinned packages:
|
|
443
|
-
- `./bin/importmap outdated` checks the NPM registry for new versions
|
|
444
|
-
- `./bin/importmap audit` checks the NPM registry for known security issues
|
|
445
|
-
|
|
446
|
-
## Supporting legacy browsers such as Safari on iOS 15
|
|
447
|
-
|
|
448
|
-
If you want to support [legacy browsers that do not support import maps](https://caniuse.com/import-maps) such as [iOS 15.8.1 released on 22 Jan 2024](https://support.apple.com/en-us/HT201222), insert [`es-module-shims`](https://github.com/guybedford/es-module-shims) before `javascript_importmap_tags` as below.
|
|
449
|
-
|
|
450
|
-
```erb
|
|
451
|
-
<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js" data-turbo-track="reload"></script>
|
|
452
|
-
<%= javascript_importmap_tags %>
|
|
453
|
-
```
|
|
59
|
+
Releases are cut with `bin/release`, which tags a version and publishes a GitHub Release; that fires `.github/workflows/release.yml` (RubyGems trusted publishing) and `.github/workflows/deploy-docs.yml` (the docs site).
|
|
454
60
|
|
|
455
61
|
## License
|
|
456
62
|
|
|
457
|
-
Importmap for Rails is released under the [MIT License](https://opensource.org/licenses/MIT).
|
|
63
|
+
Importmap for Rails is released under the [MIT License](https://opensource.org/licenses/MIT), and so is importmap-plus.
|
data/lib/importmap/commands.rb
CHANGED
|
@@ -15,13 +15,33 @@ class Importmap::Commands < Thor
|
|
|
15
15
|
option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
|
|
16
16
|
option :remote, type: :boolean, default: false, desc: "Pin to the remote URL instead of vendoring a download"
|
|
17
17
|
option :minify, type: :boolean, desc: "Minify the vendored download with bun, esbuild or terser"
|
|
18
|
+
option :lock, type: :boolean, desc: "Lock the pin at this version; update, pin and pristine leave it there until unlocked"
|
|
19
|
+
option :force, type: :boolean, default: false, desc: "Re-pin locked packages, keeping each lock at the new version"
|
|
18
20
|
def pin(*packages)
|
|
21
|
+
packages = without_locked(packages, lock: options[:lock], force: options[:force])
|
|
22
|
+
# jspm resolves a package together with its dependencies; --lock and
|
|
23
|
+
# --no-lock are about the packages that were asked for, not those.
|
|
24
|
+
requested = packages.map { |spec| packager.package_key_for(spec) }
|
|
25
|
+
|
|
19
26
|
for_each_import_grouped_by_provider(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
27
|
+
next if keep_locked_dependency(package, requested)
|
|
28
|
+
|
|
20
29
|
pin_package(package, url, preload: options[:preload], remote: options[:remote], env: options[:env],
|
|
21
|
-
minify: options[:minify], from: options[:from]
|
|
30
|
+
minify: options[:minify], from: options[:from],
|
|
31
|
+
lock: requested.include?(package) ? options[:lock] : nil)
|
|
22
32
|
end
|
|
23
33
|
end
|
|
24
34
|
|
|
35
|
+
desc "lock [*PACKAGES]", "Lock packages at their pinned version"
|
|
36
|
+
def lock(*packages)
|
|
37
|
+
exit 1 unless packages.map { |package| lock_package(package) }.all?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "unlock [*PACKAGES]", "Let locked packages be updated again"
|
|
41
|
+
def unlock(*packages)
|
|
42
|
+
exit 1 unless packages.map { |package| unlock_package(package) }.all?
|
|
43
|
+
end
|
|
44
|
+
|
|
25
45
|
desc "unpin [*PACKAGES]", "Unpin existing packages"
|
|
26
46
|
option :env, type: :string, aliases: :e, default: "production"
|
|
27
47
|
option :from, type: :string, aliases: :f, default: "jspm"
|
|
@@ -44,6 +64,8 @@ class Importmap::Commands < Thor
|
|
|
44
64
|
for_each_import_grouped_by_provider(packages, env: options[:env], from: options[:from]) do |package, url|
|
|
45
65
|
if packager.remote_pin?(package)
|
|
46
66
|
puts %(Skipping "#{package}" (pinned to remote URL))
|
|
67
|
+
elsif (resolved = version_drift_of_locked(package, url))
|
|
68
|
+
puts %(Skipping "#{package}" (locked at #{packager.pin_provenance(package)[:version]}, CDN resolved #{resolved}))
|
|
47
69
|
else
|
|
48
70
|
minify = options[:minify].nil? ? vendored_minified?(package) : options[:minify]
|
|
49
71
|
|
|
@@ -85,28 +107,56 @@ class Importmap::Commands < Thor
|
|
|
85
107
|
desc "outdated", "Check for outdated packages"
|
|
86
108
|
def outdated
|
|
87
109
|
if (outdated_packages = npm.outdated_packages).any?
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
locked = outdated_packages.select { |p| locked_pin_covering(p.name) }
|
|
111
|
+
|
|
112
|
+
table = [["Package", "Current", "Latest", "Locked"]]
|
|
113
|
+
outdated_packages.each { |p| table << [p.name, p.current_version, p.latest_version || p.error, locked.include?(p) ? "yes" : ""] }
|
|
90
114
|
|
|
91
115
|
puts_table(table)
|
|
92
116
|
packages = 'package'.pluralize(outdated_packages.size)
|
|
93
|
-
puts " #{outdated_packages.size} outdated #{packages} found"
|
|
117
|
+
puts " #{outdated_packages.size} outdated #{packages} found#{" (#{locked.size} locked)" if locked.any?}"
|
|
94
118
|
|
|
95
|
-
|
|
119
|
+
# A lock is a version the app chose, so only the rest count as drift.
|
|
120
|
+
exit 1 if locked.size < outdated_packages.size
|
|
96
121
|
else
|
|
97
122
|
puts "No outdated packages found"
|
|
98
123
|
end
|
|
99
124
|
end
|
|
100
125
|
|
|
101
|
-
desc "update", "Update outdated package pins"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
desc "update [*PACKAGES]", "Update outdated package pins"
|
|
127
|
+
option :all, type: :boolean, default: false, desc: "Update every outdated package (the default when no names are given)"
|
|
128
|
+
option :force, type: :boolean, default: false, desc: "Update locked packages too, keeping each lock at the new version"
|
|
129
|
+
def update(*packages)
|
|
130
|
+
if packages.any? && options[:all]
|
|
131
|
+
puts "Pass package names or --all, not both"
|
|
132
|
+
exit 1
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A package the registry couldn't answer for has no latest_version, so
|
|
136
|
+
# nothing established that it moved: re-pinning would let a blip
|
|
137
|
+
# re-resolve the pin against the CDN and carry it somewhere new. Each
|
|
138
|
+
# pin is independent of the others, so the rest still update; the exit
|
|
139
|
+
# code says the command didn't do all it was asked.
|
|
140
|
+
outdated_packages, unchecked_packages = npm.outdated_packages(only: packages.presence).partition(&:latest_version)
|
|
141
|
+
unchecked_packages.each { |p| puts %(Couldn't check "#{p.name}": #{p.error}) }
|
|
142
|
+
|
|
143
|
+
exit 1 unless every_package_known?(packages, outdated_packages, unchecked_packages)
|
|
144
|
+
|
|
145
|
+
if outdated_packages.empty?
|
|
146
|
+
puts "No outdated packages found" if unchecked_packages.empty?
|
|
147
|
+
elsif (names = without_locked_updates(outdated_packages.map(&:name), force: options[:force])).empty?
|
|
148
|
+
puts "Nothing to update (every outdated package is locked; pass --force)"
|
|
149
|
+
else
|
|
150
|
+
keys = packages.any? ? requested_keys_for(packages, names) : outdated_keys_for(names)
|
|
151
|
+
|
|
152
|
+
for_each_import_grouped_by_provider(keys, env: "production") do |package, url|
|
|
153
|
+
next if keep_locked_dependency(package, keys)
|
|
154
|
+
|
|
105
155
|
pin_package(package, url)
|
|
106
156
|
end
|
|
107
|
-
else
|
|
108
|
-
puts "No outdated packages found"
|
|
109
157
|
end
|
|
158
|
+
|
|
159
|
+
exit 1 if unchecked_packages.any?
|
|
110
160
|
end
|
|
111
161
|
|
|
112
162
|
desc "packages", "Print out packages with version numbers"
|
|
@@ -123,32 +173,181 @@ class Importmap::Commands < Thor
|
|
|
123
173
|
@npm ||= Importmap::Npm.new
|
|
124
174
|
end
|
|
125
175
|
|
|
126
|
-
|
|
176
|
+
# A lock outlives a rewrite unless the caller says otherwise, so update
|
|
177
|
+
# and --force re-lock at the version they move to.
|
|
178
|
+
def pin_package(package, url, preload: nil, remote: false, env: "production", minify: nil, from: nil, lock: nil)
|
|
127
179
|
existing_options = packager.extract_existing_pin_options(package)[package] || {}
|
|
128
180
|
preload = existing_options[:preload] if preload.nil?
|
|
181
|
+
integrity = existing_options[:integrity]
|
|
182
|
+
locked = lock.nil? ? packager.locked?(package) : lock
|
|
129
183
|
existing_url = existing_options[:to] if existing_options[:to].to_s.match?(Importmap::Packager::REMOTE_URL_REGEXP)
|
|
130
184
|
|
|
131
185
|
if existing_url
|
|
132
|
-
repin_remote_package(package, url, existing_url, preload, env: env, from: from)
|
|
186
|
+
repin_remote_package(package, url, existing_url, preload, env: env, from: from, integrity: integrity, locked: locked)
|
|
133
187
|
elsif remote
|
|
134
|
-
pin_remote_package(package, url, preload)
|
|
188
|
+
pin_remote_package(package, url, preload, integrity: integrity, locked: locked)
|
|
135
189
|
else
|
|
136
|
-
pin_vendored_package(package, url, preload, minify: minify)
|
|
190
|
+
pin_vendored_package(package, url, preload, minify: minify, integrity: integrity, locked: locked)
|
|
137
191
|
end
|
|
138
192
|
end
|
|
139
193
|
|
|
140
|
-
def pin_vendored_package(package, url, preload, minify: nil)
|
|
194
|
+
def pin_vendored_package(package, url, preload, minify: nil, integrity: nil, locked: false)
|
|
141
195
|
minify = vendored_minified?(package) if minify.nil?
|
|
142
196
|
|
|
143
197
|
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url}#{" (minified)" if minify})
|
|
144
198
|
|
|
145
199
|
dependencies = packager.download(package, url, minify: minify)
|
|
146
200
|
|
|
147
|
-
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, preload, minify: minify))
|
|
201
|
+
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, preload, minify: minify, integrity: integrity, locked: locked))
|
|
202
|
+
report_lock(package) if locked
|
|
148
203
|
|
|
149
204
|
pin_esm_run_dependencies(dependencies, preload: preload, minify: minify)
|
|
150
205
|
end
|
|
151
206
|
|
|
207
|
+
def lock_package(spec)
|
|
208
|
+
package = packager.package_key_for(spec)
|
|
209
|
+
|
|
210
|
+
if package != spec
|
|
211
|
+
puts %(Use "bin/importmap pin #{spec} --lock" to lock at a different version)
|
|
212
|
+
elsif !packager.packaged?(package)
|
|
213
|
+
puts %(Couldn't find a pin for "#{package}")
|
|
214
|
+
elsif packager.locked?(package)
|
|
215
|
+
puts %("#{package}" is already locked at #{packager.pin_provenance(package)[:version]})
|
|
216
|
+
return true
|
|
217
|
+
elsif (line = packager.locked_pin_line(package))
|
|
218
|
+
update_importmap_with_pin(package, line)
|
|
219
|
+
report_lock(package)
|
|
220
|
+
return true
|
|
221
|
+
else
|
|
222
|
+
puts %(Can't lock "#{package}": its pin has no version)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def unlock_package(spec)
|
|
229
|
+
package = packager.package_key_for(spec)
|
|
230
|
+
|
|
231
|
+
if !packager.packaged?(package)
|
|
232
|
+
puts %(Couldn't find a pin for "#{package}")
|
|
233
|
+
false
|
|
234
|
+
elsif !packager.locked?(package)
|
|
235
|
+
puts %("#{package}" isn't locked)
|
|
236
|
+
true
|
|
237
|
+
else
|
|
238
|
+
update_importmap_with_pin(package, packager.unlocked_pin_line(package))
|
|
239
|
+
puts %(Unlocked "#{package}")
|
|
240
|
+
true
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def report_lock(package)
|
|
245
|
+
puts %(Locked "#{package}" at #{packager.pin_provenance(package)[:version]})
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Specs the user named that point at a locked pin are dropped, with a
|
|
249
|
+
# note, unless the lock is what they are here to change.
|
|
250
|
+
def without_locked(specs, lock: nil, force: false)
|
|
251
|
+
return specs if force || !lock.nil?
|
|
252
|
+
|
|
253
|
+
specs.reject do |spec|
|
|
254
|
+
package = packager.package_key_for(spec)
|
|
255
|
+
|
|
256
|
+
packager.locked?(package).tap { |locked| puts skip_locked_message(package) if locked }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# update sees npm names (apexcharts) where pins are import-map keys
|
|
261
|
+
# (apexcharts/core), so a lock on any pin of the package holds it.
|
|
262
|
+
def without_locked_updates(names, force: false)
|
|
263
|
+
return names if force
|
|
264
|
+
|
|
265
|
+
names.reject do |name|
|
|
266
|
+
(locked = locked_pin_covering(name)).tap { puts skip_locked_message(locked) if locked }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def skip_locked_message(package)
|
|
271
|
+
%(Skipping "#{package}" (locked at #{packager.pin_provenance(package)[:version]}; run bin/importmap unlock #{package} or pass --force))
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Says why a named package won't be updated. A name with no pin at all
|
|
275
|
+
# is a typo until proven otherwise, so nothing is updated in that case.
|
|
276
|
+
# One the registry couldn't be asked about was already reported and is
|
|
277
|
+
# no reason to hold back the others.
|
|
278
|
+
def every_package_known?(names, outdated_packages, unchecked_packages)
|
|
279
|
+
versioned = npm.packages_with_versions.to_h
|
|
280
|
+
outdated = outdated_packages.map(&:name)
|
|
281
|
+
unchecked = unchecked_packages.map(&:name)
|
|
282
|
+
|
|
283
|
+
names.map do |name|
|
|
284
|
+
key = packager.package_key_for(name)
|
|
285
|
+
package = packager.package_name_for(key)
|
|
286
|
+
|
|
287
|
+
if !packager.packaged?(key)
|
|
288
|
+
puts %(Couldn't find a pin for "#{name}")
|
|
289
|
+
next false
|
|
290
|
+
elsif outdated.include?(package) || unchecked.include?(package)
|
|
291
|
+
next true
|
|
292
|
+
elsif versioned.key?(package)
|
|
293
|
+
puts %("#{name}" is already up to date (#{versioned[package]}))
|
|
294
|
+
else
|
|
295
|
+
puts %(Can't tell whether "#{name}" is outdated: its pin has no version)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
true
|
|
299
|
+
end.all?
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The registry knows a package by name and the import map by key: a pin of
|
|
303
|
+
# "photoswipe/lightbox" is outdated when "photoswipe" is. A named update
|
|
304
|
+
# re-pins the keys that were asked for, not the name the registry answered with.
|
|
305
|
+
def requested_keys_for(specs, outdated_names)
|
|
306
|
+
specs.map { |spec| packager.package_key_for(spec) }
|
|
307
|
+
.select { |key| outdated_names.include?(packager.package_name_for(key)) }
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# An outdated package is outdated in every pin that carries it, so a bare
|
|
311
|
+
# update re-pins those keys: "photoswipe" moving updates the app's
|
|
312
|
+
# "photoswipe/lightbox" pin rather than appending a bare one beside it.
|
|
313
|
+
#
|
|
314
|
+
# Only pins that declare a version count, the same ones outdated reports
|
|
315
|
+
# on. An app file pinned under a package's namespace — pin "md5/helpers",
|
|
316
|
+
# to: "md5/helpers.js" — names no version and is none of the registry's
|
|
317
|
+
# business; asking a CDN for it 404s the whole batch and nothing updates.
|
|
318
|
+
#
|
|
319
|
+
# A name no pin's key names is one whose version came from a URL that
|
|
320
|
+
# doesn't match its key (pin "buffer", to: ".../npm:jspm-core@..."); it is
|
|
321
|
+
# still the only handle there is, so it goes through as itself.
|
|
322
|
+
def outdated_keys_for(names)
|
|
323
|
+
keys = packager.pinned_packages.select { |key| packager.pin_version(key) }
|
|
324
|
+
.group_by { |key| packager.package_name_for(key) }
|
|
325
|
+
|
|
326
|
+
names.flat_map { |name| keys[name] || [ name ] }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def locked_pin_covering(name)
|
|
330
|
+
packager.locked_pins.find { |key| key == name || key.start_with?("#{name}/") }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# A CDN resolves a package together with its dependencies. One the app has
|
|
334
|
+
# locked stays where it is: only a package named on the command line moves.
|
|
335
|
+
def keep_locked_dependency(package, requested)
|
|
336
|
+
return false if requested.include?(package) || !packager.locked?(package)
|
|
337
|
+
|
|
338
|
+
puts %(Keeping existing pin for "#{package}" (locked at #{packager.pin_provenance(package)[:version]}))
|
|
339
|
+
true
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# pristine asks the CDN for the pinned version, so a locked package only
|
|
343
|
+
# drifts if the CDN answers with another one.
|
|
344
|
+
def version_drift_of_locked(package, url)
|
|
345
|
+
return unless packager.locked?(package)
|
|
346
|
+
|
|
347
|
+
resolved = packager.extract_package_version_from(url).to_s.delete_prefix("@")
|
|
348
|
+
resolved if resolved != packager.pin_provenance(package)[:version]
|
|
349
|
+
end
|
|
350
|
+
|
|
152
351
|
# An esm.run bundle imports its dependencies as bare specifiers after
|
|
153
352
|
# download, so each one needs a pin. Pins the app already has win: the
|
|
154
353
|
# bundle then resolves to whatever version the app chose.
|
|
@@ -167,9 +366,11 @@ class Importmap::Commands < Thor
|
|
|
167
366
|
# comment those are recorded in. Left alone otherwise: a pin may carry
|
|
168
367
|
# options, such as integrity, that a rewrite would drop.
|
|
169
368
|
def record_provenance(package, url, minify)
|
|
170
|
-
|
|
369
|
+
existing_options = packager.extract_existing_pin_options(package)[package] || {}
|
|
171
370
|
|
|
172
|
-
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, preload,
|
|
371
|
+
update_importmap_with_pin(package, packager.vendored_pin_for(package, url, existing_options[:preload],
|
|
372
|
+
minify: minify, integrity: existing_options[:integrity],
|
|
373
|
+
locked: packager.locked?(package)))
|
|
173
374
|
end
|
|
174
375
|
|
|
175
376
|
def provenance_changed?(package, url, minify)
|
|
@@ -196,27 +397,28 @@ class Importmap::Commands < Thor
|
|
|
196
397
|
packager.pin_provenance(packager.package_key_for(spec))&.dig(:provider)
|
|
197
398
|
end
|
|
198
399
|
|
|
199
|
-
def pin_remote_package(package, url, preload)
|
|
400
|
+
def pin_remote_package(package, url, preload, integrity: nil, locked: false)
|
|
200
401
|
puts %(Pinning "#{package}" to #{url})
|
|
201
402
|
|
|
202
403
|
packager.remove_existing_package_file(package)
|
|
203
404
|
|
|
204
|
-
update_importmap_with_pin(package, packager.pin_for(package, url, preloads: preload))
|
|
405
|
+
update_importmap_with_pin(package, packager.pin_for(package, url, preloads: preload, integrity: integrity, locked: locked))
|
|
406
|
+
report_lock(package) if locked
|
|
205
407
|
end
|
|
206
408
|
|
|
207
|
-
def repin_remote_package(package, url, existing_url, preload, env:, from: nil)
|
|
409
|
+
def repin_remote_package(package, url, existing_url, preload, env:, from: nil, integrity: nil, locked: false)
|
|
208
410
|
# `url` was already resolved from the requested CDN, so an explicit
|
|
209
411
|
# --from moves the pin instead of being overruled by its current one.
|
|
210
|
-
return pin_remote_package(package, url, preload) if from
|
|
412
|
+
return pin_remote_package(package, url, preload, integrity: integrity, locked: locked) if from
|
|
211
413
|
|
|
212
414
|
provider = packager.provider_for_url(existing_url)
|
|
213
415
|
|
|
214
416
|
if provider.nil?
|
|
215
417
|
puts %(Skipping "#{package}" pinned to custom URL #{existing_url})
|
|
216
418
|
elsif provider == packager.provider_for_url(url)
|
|
217
|
-
pin_remote_package(package, url, preload)
|
|
419
|
+
pin_remote_package(package, url, preload, integrity: integrity, locked: locked)
|
|
218
420
|
elsif (provider_url = resolve_url_from_provider(package, url, provider, env: env))
|
|
219
|
-
pin_remote_package(package, provider_url, preload)
|
|
421
|
+
pin_remote_package(package, provider_url, preload, integrity: integrity, locked: locked)
|
|
220
422
|
else
|
|
221
423
|
puts %(Keeping "#{package}" pinned to #{existing_url} (couldn't resolve it from #{provider}))
|
|
222
424
|
end
|
|
@@ -224,7 +426,7 @@ class Importmap::Commands < Thor
|
|
|
224
426
|
|
|
225
427
|
def resolve_url_from_provider(package, reference_url, provider, env:)
|
|
226
428
|
version = packager.extract_package_version_from(reference_url)
|
|
227
|
-
response = packager.import(
|
|
429
|
+
response = packager.import(packager.package_spec_for(package, version), env: env, from: provider)
|
|
228
430
|
|
|
229
431
|
response && response[:imports][package]
|
|
230
432
|
rescue Importmap::Packager::Error => error
|
data/lib/importmap/npm.rb
CHANGED
|
@@ -19,8 +19,13 @@ class Importmap::Npm
|
|
|
19
19
|
@vendor_path = Pathname.new(vendor_path)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# With +only:+, just those packages are looked up; names may carry a
|
|
23
|
+
# subpath (apexcharts/core), which the registry doesn't know about.
|
|
24
|
+
def outdated_packages(only: nil)
|
|
25
|
+
wanted = only&.map { |name| extract_base_package_name(name) }
|
|
26
|
+
candidates = wanted ? packages_with_versions.select { |package, _| wanted.include?(package) } : packages_with_versions
|
|
27
|
+
|
|
28
|
+
candidates.each_with_object([]) do |(package, current_version), outdated_packages|
|
|
24
29
|
outdated_package = OutdatedPackage.new(name: package, current_version: current_version)
|
|
25
30
|
|
|
26
31
|
if !(response = get_package(package))
|
|
@@ -51,21 +56,25 @@ class Importmap::Npm
|
|
|
51
56
|
end.sort_by { |p| [p.name, p.severity] }
|
|
52
57
|
end
|
|
53
58
|
|
|
59
|
+
# Memoized: a command that asks twice would otherwise report the
|
|
60
|
+
# unversioned packages twice.
|
|
54
61
|
def packages_with_versions
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
importmap.scan(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
@packages_with_versions ||= begin
|
|
63
|
+
# We cannot use the name after "pin" because some dependencies are loaded from inside packages
|
|
64
|
+
# Eg. pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js"
|
|
65
|
+
with_versions = importmap.scan(/^pin .*(?<=npm:|npm\/|skypack\.dev\/|unpkg\.com\/|esm\.sh\/|esm\.sh\/\*)([^@\/]+)@(\d+\.\d+\.\d+(?:[^\/\s"']*))/) |
|
|
66
|
+
importmap.scan(/#{PIN_REGEX} #.*@(\d+\.\d+\.\d+(?:[^\s]*)).*$/)
|
|
67
|
+
|
|
68
|
+
with_versions.map! do |package, version|
|
|
69
|
+
[extract_base_package_name(package), version]
|
|
70
|
+
end.uniq!
|
|
71
|
+
|
|
72
|
+
vendored_packages_without_version(with_versions).each do |package, path|
|
|
73
|
+
$stdout.puts "Ignoring #{package} (#{path}) since no version is specified in the importmap"
|
|
74
|
+
end
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
$stdout.puts "Ignoring #{package} (#{path}) since no version is specified in the importmap"
|
|
76
|
+
with_versions
|
|
66
77
|
end
|
|
67
|
-
|
|
68
|
-
with_versions
|
|
69
78
|
end
|
|
70
79
|
|
|
71
80
|
private
|
|
@@ -84,6 +93,11 @@ class Importmap::Npm
|
|
|
84
93
|
JSON.parse(response)
|
|
85
94
|
rescue JSON::ParserError
|
|
86
95
|
nil
|
|
96
|
+
rescue HTTPError => error
|
|
97
|
+
# One package the registry won't answer for shouldn't end the run: the
|
|
98
|
+
# caller records it as unchecked, so the rest are still reported on.
|
|
99
|
+
# with_retries has already spent its attempts by the time we get here.
|
|
100
|
+
{ "error" => error.message }
|
|
87
101
|
end
|
|
88
102
|
|
|
89
103
|
def get_json(uri)
|
data/lib/importmap/packager.rb
CHANGED
|
@@ -10,6 +10,9 @@ class Importmap::Packager
|
|
|
10
10
|
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
|
|
11
11
|
PRELOAD_OPTION_REGEXP = /preload:\s*(\[[^\]]+\]|true|false|["'][^"']*["'])/.freeze # :nodoc:
|
|
12
12
|
TO_OPTION_REGEXP = /to:\s*["']([^"']*)["']/.freeze # :nodoc:
|
|
13
|
+
# Only the booleans: a hash string is tied to the file it was computed for,
|
|
14
|
+
# so a rewrite that changes the URL has to drop it.
|
|
15
|
+
INTEGRITY_OPTION_REGEXP = /integrity:\s*(true|false)\b/.freeze # :nodoc:
|
|
13
16
|
REMOTE_URL_REGEXP = %r{\Ahttps?://}.freeze # :nodoc:
|
|
14
17
|
|
|
15
18
|
PROVIDER_HOSTS = {
|
|
@@ -41,6 +44,12 @@ class Importmap::Packager
|
|
|
41
44
|
# pin "luxon" # @3.7.2
|
|
42
45
|
# pin "luxon" # @3.7.2 (esm.run, minified)
|
|
43
46
|
PIN_PROVENANCE_REGEXP = /#\s*@([^\s(]+)(?:\s+\(([^)]*)\))?/.freeze # :nodoc:
|
|
47
|
+
# A lock is one more detail in that list, always last:
|
|
48
|
+
# pin "luxon" # @3.7.2 (esm.run, minified, locked)
|
|
49
|
+
# "locked: <range>" is reserved for range locks, so it is read as a lock
|
|
50
|
+
# today rather than mistaken for a provider name.
|
|
51
|
+
LOCK_DETAIL = "locked".freeze # :nodoc:
|
|
52
|
+
LOCK_DETAIL_REGEXP = /\Alocked(?::\s*(.+))?\z/.freeze # :nodoc:
|
|
44
53
|
DEFAULT_PROVIDER = "jspm.io".freeze # :nodoc:
|
|
45
54
|
|
|
46
55
|
Error = Class.new(StandardError)
|
|
@@ -103,45 +112,101 @@ class Importmap::Packager
|
|
|
103
112
|
end
|
|
104
113
|
end
|
|
105
114
|
|
|
106
|
-
|
|
115
|
+
# A remote pin has no version comment unless it is locked; then the version
|
|
116
|
+
# in its URL is written out so the lock has something to hold:
|
|
117
|
+
#
|
|
118
|
+
# pin "md5", to: "https://cdn.jsdelivr.net/npm/md5@2.2.0/md5.js" # @2.2.0 (locked)
|
|
119
|
+
#
|
|
120
|
+
def pin_for(package, url = nil, preloads: nil, integrity: nil, locked: false)
|
|
107
121
|
to = url ? %(, to: "#{url}") : ""
|
|
108
122
|
preload_param = preload(preloads)
|
|
123
|
+
integrity_param = integrity.nil? ? "" : %(, integrity: #{integrity})
|
|
124
|
+
version = extract_package_version_from(url.to_s) if locked
|
|
109
125
|
|
|
110
|
-
|
|
126
|
+
%(pin "#{package}") + to + preload_param + integrity_param + (version ? provenance_comment(version, locked: true) : "")
|
|
111
127
|
end
|
|
112
128
|
|
|
113
129
|
# The pin line for a vendored download. The version comment also records
|
|
114
|
-
# the CDN when it isn't jspm
|
|
115
|
-
# update or pristine can do the same again:
|
|
130
|
+
# the CDN when it isn't jspm, whether the file was minified and whether the
|
|
131
|
+
# version is locked, so a later update or pristine can do the same again:
|
|
116
132
|
#
|
|
117
133
|
# pin "luxon" # @3.7.2
|
|
118
|
-
# pin "luxon" # @3.7.2 (esm.run, minified)
|
|
134
|
+
# pin "luxon" # @3.7.2 (esm.run, minified, locked)
|
|
119
135
|
#
|
|
120
|
-
def vendored_pin_for(package, url, preloads = nil, minify: false)
|
|
136
|
+
def vendored_pin_for(package, url, preloads = nil, minify: false, integrity: nil, locked: false)
|
|
121
137
|
filename = package_filename(package)
|
|
122
138
|
version = extract_package_version_from(url)
|
|
123
139
|
to = "#{package}.js" != filename ? filename : nil
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
provenance << "minified" if minify
|
|
128
|
-
|
|
129
|
-
pin_for(package, to, preloads: preloads) + %( # #{version}) + (provenance.any? ? %( (#{provenance.join(", ")})) : "")
|
|
141
|
+
pin_for(package, to, preloads: preloads, integrity: integrity) +
|
|
142
|
+
provenance_comment(version, provider: provider_for_url(url), minified: minify, locked: locked)
|
|
130
143
|
end
|
|
131
144
|
|
|
132
|
-
# What the pin's version comment says a
|
|
133
|
-
# { version:, provider:, minified: }, or nil for a pin without one.
|
|
145
|
+
# What the pin's version comment says a package was built with:
|
|
146
|
+
# { version:, provider:, minified:, locked: }, or nil for a pin without one.
|
|
134
147
|
def pin_provenance(package)
|
|
148
|
+
provenance_of(pin_line_for(package))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The line that pins +package+, without its newline, or nil.
|
|
152
|
+
def pin_line_for(package)
|
|
135
153
|
return unless @importmap_path.exist?
|
|
136
154
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
importmap.lines.find { |candidate| candidate.match?(Importmap::Map.pin_line_regexp_for(package)) }&.chomp
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# The version a pin declares, in its provenance comment or in the CDN URL it
|
|
159
|
+
# points at, or nil when it names none — as one of the app's own files
|
|
160
|
+
# doesn't. Only a pin with a version can be told to be outdated.
|
|
161
|
+
def pin_version(package)
|
|
162
|
+
provenance = pin_provenance(package)
|
|
163
|
+
return provenance[:version] if provenance
|
|
164
|
+
|
|
165
|
+
to = (extract_existing_pin_options(package)[package] || {})[:to]
|
|
166
|
+
extract_package_version_from(to.to_s)&.delete_prefix("@")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def locked?(package)
|
|
170
|
+
pin_provenance(package)&.dig(:locked) || false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The import-map keys of every pin, in file order.
|
|
174
|
+
def pinned_packages
|
|
175
|
+
return [] unless @importmap_path.exist?
|
|
176
|
+
|
|
177
|
+
importmap.lines.filter_map { |line| line.strip[PIN_REGEX, 1] }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# The import-map keys of every locked pin, in file order.
|
|
181
|
+
def locked_pins
|
|
182
|
+
return [] unless @importmap_path.exist?
|
|
183
|
+
|
|
184
|
+
importmap.lines.filter_map do |line|
|
|
185
|
+
name = line.strip[PIN_REGEX, 1]
|
|
186
|
+
name if name && provenance_of(line)&.dig(:locked)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The pin line with a lock added — to the version comment it has, or as a
|
|
191
|
+
# new comment carrying the version from its URL. Nothing else on the line is
|
|
192
|
+
# touched. Nil when the pin has no version to lock at, or isn't there.
|
|
193
|
+
def locked_pin_line(package)
|
|
194
|
+
line = pin_line_for(package)
|
|
195
|
+
return unless line
|
|
196
|
+
|
|
197
|
+
if line.match?(PIN_PROVENANCE_REGEXP)
|
|
198
|
+
rewrite_provenance(line) { |details| without_lock(details) << LOCK_DETAIL }
|
|
199
|
+
elsif (to = (extract_existing_pin_options(package)[package] || {})[:to].to_s).match?(REMOTE_URL_REGEXP) &&
|
|
200
|
+
(version = extract_package_version_from(to))
|
|
201
|
+
line + provenance_comment(version, locked: true)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
140
204
|
|
|
141
|
-
|
|
142
|
-
|
|
205
|
+
# The pin line with its lock removed; an empty detail list drops its parens.
|
|
206
|
+
def unlocked_pin_line(package)
|
|
207
|
+
line = pin_line_for(package)
|
|
143
208
|
|
|
144
|
-
{
|
|
209
|
+
rewrite_provenance(line) { |details| without_lock(details) } if line
|
|
145
210
|
end
|
|
146
211
|
|
|
147
212
|
def packaged?(package)
|
|
@@ -216,6 +281,23 @@ class Importmap::Packager
|
|
|
216
281
|
name ? "#{name}#{subpath}" : spec.to_s
|
|
217
282
|
end
|
|
218
283
|
|
|
284
|
+
# The package a spec or key belongs to, as the registry knows it:
|
|
285
|
+
# "photoswipe/lightbox" and "@hotwired/stimulus@3" are pins of "photoswipe"
|
|
286
|
+
# and "@hotwired/stimulus".
|
|
287
|
+
def package_name_for(spec)
|
|
288
|
+
spec.to_s.match(PACKAGE_SPEC_REGEXP)&.captures&.first || spec.to_s
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# The spec that asks a CDN for +key+ at +version+. A version belongs on the
|
|
292
|
+
# package name, ahead of the subpath: "photoswipe/lightbox" at "@5.4.4" is
|
|
293
|
+
# "photoswipe@5.4.4/lightbox", where "photoswipe/lightbox@5.4.4" is a path
|
|
294
|
+
# no CDN has.
|
|
295
|
+
def package_spec_for(key, version)
|
|
296
|
+
name, _requested, subpath = key.to_s.match(PACKAGE_SPEC_REGEXP)&.captures
|
|
297
|
+
|
|
298
|
+
name ? "#{name}#{version}#{subpath}" : "#{key}#{version}"
|
|
299
|
+
end
|
|
300
|
+
|
|
219
301
|
def remove_existing_package_file(package)
|
|
220
302
|
FileUtils.rm_rf vendored_package_path(package)
|
|
221
303
|
end
|
|
@@ -225,6 +307,43 @@ class Importmap::Packager
|
|
|
225
307
|
end
|
|
226
308
|
|
|
227
309
|
private
|
|
310
|
+
def provenance_of(line)
|
|
311
|
+
match = line&.match(PIN_PROVENANCE_REGEXP)
|
|
312
|
+
return unless match
|
|
313
|
+
|
|
314
|
+
details = match[2].to_s.split(",").map(&:strip)
|
|
315
|
+
minified = details.delete("minified") ? true : false
|
|
316
|
+
locked = without_lock(details).size != details.size
|
|
317
|
+
|
|
318
|
+
{ version: match[1], provider: without_lock(details).first, minified: minified, locked: locked }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def without_lock(details)
|
|
322
|
+
details.reject { |detail| detail.match?(LOCK_DETAIL_REGEXP) }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# " # @3.7.2 (esm.run, minified, locked)" — the details in their fixed
|
|
326
|
+
# order, the parens only when there is something to say.
|
|
327
|
+
def provenance_comment(version, provider: nil, minified: false, locked: false)
|
|
328
|
+
details = []
|
|
329
|
+
details << provider if provider && provider != DEFAULT_PROVIDER
|
|
330
|
+
details << "minified" if minified
|
|
331
|
+
details << LOCK_DETAIL if locked
|
|
332
|
+
|
|
333
|
+
%( # @#{version.to_s.delete_prefix("@")}) + (details.any? ? %( (#{details.join(", ")})) : "")
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Rewrites only the version comment of +line+, handing the block the
|
|
337
|
+
# current details and writing back what it returns.
|
|
338
|
+
def rewrite_provenance(line)
|
|
339
|
+
line.sub(PIN_PROVENANCE_REGEXP) do
|
|
340
|
+
version = $1
|
|
341
|
+
details = yield($2.to_s.split(",").map(&:strip))
|
|
342
|
+
|
|
343
|
+
"# @#{version}" + (details.any? ? " (#{details.join(", ")})" : "")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
228
347
|
def build_package_options_lookup(lines)
|
|
229
348
|
lines.each_with_object({}) do |line, package_options|
|
|
230
349
|
match = line.strip.match(PIN_REGEX)
|
|
@@ -243,6 +362,10 @@ class Importmap::Packager
|
|
|
243
362
|
options[:to] = to_match[1]
|
|
244
363
|
end
|
|
245
364
|
|
|
365
|
+
if (integrity_match = options_part.match(INTEGRITY_OPTION_REGEXP))
|
|
366
|
+
options[:integrity] = integrity_match[1] == "true"
|
|
367
|
+
end
|
|
368
|
+
|
|
246
369
|
package_options[package_name] = options if options.any?
|
|
247
370
|
end
|
|
248
371
|
end
|
data/lib/importmap/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: importmap-plus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Heinemeier Hansson
|
|
@@ -54,8 +54,9 @@ dependencies:
|
|
|
54
54
|
version: 6.0.0
|
|
55
55
|
description: 'A drop-in replacement for importmap-rails. Same Importmap:: API, same
|
|
56
56
|
pins, same import map, plus `bin/importmap pin --minify`, `--from esm.run` for jsDelivr''s
|
|
57
|
-
bundled builds,
|
|
58
|
-
updates keep them. Use this gem
|
|
57
|
+
bundled builds, `--lock` to hold a package at a version, and a pin comment that
|
|
58
|
+
records the CDN, minification and lock so later updates keep them. Use this gem
|
|
59
|
+
or importmap-rails, never both.'
|
|
59
60
|
email: mikael@mhenrixon.com
|
|
60
61
|
executables: []
|
|
61
62
|
extensions: []
|
|
@@ -103,8 +104,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
103
104
|
- !ruby/object:Gem::Version
|
|
104
105
|
version: '0'
|
|
105
106
|
requirements: []
|
|
106
|
-
rubygems_version:
|
|
107
|
+
rubygems_version: 3.6.9
|
|
107
108
|
specification_version: 4
|
|
108
|
-
summary: importmap-rails with vendoring that minifies, bundles from esm.run,
|
|
109
|
-
where each package came from.
|
|
109
|
+
summary: importmap-rails with vendoring that minifies, bundles from esm.run, locks
|
|
110
|
+
versions, and remembers where each package came from.
|
|
110
111
|
test_files: []
|