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.
@@ -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 = /\/+$/.freeze
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
- # [*extensions] One or more file extension(s) to associate with this delegate.
31
- # [delegate] An HTTP MIME type string or a Delegate subclass.
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
- def self.register(*extensions, delegate, &block)
34
- if delegate.kind_of?(String)
35
- content_type = delegate
36
-
37
- if delegate[0] == '.'
38
- extensions << delegate
39
- content_type = nil
40
- end
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 delegate && delegate < Delegate
45
- delegate = block ? Class.new(delegate, &block) : delegate
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
- # [extension] File extension of the desired delegate.
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
- minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL)
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! { |host| host.sub(TRAILING_SLASHES, '') }
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
- # Walks all load paths and refreshes any assets that have been modified on disk since the last call to
121
- # this method. Returns false if processing was skipped due to previous call happening less than
122
- # min_process_interval ago or because another thread was already processing; returns true otherwise.
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)).sort.each do |file|
149
+ Dir.glob(File.join(load_path, @@glob)).each do |file|
139
150
  path = file.sub(load_path, '')
140
151
 
141
- if index = (path =~ Asset::INVALID_PATH_REGEX)
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(path, file, self,
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.each do |path, asset|
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
- (result && @error) ? raise(@error) : result
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
- # Returns an Asset object, given its external path. An external path includes any prefix and and can be
203
- # either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the asset comes
204
- # in). For example, to get the Asset object with path +/js/app.js+ when prefix is +/assets+:
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
- # darkroom.asset('/assets/js/app.<hash>.js')
207
- # darkroom.asset('/assets/js/app.js')
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
- # [path] External path of the asset.
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
- # Returns the external asset path, given its internal path. An external path includes any prefix and and
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
- # darkroom.asset_path('/js/app.js') # => /assets/js/app.<hash>.js
222
- # darkroom.asset_path('/js/app.js', versioned: false) # => /assets/js/app.js
231
+ # path - String internal path of the asset.
232
+ # versioned: - Boolean specifying either the versioned or unversioned path to be returned.
223
233
  #
224
- # Raises an AssetNotFoundError if the asset doesn't exist.
234
+ # Examples
225
235
  #
226
- # [path] Internal path of the asset.
227
- # [versioned:] Boolean indicating whether the versioned or unversioned path should be returned.
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
- host = @hosts.empty? ? '' : @hosts[
232
- Thread.current[:darkroom_host_index] = (Thread.current[:darkroom_host_index] + 1) % @hosts.size
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
- # [path] Internal path of the asset.
243
- # [algorithm] Hash algorithm to use to generate the integrity string (see Asset#integrity).
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
- # [path] Internal path of the asset.
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
- # Writes assets to disk. This is useful when deploying to a production environment where assets will be
262
- # uploaded to and served from a CDN or proxy server.
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
- # [dir] Directory to write the assets to.
265
- # [clear:] Boolean indicating whether or not the existing contents of the directory should be deleted
266
- # before performing the dump.
267
- # [include_pristine:] Boolean indicating whether or not to include pristine assets (when dumping for the
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
- # [path] Path to check.
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
- # [path] Path to check.
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))