darkroom 0.0.9 → 0.0.10
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 +9 -0
- data/README.md +175 -131
- data/VERSION +1 -1
- data/lib/darkroom/asset.rb +173 -151
- data/lib/darkroom/darkroom.rb +140 -124
- data/lib/darkroom/delegate.rb +208 -101
- data/lib/darkroom/delegates/{css.rb → css_delegate.rb} +1 -0
- data/lib/darkroom/delegates/{html.rb → html_delegate.rb} +4 -3
- data/lib/darkroom/delegates/{htx.rb → htx_delegate.rb} +3 -2
- data/lib/darkroom/delegates/{javascript.rb → javascript_delegate.rb} +9 -8
- data/lib/darkroom/errors/asset_error.rb +6 -17
- data/lib/darkroom/errors/asset_not_found_error.rb +4 -8
- data/lib/darkroom/errors/circular_reference_error.rb +4 -8
- data/lib/darkroom/errors/duplicate_asset_error.rb +7 -16
- data/lib/darkroom/errors/invalid_path_error.rb +5 -14
- data/lib/darkroom/errors/missing_library_error.rb +7 -16
- data/lib/darkroom/errors/processing_error.rb +13 -20
- data/lib/darkroom/errors/unrecognized_extension_error.rb +4 -8
- data/lib/darkroom/version.rb +1 -1
- data/lib/darkroom.rb +4 -6
- metadata +17 -21
data/lib/darkroom/darkroom.rb
CHANGED
@@ -3,17 +3,16 @@
|
|
3
3
|
require('set')
|
4
4
|
|
5
5
|
require_relative('asset')
|
6
|
+
require_relative('delegate')
|
6
7
|
require_relative('errors/asset_not_found_error')
|
7
8
|
require_relative('errors/duplicate_asset_error')
|
8
9
|
require_relative('errors/invalid_path_error')
|
9
10
|
require_relative('errors/processing_error')
|
10
11
|
|
11
|
-
|
12
|
-
# Main class providing fast, lightweight, and straightforward web asset management.
|
13
|
-
#
|
12
|
+
# Main class providing simple and straightforward web asset management.
|
14
13
|
class Darkroom
|
15
14
|
DEFAULT_MINIFIED = /(\.|-)min\.\w+$/.freeze
|
16
|
-
TRAILING_SLASHES =
|
15
|
+
TRAILING_SLASHES = %r{/+$}.freeze
|
17
16
|
PRISTINE = Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
|
18
17
|
MIN_PROCESS_INTERVAL = 0.5
|
19
18
|
|
@@ -24,25 +23,36 @@ class Darkroom
|
|
24
23
|
|
25
24
|
class << self; attr_accessor(:javascript_iife) end
|
26
25
|
|
27
|
-
|
28
|
-
# Registers an asset delegate.
|
26
|
+
# Public: Register a delegate for handling a specific kind of asset.
|
29
27
|
#
|
30
|
-
#
|
31
|
-
#
|
28
|
+
# args - One or more String file extensions to associate with this delegate, optionally followed by
|
29
|
+
# either an HTTP MIME type String or a Delegate subclass.
|
30
|
+
# block - Block to call that defines or extends the Delegate.
|
32
31
|
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
32
|
+
# Examples
|
33
|
+
#
|
34
|
+
# Darkroom.register('.ext1', '.ext2', 'content/type')
|
35
|
+
# Darkroom.register('.ext', MyDelegateSubclass)
|
36
|
+
#
|
37
|
+
# Darkroom.register('.scss', 'text/css') do
|
38
|
+
# compile(lib: 'sassc') { ... }
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# Darkroom.register('.scss', SCSSDelegate) do
|
42
|
+
# # Modifications/overrides of the SCSSDelegate class...
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# Returns the Delegate class.
|
46
|
+
def self.register(*args, &block)
|
47
|
+
last_arg = args.pop unless args.last.kind_of?(String) && args.last[0] == '.'
|
48
|
+
extensions = args
|
41
49
|
|
50
|
+
if last_arg.nil? || last_arg.kind_of?(String)
|
51
|
+
content_type = last_arg
|
42
52
|
delegate = Class.new(Delegate, &block)
|
43
53
|
delegate.content_type(content_type) if content_type && !delegate.content_type
|
44
|
-
elsif
|
45
|
-
delegate = block ? Class.new(
|
54
|
+
elsif last_arg.kind_of?(Class) && last_arg < Delegate
|
55
|
+
delegate = block ? Class.new(last_arg, &block) : last_arg
|
46
56
|
end
|
47
57
|
|
48
58
|
extensions.each do |extension|
|
@@ -54,45 +64,38 @@ class Darkroom
|
|
54
64
|
delegate
|
55
65
|
end
|
56
66
|
|
57
|
-
|
58
|
-
# Returns the delegate associated with a file extension.
|
67
|
+
# Public: Get the Delegate associated with a file extension.
|
59
68
|
#
|
60
|
-
#
|
69
|
+
# extension - String file extension of the desired delegate (e.g. '.js')
|
61
70
|
#
|
71
|
+
# Returns the Delegate class.
|
62
72
|
def self.delegate(extension)
|
63
73
|
@@delegates[extension]
|
64
74
|
end
|
65
75
|
|
66
|
-
|
67
|
-
# Utility method that prints a warning with file and line number of a deprecated call.
|
68
|
-
#
|
69
|
-
def self.deprecated(message)
|
70
|
-
location = caller_locations(2, 1).first
|
71
|
-
|
72
|
-
warn("#{location.path}:#{location.lineno}: #{message}")
|
73
|
-
end
|
74
|
-
|
75
|
-
##
|
76
|
-
# Creates a new instance.
|
77
|
-
#
|
78
|
-
# [*load_paths] One or more paths where assets are located on disk.
|
79
|
-
# [host:] Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
|
80
|
-
# are specified, they will be round-robined within each thread for each call to +#asset_path+.
|
81
|
-
# [hosts:] Alias of +host:+.
|
82
|
-
# [prefix:] Prefix to prepend to asset paths (e.g. +/assets+).
|
83
|
-
# [pristine:] Path(s) that should not include prefix and for which unversioned form should be provided by
|
84
|
-
# default (e.g. +/favicon.ico+).
|
85
|
-
# [entries:] String, regex, or array of strings and regexes specifying entry point paths / path patterns.
|
86
|
-
# [minify:] Boolean specifying whether or not to minify assets.
|
87
|
-
# [minified:] String, regex, or array of strings and regexes specifying paths of assets that are already
|
88
|
-
# minified and thus should be skipped for minification.
|
89
|
-
# [min_process_interval:] Minimum time required between one run of asset processing and another.
|
76
|
+
# Public: Create a new instance.
|
90
77
|
#
|
78
|
+
# load_paths - One or more String paths where assets are located on disk.
|
79
|
+
# host: - String host or Array of String hosts to prepend to paths (useful when serving
|
80
|
+
# from a CDN in production). If multiple hosts are specified, they will be round-
|
81
|
+
# robined within each thread for each call to #asset_path.
|
82
|
+
# hosts: - String or Array of Strings (alias of host:).
|
83
|
+
# prefix: - String prefix to prepend to asset paths (e.g. '/assets').
|
84
|
+
# pristine: - String, Array of String, or Set of String paths that should not include the
|
85
|
+
# prefix and for which the unversioned form should be provided by default (e.g.
|
86
|
+
# '/favicon.ico').
|
87
|
+
# entries: - String, Regexp, or Array of String and/or Regexp specifying entry point paths /
|
88
|
+
# path patterns.
|
89
|
+
# minify: - Boolean specifying if assets that support it should be minified.
|
90
|
+
# minified: - String, Regexp, or Array of String and/or Regexp specifying paths of assets that
|
91
|
+
# are already minified and thus shouldn't be minified.
|
92
|
+
# min_process_interval: - Numeric minimum number of seconds required between one run of asset processing
|
93
|
+
# and another.
|
91
94
|
def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil,
|
92
|
-
|
95
|
+
minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL)
|
93
96
|
@load_paths = load_paths.map { |load_path| File.expand_path(load_path) }
|
94
97
|
|
95
|
-
@hosts = (Array(host) + Array(hosts)).map! { |
|
98
|
+
@hosts = (Array(host) + Array(hosts)).map! { |h| h.sub(TRAILING_SLASHES, '') }
|
96
99
|
@entries = Array(entries)
|
97
100
|
@minify = minify
|
98
101
|
@minified = Array(minified)
|
@@ -116,16 +119,24 @@ class Darkroom
|
|
116
119
|
Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
|
117
120
|
end
|
118
121
|
|
119
|
-
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
122
|
+
# Public: Walk all load paths and refresh any assets that have been modified on disk since the last call
|
123
|
+
# to this method. Processing is skipped if either a) a previous call to this method happened
|
124
|
+
# less than min_process_interval seconds ago or b) another thread is currently executing this method.
|
125
|
+
#
|
126
|
+
# A mutex is used to ensure that, say, multiple web request threads do not trample each other. If the
|
127
|
+
# mutex is locked when this method is called, it will wait until the mutex is released to ensure that the
|
128
|
+
# caller does not then start working with stale / invalid Asset objects due to the work of the other
|
129
|
+
# thread's active call to #process being incomplete.
|
123
130
|
#
|
131
|
+
# If any errors are encountered during processing, they must be checked for manually afterward via #error
|
132
|
+
# or #errors. If a raise is preferred, use #process! instead.
|
133
|
+
#
|
134
|
+
# Returns boolean indicating if processing actually happened (true) or was skipped (false).
|
124
135
|
def process
|
125
136
|
return false if Time.now.to_f - @last_processed_at < @min_process_interval
|
126
137
|
|
127
138
|
if @mutex.locked?
|
128
|
-
@mutex.synchronize {}
|
139
|
+
@mutex.synchronize {} # Wait until other #process call is done to avoid stale/invalid assets.
|
129
140
|
return false
|
130
141
|
end
|
131
142
|
|
@@ -135,10 +146,10 @@ class Darkroom
|
|
135
146
|
found = {}
|
136
147
|
|
137
148
|
@load_paths.each do |load_path|
|
138
|
-
Dir.glob(File.join(load_path, @@glob)).
|
149
|
+
Dir.glob(File.join(load_path, @@glob)).each do |file|
|
139
150
|
path = file.sub(load_path, '')
|
140
151
|
|
141
|
-
if index = (
|
152
|
+
if (index = path.index(Asset::INVALID_PATH_REGEX))
|
142
153
|
@errors << InvalidPathError.new(path, index)
|
143
154
|
elsif found.key?(path)
|
144
155
|
@errors << DuplicateAssetError.new(path, found[path], load_path)
|
@@ -148,7 +159,8 @@ class Darkroom
|
|
148
159
|
unless @manifest.key?(path)
|
149
160
|
entry = entry?(path)
|
150
161
|
|
151
|
-
@manifest[path] = Asset.new(
|
162
|
+
@manifest[path] = Asset.new(
|
163
|
+
path, file, self,
|
152
164
|
prefix: (@prefix unless @pristine.include?(path)),
|
153
165
|
entry: entry,
|
154
166
|
minify: entry && @minify && !minified?(path),
|
@@ -162,7 +174,7 @@ class Darkroom
|
|
162
174
|
@manifest_unversioned.clear
|
163
175
|
@manifest_versioned.clear
|
164
176
|
|
165
|
-
@manifest.
|
177
|
+
@manifest.each_value do |asset|
|
166
178
|
asset.process
|
167
179
|
|
168
180
|
if asset.entry?
|
@@ -180,94 +192,102 @@ class Darkroom
|
|
180
192
|
end
|
181
193
|
end
|
182
194
|
|
183
|
-
|
184
|
-
# Calls #process. If processing was skipped, returns false. If processing was performed, raises an
|
185
|
-
# exception if any errors were encountered and returns true otherwise.
|
195
|
+
# Public: Call #process but raise an error if there were errors.
|
186
196
|
#
|
197
|
+
# Returns boolean indicating if processing actually happened (true) or was skipped (false).
|
198
|
+
# Raises ProcessingError if processing actually happened from this call and error(s) were encountered.
|
187
199
|
def process!
|
188
200
|
result = process
|
189
201
|
|
190
|
-
|
202
|
+
result && @error ? raise(@error) : result
|
191
203
|
end
|
192
204
|
|
193
|
-
|
194
|
-
# Returns boolean indicating whether or not there were any errors encountered the last time assets were
|
195
|
-
# processed.
|
205
|
+
# Public: Check if there were any errors encountered the last time assets were processed.
|
196
206
|
#
|
207
|
+
# Returns the boolean result.
|
197
208
|
def error?
|
198
209
|
!!@error
|
199
210
|
end
|
200
211
|
|
201
|
-
|
202
|
-
#
|
203
|
-
#
|
204
|
-
#
|
212
|
+
# Public: Get an Asset object, given its external path. An external path includes any prefix and can be
|
213
|
+
# either the versioned or unversioned form (i.e. how an HTTP request for the asset comes in).
|
214
|
+
#
|
215
|
+
# Examples
|
205
216
|
#
|
206
|
-
#
|
207
|
-
# darkroom.asset('/assets/js/app
|
217
|
+
# # Suppose the asset's internal path is '/js/app.js' and the prefix is '/assets'.
|
218
|
+
# darkroom.asset('/assets/js/app-<hash>.js') # => #<Darkroom::Asset [...]>
|
219
|
+
# darkroom.asset('/assets/js/app.js') # => #<Darkroom::Asset [...]>
|
208
220
|
#
|
209
|
-
#
|
221
|
+
# path - String external path of the asset.
|
210
222
|
#
|
223
|
+
# Returns the Asset object if it exists or nil otherwise.
|
211
224
|
def asset(path)
|
212
225
|
@manifest_versioned[path] || @manifest_unversioned[path]
|
213
226
|
end
|
214
227
|
|
215
|
-
|
216
|
-
#
|
217
|
-
# can be either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the
|
218
|
-
# asset comes in). For example, to get the external path for the Asset object with path +/js/app.js+ when
|
219
|
-
# prefix is +/assets+:
|
228
|
+
# Public: Get the external asset path, given its internal path. An external path includes any prefix and
|
229
|
+
# can be either the versioned or unversioned form (i.e. how an HTTP request for the asset comes in).
|
220
230
|
#
|
221
|
-
#
|
222
|
-
#
|
231
|
+
# path - String internal path of the asset.
|
232
|
+
# versioned: - Boolean specifying either the versioned or unversioned path to be returned.
|
223
233
|
#
|
224
|
-
#
|
234
|
+
# Examples
|
225
235
|
#
|
226
|
-
#
|
227
|
-
#
|
236
|
+
# # Suppose the asset's internal path is '/js/app.js' and the prefix is '/assets'.
|
237
|
+
# darkroom.asset_path('/js/app.js') # => "/assets/js/app-<hash>.js"
|
238
|
+
# darkroom.asset_path('/js/app.js', versioned: false) # => "/assets/js/app.js"
|
228
239
|
#
|
240
|
+
# Returns the String external asset path.
|
241
|
+
# Raises AssetNotFoundError if the asset doesn't exist.
|
229
242
|
def asset_path(path, versioned: !@pristine.include?(path))
|
230
243
|
asset = @manifest[path] or raise(AssetNotFoundError.new(path))
|
231
|
-
|
232
|
-
|
233
|
-
|
244
|
+
|
245
|
+
unless @hosts.empty?
|
246
|
+
host_index = (Thread.current[:darkroom_host_index] + 1) % @hosts.size
|
247
|
+
host = @hosts[host_index]
|
248
|
+
|
249
|
+
Thread.current[:darkroom_host_index] = host_index
|
250
|
+
end
|
234
251
|
|
235
252
|
"#{host}#{versioned ? asset.path_versioned : asset.path_unversioned}"
|
236
253
|
end
|
237
254
|
|
238
|
-
|
239
|
-
# Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
|
240
|
-
# exist.
|
255
|
+
# Public: Get an asset's subresource integrity string.
|
241
256
|
#
|
242
|
-
#
|
243
|
-
#
|
257
|
+
# path - String internal path of the asset.
|
258
|
+
# algorithm - Symbol hash algorithm name to use to generate the integrity string (must be one of
|
259
|
+
# :sha256, :sha384, :sha512).
|
244
260
|
#
|
261
|
+
# Returns the asset's subresource integrity String.
|
262
|
+
# Raises AssetNotFoundError if the asset doesn't exist.
|
245
263
|
def asset_integrity(path, algorithm = nil)
|
246
264
|
asset = @manifest[path] or raise(AssetNotFoundError.new(path))
|
247
265
|
|
248
266
|
algorithm ? asset.integrity(algorithm) : asset.integrity
|
249
267
|
end
|
250
268
|
|
251
|
-
|
252
|
-
# Returns the asset from the manifest hash associated with the given path.
|
269
|
+
# Public: Get the Asset object from the manifest Hash associated with the given path.
|
253
270
|
#
|
254
|
-
#
|
271
|
+
# path - String internal path of the asset.
|
255
272
|
#
|
273
|
+
# Returns the Asset object if it exists or nil otherwise.
|
256
274
|
def manifest(path)
|
257
275
|
@manifest[path]
|
258
276
|
end
|
259
277
|
|
260
|
-
|
261
|
-
#
|
262
|
-
#
|
278
|
+
# Public: Write assets to disk. This is useful when deploying to a production environment where assets
|
279
|
+
# will be uploaded to and served from a CDN or proxy server. Note that #process must be called manually
|
280
|
+
# before calling this method.
|
263
281
|
#
|
264
|
-
#
|
265
|
-
#
|
266
|
-
#
|
267
|
-
#
|
282
|
+
# dir - String directory path to write the assets to.
|
283
|
+
# clear: - Boolean indicating if the existing contents of the directory should be deleted
|
284
|
+
# before writing files.
|
285
|
+
# include_pristine: - Boolean indicating if pristine assets should be included (when dumping for the
|
268
286
|
# purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't
|
269
287
|
# need to be included).
|
270
288
|
#
|
289
|
+
# Returns nothing.
|
290
|
+
# Raises ProcessingError if errors were encountered during the last #process run.
|
271
291
|
def dump(dir, clear: false, include_pristine: true)
|
272
292
|
raise(@error) if @error
|
273
293
|
|
@@ -281,45 +301,41 @@ class Darkroom
|
|
281
301
|
@manifest_versioned.each do |path, asset|
|
282
302
|
next if @pristine.include?(asset.path) && !include_pristine
|
283
303
|
|
284
|
-
file_path = File.join(dir,
|
285
|
-
@pristine.include?(asset.path) ? asset.path_unversioned : path
|
286
|
-
)
|
304
|
+
file_path = File.join(dir, @pristine.include?(asset.path) ? asset.path_unversioned : path)
|
287
305
|
|
288
306
|
FileUtils.mkdir_p(File.dirname(file_path))
|
289
307
|
File.write(file_path, asset.content)
|
290
308
|
end
|
291
309
|
end
|
292
310
|
|
293
|
-
|
294
|
-
# Returns high-level object info string.
|
311
|
+
# Public: Get a high-level object info string about this Darkroom instance.
|
295
312
|
#
|
313
|
+
# Returns the String.
|
296
314
|
def inspect
|
297
|
-
"#<#{self.class} "\
|
298
|
-
"@entries=#{@entries.inspect}, "\
|
299
|
-
"@errors=#{@errors.inspect}, "\
|
300
|
-
"@hosts=#{@hosts.inspect}, "\
|
301
|
-
"@last_processed_at=#{@last_processed_at.inspect}, "\
|
302
|
-
"@load_paths=#{@load_paths.inspect}, "\
|
303
|
-
"@min_process_interval=#{@min_process_interval.inspect}, "\
|
304
|
-
"@minified=#{@minified.inspect}, "\
|
305
|
-
"@minify=#{@minify.inspect}, "\
|
306
|
-
"@prefix=#{@prefix.inspect}, "\
|
307
|
-
"@pristine=#{@pristine.inspect}, "\
|
308
|
-
"@process_key=#{@process_key.inspect}"\
|
315
|
+
"#<#{self.class} " \
|
316
|
+
"@entries=#{@entries.inspect}, " \
|
317
|
+
"@errors=#{@errors.inspect}, " \
|
318
|
+
"@hosts=#{@hosts.inspect}, " \
|
319
|
+
"@last_processed_at=#{@last_processed_at.inspect}, " \
|
320
|
+
"@load_paths=#{@load_paths.inspect}, " \
|
321
|
+
"@min_process_interval=#{@min_process_interval.inspect}, " \
|
322
|
+
"@minified=#{@minified.inspect}, " \
|
323
|
+
"@minify=#{@minify.inspect}, " \
|
324
|
+
"@prefix=#{@prefix.inspect}, " \
|
325
|
+
"@pristine=#{@pristine.inspect}, " \
|
326
|
+
"@process_key=#{@process_key.inspect}" \
|
309
327
|
'>'
|
310
328
|
end
|
311
329
|
|
312
330
|
private
|
313
331
|
|
314
|
-
|
315
|
-
# Returns boolean indicating whether or not the provided path is an entry point.
|
332
|
+
# Internal: Check if an asset's path indicates that it's an entry point.
|
316
333
|
#
|
317
|
-
#
|
334
|
+
# path - String asset path to check.
|
318
335
|
#
|
336
|
+
# Returns the boolean result.
|
319
337
|
def entry?(path)
|
320
|
-
if @pristine.include?(path)
|
321
|
-
true
|
322
|
-
elsif @entries.empty?
|
338
|
+
if @pristine.include?(path) || @entries.empty?
|
323
339
|
true
|
324
340
|
else
|
325
341
|
@entries.any? do |entry|
|
@@ -328,11 +344,11 @@ class Darkroom
|
|
328
344
|
end
|
329
345
|
end
|
330
346
|
|
331
|
-
|
332
|
-
# Returns boolean indicating whether or not the asset with the provided path is already minified.
|
347
|
+
# Internal: Check if an asset's path indicates that it's already minified.
|
333
348
|
#
|
334
|
-
#
|
349
|
+
# path - String asset path to check.
|
335
350
|
#
|
351
|
+
# Returns the boolean result.
|
336
352
|
def minified?(path)
|
337
353
|
@minified.any? do |minified|
|
338
354
|
path == minified || (minified.kind_of?(Regexp) && path.match?(minified))
|