darkroom 0.0.8 → 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,29 +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.kind_of?(Hash)
45
- deprecated("#{self.name}.register with a Hash is deprecated: use the Delegate DSL inside a block "\
46
- 'instead')
47
- delegate = Delegate.deprecated_from_hash(**delegate)
48
- elsif delegate && delegate < Delegate
49
- 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
50
56
  end
51
57
 
52
58
  extensions.each do |extension|
@@ -58,66 +64,41 @@ class Darkroom
58
64
  delegate
59
65
  end
60
66
 
61
- ##
62
- # Returns the delegate associated with a file extension.
67
+ # Public: Get the Delegate associated with a file extension.
63
68
  #
64
- # [extension] File extension of the desired delegate.
69
+ # extension - String file extension of the desired delegate (e.g. '.js')
65
70
  #
71
+ # Returns the Delegate class.
66
72
  def self.delegate(extension)
67
73
  @@delegates[extension]
68
74
  end
69
75
 
70
- ##
71
- # Utility method that prints a warning with file and line number of a deprecated call.
72
- #
73
- def self.deprecated(message)
74
- location = caller_locations(2, 1).first
75
-
76
- warn("#{location.path}:#{location.lineno}: #{message}")
77
- end
78
-
79
- ##
80
- # Creates a new instance.
81
- #
82
- # [*load_paths] One or more paths where assets are located on disk.
83
- # [host:] Host(s) to prepend to paths (useful when serving from a CDN in production). If multiple hosts
84
- # are specified, they will be round-robined within each thread for each call to +#asset_path+.
85
- # [hosts:] Alias of +host:+.
86
- # [prefix:] Prefix to prepend to asset paths (e.g. +/assets+).
87
- # [pristine:] Path(s) that should not include prefix and for which unversioned form should be provided by
88
- # default (e.g. +/favicon.ico+).
89
- # [entries:] String, regex, or array of strings and regexes specifying entry point paths / path patterns.
90
- # [minify:] Boolean specifying whether or not to minify assets.
91
- # [minified:] String, regex, or array of strings and regexes specifying paths of assets that are already
92
- # minified and thus should be skipped for minification.
93
- # [minified_pattern:] DEPRECATED: use +minified:+ instead. Regex used against asset paths to determine if
94
- # they are already minified and should therefore be skipped over for minification.
95
- # [internal_pattern:] DEPRECATED: use +entries:+ instead. Regex used against asset paths to determine if
96
- # they should be marked as internal and therefore made inaccessible externally.
97
- # [min_process_interval:] Minimum time required between one run of asset processing and another.
76
+ # Public: Create a new instance.
98
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.
99
94
  def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil,
100
- minify: false, minified: DEFAULT_MINIFIED, minified_pattern: nil, internal_pattern: nil,
101
- min_process_interval: MIN_PROCESS_INTERVAL)
95
+ minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL)
102
96
  @load_paths = load_paths.map { |load_path| File.expand_path(load_path) }
103
97
 
104
- @hosts = (Array(host) + Array(hosts)).map! { |host| host.sub(TRAILING_SLASHES, '') }
98
+ @hosts = (Array(host) + Array(hosts)).map! { |h| h.sub(TRAILING_SLASHES, '') }
105
99
  @entries = Array(entries)
106
100
  @minify = minify
107
101
  @minified = Array(minified)
108
- @internal_pattern = internal_pattern
109
-
110
- if minified_pattern
111
- self.class.deprecated("#{self.class.name} :minified_pattern is deprecated: use :minified instead "\
112
- 'and pass a string, regex, or array of strings and regexes')
113
- @minified = [minified_pattern]
114
- end
115
-
116
- if @internal_pattern
117
- self.class.deprecated("#{self.class.name} :internal_pattern is deprecated: use :entries to instead "\
118
- 'specify which assets are entry points (i.e. available externally) and pass a string, regex, or '\
119
- 'array of strings and regexes')
120
- end
121
102
 
122
103
  @prefix = prefix&.sub(TRAILING_SLASHES, '')
123
104
  @prefix = nil if @prefix && @prefix.empty?
@@ -138,16 +119,24 @@ class Darkroom
138
119
  Thread.current[:darkroom_host_index] = -1 unless @hosts.empty?
139
120
  end
140
121
 
141
- ##
142
- # Walks all load paths and refreshes any assets that have been modified on disk since the last call to
143
- # this method. Returns false if processing was skipped due to previous call happening less than
144
- # 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.
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.
145
133
  #
