assiette 0.3.1 → 0.4.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 +33 -51
- data/lib/assiette/dependency_graph.rb +310 -0
- data/lib/assiette/rewriter.rb +25 -4
- data/lib/assiette/server.rb +7 -14
- data/lib/assiette/version.rb +1 -1
- data/lib/assiette.rb +1 -0
- metadata +2 -2
- data/lib/assiette/version_tag.rb +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f24fbf1362c382584825ebb9ea4adf6b0abf0e24ad0b00c0792810b469152be
|
|
4
|
+
data.tar.gz: 747cafe7a7af67efec260a8ff506eecb390b69225fd8aebf7ff52122175f7a4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: df6146e2288d39315cd1d045e1af7c7b75e07fc89022d3288c7d52f544e01f4be7bdc1ee5a5a9fcf49d788cf967419801a7259e8477a57f3b3274a59475db1ad
|
|
7
|
+
data.tar.gz: d29c94ba6524db67f1c8f85a024c1648a9e1ea9ccf60078528ebcf24b1fbfd818181ce3c379b061519bfbd53a75ede042a15fda3734c47baddb7832cde3d4fd3
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "digest/sha1"
|
|
4
|
-
require "digest/sha2"
|
|
5
|
-
require "base64"
|
|
6
3
|
require "pathname"
|
|
7
|
-
require_relative "version_tag"
|
|
8
4
|
|
|
9
5
|
module Assiette
|
|
10
6
|
class AssetHandler
|
|
@@ -19,13 +15,28 @@ module Assiette
|
|
|
19
15
|
|
|
20
16
|
JS_EXTENSIONS = %w[.js .mjs].to_set.freeze
|
|
21
17
|
|
|
18
|
+
attr_reader :dependency_graph
|
|
19
|
+
|
|
22
20
|
def initialize(root:, additional_directory_mappings: {})
|
|
23
21
|
@mappings = build_mappings(root, additional_directory_mappings)
|
|
24
|
-
@
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
@dependency_graph = DependencyGraph.new(self)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Yields (url_path, abs_path) for every file with a recognized extension.
|
|
26
|
+
def each_mapped_file
|
|
27
|
+
@mappings.each do |prefix, root|
|
|
28
|
+
CONTENT_TYPES.each_key do |ext|
|
|
29
|
+
Dir[File.join(root, "**/*#{ext}")].each do |abs|
|
|
30
|
+
relative = Pathname.new(abs).relative_path_from(root).to_s
|
|
31
|
+
url_path = if prefix.empty?
|
|
32
|
+
relative
|
|
33
|
+
else
|
|
34
|
+
"#{prefix}/#{relative}"
|
|
35
|
+
end
|
|
36
|
+
yield url_path, abs
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
29
40
|
end
|
|
30
41
|
|
|
31
42
|
def resolve_file(path)
|
|
@@ -51,40 +62,25 @@ module Assiette
|
|
|
51
62
|
def absolute_asset_url_path(path, script_name = "")
|
|
52
63
|
clean = path.sub(%r{\A/}, "")
|
|
53
64
|
return nil unless resolve_file(clean)
|
|
54
|
-
|
|
65
|
+
hash = @dependency_graph.tree_sha(clean) || "00000000"
|
|
66
|
+
"#{script_name}/#{clean}?s=#{hash}"
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
def asset_integrity(path)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@integrity_cache = {}
|
|
62
|
-
@integrity_version = version_tag
|
|
63
|
-
end
|
|
64
|
-
return @integrity_cache[path] if @integrity_cache.key?(path)
|
|
65
|
-
|
|
66
|
-
clean = path.sub(%r{\A/}, "")
|
|
67
|
-
@integrity_cache[path] = compute_integrity(clean, version_tag)
|
|
68
|
-
end
|
|
70
|
+
clean = path.sub(%r{\A/}, "")
|
|
71
|
+
return nil unless resolve_file(clean)
|
|
72
|
+
@dependency_graph.tree_integrity(clean)
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
def js_modules
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
mod_path = "/#{"#{prefix}/" unless prefix.empty?}#{relative}".squeeze("/")
|
|
81
|
-
{path: mod_path, integrity: asset_integrity(mod_path)}
|
|
82
|
-
}
|
|
83
|
-
}.uniq { |m| m[:path] }.sort_by { |m| m[:path] }
|
|
84
|
-
|
|
85
|
-
@modules_version = version_tag
|
|
86
|
-
@modules_cache
|
|
87
|
-
end
|
|
76
|
+
@mappings.flat_map { |prefix, root|
|
|
77
|
+
Dir[File.join(root, "**/*.{js,mjs}")].filter_map { |abs|
|
|
78
|
+
next unless File.foreach(abs).any? { |line| line.match?(/\A\s*(import|export)\s/) }
|
|
79
|
+
relative = Pathname.new(abs).relative_path_from(root).to_s
|
|
80
|
+
mod_path = "/#{"#{prefix}/" unless prefix.empty?}#{relative}".squeeze("/")
|
|
81
|
+
{path: mod_path, integrity: asset_integrity(mod_path)}
|
|
82
|
+
}
|
|
83
|
+
}.uniq { |m| m[:path] }.sort_by { |m| m[:path] }
|
|
88
84
|
end
|
|
89
85
|
|
|
90
86
|
private
|
|
@@ -97,19 +93,5 @@ module Assiette
|
|
|
97
93
|
end
|
|
98
94
|
mappings
|
|
99
95
|
end
|
|
100
|
-
|
|
101
|
-
def compute_integrity(clean, version_tag)
|
|
102
|
-
file_path = resolve_file(clean)
|
|
103
|
-
return nil unless file_path
|
|
104
|
-
|
|
105
|
-
raw = File.read(file_path)
|
|
106
|
-
ext = File.extname(clean)
|
|
107
|
-
served = case ext
|
|
108
|
-
when ".js", ".mjs" then Rewriter.rewrite_js_imports(raw, version_tag)
|
|
109
|
-
when ".css" then Rewriter.rewrite_css_urls(raw, version_tag)
|
|
110
|
-
else raw
|
|
111
|
-
end
|
|
112
|
-
"sha256-#{Base64.strict_encode64(Digest::SHA256.digest(served))}"
|
|
113
|
-
end
|
|
114
96
|
end
|
|
115
97
|
end
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest/sha2"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Assiette
|
|
8
|
+
class DependencyGraph
|
|
9
|
+
# Per-file node in the dependency graph.
|
|
10
|
+
class Asset
|
|
11
|
+
attr_reader :url_path
|
|
12
|
+
attr_accessor :abs_path, :mtime, :digest, :deps, :dependents
|
|
13
|
+
|
|
14
|
+
def initialize(url_path:, abs_path:, mtime:)
|
|
15
|
+
@url_path = url_path.freeze
|
|
16
|
+
@abs_path = abs_path
|
|
17
|
+
@mtime = mtime
|
|
18
|
+
@digest = nil
|
|
19
|
+
@deps = []
|
|
20
|
+
@dependents = Set.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# 8-char hex hash for ?s= cache-busting query params.
|
|
24
|
+
def checksum_tag
|
|
25
|
+
digest&.unpack1("H8")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# SRI integrity string for integrity= attributes.
|
|
29
|
+
def sri_integrity
|
|
30
|
+
return nil unless digest
|
|
31
|
+
"sha256-#{Base64.strict_encode64(digest)}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether this asset's file has changed since last scan.
|
|
35
|
+
def stale?
|
|
36
|
+
File.mtime(abs_path) != mtime
|
|
37
|
+
rescue Errno::ENOENT
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether the file no longer exists on disk.
|
|
42
|
+
def deleted?
|
|
43
|
+
!File.exist?(abs_path.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(handler)
|
|
48
|
+
@handler = handler
|
|
49
|
+
@mutex = Mutex.new
|
|
50
|
+
@assets = {}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the Asset for a url_path, or nil.
|
|
54
|
+
def [](url_path)
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
ensure_asset!(url_path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the 8-char hex content hash for a URL path.
|
|
61
|
+
def tree_sha(url_path)
|
|
62
|
+
self[url_path]&.checksum_tag
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the SRI integrity string for a URL path.
|
|
66
|
+
def tree_integrity(url_path)
|
|
67
|
+
self[url_path]&.sri_integrity
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Resolves a relative import path to a URL path.
|
|
71
|
+
def resolve_import_for(from_url_path, relative_path)
|
|
72
|
+
if relative_path.start_with?("/")
|
|
73
|
+
relative_path.sub(%r{\A/}, "")
|
|
74
|
+
else
|
|
75
|
+
dir = File.dirname(from_url_path)
|
|
76
|
+
File.expand_path(relative_path, "/#{dir}").sub(%r{\A/}, "")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Rewrites content with per-dependency hashes. Thread-safe.
|
|
81
|
+
def rewrite_content(url_path, raw_content)
|
|
82
|
+
@mutex.synchronize do
|
|
83
|
+
ensure_asset!(url_path)
|
|
84
|
+
rewrite_content_internal(url_path, raw_content)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Forces a full rebuild on next access.
|
|
89
|
+
def invalidate!
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
@assets = {}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Lazily resolve a single asset and its transitive dependencies.
|
|
98
|
+
# Returns the Asset or nil if the file doesn't exist.
|
|
99
|
+
def ensure_asset!(url_path)
|
|
100
|
+
@resolving = Set.new
|
|
101
|
+
@checked = Set.new
|
|
102
|
+
@cycle_groups = []
|
|
103
|
+
asset = resolve_asset!(url_path)
|
|
104
|
+
# Process any cycles detected during resolution
|
|
105
|
+
@cycle_groups.each { |scc| compute_cycle_digests(scc) }
|
|
106
|
+
@resolving = nil
|
|
107
|
+
@checked = nil
|
|
108
|
+
@cycle_groups = nil
|
|
109
|
+
asset
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Recursive resolution. Detects cycles via @resolving set.
|
|
113
|
+
# @checked prevents re-verifying the same asset within one ensure_asset! call.
|
|
114
|
+
# Returns the Asset or nil. Also returns whether anything changed downstream.
|
|
115
|
+
def resolve_asset!(url_path)
|
|
116
|
+
# Already verified fresh in this ensure_asset! call
|
|
117
|
+
return @assets[url_path] if @checked.include?(url_path)
|
|
118
|
+
|
|
119
|
+
existing = @assets[url_path]
|
|
120
|
+
if existing
|
|
121
|
+
if existing.deleted?
|
|
122
|
+
remove_asset!(url_path)
|
|
123
|
+
return nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if existing.stale?
|
|
127
|
+
rescan_asset!(existing)
|
|
128
|
+
@checked << url_path
|
|
129
|
+
return existing
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Not stale itself — but deps might be. Check them recursively.
|
|
133
|
+
@checked << url_path
|
|
134
|
+
old_dep_digests = existing.deps.map { |dp| @assets[dp]&.digest }
|
|
135
|
+
existing.deps.each { |dp| resolve_asset!(dp) }
|
|
136
|
+
new_dep_digests = existing.deps.map { |dp| @assets[dp]&.digest }
|
|
137
|
+
|
|
138
|
+
if old_dep_digests != new_dep_digests
|
|
139
|
+
compute_digest_for(url_path)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
return existing
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Resolve url_path to an absolute path
|
|
146
|
+
abs_path = @handler.resolve_file(url_path)
|
|
147
|
+
return nil unless abs_path
|
|
148
|
+
|
|
149
|
+
# Cycle detection
|
|
150
|
+
unless @resolving.add?(url_path)
|
|
151
|
+
return @assets[url_path]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Create the asset node
|
|
155
|
+
asset = @assets[url_path] = Asset.new(
|
|
156
|
+
url_path: url_path,
|
|
157
|
+
abs_path: abs_path,
|
|
158
|
+
mtime: File.mtime(abs_path)
|
|
159
|
+
)
|
|
160
|
+
asset.deps = parse_deps(url_path, File.read(abs_path))
|
|
161
|
+
|
|
162
|
+
# Recursively resolve dependencies
|
|
163
|
+
in_cycle = false
|
|
164
|
+
asset.deps.each do |dep_path|
|
|
165
|
+
if @resolving.include?(dep_path) && !@assets[dep_path]&.digest
|
|
166
|
+
in_cycle = true
|
|
167
|
+
end
|
|
168
|
+
dep = resolve_asset!(dep_path)
|
|
169
|
+
dep.dependents << url_path if dep
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if in_cycle || asset.deps.any? { |dp| @resolving.include?(dp) && !@assets[dp]&.digest }
|
|
173
|
+
scc = collect_cycle(url_path)
|
|
174
|
+
@cycle_groups << scc unless scc.empty?
|
|
175
|
+
else
|
|
176
|
+
compute_digest_for(url_path)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
@checked << url_path
|
|
180
|
+
@resolving.delete(url_path)
|
|
181
|
+
asset
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Collect strongly connected component members starting from url_path.
|
|
185
|
+
def collect_cycle(url_path)
|
|
186
|
+
visited = Set.new
|
|
187
|
+
stack = [url_path]
|
|
188
|
+
members = Set.new
|
|
189
|
+
|
|
190
|
+
while (node = stack.pop)
|
|
191
|
+
next unless visited.add?(node)
|
|
192
|
+
asset = @assets[node]
|
|
193
|
+
next unless asset
|
|
194
|
+
members << node if @resolving.include?(node)
|
|
195
|
+
asset.deps.each do |dep|
|
|
196
|
+
stack << dep if @resolving.include?(dep)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
members.to_a
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Re-read a stale asset, re-parse deps, recursively ensure deps fresh, recompute digest.
|
|
204
|
+
def rescan_asset!(asset)
|
|
205
|
+
url_path = asset.url_path
|
|
206
|
+
asset.mtime = File.mtime(asset.abs_path)
|
|
207
|
+
|
|
208
|
+
# Remove old reverse links
|
|
209
|
+
asset.deps.each do |dep_path|
|
|
210
|
+
dep = @assets[dep_path]
|
|
211
|
+
dep&.dependents&.delete(url_path)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Re-parse imports
|
|
215
|
+
raw = File.read(asset.abs_path)
|
|
216
|
+
asset.deps = parse_deps(url_path, raw)
|
|
217
|
+
|
|
218
|
+
# Recursively ensure deps are fresh
|
|
219
|
+
asset.deps.each do |dep_path|
|
|
220
|
+
dep = resolve_asset!(dep_path)
|
|
221
|
+
dep.dependents << url_path if dep
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Recompute digest
|
|
225
|
+
compute_digest_for(url_path)
|
|
226
|
+
|
|
227
|
+
# Propagate to dependents already in the graph
|
|
228
|
+
propagate_to_dependents!(asset)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Recompute digests for all transitive dependents of an asset.
|
|
232
|
+
def propagate_to_dependents!(asset)
|
|
233
|
+
queue = asset.dependents.to_a
|
|
234
|
+
visited = Set.new
|
|
235
|
+
while (dep_url = queue.shift)
|
|
236
|
+
next unless visited.add?(dep_url)
|
|
237
|
+
dep_asset = @assets[dep_url]
|
|
238
|
+
next unless dep_asset
|
|
239
|
+
compute_digest_for(dep_url)
|
|
240
|
+
queue.concat(dep_asset.dependents.to_a)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def parse_deps(url_path, raw)
|
|
245
|
+
ext = File.extname(url_path)
|
|
246
|
+
import_paths = case ext
|
|
247
|
+
when ".js", ".mjs", ".es" then Rewriter.extract_js_imports(raw)
|
|
248
|
+
when ".css" then Rewriter.extract_css_urls(raw)
|
|
249
|
+
else []
|
|
250
|
+
end
|
|
251
|
+
import_paths.map { |p| resolve_import_for(url_path, p) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def compute_digest_for(url_path)
|
|
255
|
+
asset = @assets[url_path]
|
|
256
|
+
return unless asset
|
|
257
|
+
|
|
258
|
+
raw = File.read(asset.abs_path)
|
|
259
|
+
if asset.deps.empty?
|
|
260
|
+
asset.digest = Digest::SHA256.digest(raw)
|
|
261
|
+
else
|
|
262
|
+
rewritten = rewrite_content_internal(url_path, raw)
|
|
263
|
+
asset.digest = Digest::SHA256.digest(rewritten)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# For cyclic dependencies: hash all members' raw contents together.
|
|
268
|
+
def compute_cycle_digests(scc)
|
|
269
|
+
combined = scc.sort.filter_map { |url_path|
|
|
270
|
+
asset = @assets[url_path]
|
|
271
|
+
next unless asset
|
|
272
|
+
File.read(asset.abs_path)
|
|
273
|
+
}.join("\0")
|
|
274
|
+
|
|
275
|
+
digest = Digest::SHA256.digest(combined)
|
|
276
|
+
scc.each do |url_path|
|
|
277
|
+
asset = @assets[url_path]
|
|
278
|
+
asset.digest = digest if asset
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def rewrite_content_internal(url_path, raw_content)
|
|
283
|
+
ext = File.extname(url_path)
|
|
284
|
+
case ext
|
|
285
|
+
when ".js", ".mjs", ".es"
|
|
286
|
+
Rewriter.rewrite_js_imports(raw_content) do |import_path|
|
|
287
|
+
resolved = resolve_import_for(url_path, import_path)
|
|
288
|
+
@assets[resolved]&.checksum_tag || "00000000"
|
|
289
|
+
end
|
|
290
|
+
when ".css"
|
|
291
|
+
Rewriter.rewrite_css_urls(raw_content) do |ref_path|
|
|
292
|
+
resolved = resolve_import_for(url_path, ref_path)
|
|
293
|
+
@assets[resolved]&.checksum_tag || "00000000"
|
|
294
|
+
end
|
|
295
|
+
else
|
|
296
|
+
raw_content
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def remove_asset!(url_path)
|
|
301
|
+
asset = @assets.delete(url_path)
|
|
302
|
+
return unless asset
|
|
303
|
+
|
|
304
|
+
asset.deps.each do |dep_path|
|
|
305
|
+
dep = @assets[dep_path]
|
|
306
|
+
dep&.dependents&.delete(url_path)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
data/lib/assiette/rewriter.rb
CHANGED
|
@@ -15,16 +15,37 @@ module Assiette
|
|
|
15
15
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
# Rewrites JS imports with per-import hashes via a block.
|
|
19
|
+
# The block receives the import path and must return the hash for that import.
|
|
20
|
+
def rewrite_js_imports(source, &block)
|
|
19
21
|
source.gsub(JS_IMPORT_RE) do
|
|
20
|
-
|
|
22
|
+
quote = $1
|
|
23
|
+
path = $2
|
|
24
|
+
hash = yield(path)
|
|
25
|
+
"#{quote}#{path}?s=#{hash}#{quote}"
|
|
21
26
|
end
|
|
22
27
|
end
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
# Rewrites CSS url() references with per-url hashes via a block.
|
|
30
|
+
# The block receives the url path and must return the hash for that url.
|
|
31
|
+
def rewrite_css_urls(source, &block)
|
|
25
32
|
source.gsub(CSS_URL_RE) do
|
|
26
|
-
|
|
33
|
+
open = $1
|
|
34
|
+
path = $2
|
|
35
|
+
close = $3
|
|
36
|
+
hash = yield(path)
|
|
37
|
+
"url(#{open}#{path}?s=#{hash}#{close})"
|
|
27
38
|
end
|
|
28
39
|
end
|
|
40
|
+
|
|
41
|
+
# Returns an array of import paths found in JS source.
|
|
42
|
+
def extract_js_imports(source)
|
|
43
|
+
source.scan(JS_IMPORT_RE).map { |m| m[1] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns an array of url() paths found in CSS source.
|
|
47
|
+
def extract_css_urls(source)
|
|
48
|
+
source.scan(CSS_URL_RE).map { |m| m[1] }
|
|
49
|
+
end
|
|
29
50
|
end
|
|
30
51
|
end
|
data/lib/assiette/server.rb
CHANGED
|
@@ -37,24 +37,17 @@ module Assiette
|
|
|
37
37
|
file_path = @handler.resolve_file(path_info)
|
|
38
38
|
return unless file_path
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
40
|
+
# Use the dependency graph's content hash for the ETag — it reflects
|
|
41
|
+
# the file's own content plus all its transitive dependencies, and is
|
|
42
|
+
# already computed as part of serving. This lets us short-circuit with
|
|
43
|
+
# a 304 before reading the file for rewriting.
|
|
44
|
+
etag = %("#{@handler.dependency_graph.tree_sha(path_info) || "0"}")
|
|
44
45
|
if env["HTTP_IF_NONE_MATCH"] == etag
|
|
45
46
|
return [304, {"etag" => etag, "cache-control" => CACHE_CONTROL}, []]
|
|
46
47
|
end
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
body = if AssetHandler::JS_EXTENSIONS.include?(extension)
|
|
52
|
-
Rewriter.rewrite_js_imports(raw_bytes, tag)
|
|
53
|
-
elsif extension == ".css"
|
|
54
|
-
Rewriter.rewrite_css_urls(raw_bytes, tag)
|
|
55
|
-
else
|
|
56
|
-
raw_bytes
|
|
57
|
-
end
|
|
49
|
+
raw_bytes = File.binread(file_path)
|
|
50
|
+
body = @handler.dependency_graph.rewrite_content(path_info, raw_bytes)
|
|
58
51
|
|
|
59
52
|
headers = {
|
|
60
53
|
"content-type" => content_type,
|
data/lib/assiette/version.rb
CHANGED
data/lib/assiette.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "assiette/version"
|
|
|
5
5
|
module Assiette
|
|
6
6
|
autoload :Rewriter, File.expand_path("assiette/rewriter", __dir__)
|
|
7
7
|
autoload :AssetHandler, File.expand_path("assiette/asset_handler", __dir__)
|
|
8
|
+
autoload :DependencyGraph, File.expand_path("assiette/dependency_graph", __dir__)
|
|
8
9
|
autoload :Server, File.expand_path("assiette/server", __dir__)
|
|
9
10
|
autoload :Helpers, File.expand_path("assiette/helpers", __dir__)
|
|
10
11
|
autoload :RailsAssetUrlHelper, File.expand_path("assiette/rails_asset_url_helper", __dir__)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: assiette
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julik Tarkhanov
|
|
@@ -35,13 +35,13 @@ files:
|
|
|
35
35
|
- Rakefile
|
|
36
36
|
- lib/assiette.rb
|
|
37
37
|
- lib/assiette/asset_handler.rb
|
|
38
|
+
- lib/assiette/dependency_graph.rb
|
|
38
39
|
- lib/assiette/helpers.rb
|
|
39
40
|
- lib/assiette/rails_asset_url_helper.rb
|
|
40
41
|
- lib/assiette/railtie.rb
|
|
41
42
|
- lib/assiette/rewriter.rb
|
|
42
43
|
- lib/assiette/server.rb
|
|
43
44
|
- lib/assiette/version.rb
|
|
44
|
-
- lib/assiette/version_tag.rb
|
|
45
45
|
- lib/generators/assiette/install/install_generator.rb
|
|
46
46
|
- lib/generators/assiette/install/templates/initializer.rb.tt
|
|
47
47
|
homepage: https://github.com/julik/assiette
|
data/lib/assiette/version_tag.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest/sha1"
|
|
4
|
-
|
|
5
|
-
module Assiette
|
|
6
|
-
# Computes a short version tag for cache busting.
|
|
7
|
-
# In development: timestamp for instant invalidation on each request
|
|
8
|
-
# In production: derived from APP_REVISION env var or Gemfile.lock digest
|
|
9
|
-
def self.version_tag
|
|
10
|
-
@version_tag ||= compute_version_tag
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def self.reset_version_tag!
|
|
14
|
-
@version_tag = nil
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def self.compute_version_tag
|
|
18
|
-
if Rails.env.development?
|
|
19
|
-
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
20
|
-
elsif (app_revision = ENV["APP_REVISION"]).present?
|
|
21
|
-
Digest::SHA1.hexdigest(app_revision)[0, 4]
|
|
22
|
-
else
|
|
23
|
-
gemfile_lock_path = Rails.root.join("Gemfile.lock")
|
|
24
|
-
if gemfile_lock_path.exist?
|
|
25
|
-
Digest::SHA1.file(gemfile_lock_path).hexdigest[0, 4]
|
|
26
|
-
else
|
|
27
|
-
(Time.now.utc.to_i / 300).to_s(16)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private_class_method :compute_version_tag
|
|
33
|
-
end
|