importmap-plus 2.0.0 → 2.0.2
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 +48 -0
- data/lib/importmap/doctor.rb +15 -3
- data/lib/importmap/import_scanner.rb +17 -6
- data/lib/importmap/module_inspector.rb +13 -2
- data/lib/importmap/packager.rb +54 -5
- data/lib/importmap/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '068b0fd351d34f6741037740731f5545ec5e98530ddd935abc45d18ac92b30e1'
|
|
4
|
+
data.tar.gz: 6482173bb6db3322fa43711fce8ccf0c9ac48d7b35c0b572e5b68d45f11d0445
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22b1de80057a17f315ab8230d5537400985c9f67c1c50faa184336856de9e3e1d8f06ea815392c78a0fef30bffa3373bd25b206ab638cf056c8447faa674fe44
|
|
7
|
+
data.tar.gz: 444fca5f51ec92e43ab7163bb14ce92791727b3a93242298e776a753cc243e218f17fdf3acb6c873842012bf38867862fa37bf4eb99e0464ed8e72061254c33f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`doctor` leaves a bundle installed inside the app alone.** A file an engine
|
|
8
|
+
serves out of its gem is skipped, because no `bin/importmap` command can fix
|
|
9
|
+
it — but "out of its gem" was read as "outside the app root". With
|
|
10
|
+
`bundle config set path vendor/bundle`, as a CI caching its gems has, the
|
|
11
|
+
gems are under the root, so an engine's controller importing a package the
|
|
12
|
+
app never pinned failed `doctor` in CI and nowhere else. Files under
|
|
13
|
+
`Gem.path` or `Bundler.bundle_path` are now skipped wherever they sit.
|
|
14
|
+
|
|
15
|
+
- **A `preload: %w[...]` survives a rewrite.** `pin`, `update` and `pristine`
|
|
16
|
+
carried over `preload: ["admin", "app"]` but not the same list written as a
|
|
17
|
+
word array, which RuboCop and Standard prefer (`Style/WordArray`). The option
|
|
18
|
+
was dropped without a word, so an updated package quietly stopped being
|
|
19
|
+
preloaded on the pages that named it. It is now read — backslash escapes and
|
|
20
|
+
nested brackets included, so `%w[my\ app]` is the one entry point Ruby sees and
|
|
21
|
+
`%w[foo [bar]]` keeps its `[bar]` — and written back in the form the app chose:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
pin "tailwindcss-stimulus-components", preload: %w[application webcad] # @6.1.3
|
|
25
|
+
# after bin/importmap update
|
|
26
|
+
pin "tailwindcss-stimulus-components", preload: %w[application webcad] # @6.1.4
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **A quoted preload keeps a bracket or quote inside it.** `preload: ["a]", "b"]`
|
|
30
|
+
was cut at the first `]` and `preload: "it's"` at the `'`, so a rewrite wrote
|
|
31
|
+
back a shorter list or a different entry point. Quoted strings are now read
|
|
32
|
+
whole, and a quoted preload — one entry point or a bracketed list — is written
|
|
33
|
+
back exactly as the app wrote it, quotes included, so `"\u0061pp"` stays the
|
|
34
|
+
`app` entry point and `"#{prefix}app"` still interpolates (both used to come
|
|
35
|
+
back as a different name). `preload: 'say "hi"'` no longer becomes `"say "hi""`.
|
|
36
|
+
|
|
37
|
+
## 2.0.1
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`bin/importmap doctor` no longer reports comments and prose as unpinned
|
|
42
|
+
imports.** The import scanner read `from "…"` and `import "…"` wherever the
|
|
43
|
+
text appeared, so a line comment such as `// e.g. 'en' from 'en-US'` and
|
|
44
|
+
strings such as turbo_power's `` `…provided in the "from" attribute for
|
|
45
|
+
the "…` `` came back as `error … imports " attribute for the ", which isn't
|
|
46
|
+
pinned`. Line comments are now discounted before scanning (strings and
|
|
47
|
+
regex literals are still consumed whole, so `"https://…"` is safe), and a
|
|
48
|
+
quoted text that can't be a module specifier — whitespace, a backtick, a
|
|
49
|
+
`${` interpolation, or no word character at all — is not reported.
|
|
50
|
+
|
|
3
51
|
## 1.2.0
|
|
4
52
|
|
|
5
53
|
### Added
|
data/lib/importmap/doctor.rb
CHANGED
|
@@ -59,11 +59,18 @@ class Importmap::Doctor
|
|
|
59
59
|
# against the importing file's own URL, or fetches outright, is not one.
|
|
60
60
|
SCHEME_REGEXP = %r{\A[a-zA-Z][a-zA-Z0-9+\-.]*:}.freeze # :nodoc:
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
# Where installed gems live: the default +gem_paths+. A bundle installed
|
|
63
|
+
# with `path: vendor/bundle` puts Bundler's under the app root.
|
|
64
|
+
def self.gem_paths
|
|
65
|
+
[ *Gem.path, (Bundler.bundle_path.to_s if defined?(Bundler)) ].compact.uniq
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialize(importmap:, resolver:, root:, asset_paths: [], gem_paths: self.class.gem_paths, online: false)
|
|
63
69
|
@importmap = importmap
|
|
64
70
|
@resolver = resolver
|
|
65
71
|
@root = Pathname.new(root)
|
|
66
72
|
@asset_paths = Array(asset_paths).map { |path| Pathname.new(path) }
|
|
73
|
+
@gem_paths = Array(gem_paths).map { |path| Pathname.new(path) }
|
|
67
74
|
@online = online
|
|
68
75
|
end
|
|
69
76
|
|
|
@@ -188,9 +195,14 @@ class Importmap::Doctor
|
|
|
188
195
|
|
|
189
196
|
# The files this gem may read: the app's own, not a file an engine serves
|
|
190
197
|
# out of its gem, which no `bin/importmap` command can do anything about.
|
|
191
|
-
#
|
|
198
|
+
# Being under the root isn't enough to be the app's: a bundle installed
|
|
199
|
+
# into vendor/bundle is too. One file serving two keys is scanned once.
|
|
192
200
|
def scannable
|
|
193
|
-
@scannable ||= entries.select { |entry| entry.file &&
|
|
201
|
+
@scannable ||= entries.select { |entry| entry.file && app_file?(entry.file) }.uniq(&:file)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def app_file?(file)
|
|
205
|
+
under?(file, @root) && @gem_paths.none? { |gem_path| under?(file, gem_path) }
|
|
194
206
|
end
|
|
195
207
|
|
|
196
208
|
def vendored
|
|
@@ -6,16 +6,19 @@ require "importmap/module_inspector"
|
|
|
6
6
|
#
|
|
7
7
|
# Like Importmap::ModuleInspector and Importmap::PackageGraph::IMPORT_REGEXP
|
|
8
8
|
# this reads the source with regexes rather than parsing JavaScript, so an
|
|
9
|
-
# import statement spelled out inside a string literal is reported too
|
|
10
|
-
#
|
|
9
|
+
# import statement spelled out inside a string literal is reported too — unless
|
|
10
|
+
# what it names can't be a module specifier at all — and a form the regexp
|
|
11
|
+
# can't read — a magic comment between the keyword and the
|
|
11
12
|
# specifier, a specifier built at runtime — isn't reported at all. Both
|
|
12
13
|
# mistakes are the cautious direction for the one caller: a specifier reported
|
|
13
14
|
# that the browser never asks for costs a pin, and a specifier missed leaves
|
|
14
15
|
# the app exactly as broken as it was before anyone ran the check.
|
|
15
16
|
#
|
|
16
|
-
#
|
|
17
|
-
# published bundle is
|
|
18
|
-
#
|
|
17
|
+
# Comments are discounted first, through
|
|
18
|
+
# ModuleInspector#code_without_line_comments, because a published bundle is
|
|
19
|
+
# full of `/** @typedef {import('./slide.js').Slide} */` — type annotations
|
|
20
|
+
# naming files the package never loads — and app code is full of prose like
|
|
21
|
+
# `// 'en' from 'en-US'`.
|
|
19
22
|
class Importmap::ImportScanner
|
|
20
23
|
Import = Struct.new(:specifier, :kind, keyword_init: true) # :nodoc:
|
|
21
24
|
|
|
@@ -38,6 +41,12 @@ class Importmap::ImportScanner
|
|
|
38
41
|
(?:from|import)\s*(["'])([^"'\n]*)\3
|
|
39
42
|
)
|
|
40
43
|
/x.freeze # :nodoc:
|
|
44
|
+
# What a quoted `from "…"` or `import "…"` has to hold to name a module
|
|
45
|
+
# rather than be prose the regexp above found in a string: something, no
|
|
46
|
+
# whitespace or backtick, no `${` interpolation, and at least one word
|
|
47
|
+
# character — `"from" attribute for the "`, `["import",","]` and
|
|
48
|
+
# `` `import("${url}")` `` are all strings, never specifiers.
|
|
49
|
+
SPECIFIER_REGEXP = /\A(?!.*\$\{)[^\s`]*\w[^\s`]*\z/.freeze # :nodoc:
|
|
41
50
|
|
|
42
51
|
attr_reader :source
|
|
43
52
|
|
|
@@ -46,7 +55,9 @@ class Importmap::ImportScanner
|
|
|
46
55
|
end
|
|
47
56
|
|
|
48
57
|
def imports
|
|
49
|
-
@imports ||= Importmap::ModuleInspector.new(source).
|
|
58
|
+
@imports ||= Importmap::ModuleInspector.new(source).code_without_line_comments.scan(IMPORT_REGEXP).filter_map do |_, dynamic, _, static|
|
|
59
|
+
next unless (dynamic || static).match?(SPECIFIER_REGEXP)
|
|
60
|
+
|
|
50
61
|
dynamic ? Import.new(specifier: dynamic, kind: :dynamic) : Import.new(specifier: static, kind: :static)
|
|
51
62
|
end
|
|
52
63
|
end
|
|
@@ -144,6 +144,14 @@ class Importmap::ModuleInspector
|
|
|
144
144
|
@code ||= without_block_comments
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
# #code with its line comments discounted too, for Importmap::ImportScanner:
|
|
148
|
+
# prose such as `// e.g. 'en' from 'en-US'` reads as an import otherwise.
|
|
149
|
+
# Strings and regex literals are still consumed whole first, so the `//` of
|
|
150
|
+
# `"https://…"` or `/\/\//` never opens a comment.
|
|
151
|
+
def code_without_line_comments
|
|
152
|
+
@code_without_line_comments ||= without_block_comments(line_comments: true)
|
|
153
|
+
end
|
|
154
|
+
|
|
147
155
|
private
|
|
148
156
|
def statements
|
|
149
157
|
@statements ||= without_block_comments(statements_only: true)
|
|
@@ -170,12 +178,15 @@ class Importmap::ModuleInspector
|
|
|
170
178
|
# reads as an import statement while the text inside it stops being read
|
|
171
179
|
# as code at all. That mode feeds only the ES-module check, where the
|
|
172
180
|
# `[//]` trap above can at worst send a package on to the next CDN.
|
|
173
|
-
|
|
181
|
+
# With +line_comments+ alone they go but literals are kept, for the import
|
|
182
|
+
# scanner: the trap there can at worst miss an import in the doctor's
|
|
183
|
+
# report, where prose in a comment is a false error on every run.
|
|
184
|
+
def without_block_comments(statements_only: false, line_comments: statements_only)
|
|
174
185
|
scanner = StringScanner.new(source)
|
|
175
186
|
kept = +""
|
|
176
187
|
|
|
177
188
|
until scanner.eos?
|
|
178
|
-
if scanner.skip(BLOCK_COMMENT_REGEXP) || (
|
|
189
|
+
if scanner.skip(BLOCK_COMMENT_REGEXP) || (line_comments && scanner.skip(LINE_COMMENT_REGEXP))
|
|
179
190
|
next
|
|
180
191
|
elsif (literal = scanner.scan(STRING_REGEXP)) ||
|
|
181
192
|
(regexp_literal_next?(kept, scanner) && (literal = scanner.scan(REGEXP_LITERAL_REGEXP)))
|
data/lib/importmap/packager.rb
CHANGED
|
@@ -13,10 +13,47 @@ class Importmap::Packager
|
|
|
13
13
|
include Importmap::HttpRetries
|
|
14
14
|
|
|
15
15
|
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
|
|
16
|
+
# A Ruby string literal, escapes included, so a quote or bracket inside one
|
|
17
|
+
# doesn't end the match early (`"it's"`, `["a]"]`).
|
|
18
|
+
QUOTED_STRING_REGEXP = /"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'/m.freeze # :nodoc:
|
|
16
19
|
# The bracketed form matches an empty array too: `preload: []` is a pin an
|
|
17
20
|
# app wrote, and a rewrite that dropped it would start preloading the package
|
|
18
|
-
# on every page.
|
|
19
|
-
|
|
21
|
+
# on every page. A word array nests its own delimiter the way Ruby does
|
|
22
|
+
# (`%w[foo [bar]]` is "foo" and "[bar]"), hence the recursive groups; with
|
|
23
|
+
# named groups in the pattern, the value has to be named too to stay [1].
|
|
24
|
+
PRELOAD_OPTION_REGEXP = /preload:\s*(?<value>\[(?:#{QUOTED_STRING_REGEXP}|[^\]"'])*\]|%w(?<brackets>\[(?:\\.|[^\[\]\\]|\g<brackets>)*\])|%w(?<parens>\((?:\\.|[^()\\]|\g<parens>)*\))|true|false|#{QUOTED_STRING_REGEXP})/.freeze # :nodoc:
|
|
25
|
+
# A `preload: %w[...]` read from a pin, so a rewrite writes it back in the
|
|
26
|
+
# form the app chose (RuboCop's Style/WordArray flags `["a", "b"]`). Read
|
|
27
|
+
# with Ruby's escapes rather than split on whitespace: `%w[my\ app]` is one
|
|
28
|
+
# word, and a backslash before anything but whitespace, a backslash or the
|
|
29
|
+
# literal's own delimiter stays in it.
|
|
30
|
+
class WordArray < Array # :nodoc:
|
|
31
|
+
LITERAL_REGEXP = /\A%w([\[(])(.*)[\])]\z/m.freeze
|
|
32
|
+
|
|
33
|
+
def self.parse(literal)
|
|
34
|
+
open, body = literal.match(LITERAL_REGEXP)&.captures
|
|
35
|
+
return unless open
|
|
36
|
+
|
|
37
|
+
escapable = /\\([\s\\#{Regexp.escape(open == "[" ? "[]" : "()")}])/
|
|
38
|
+
new(body.scan(/(?:\\.|[^\s\\])+/m).map { |word| word.gsub(escapable, '\1') })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
"%w[#{map { |word| word.gsub(/[\s\\\[\]]/) { "\\#{$&}" } }.join(" ")}]"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
# A quoted preload read from a pin — a string or a bracketed array — keeps
|
|
46
|
+
# the literal it was read from, and a rewrite writes that back untouched.
|
|
47
|
+
# The names are read with only the simple escapes; the rest of Ruby's string
|
|
48
|
+
# semantics ("\u0061pp", "#{prefix}app") is Ruby's to evaluate, so writing
|
|
49
|
+
# the value read back through inspect would change the entry point.
|
|
50
|
+
module Literal # :nodoc:
|
|
51
|
+
attr_accessor :source
|
|
52
|
+
|
|
53
|
+
def self.of(value, source)
|
|
54
|
+
value.extend(self).tap { |literal| literal.source = source }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
20
57
|
TO_OPTION_REGEXP = /to:\s*["']([^"']*)["']/.freeze # :nodoc:
|
|
21
58
|
# Only the booleans: a hash string is tied to the file it was computed for,
|
|
22
59
|
# so a rewrite that changes the URL has to drop it.
|
|
@@ -537,21 +574,33 @@ class Importmap::Packager
|
|
|
537
574
|
true
|
|
538
575
|
when "false"
|
|
539
576
|
false
|
|
577
|
+
when WordArray::LITERAL_REGEXP
|
|
578
|
+
WordArray.parse(value)
|
|
540
579
|
when /^\[.*\]$/
|
|
541
580
|
# config/importmap.rb is Ruby, not JSON, and a single-quoted pin is a
|
|
542
581
|
# supported shape here, so the entry points are scanned out of the
|
|
543
582
|
# literal rather than parsed. JSON.parse raised on every one of them.
|
|
544
|
-
value.scan(
|
|
583
|
+
Literal.of(value.scan(QUOTED_STRING_REGEXP).map { |double, single| unquote(double, single) }, value)
|
|
545
584
|
else
|
|
546
|
-
value.
|
|
585
|
+
Literal.of(unquote(*value.match(/\A#{QUOTED_STRING_REGEXP}\z/).captures), value)
|
|
547
586
|
end
|
|
548
587
|
end
|
|
549
588
|
|
|
589
|
+
# A single-quoted string unescapes only \\ and \'. In a double-quoted one
|
|
590
|
+
# a backslash before any character an entry point would hold is that
|
|
591
|
+
# character, which is all a preload name needs. The value is only read;
|
|
592
|
+
# a rewrite writes the Literal's source.
|
|
593
|
+
def unquote(double, single)
|
|
594
|
+
double ? double.gsub(/\\(.)/m, '\1') : single.gsub(/\\([\\'])/, '\1')
|
|
595
|
+
end
|
|
596
|
+
|
|
550
597
|
def preload(preloads)
|
|
551
598
|
# nil is "the pin carries no preload:"; [] is the pin carrying one that
|
|
552
599
|
# names no entry point. Array() flattens both to [], so the difference
|
|
553
600
|
# has to be read before it.
|
|
554
601
|
return "" if preloads.nil?
|
|
602
|
+
return %(, preload: #{preloads}) if preloads.is_a?(WordArray)
|
|
603
|
+
return %(, preload: #{preloads.source}) if preloads.is_a?(Literal)
|
|
555
604
|
|
|
556
605
|
case Array(preloads)
|
|
557
606
|
in []
|
|
@@ -561,7 +610,7 @@ class Importmap::Packager
|
|
|
561
610
|
in ["false"] | [false]
|
|
562
611
|
%(, preload: false)
|
|
563
612
|
in [string]
|
|
564
|
-
%(, preload:
|
|
613
|
+
%(, preload: #{string.to_s.inspect})
|
|
565
614
|
else
|
|
566
615
|
%(, preload: #{preloads})
|
|
567
616
|
end
|
data/lib/importmap/version.rb
CHANGED