importmap-plus 1.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 +7 -0
- data/CHANGELOG.md +46 -0
- data/MIT-LICENSE +20 -0
- data/README.md +457 -0
- data/Rakefile +17 -0
- data/app/controllers/importmap/freshness.rb +5 -0
- data/app/helpers/importmap/importmap_tags_helper.rb +47 -0
- data/lib/importmap/commands.rb +297 -0
- data/lib/importmap/engine.rb +73 -0
- data/lib/importmap/http_retries.rb +35 -0
- data/lib/importmap/map.rb +319 -0
- data/lib/importmap/minifier.rb +76 -0
- data/lib/importmap/npm.rb +189 -0
- data/lib/importmap/packager.rb +428 -0
- data/lib/importmap/reloader.rb +23 -0
- data/lib/importmap/version.rb +6 -0
- data/lib/importmap-plus.rb +4 -0
- data/lib/importmap-rails.rb +6 -0
- data/lib/install/bin/importmap +4 -0
- data/lib/install/config/importmap.rb +3 -0
- data/lib/install/install.rb +32 -0
- data/lib/tasks/importmap_tasks.rake +9 -0
- metadata +110 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "uri"
|
|
3
|
+
require "json"
|
|
4
|
+
require "importmap/minifier"
|
|
5
|
+
require "importmap/http_retries"
|
|
6
|
+
|
|
7
|
+
class Importmap::Packager
|
|
8
|
+
include Importmap::HttpRetries
|
|
9
|
+
|
|
10
|
+
PIN_REGEX = /#{Importmap::Map::PIN_REGEX}(.*)/.freeze # :nodoc:
|
|
11
|
+
PRELOAD_OPTION_REGEXP = /preload:\s*(\[[^\]]+\]|true|false|["'][^"']*["'])/.freeze # :nodoc:
|
|
12
|
+
TO_OPTION_REGEXP = /to:\s*["']([^"']*)["']/.freeze # :nodoc:
|
|
13
|
+
REMOTE_URL_REGEXP = %r{\Ahttps?://}.freeze # :nodoc:
|
|
14
|
+
|
|
15
|
+
PROVIDER_HOSTS = {
|
|
16
|
+
"ga.jspm.io" => "jspm.io",
|
|
17
|
+
"unpkg.com" => "unpkg",
|
|
18
|
+
"cdn.jsdelivr.net" => "jsdelivr",
|
|
19
|
+
"cdn.skypack.dev" => "skypack",
|
|
20
|
+
"esm.sh" => "esm.sh"
|
|
21
|
+
}.freeze # :nodoc:
|
|
22
|
+
|
|
23
|
+
# jsDelivr's bundling endpoint (https://www.jsdelivr.com/esm): one minified
|
|
24
|
+
# ESM file per package, with its dependencies referenced as /npm/dep@ver/+esm.
|
|
25
|
+
ESM_RUN_PROVIDER = "esm.run".freeze # :nodoc:
|
|
26
|
+
ESM_RUN_CDN = "https://cdn.jsdelivr.net/npm/".freeze # :nodoc:
|
|
27
|
+
ESM_RUN_URL_REGEXP = %r{\Ahttps://cdn\.jsdelivr\.net/npm/.+/\+esm\z}.freeze # :nodoc:
|
|
28
|
+
# An esm.run bundle's own imports: `from"/npm/dep@1.2.3/+esm"`, `import"…"`,
|
|
29
|
+
# `import("…")`, `export … from"…"`. Anchored on the keyword so an ordinary
|
|
30
|
+
# string that happens to look like a bundle URL is left alone. What it does
|
|
31
|
+
# not do is parse JavaScript, so the same text inside a string or a comment
|
|
32
|
+
# would still be rewritten — a jsDelivr bundle is esbuild output whose only
|
|
33
|
+
# surviving comment is the banner, and a root-relative /npm/ URL is
|
|
34
|
+
# meaningless anywhere but in one of its own imports.
|
|
35
|
+
ESM_RUN_IMPORT_REGEXP =
|
|
36
|
+
%r{((?:\bfrom|\bimport)\s*\(?\s*)(["'])/npm/((?:@[^/"'@]+/)?[^/"'@]+)@([^/"']+)((?:/[^"']*?)?)/\+esm\2}.freeze # :nodoc:
|
|
37
|
+
# name[@version][/subpath] — a leading "@" distinguishes a scoped name
|
|
38
|
+
# (@scope/pkg) from an unscoped name with a subpath (apexcharts/core).
|
|
39
|
+
PACKAGE_SPEC_REGEXP = %r{\A(@[^@/]+/[^@/]+|[^@/]+)(?:@([^/]+))?(/.+)?\z}.freeze # :nodoc:
|
|
40
|
+
# The version comment on a vendored pin, plus what it was built with:
|
|
41
|
+
# pin "luxon" # @3.7.2
|
|
42
|
+
# pin "luxon" # @3.7.2 (esm.run, minified)
|
|
43
|
+
PIN_PROVENANCE_REGEXP = /#\s*@([^\s(]+)(?:\s+\(([^)]*)\))?/.freeze # :nodoc:
|
|
44
|
+
DEFAULT_PROVIDER = "jspm.io".freeze # :nodoc:
|
|
45
|
+
|
|
46
|
+
Error = Class.new(StandardError)
|
|
47
|
+
HTTPError = Class.new(Error)
|
|
48
|
+
ServiceError = Error.new(Error)
|
|
49
|
+
|
|
50
|
+
singleton_class.attr_accessor :endpoint
|
|
51
|
+
self.endpoint = URI("https://api.jspm.io/generate")
|
|
52
|
+
|
|
53
|
+
singleton_class.attr_accessor :esm_run_resolver
|
|
54
|
+
self.esm_run_resolver = URI("https://data.jsdelivr.com/v1/packages/npm/")
|
|
55
|
+
|
|
56
|
+
# CDNs reset connections and rate-limit bursts. Each request is tried this
|
|
57
|
+
# many times, pausing retry_wait × attempt between tries, before it fails.
|
|
58
|
+
# Shared with Importmap::Npm, which talks to the registry the same way.
|
|
59
|
+
class << self
|
|
60
|
+
def retry_attempts = Importmap::HttpRetries.attempts
|
|
61
|
+
def retry_attempts=(value)
|
|
62
|
+
Importmap::HttpRetries.attempts = value
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def retry_wait = Importmap::HttpRetries.wait
|
|
66
|
+
def retry_wait=(value)
|
|
67
|
+
Importmap::HttpRetries.wait = value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Anything responding to #call(source) => String. Defaults to the first of
|
|
72
|
+
# bun, esbuild or terser found on the machine.
|
|
73
|
+
singleton_class.attr_writer :minifier
|
|
74
|
+
|
|
75
|
+
def self.minifier
|
|
76
|
+
@minifier ||= Importmap::Minifier.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
attr_reader :vendor_path
|
|
80
|
+
|
|
81
|
+
def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/javascript")
|
|
82
|
+
@importmap_path = Pathname.new(importmap_path)
|
|
83
|
+
@vendor_path = Pathname.new(vendor_path)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def import(*packages, env: "production", from: "jspm")
|
|
87
|
+
return import_from_esm_run(packages) if esm_run?(from)
|
|
88
|
+
|
|
89
|
+
response = post_json({
|
|
90
|
+
"install" => Array(packages),
|
|
91
|
+
"flattenScope" => true,
|
|
92
|
+
"env" => [ "browser", "module", env ],
|
|
93
|
+
"provider" => normalize_provider(from),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
case response.code
|
|
97
|
+
when "200"
|
|
98
|
+
extract_parsed_response(response)
|
|
99
|
+
when "404", "401"
|
|
100
|
+
nil
|
|
101
|
+
else
|
|
102
|
+
handle_failure_response(response)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def pin_for(package, url = nil, preloads: nil)
|
|
107
|
+
to = url ? %(, to: "#{url}") : ""
|
|
108
|
+
preload_param = preload(preloads)
|
|
109
|
+
|
|
110
|
+
%(pin "#{package}") + to + preload_param
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The pin line for a vendored download. The version comment also records
|
|
114
|
+
# the CDN when it isn't jspm and whether the file was minified, so a later
|
|
115
|
+
# update or pristine can do the same again:
|
|
116
|
+
#
|
|
117
|
+
# pin "luxon" # @3.7.2
|
|
118
|
+
# pin "luxon" # @3.7.2 (esm.run, minified)
|
|
119
|
+
#
|
|
120
|
+
def vendored_pin_for(package, url, preloads = nil, minify: false)
|
|
121
|
+
filename = package_filename(package)
|
|
122
|
+
version = extract_package_version_from(url)
|
|
123
|
+
to = "#{package}.js" != filename ? filename : nil
|
|
124
|
+
|
|
125
|
+
provenance = []
|
|
126
|
+
provenance << provider_for_url(url) if provider_for_url(url) && provider_for_url(url) != DEFAULT_PROVIDER
|
|
127
|
+
provenance << "minified" if minify
|
|
128
|
+
|
|
129
|
+
pin_for(package, to, preloads: preloads) + %( # #{version}) + (provenance.any? ? %( (#{provenance.join(", ")})) : "")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# What the pin's version comment says a vendored package was built with:
|
|
133
|
+
# { version:, provider:, minified: }, or nil for a pin without one.
|
|
134
|
+
def pin_provenance(package)
|
|
135
|
+
return unless @importmap_path.exist?
|
|
136
|
+
|
|
137
|
+
line = importmap.lines.find { |candidate| candidate.match?(Importmap::Map.pin_line_regexp_for(package)) }
|
|
138
|
+
match = line&.match(PIN_PROVENANCE_REGEXP)
|
|
139
|
+
return unless match
|
|
140
|
+
|
|
141
|
+
details = match[2].to_s.split(",").map(&:strip)
|
|
142
|
+
minified = details.delete("minified") ? true : false
|
|
143
|
+
|
|
144
|
+
{ version: match[1], provider: details.first, minified: minified }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def packaged?(package)
|
|
148
|
+
importmap.match(Importmap::Map.pin_line_regexp_for(package))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Downloads +url+ into vendor/javascript. With +minify: true+ the source is
|
|
152
|
+
# run through .minifier first and the file header records it, so later
|
|
153
|
+
# updates keep minifying. Returns the dependencies an esm.run bundle imports
|
|
154
|
+
# as [package, url] pairs (empty for every other provider), with the bundle's
|
|
155
|
+
# absolute /npm/... imports rewritten to bare specifiers on the way in.
|
|
156
|
+
def download(package, url, minify: false)
|
|
157
|
+
ensure_vendor_directory_exists
|
|
158
|
+
remove_existing_package_file(package)
|
|
159
|
+
download_package_file(package, url, minify: minify)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def remove(package)
|
|
163
|
+
remove_existing_package_file(package)
|
|
164
|
+
remove_package_from_importmap(package)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_existing_pin_options(packages)
|
|
168
|
+
return {} unless @importmap_path.exist?
|
|
169
|
+
|
|
170
|
+
packages = Array(packages)
|
|
171
|
+
|
|
172
|
+
all_package_options = build_package_options_lookup(importmap.lines)
|
|
173
|
+
|
|
174
|
+
packages.to_h do |package|
|
|
175
|
+
[package, all_package_options[package] || {}]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Drops the cached import map so a read after a write sees the new file:
|
|
180
|
+
# pinning an esm.run bundle appends pins and then asks the map what its
|
|
181
|
+
# dependencies still need.
|
|
182
|
+
def reload!
|
|
183
|
+
@importmap = nil
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The provenance a pin would record for +url+, shaped like #pin_provenance
|
|
188
|
+
# returns, so a caller can tell whether the existing pin already says this.
|
|
189
|
+
def provenance_for(url, minify: false)
|
|
190
|
+
provider = provider_for_url(url)
|
|
191
|
+
|
|
192
|
+
{ provider: provider == DEFAULT_PROVIDER ? nil : provider, minified: minify ? true : false }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def remote_pin?(package)
|
|
196
|
+
options = extract_existing_pin_options(package)[package] || {}
|
|
197
|
+
options[:to].to_s.match?(REMOTE_URL_REGEXP)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def provider_for_url(url)
|
|
201
|
+
return ESM_RUN_PROVIDER if url.to_s.match?(ESM_RUN_URL_REGEXP)
|
|
202
|
+
|
|
203
|
+
PROVIDER_HOSTS[URI(url.to_s).host]
|
|
204
|
+
rescue URI::InvalidURIError
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def esm_run?(provider)
|
|
209
|
+
provider.to_s == ESM_RUN_PROVIDER
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The import-map key a package spec pins: "apexcharts@7.1.0/core" pins
|
|
213
|
+
# "apexcharts/core", "@hotwired/stimulus@3" pins "@hotwired/stimulus".
|
|
214
|
+
def package_key_for(spec)
|
|
215
|
+
name, _version, subpath = spec.to_s.match(PACKAGE_SPEC_REGEXP)&.captures
|
|
216
|
+
name ? "#{name}#{subpath}" : spec.to_s
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def remove_existing_package_file(package)
|
|
220
|
+
FileUtils.rm_rf vendored_package_path(package)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def extract_package_version_from(url)
|
|
224
|
+
url.match(/@\d+\.\d+\.\d+[^\/\s"']*/)&.to_a&.first
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
def build_package_options_lookup(lines)
|
|
229
|
+
lines.each_with_object({}) do |line, package_options|
|
|
230
|
+
match = line.strip.match(PIN_REGEX)
|
|
231
|
+
|
|
232
|
+
if match
|
|
233
|
+
package_name = match[1]
|
|
234
|
+
options_part = match[2]
|
|
235
|
+
|
|
236
|
+
options = {}
|
|
237
|
+
|
|
238
|
+
if (preload_match = options_part.match(PRELOAD_OPTION_REGEXP))
|
|
239
|
+
options[:preload] = preload_from_string(preload_match[1])
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
if (to_match = options_part.match(TO_OPTION_REGEXP))
|
|
243
|
+
options[:to] = to_match[1]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
package_options[package_name] = options if options.any?
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def preload_from_string(value)
|
|
252
|
+
case value
|
|
253
|
+
when "true"
|
|
254
|
+
true
|
|
255
|
+
when "false"
|
|
256
|
+
false
|
|
257
|
+
when /^\[.*\]$/
|
|
258
|
+
JSON.parse(value)
|
|
259
|
+
else
|
|
260
|
+
value.gsub(/["']/, "")
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def preload(preloads)
|
|
265
|
+
case Array(preloads)
|
|
266
|
+
in []
|
|
267
|
+
""
|
|
268
|
+
in ["true"] | [true]
|
|
269
|
+
%(, preload: true)
|
|
270
|
+
in ["false"] | [false]
|
|
271
|
+
%(, preload: false)
|
|
272
|
+
in [string]
|
|
273
|
+
%(, preload: "#{string}")
|
|
274
|
+
else
|
|
275
|
+
%(, preload: #{preloads})
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def post_json(body)
|
|
280
|
+
with_retries("posting to #{self.class.endpoint}") do
|
|
281
|
+
Net::HTTP.post(self.class.endpoint, body.to_json, "Content-Type" => "application/json")
|
|
282
|
+
end
|
|
283
|
+
rescue HTTPError
|
|
284
|
+
raise
|
|
285
|
+
rescue => error
|
|
286
|
+
raise HTTPError, "Unexpected transport error (#{error.class}: #{error.message})"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def normalize_provider(name)
|
|
290
|
+
name.to_s == "jspm" ? "jspm.io" : name.to_s
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def extract_parsed_response(response)
|
|
294
|
+
parsed = JSON.parse(response.body)
|
|
295
|
+
imports = parsed.dig("map", "imports")
|
|
296
|
+
|
|
297
|
+
{
|
|
298
|
+
imports: imports,
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def handle_failure_response(response)
|
|
303
|
+
if error_message = parse_service_error(response)
|
|
304
|
+
raise ServiceError, error_message
|
|
305
|
+
else
|
|
306
|
+
raise HTTPError, "Unexpected response code (#{response.code})"
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def parse_service_error(response)
|
|
311
|
+
JSON.parse(response.body.to_s)["error"]
|
|
312
|
+
rescue JSON::ParserError
|
|
313
|
+
nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def importmap
|
|
317
|
+
@importmap ||= File.read(@importmap_path)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def ensure_vendor_directory_exists
|
|
322
|
+
FileUtils.mkdir_p @vendor_path
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def remove_package_from_importmap(package)
|
|
326
|
+
all_lines = File.readlines(@importmap_path)
|
|
327
|
+
with_lines_removed = all_lines.grep_v(Importmap::Map.pin_line_regexp_for(package))
|
|
328
|
+
|
|
329
|
+
File.open(@importmap_path, "w") do |file|
|
|
330
|
+
with_lines_removed.each { |line| file.write(line) }
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def download_package_file(package, url, minify: false)
|
|
335
|
+
response = with_retries("downloading #{url}") { Net::HTTP.get_response(URI(url)) }
|
|
336
|
+
|
|
337
|
+
if response.code == "200"
|
|
338
|
+
source = response.body.dup.force_encoding("UTF-8")
|
|
339
|
+
source, dependencies = rewrite_esm_run_imports(source) if url.match?(ESM_RUN_URL_REGEXP)
|
|
340
|
+
source = self.class.minifier.call(source) if minify
|
|
341
|
+
|
|
342
|
+
save_vendored_package(package, url, source, minified: minify)
|
|
343
|
+
|
|
344
|
+
dependencies || []
|
|
345
|
+
else
|
|
346
|
+
handle_failure_response(response)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def save_vendored_package(package, url, source, minified: false)
|
|
351
|
+
File.open(vendored_package_path(package), "w+") do |vendored_package|
|
|
352
|
+
vendored_package.write "// #{package}#{extract_package_version_from(url)} downloaded from #{url}#{" (minified)" if minified}\n\n"
|
|
353
|
+
|
|
354
|
+
vendored_package.write remove_sourcemap_comment_from(source).force_encoding("UTF-8")
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Turns import "/npm/dep@1.2.3/+esm" into import "dep" so the bundle
|
|
359
|
+
# resolves through the import map, and lists what it needs pinned.
|
|
360
|
+
def rewrite_esm_run_imports(source)
|
|
361
|
+
dependencies = {}
|
|
362
|
+
versions = Hash.new { |hash, key| hash[key] = [] }
|
|
363
|
+
|
|
364
|
+
rewritten = source.gsub(ESM_RUN_IMPORT_REGEXP) do
|
|
365
|
+
keyword, quote, name, version, subpath = $1, $2, $3, $4, $5.to_s
|
|
366
|
+
key = "#{name}#{subpath}"
|
|
367
|
+
dependencies[key] ||= "#{ESM_RUN_CDN}#{name}@#{version}#{subpath}/+esm"
|
|
368
|
+
versions[key] << version unless versions[key].include?(version)
|
|
369
|
+
"#{keyword}#{quote}#{name}#{subpath}#{quote}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# An import map maps a bare specifier to one file, so a bundle that
|
|
373
|
+
# imports the same package at two versions can only get the first one
|
|
374
|
+
# it asked for. Say so rather than pick silently.
|
|
375
|
+
versions.each do |key, seen|
|
|
376
|
+
next if seen.one?
|
|
377
|
+
|
|
378
|
+
warn %(#{key} is imported at #{seen.join(", ")} by this bundle; pinning @#{seen.first}, an import map holds one version)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
[rewritten, dependencies.to_a]
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def import_from_esm_run(packages)
|
|
385
|
+
imports = packages.to_h do |spec|
|
|
386
|
+
name, requested, subpath = spec.match(PACKAGE_SPEC_REGEXP)&.captures
|
|
387
|
+
raise Error, "Can't parse package spec #{spec.inspect}" unless name
|
|
388
|
+
|
|
389
|
+
version = resolve_esm_run_version(name, requested)
|
|
390
|
+
return nil unless version
|
|
391
|
+
|
|
392
|
+
["#{name}#{subpath}", "#{ESM_RUN_CDN}#{name}@#{version}#{subpath}/+esm"]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
{ imports: imports }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def resolve_esm_run_version(name, requested)
|
|
399
|
+
uri = self.class.esm_run_resolver.dup
|
|
400
|
+
uri.path += "#{name}/resolved"
|
|
401
|
+
uri.query = "specifier=#{URI.encode_www_form_component(requested)}" if requested
|
|
402
|
+
|
|
403
|
+
response = with_retries("resolving #{uri}") { Net::HTTP.get_response(uri) }
|
|
404
|
+
|
|
405
|
+
case response.code
|
|
406
|
+
when "200"
|
|
407
|
+
JSON.parse(response.body)["version"]
|
|
408
|
+
when "404"
|
|
409
|
+
nil
|
|
410
|
+
else
|
|
411
|
+
handle_failure_response(response)
|
|
412
|
+
end
|
|
413
|
+
rescue JSON::ParserError
|
|
414
|
+
raise HTTPError, "Unexpected response from #{uri}"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def remove_sourcemap_comment_from(source)
|
|
418
|
+
source.gsub(/^\/\/# sourceMappingURL=.*/, "")
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def vendored_package_path(package)
|
|
422
|
+
@vendor_path.join(package_filename(package))
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def package_filename(package)
|
|
426
|
+
package.gsub("/", "--") + ".js"
|
|
427
|
+
end
|
|
428
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "active_support"
|
|
2
|
+
require "active_support/core_ext/module/delegation"
|
|
3
|
+
|
|
4
|
+
class Importmap::Reloader
|
|
5
|
+
delegate :execute_if_updated, :execute, :updated?, to: :updater
|
|
6
|
+
|
|
7
|
+
def reload!
|
|
8
|
+
import_map_paths.each { |path| Rails.application.importmap.draw(path) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
def updater
|
|
13
|
+
@updater ||= config.file_watcher.new(import_map_paths) { reload! }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def import_map_paths
|
|
17
|
+
config.importmap.paths
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config
|
|
21
|
+
Rails.application.config
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
APPLICATION_LAYOUT_PATH = Rails.root.join("app/views/layouts/application.html.erb")
|
|
2
|
+
|
|
3
|
+
if APPLICATION_LAYOUT_PATH.exist?
|
|
4
|
+
say "Add Importmap include tags in application layout"
|
|
5
|
+
insert_into_file APPLICATION_LAYOUT_PATH.to_s, "\n <%= javascript_importmap_tags %>", before: /\s*<\/head>/
|
|
6
|
+
else
|
|
7
|
+
say "Default application.html.erb is missing!", :red
|
|
8
|
+
say " Add <%= javascript_importmap_tags %> within the <head> tag in your custom layout."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
say "Create application.js module as entrypoint"
|
|
12
|
+
create_file Rails.root.join("app/javascript/application.js") do <<-JS
|
|
13
|
+
// Configure your import map in config/importmap.rb. Read more: https://github.com/zoolutions/importmap-plus
|
|
14
|
+
JS
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
say "Use vendor/javascript for downloaded pins"
|
|
18
|
+
empty_directory "vendor/javascript"
|
|
19
|
+
keep_file "vendor/javascript"
|
|
20
|
+
|
|
21
|
+
if (sprockets_manifest_path = Rails.root.join("app/assets/config/manifest.js")).exist?
|
|
22
|
+
say "Ensure JavaScript files are in the Sprocket manifest"
|
|
23
|
+
append_to_file sprockets_manifest_path,
|
|
24
|
+
%(//= link_tree ../../javascript .js\n//= link_tree ../../../vendor/javascript .js\n)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
say "Configure importmap paths in config/importmap.rb"
|
|
28
|
+
copy_file "#{__dir__}/config/importmap.rb", "config/importmap.rb"
|
|
29
|
+
|
|
30
|
+
say "Copying binstub"
|
|
31
|
+
copy_file "#{__dir__}/bin/importmap", "bin/importmap"
|
|
32
|
+
chmod "bin", 0755 & ~File.umask, verbose: false
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
namespace :importmap do
|
|
2
|
+
desc "Setup Importmap for the app"
|
|
3
|
+
task :install do
|
|
4
|
+
previous_location = ENV["LOCATION"]
|
|
5
|
+
ENV["LOCATION"] = File.expand_path("../install/install.rb", __dir__)
|
|
6
|
+
Rake::Task["app:template"].invoke
|
|
7
|
+
ENV["LOCATION"] = previous_location
|
|
8
|
+
end
|
|
9
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: importmap-plus
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Heinemeier Hansson
|
|
8
|
+
- Mikael Henriksson
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 6.0.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 6.0.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 6.0.0
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 6.0.0
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: actionpack
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 6.0.0
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 6.0.0
|
|
55
|
+
description: 'A drop-in replacement for importmap-rails. Same Importmap:: API, same
|
|
56
|
+
pins, same import map, plus `bin/importmap pin --minify`, `--from esm.run` for jsDelivr''s
|
|
57
|
+
bundled builds, and a pin comment that records the CDN and minification so later
|
|
58
|
+
updates keep them. Use this gem or importmap-rails, never both.'
|
|
59
|
+
email: mikael@mhenrixon.com
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- MIT-LICENSE
|
|
66
|
+
- README.md
|
|
67
|
+
- Rakefile
|
|
68
|
+
- app/controllers/importmap/freshness.rb
|
|
69
|
+
- app/helpers/importmap/importmap_tags_helper.rb
|
|
70
|
+
- lib/importmap-plus.rb
|
|
71
|
+
- lib/importmap-rails.rb
|
|
72
|
+
- lib/importmap/commands.rb
|
|
73
|
+
- lib/importmap/engine.rb
|
|
74
|
+
- lib/importmap/http_retries.rb
|
|
75
|
+
- lib/importmap/map.rb
|
|
76
|
+
- lib/importmap/minifier.rb
|
|
77
|
+
- lib/importmap/npm.rb
|
|
78
|
+
- lib/importmap/packager.rb
|
|
79
|
+
- lib/importmap/reloader.rb
|
|
80
|
+
- lib/importmap/version.rb
|
|
81
|
+
- lib/install/bin/importmap
|
|
82
|
+
- lib/install/config/importmap.rb
|
|
83
|
+
- lib/install/install.rb
|
|
84
|
+
- lib/tasks/importmap_tasks.rake
|
|
85
|
+
homepage: https://github.com/zoolutions/importmap-plus
|
|
86
|
+
licenses:
|
|
87
|
+
- MIT
|
|
88
|
+
metadata:
|
|
89
|
+
homepage_uri: https://github.com/zoolutions/importmap-plus
|
|
90
|
+
source_code_uri: https://github.com/zoolutions/importmap-plus
|
|
91
|
+
changelog_uri: https://github.com/zoolutions/importmap-plus/blob/main/CHANGELOG.md
|
|
92
|
+
rdoc_options: []
|
|
93
|
+
require_paths:
|
|
94
|
+
- lib
|
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: 3.1.0
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
requirements:
|
|
102
|
+
- - ">="
|
|
103
|
+
- !ruby/object:Gem::Version
|
|
104
|
+
version: '0'
|
|
105
|
+
requirements: []
|
|
106
|
+
rubygems_version: 4.0.19
|
|
107
|
+
specification_version: 4
|
|
108
|
+
summary: importmap-rails with vendoring that minifies, bundles from esm.run, and remembers
|
|
109
|
+
where each package came from.
|
|
110
|
+
test_files: []
|