134
+ # Returns boolean indicating if processing actually happened (true) or was skipped (false).
146
135
  def process
147
136
  return false if Time.now.to_f - @last_processed_at < @min_process_interval
148
137
 
149
138
  if @mutex.locked?
150
- @mutex.synchronize {}
139
+ @mutex.synchronize {} # Wait until other #process call is done to avoid stale/invalid assets.
151
140
  return false
152
141
  end
153
142
 
@@ -157,10 +146,10 @@ class Darkroom
157
146
  found = {}
158
147
 
159
148
  @load_paths.each do |load_path|
160
- Dir.glob(File.join(load_path, @@glob)).sort.each do |file|
149
+ Dir.glob(File.join(load_path, @@glob)).each do |file|
161
150
  path = file.sub(load_path, '')
162
151
 
163
- if index = (path =~ Asset::INVALID_PATH_REGEX)
152
+ if (index = path.index(Asset::INVALID_PATH_REGEX))
164
153
  @errors << InvalidPathError.new(path, index)
165
154
  elsif found.key?(path)
166
155
  @errors << DuplicateAssetError.new(path, found[path], load_path)
@@ -170,7 +159,8 @@ class Darkroom
170
159
  unless @manifest.key?(path)
171
160
  entry = entry?(path)
172
161
 
