assiette 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/assiette/asset_handler.rb +111 -12
- data/lib/assiette/dependency_graph.rb +62 -21
- data/lib/assiette/helpers.rb +9 -2
- data/lib/assiette/rewriter.rb +26 -6
- data/lib/assiette/server.rb +4 -0
- data/lib/assiette/version.rb +1 -1
- data/lib/generators/assiette/install/templates/initializer.rb.tt +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0f3b02555b0eb18e84813e36095d9fbd3528e9d7a61ed529e00c5b3dd6ecd5a
|
|
4
|
+
data.tar.gz: 20a0ec6ce64278f556608150ba66bf9429bef13f8bc45d41bc71e360f3f0151f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6e01429228aefba47fc36242f8180b074c58c65297ed490ac6b94562988396d1a375acb8b6e31c6a3514f009960f5caf0dc7ffb56dea75d2247886aa89044ef
|
|
7
|
+
data.tar.gz: 5094d53275d9d5fc760f97c24905a840d55254e768aebfd979928d9afcb8c014819e443ee7e658600dd70aaa26825dcf4b95733581817cc7a9e98b33e91b7fc2
|
|
@@ -22,6 +22,9 @@ module Assiette
|
|
|
22
22
|
|
|
23
23
|
JS_EXTENSIONS = %w[.js .mjs].to_set.freeze
|
|
24
24
|
|
|
25
|
+
# What #expose takes as a handle: "@" and a name, as in "@task_progress"
|
|
26
|
+
HANDLE_RE = /\A@[a-z0-9][\w.-]*\z/i
|
|
27
|
+
|
|
25
28
|
attr_reader :dependency_graph
|
|
26
29
|
|
|
27
30
|
# Every extension this handler serves, as ".ext" => content type. This is
|
|
@@ -42,9 +45,46 @@ module Assiette
|
|
|
42
45
|
def initialize(root:, additional_directory_mappings: {}, content_types: {})
|
|
43
46
|
@content_types = CONTENT_TYPES.merge(content_types.to_h { |ext, type| [normalize_extension(ext), type] }).freeze
|
|
44
47
|
@mappings = build_mappings(root, additional_directory_mappings)
|
|
48
|
+
@exposed = {}
|
|
45
49
|
@dependency_graph = DependencyGraph.new(self)
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
# Makes a directory - a gem's, say - available as "@handle/...". Its files
|
|
53
|
+
# are served at /@handle/..., and an import of "@handle/file.js" from any
|
|
54
|
+
# module this handler serves is rewritten to that file, fingerprinted:
|
|
55
|
+
#
|
|
56
|
+
# handler.expose("@task_progress", TaskProgress.js_path)
|
|
57
|
+
# import { followProgress } from "@task_progress/task_tree.js"
|
|
58
|
+
#
|
|
59
|
+
# Several directories under one handle are searched in the order given, and
|
|
60
|
+
# the root still comes first, so the app's own app/assets/@handle/file.js
|
|
61
|
+
# wins over the one exposed.
|
|
62
|
+
#
|
|
63
|
+
# Exposing a handle again with the same directories does nothing - an
|
|
64
|
+
# initializer that runs twice is the likely cause, and harmless. With other
|
|
65
|
+
# directories, the new ones replace the old, with a warning.
|
|
66
|
+
#
|
|
67
|
+
# Call it while setting up, before the handler serves anything.
|
|
68
|
+
def expose(handle, directories)
|
|
69
|
+
handle = handle.to_s
|
|
70
|
+
raise ArgumentError, "#{handle.inspect} is not a handle - it needs to look like \"@name\"" unless handle.match?(HANDLE_RE)
|
|
71
|
+
roots = Array(directories).map { |dir| Pathname.new(dir).expand_path }
|
|
72
|
+
return self if @exposed[handle] == roots
|
|
73
|
+
if @exposed.key?(handle)
|
|
74
|
+
warn "Assiette: #{handle} was exposed from #{@exposed[handle].join(", ")}, and now from #{roots.join(", ")} instead"
|
|
75
|
+
end
|
|
76
|
+
@exposed[handle] = roots
|
|
77
|
+
# A new array rather than changing this one, so a thread walking it is not disturbed
|
|
78
|
+
@mappings = @mappings.reject { |prefix, _root| prefix == handle } + roots.map { |root| [handle, root] }
|
|
79
|
+
@watched_directories = nil
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The exposed handles, each with the directories behind it
|
|
84
|
+
def exposed
|
|
85
|
+
@exposed.dup
|
|
86
|
+
end
|
|
87
|
+
|
|
48
88
|
# The content type this handler serves `path` as, or nil if its extension
|
|
49
89
|
# is not one this handler knows.
|
|
50
90
|
def content_type_for(path)
|
|
@@ -91,8 +131,9 @@ module Assiette
|
|
|
91
131
|
def absolute_asset_url_path(path, script_name = "")
|
|
92
132
|
clean = path.sub(%r{\A/}, "")
|
|
93
133
|
return nil unless resolve_file(clean)
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
# No digest means the file went away mid-call. A constant tag would get cached like a real version
|
|
135
|
+
hash = @dependency_graph.tree_sha(clean)
|
|
136
|
+
hash ? "#{script_name}/#{clean}?s=#{hash}" : "#{script_name}/#{clean}"
|
|
96
137
|
end
|
|
97
138
|
|
|
98
139
|
def asset_integrity(path)
|
|
@@ -101,16 +142,37 @@ module Assiette
|
|
|
101
142
|
@dependency_graph.tree_integrity(clean)
|
|
102
143
|
end
|
|
103
144
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
# The modules to preload: every module under the handler's own roots, and
|
|
146
|
+
# of the exposed directories only what those modules reach through their
|
|
147
|
+
# imports. A static import and a literal `import("...")` count alike, since
|
|
148
|
+
# the graph records both, so the browser is handed the whole tree up front
|
|
149
|
+
# and discovers nothing on its own. An app that imports one file of a gem
|
|
150
|
+
# preloads that file and what it pulls in, and none of the gem's other
|
|
151
|
+
# modules.
|
|
152
|
+
#
|
|
153
|
+
# `exposed:` changes what the exposed directories contribute:
|
|
154
|
+
#
|
|
155
|
+
# :imported what the own modules reach (the default)
|
|
156
|
+
# :all every module of every exposed directory
|
|
157
|
+
# :none, [] nothing
|
|
158
|
+
# "@gem", ["@a", ..] every module of those handles, and in the other
|
|
159
|
+
# handles still what is reached - for an app whose
|
|
160
|
+
# `import()` of a gem file is computed at run time,
|
|
161
|
+
# which no scan can see
|
|
162
|
+
def js_modules(exposed: :imported)
|
|
163
|
+
own_mappings, exposed_mappings = @mappings.partition { |prefix, _root| !@exposed.key?(prefix) }
|
|
164
|
+
own = modules_under(own_mappings)
|
|
165
|
+
whole_handles, follow_imports = case exposed
|
|
166
|
+
when :imported then [[], true]
|
|
167
|
+
when :all, true then [@exposed.keys, false]
|
|
168
|
+
when :none, nil then [[], false]
|
|
169
|
+
else
|
|
170
|
+
handles = Array(exposed).map(&:to_s)
|
|
171
|
+
[handles, handles.any?]
|
|
172
|
+
end
|
|
173
|
+
listed = modules_under(exposed_mappings.select { |mapping| whole_handles.include?(mapping.first) })
|
|
174
|
+
reached = follow_imports ? exposed_modules_imported_by(own) : []
|
|
175
|
+
(own + listed + reached).uniq { |m| m[:path] }.sort_by { |m| m[:path] }
|
|
114
176
|
end
|
|
115
177
|
|
|
116
178
|
# A single 16-char hex hash covering every asset this handler can serve.
|
|
@@ -154,6 +216,43 @@ module Assiette
|
|
|
154
216
|
|
|
155
217
|
private
|
|
156
218
|
|
|
219
|
+
# Every file under `mappings` with a JavaScript extension that imports or
|
|
220
|
+
# exports something, as {path:, integrity:}
|
|
221
|
+
def modules_under(mappings)
|
|
222
|
+
js_glob = "**/*{#{javascript_extensions.map { |ext| case_insensitive(ext) }.join(",")}}"
|
|
223
|
+
mappings.flat_map { |prefix, root|
|
|
224
|
+
Dir.glob(File.join(root, js_glob)).filter_map { |abs|
|
|
225
|
+
next unless File.foreach(abs).any? { |line| line.match?(/\A\s*(import|export)\s/) }
|
|
226
|
+
relative = Pathname.new(abs).relative_path_from(root).to_s
|
|
227
|
+
mod_path = "/#{"#{prefix}/" unless prefix.empty?}#{relative}".squeeze("/")
|
|
228
|
+
{path: mod_path, integrity: asset_integrity(mod_path)}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# The exposed JavaScript modules reached from `modules` by walking the
|
|
234
|
+
# graph's deps. Listing `modules` resolved each of them already, so the
|
|
235
|
+
# walk is over nodes that are in memory, with their mtimes checked.
|
|
236
|
+
def exposed_modules_imported_by(modules)
|
|
237
|
+
seen = Set.new
|
|
238
|
+
queue = modules.map { |m| m[:path].delete_prefix("/") }
|
|
239
|
+
found = []
|
|
240
|
+
until queue.empty?
|
|
241
|
+
url_path = queue.shift
|
|
242
|
+
next unless seen.add?(url_path)
|
|
243
|
+
asset = @dependency_graph[url_path]
|
|
244
|
+
next unless asset
|
|
245
|
+
found << {path: "/#{url_path}", integrity: asset.sri_integrity} if exposed_javascript?(url_path)
|
|
246
|
+
queue.concat(asset.deps)
|
|
247
|
+
end
|
|
248
|
+
found
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def exposed_javascript?(url_path)
|
|
252
|
+
@exposed.each_key.any? { |handle| url_path.start_with?("#{handle}/") } &&
|
|
253
|
+
javascript_extensions.include?(normalize_extension(File.extname(url_path)))
|
|
254
|
+
end
|
|
255
|
+
|
|
157
256
|
# The extensions served as JavaScript: .js and .mjs, plus anything this
|
|
158
257
|
# handler had registered with a JavaScript content type.
|
|
159
258
|
def javascript_extensions
|
|
@@ -10,12 +10,15 @@ module Assiette
|
|
|
10
10
|
class Asset
|
|
11
11
|
attr_reader :url_path
|
|
12
12
|
attr_accessor :abs_path, :mtime, :digest, :deps, :dependents
|
|
13
|
+
# Set only for cycle members, whose served bytes differ from what the shared digest covers
|
|
14
|
+
attr_accessor :served_digest
|
|
13
15
|
|
|
14
16
|
def initialize(url_path:, abs_path:, mtime:)
|
|
15
17
|
@url_path = url_path.freeze
|
|
16
18
|
@abs_path = abs_path
|
|
17
19
|
@mtime = mtime
|
|
18
20
|
@digest = nil
|
|
21
|
+
@served_digest = nil
|
|
19
22
|
@deps = []
|
|
20
23
|
@dependents = Set.new
|
|
21
24
|
end
|
|
@@ -27,8 +30,9 @@ module Assiette
|
|
|
27
30
|
|
|
28
31
|
# SRI integrity string for integrity= attributes.
|
|
29
32
|
def sri_integrity
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
sri_digest = served_digest || digest
|
|
34
|
+
return nil unless sri_digest
|
|
35
|
+
"sha256-#{Base64.strict_encode64(sri_digest)}"
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
# Whether this asset's file has changed since last scan.
|
|
@@ -67,9 +71,12 @@ module Assiette
|
|
|
67
71
|
self[url_path]&.sri_integrity
|
|
68
72
|
end
|
|
69
73
|
|
|
70
|
-
# Resolves a relative import path to a URL path.
|
|
74
|
+
# Resolves a relative import path to a URL path. An "@handle/..." path is
|
|
75
|
+
# already one - see AssetHandler#expose
|
|
71
76
|
def resolve_import_for(from_url_path, relative_path)
|
|
72
|
-
if relative_path.start_with?("
|
|
77
|
+
if relative_path.start_with?("@")
|
|
78
|
+
relative_path
|
|
79
|
+
elsif relative_path.start_with?("/")
|
|
73
80
|
relative_path.sub(%r{\A/}, "")
|
|
74
81
|
else
|
|
75
82
|
dir = File.dirname(from_url_path)
|
|
@@ -155,7 +162,7 @@ module Assiette
|
|
|
155
162
|
end
|
|
156
163
|
|
|
157
164
|
if existing.stale?
|
|
158
|
-
rescan_asset!(existing)
|
|
165
|
+
return nil unless rescan_asset!(existing)
|
|
159
166
|
@checked << url_path
|
|
160
167
|
return existing
|
|
161
168
|
end
|
|
@@ -239,9 +246,12 @@ module Assiette
|
|
|
239
246
|
end
|
|
240
247
|
|
|
241
248
|
# Re-read a stale asset, re-parse deps, recursively ensure deps fresh, recompute digest.
|
|
249
|
+
# Returns true, or false if the file went away while we were looking at it.
|
|
242
250
|
def rescan_asset!(asset)
|
|
243
251
|
url_path = asset.url_path
|
|
244
|
-
|
|
252
|
+
raw, mtime = read_or_drop!(asset)
|
|
253
|
+
return false unless raw
|
|
254
|
+
asset.mtime = mtime
|
|
245
255
|
|
|
246
256
|
# Remove old reverse links
|
|
247
257
|
asset.deps.each do |dep_path|
|
|
@@ -249,8 +259,6 @@ module Assiette
|
|
|
249
259
|
dep&.dependents&.delete(url_path)
|
|
250
260
|
end
|
|
251
261
|
|
|
252
|
-
# Re-parse imports
|
|
253
|
-
raw = File.read(asset.abs_path)
|
|
254
262
|
asset.deps = parse_deps(url_path, raw)
|
|
255
263
|
|
|
256
264
|
# Recursively ensure deps are fresh
|
|
@@ -264,9 +272,20 @@ module Assiette
|
|
|
264
272
|
|
|
265
273
|
# Propagate to dependents already in the graph
|
|
266
274
|
propagate_to_dependents!(asset)
|
|
275
|
+
|
|
276
|
+
# Not the walk's value: a `while` evaluates to nil, which reads as "gone"
|
|
277
|
+
true
|
|
267
278
|
end
|
|
268
279
|
|
|
269
280
|
# Recompute digests for all transitive dependents of an asset.
|
|
281
|
+
#
|
|
282
|
+
# A dependent whose own file has since gone is dropped rather than
|
|
283
|
+
# recomputed. Nothing ever resolves an orphan from above — it is only
|
|
284
|
+
# reached from below, through this very walk — so without this a file
|
|
285
|
+
# renamed away lingers in the graph as an importer, and the next edit to
|
|
286
|
+
# anything it used to import tries to read a path that is not there. Its
|
|
287
|
+
# own dependents still have to hear about it: they keep the vanished node
|
|
288
|
+
# in their deps until they are rescanned, and it rewrites without ?s= now.
|
|
270
289
|
def propagate_to_dependents!(asset)
|
|
271
290
|
queue = asset.dependents.to_a
|
|
272
291
|
visited = Set.new
|
|
@@ -274,8 +293,12 @@ module Assiette
|
|
|
274
293
|
next unless visited.add?(dep_url)
|
|
275
294
|
dep_asset = @assets[dep_url]
|
|
276
295
|
next unless dep_asset
|
|
277
|
-
compute_digest_for(dep_url)
|
|
278
296
|
queue.concat(dep_asset.dependents.to_a)
|
|
297
|
+
if dep_asset.deleted?
|
|
298
|
+
remove_asset!(dep_url)
|
|
299
|
+
else
|
|
300
|
+
compute_digest_for(dep_url)
|
|
301
|
+
end
|
|
279
302
|
end
|
|
280
303
|
end
|
|
281
304
|
|
|
@@ -289,11 +312,27 @@ module Assiette
|
|
|
289
312
|
import_paths.map { |p| resolve_import_for(url_path, p) }
|
|
290
313
|
end
|
|
291
314
|
|
|
315
|
+
# Reads an asset's bytes, together with the mtime they were read at, and
|
|
316
|
+
# returns nil after dropping the node if the file is not there any more.
|
|
317
|
+
#
|
|
318
|
+
# Every read of a path the graph remembers goes through here. Those paths
|
|
319
|
+
# come from an earlier request, and while the server is up any of them can
|
|
320
|
+
# be renamed or deleted between two renders — a disappearance has to read
|
|
321
|
+
# as "gone from the graph", never as an exception out of a view helper.
|
|
322
|
+
def read_or_drop!(asset)
|
|
323
|
+
[File.read(asset.abs_path), File.mtime(asset.abs_path)]
|
|
324
|
+
rescue Errno::ENOENT
|
|
325
|
+
remove_asset!(asset.url_path)
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
292
329
|
def compute_digest_for(url_path)
|
|
293
330
|
asset = @assets[url_path]
|
|
294
331
|
return unless asset
|
|
295
332
|
|
|
296
|
-
raw =
|
|
333
|
+
raw, _mtime = read_or_drop!(asset)
|
|
334
|
+
return unless raw
|
|
335
|
+
asset.served_digest = nil
|
|
297
336
|
if asset.deps.empty?
|
|
298
337
|
asset.digest = Digest::SHA256.digest(raw)
|
|
299
338
|
else
|
|
@@ -303,17 +342,19 @@ module Assiette
|
|
|
303
342
|
end
|
|
304
343
|
|
|
305
344
|
# For cyclic dependencies: hash all members' raw contents together.
|
|
345
|
+
# That shared digest only fingerprints - each member is served rewritten with
|
|
346
|
+
# its partners' ?s=, so SRI has to be taken over those bytes or the browser refuses it
|
|
306
347
|
def compute_cycle_digests(scc)
|
|
307
|
-
|
|
348
|
+
raws = scc.sort.each_with_object({}) do |url_path, acc|
|
|
308
349
|
asset = @assets[url_path]
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
350
|
+
raw = asset && read_or_drop!(asset)&.first
|
|
351
|
+
acc[url_path] = raw if raw
|
|
352
|
+
end
|
|
312
353
|
|
|
313
|
-
digest = Digest::SHA256.digest(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
354
|
+
digest = Digest::SHA256.digest(raws.values.join("\0"))
|
|
355
|
+
raws.each_key { |url_path| @assets[url_path].digest = digest }
|
|
356
|
+
raws.each do |url_path, raw|
|
|
357
|
+
@assets[url_path].served_digest = Digest::SHA256.digest(rewrite_content_internal(url_path, raw))
|
|
317
358
|
end
|
|
318
359
|
end
|
|
319
360
|
|
|
@@ -321,14 +362,14 @@ module Assiette
|
|
|
321
362
|
ext = File.extname(url_path)
|
|
322
363
|
case ext
|
|
323
364
|
when ".js", ".mjs", ".es"
|
|
324
|
-
Rewriter.rewrite_js_imports(raw_content) do |import_path|
|
|
365
|
+
Rewriter.rewrite_js_imports(raw_content, from: url_path) do |import_path|
|
|
325
366
|
resolved = resolve_import_for(url_path, import_path)
|
|
326
|
-
@assets[resolved]&.checksum_tag
|
|
367
|
+
@assets[resolved]&.checksum_tag
|
|
327
368
|
end
|
|
328
369
|
when ".css"
|
|
329
370
|
Rewriter.rewrite_css_urls(raw_content) do |ref_path|
|
|
330
371
|
resolved = resolve_import_for(url_path, ref_path)
|
|
331
|
-
@assets[resolved]&.checksum_tag
|
|
372
|
+
@assets[resolved]&.checksum_tag
|
|
332
373
|
end
|
|
333
374
|
else
|
|
334
375
|
raw_content
|
data/lib/assiette/helpers.rb
CHANGED
|
@@ -32,10 +32,17 @@ module Assiette
|
|
|
32
32
|
# innermost Server in the stack — on purpose. It renders a listing of
|
|
33
33
|
# everything a handler holds, so walking the stack would put one tenant's
|
|
34
34
|
# file list on another tenant's page.
|
|
35
|
-
|
|
35
|
+
#
|
|
36
|
+
# Of the exposed directories (AssetHandler#expose) it lists what the app's
|
|
37
|
+
# own modules import, directly or down the chain, and nothing else: the
|
|
38
|
+
# import is the selection. `exposed: "@my_gem"` lists all of that handle
|
|
39
|
+
# instead, for an `import()` the app computes at run time; `exposed: :all`
|
|
40
|
+
# lists every exposed module and `exposed: []` none. See
|
|
41
|
+
# AssetHandler#js_modules.
|
|
42
|
+
def assiette_modulepreload_tags(exposed: :imported)
|
|
36
43
|
entry = assiette_stack.last
|
|
37
44
|
handler = entry[:handler]
|
|
38
|
-
safe_join(handler.js_modules.map { |mod|
|
|
45
|
+
safe_join(handler.js_modules(exposed: exposed).map { |mod|
|
|
39
46
|
tag.link(rel: "modulepreload",
|
|
40
47
|
href: handler.absolute_asset_url_path(mod[:path], entry[:script_name]),
|
|
41
48
|
integrity: mod[:integrity], crossorigin: "anonymous")
|
data/lib/assiette/rewriter.rb
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
3
5
|
module Assiette
|
|
4
6
|
module Rewriter
|
|
5
7
|
# Matches quoted strings that look like relative/absolute JS import paths.
|
|
6
|
-
# Handles ./ ../ / but NOT protocol-relative
|
|
8
|
+
# Handles ./ ../ / but NOT protocol-relative //, and "@handle/..." for a
|
|
9
|
+
# directory exposed with AssetHandler#expose
|
|
7
10
|
# $1 = quote char, $2 = path including extension
|
|
8
|
-
JS_IMPORT_RE = /(["'])(
|
|
11
|
+
JS_IMPORT_RE = /(["'])((?:\.{0,2}\/(?!\/)|@[\w.-]+\/)[^"']*\.(?:js|mjs|es))\1/
|
|
9
12
|
|
|
10
13
|
# Matches url() in CSS with relative/absolute paths.
|
|
11
14
|
# Handles url(./path), url("./path"), url('../path'), url(/path)
|
|
@@ -16,25 +19,42 @@ module Assiette
|
|
|
16
19
|
module_function
|
|
17
20
|
|
|
18
21
|
# Rewrites JS imports with per-import hashes via a block.
|
|
19
|
-
# The block receives the import path and
|
|
20
|
-
|
|
22
|
+
# The block receives the import path and returns the hash for that import,
|
|
23
|
+
# or nil to leave the import unversioned.
|
|
24
|
+
#
|
|
25
|
+
# An "@handle/..." import that gets a hash is also turned into a path
|
|
26
|
+
# relative to `from` - the URL path of the file being rewritten - since a
|
|
27
|
+
# browser cannot resolve a bare "@handle" without an import map. Relative,
|
|
28
|
+
# so it keeps working under whatever SCRIPT_NAME the handler is mounted at.
|
|
29
|
+
# One that gets no hash is left as written, for an import map to resolve.
|
|
30
|
+
def rewrite_js_imports(source, from: "", &block)
|
|
21
31
|
source.gsub(JS_IMPORT_RE) do
|
|
22
32
|
quote = $1
|
|
23
33
|
path = $2
|
|
24
34
|
hash = yield(path)
|
|
35
|
+
next "#{quote}#{path}#{quote}" unless hash
|
|
36
|
+
path = relative_to(from, path) if path.start_with?("@")
|
|
25
37
|
"#{quote}#{path}?s=#{hash}#{quote}"
|
|
26
38
|
end
|
|
27
39
|
end
|
|
28
40
|
|
|
41
|
+
# "@x/y.js" as seen from the directory of `from`, always starting with ./
|
|
42
|
+
# or ../ - "y.js" alone would be a bare specifier again
|
|
43
|
+
def relative_to(from, path)
|
|
44
|
+
relative = Pathname.new(path).relative_path_from(Pathname.new(File.dirname(from))).to_s
|
|
45
|
+
relative.start_with?("../") ? relative : "./#{relative}"
|
|
46
|
+
end
|
|
47
|
+
|
|
29
48
|
# Rewrites CSS url() references with per-url hashes via a block.
|
|
30
|
-
# The block receives the url path and
|
|
49
|
+
# The block receives the url path and returns the hash for that url,
|
|
50
|
+
# or nil to leave the url unversioned.
|
|
31
51
|
def rewrite_css_urls(source, &block)
|
|
32
52
|
source.gsub(CSS_URL_RE) do
|
|
33
53
|
open = $1
|
|
34
54
|
path = $2
|
|
35
55
|
close = $3
|
|
36
56
|
hash = yield(path)
|
|
37
|
-
"url(#{open}#{path}?s=#{hash}#{close})"
|
|
57
|
+
hash ? "url(#{open}#{path}?s=#{hash}#{close})" : "url(#{open}#{path}#{close})"
|
|
38
58
|
end
|
|
39
59
|
end
|
|
40
60
|
|
data/lib/assiette/server.rb
CHANGED
|
@@ -18,6 +18,10 @@ module Assiette
|
|
|
18
18
|
def initialize(app, handler = nil, root: nil, additional_directory_mappings: {})
|
|
19
19
|
@app = app
|
|
20
20
|
@handler = handler || AssetHandler.new(root: root, additional_directory_mappings: additional_directory_mappings)
|
|
21
|
+
# In Rails a gem exposes its directory from ActiveSupport.on_load(:assiette), which runs on
|
|
22
|
+
# every handler a Server is built with - whenever the gem's block was registered. A handler
|
|
23
|
+
# made per request is not announced: the hooks keep every handler they ran on, for ever
|
|
24
|
+
ActiveSupport.run_load_hooks(:assiette, @handler) if defined?(Assiette::Railtie) && @handler.is_a?(AssetHandler)
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def call(env)
|
data/lib/assiette/version.rb
CHANGED
|
@@ -16,6 +16,10 @@ handler = Assiette::AssetHandler.new(
|
|
|
16
16
|
}
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
+
# Gems that ship JS or CSS for Assiette expose their directories on this
|
|
20
|
+
# handler themselves, as "@some_gem" - import "@some_gem/file.js" from your own
|
|
21
|
+
# modules. Add your own with handler.expose("@name", path).
|
|
22
|
+
|
|
19
23
|
# Serve files through the shared handler.
|
|
20
24
|
Rails.application.config.middleware.use Assiette::Server, handler
|
|
21
25
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: assiette
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|