173
- @manifest[path] = Asset.new(path, file, self,
162
+ @manifest[path] = Asset.new(
163
+ path, file, self,
174
164
  prefix: (@prefix unless @pristine.include?(path)),
175
165
  entry: entry,
176
166
  minify: entry && @minify && !minified?(path),
@@ -184,7 +174,7 @@ class Darkroom
184
174
  @manifest_unversioned.clear
185
175
  @manifest_versioned.clear
186
176
 
187
- @manifest.each do |path, asset|
177
+ @manifest.each_value do |asset|
188
178
  asset.process
189
179
 
190
180
  if asset.entry?
@@ -202,94 +192,102 @@ class Darkroom
202
192
  end
203
193
  end
204
194
 
205
- ##
206
- # Calls #process. If processing was skipped, returns false. If processing was performed, raises an
207
- # exception if any errors were encountered and returns true otherwise.
195
+ # Public: Call #process but raise an error if there were errors.
208
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.
209
199
  def process!
210
200
  result = process
211
201
 
212
- (result && @error) ? raise(@error) : result
202
+ result && @error ? raise(@error) : result
213
203
  end
214
204
 
215
- ##
216
- # Returns boolean indicating whether or not there were any errors encountered the last time assets were
217
- # processed.
205
+ # Public: Check if there were any errors encountered the last time assets were processed.
218
206
  #
207
+ # Returns the boolean result.
219
208
  def error?
220
209
  !!@error
221
210
  end
222
211
 
223
- ##
224
- # Returns an Asset object, given its external path. An external path includes any prefix and and can be
225
- # either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the asset comes
226
- # 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).
227
214
  #
228
- # darkroom.asset('/assets/js/app.<hash>.js')
229
- # darkroom.asset('/assets/js/app.js')
215
+ # Examples
230
216
  #
231
- # [path] External path of the asset.
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 [...]>
232
220
  #
221
+ # path - String external path of the asset.
222
+ #
223
+ # Returns the Asset object if it exists or nil otherwise.
233
224
  def asset(path)
234
225
  @manifest_versioned[path] || @manifest_unversioned[path]
235
226
  end
236
227
 
237
- ##
238
- # Returns the external asset path, given its internal path. An external path includes any prefix and and
239
- # can be either the versioned or unversioned form of the asset path (i.e. how an HTTP request for the
240
- # asset comes in). For example, to get the external path for the Asset object with path +/js/app.js+ when
241
- # 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).
242
230
  #
243
- # darkroom.asset_path('/js/app.js') # => /assets/js/app.<hash>.js
244
- # 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.
245
233
  #
246
- # Raises an AssetNotFoundError if the asset doesn't exist.
234
+ # Examples
247
235
  #
248
- # [path] Internal path of the asset.
249
- # [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"
250
239
  #
240
+ # Returns the String external asset path.
241
+ # Raises AssetNotFoundError if the asset doesn't exist.
251
242
  def asset_path(path, versioned: !@pristine.include?(path))
252
243
  asset = @manifest[path] or raise(AssetNotFoundError.new(path))
253
- host = @hosts.empty? ? '' : @hosts[
254
- Thread.current[:darkroom_host_index] = (Thread.current[:darkroom_host_index] + 1) % @hosts.size
255
- ]
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
256
251
 
257
252
  "#{host}#{versioned ? asset.path_versioned : asset.path_unversioned}"
258
253
  end
259
254
 
260
- ##
261
- # Returns an asset's subresource integrity string. Raises an AssetNotFoundError if the asset doesn't
262
- # exist.
255
+ # Public: Get an asset's subresource integrity string.
263
256
  #
264
- # [path] Internal path of the asset.
265
- # [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).
266
260
  #
261
+ # Returns the asset's subresource integrity String.
262
+ # Raises AssetNotFoundError if the asset doesn't exist.
267
263
  def asset_integrity(path, algorithm = nil)
268
264
  asset = @manifest[path] or raise(AssetNotFoundError.new(path))
269
265
 
270
266
  algorithm ? asset.integrity(algorithm) : asset.integrity
271
267
  end
272
268
 
273
- ##
274
- # 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.
275
270
  #
276
- # [path] Internal path of the asset.
271
+ # path - String internal path of the asset.
277
272
  #
273
+ # Returns the Asset object if it exists or nil otherwise.
278
274
  def manifest(path)
279
275
  @manifest[path]
280
276
  end
281
277
 
282
- ##
283
- # Writes assets to disk. This is useful when deploying to a production environment where assets will be
284
- # 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.
285
281
  #
286
- # [dir] Directory to write the assets to.
287
- # [clear:] Boolean indicating whether or not the existing contents of the directory should be deleted
288
- # before performing the dump.
289
- # [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
290
286
  # purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't
291
287
  # need to be included).
292
288
  #
289
+ # Returns nothing.
290
+ # Raises ProcessingError if errors were encountered during the last #process run.
293
291
  def dump(dir, clear: false, include_pristine: true)
294
292
  raise(@error) if @error
295
293
 
@@ -303,48 +301,41 @@ class Darkroom
303
301
  @manifest_versioned.each do |path, asset|
304
302
  next if @pristine.include?(asset.path) && !include_pristine
305
303
 
306
- file_path = File.join(dir,
307
- @pristine.include?(asset.path) ? asset.path_unversioned : path
308
- )
304
+ file_path = File.join(dir, @pristine.include?(asset.path) ? asset.path_unversioned : path)
309
305
 
310
306
  FileUtils.mkdir_p(File.dirname(file_path))
311
307
  File.write(file_path, asset.content)
312
308
  end
313
309
  end
314
310
 
315
- ##
316
- # Returns high-level object info string.
311
+ # Public: Get a high-level object info string about this Darkroom instance.
317
312
  #
313
+ # Returns the String.
318
314
  def inspect
319
- "#<#{self.class}: "\
320
- "@entries=#{@entries.inspect}, "\
321
- "@errors=#{@errors.inspect}, "\
322
- "@hosts=#{@hosts.inspect}, "\
323
- "@internal_pattern=#{@internal_pattern.inspect}, "\
324
- "@last_processed_at=#{@last_processed_at.inspect}, "\
325
- "@load_paths=#{@load_paths.inspect}, "\
326
- "@min_process_interval=#{@min_process_interval.inspect}, "\
327
- "@minified=#{@minified.inspect}, "\
328
- "@minify=#{@minify.inspect}, "\
329
- "@prefix=#{@prefix.inspect}, "\
330
- "@pristine=#{@pristine.inspect}, "\
331
- "@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}" \
332
327
  '>'
333
328
  end
334
329
 
335
330
  private
336
331
 
337
- ##
338
- # 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.
339
333
  #
340
- # [path] Path to check.
334
+ # path - String asset path to check.
341
335
  #
336
+ # Returns the boolean result.
342
337
  def entry?(path)
343
- if @pristine.include?(path)
344
- true
345
- elsif @internal_pattern && @entries.empty?
346
- !path.match?(@internal_pattern)
347
- elsif @entries.empty?
338
+ if @pristine.include?(path) || @entries.empty?
348
339
  true
349
340
  else
350
341
  @entries.any? do |entry|
@@ -353,11 +344,11 @@ class Darkroom
353
344
  end
354
345
  end
355
346
 
356
- ##
357
- # 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.
358
348
  #
359
- # [path] Path to check.
349
+ # path - String asset path to check.
360
350
  #
351
+ # Returns the boolean result.
361
352
  def minified?(path)
362
353
  @minified.any? do |minified|
363
354
  path == minified || (minified.kind_of?(Regexp) && path.match?(minified))