Class: Darkroom
- Inherits:
-
Object
- Object
- Darkroom
- Defined in:
- lib/darkroom/darkroom.rb,
lib/darkroom/asset.rb,
lib/darkroom/version.rb,
lib/darkroom/delegate.rb,
lib/darkroom/errors/asset_error.rb,
lib/darkroom/delegates/css_delegate.rb,
lib/darkroom/delegates/htx_delegate.rb,
lib/darkroom/delegates/html_delegate.rb,
lib/darkroom/errors/processing_error.rb,
lib/darkroom/errors/invalid_path_error.rb,
lib/darkroom/errors/asset_not_found_error.rb,
lib/darkroom/errors/duplicate_asset_error.rb,
lib/darkroom/errors/missing_library_error.rb,
lib/darkroom/delegates/javascript_delegate.rb,
lib/darkroom/errors/circular_reference_error.rb,
lib/darkroom/errors/unrecognized_extension_error.rb
Overview
Main class providing simple and straightforward web asset management.
Defined Under Namespace
Classes: Asset, AssetError, AssetNotFoundError, CSSDelegate, CircularReferenceError, Delegate, DuplicateAssetError, HTMLDelegate, HTXDelegate, InvalidPathError, JavaScriptDelegate, MissingLibraryError, ProcessingError, UnrecognizedExtensionError
Constant Summary collapse
- VERSION =
'0.0.10'- DEFAULT_MINIFIED =
/(\.|-)min\.\w+$/.freeze
- TRAILING_SLASHES =
%r{/+$}.freeze
- PRISTINE =
Set.new(%w[/favicon.ico /mask-icon.svg /humans.txt /robots.txt]).freeze
- MIN_PROCESS_INTERVAL =
0.5- @@delegates =
{}
- @@glob =
''
Class Attribute Summary collapse
-
.javascript_iife ⇒ Object
Returns the value of attribute javascript_iife.
Instance Attribute Summary collapse
-
#error ⇒ Object
readonly
Returns the value of attribute error.
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
-
#process_key ⇒ Object
readonly
Returns the value of attribute process_key.
Class Method Summary collapse
-
.delegate(extension) ⇒ Object
Public: Get the Delegate associated with a file extension.
-
.register(*args, &block) ⇒ Object
Public: Register a delegate for handling a specific kind of asset.
Instance Method Summary collapse
-
#asset(path) ⇒ Object
Public: Get an Asset object, given its external path.
-
#asset_integrity(path, algorithm = nil) ⇒ Object
Public: Get an asset’s subresource integrity string.
-
#asset_path(path, versioned: !@pristine.include?(path))) ⇒ Object
Public: Get the external asset path, given its internal path.
-
#dump(dir, clear: false, include_pristine: true) ⇒ Object
Public: Write assets to disk.
-
#error? ⇒ Boolean
Public: Check if there were any errors encountered the last time assets were processed.
-
#initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil, minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL) ⇒ Darkroom
constructor
Public: Create a new instance.
-
#inspect ⇒ Object
Public: Get a high-level object info string about this Darkroom instance.
-
#manifest(path) ⇒ Object
Public: Get the Asset object from the manifest Hash associated with the given path.
-
#process ⇒ Object
Public: Walk all load paths and refresh any assets that have been modified on disk since the last call to this method.
-
#process! ⇒ Object
Public: Call #process but raise an error if there were errors.
Constructor Details
#initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil, minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL) ⇒ Darkroom
Public: Create a new instance.
load_paths - One or more String paths where assets are located on disk. host: - String host or Array of String hosts to prepend to paths (useful when serving
from a CDN in production). If multiple hosts are specified, they will be round-
robined within each thread for each call to #asset_path.
hosts: - String or Array of Strings (alias of host:). prefix: - String prefix to prepend to asset paths (e.g. ‘/assets’). pristine: - String, Array of String, or Set of String paths that should not include the
prefix and for which the unversioned form should be provided by default (e.g.
'/favicon.ico').
entries: - String, Regexp, or Array of String and/or Regexp specifying entry point paths /
path patterns.
minify: - Boolean specifying if assets that support it should be minified. minified: - String, Regexp, or Array of String and/or Regexp specifying paths of assets that
are already minified and thus shouldn't be minified.
min_process_interval: - Numeric minimum number of seconds required between one run of asset processing
and another.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/darkroom/darkroom.rb', line 94 def initialize(*load_paths, host: nil, hosts: nil, prefix: nil, pristine: nil, entries: nil, minify: false, minified: DEFAULT_MINIFIED, min_process_interval: MIN_PROCESS_INTERVAL) @load_paths = load_paths.map { |load_path| File.(load_path) } @hosts = (Array(host) + Array(hosts)).map! { |h| h.sub(TRAILING_SLASHES, '') } @entries = Array(entries) @minify = minify @minified = Array(minified) @prefix = prefix&.sub(TRAILING_SLASHES, '') @prefix = nil if @prefix && @prefix.empty? @pristine = PRISTINE.dup.merge(Array(pristine)) @min_process_interval = min_process_interval @last_processed_at = 0 @process_key = 0 @mutex = Mutex.new @manifest = {} @manifest_unversioned = {} @manifest_versioned = {} @errors = [] Thread.current[:darkroom_host_index] = -1 unless @hosts.empty? end |
Class Attribute Details
.javascript_iife ⇒ Object
Returns the value of attribute javascript_iife.
24 25 26 |
# File 'lib/darkroom/darkroom.rb', line 24 def javascript_iife @javascript_iife end |
Instance Attribute Details
#error ⇒ Object (readonly)
Returns the value of attribute error.
22 23 24 |
# File 'lib/darkroom/darkroom.rb', line 22 def error @error end |
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
22 23 24 |
# File 'lib/darkroom/darkroom.rb', line 22 def errors @errors end |
#process_key ⇒ Object (readonly)
Returns the value of attribute process_key.
22 23 24 |
# File 'lib/darkroom/darkroom.rb', line 22 def process_key @process_key end |
Class Method Details
.delegate(extension) ⇒ Object
Public: Get the Delegate associated with a file extension.
extension - String file extension of the desired delegate (e.g. ‘.js’)
Returns the Delegate class.
72 73 74 |
# File 'lib/darkroom/darkroom.rb', line 72 def self.delegate(extension) @@delegates[extension] end |
.register(*args, &block) ⇒ Object
Public: Register a delegate for handling a specific kind of asset.
args - One or more String file extensions to associate with this delegate, optionally followed by
either an HTTP MIME type String or a Delegate subclass.
block - Block to call that defines or extends the Delegate.
Examples
Darkroom.register('.ext1', '.ext2', 'content/type')
Darkroom.register('.ext', MyDelegateSubclass)
Darkroom.register('.scss', 'text/css') do
compile(lib: 'sassc') { ... }
end
Darkroom.register('.scss', SCSSDelegate) do
# Modifications/overrides of the SCSSDelegate class...
end
Returns the Delegate class.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/darkroom/darkroom.rb', line 46 def self.register(*args, &block) last_arg = args.pop unless args.last.kind_of?(String) && args.last[0] == '.' extensions = args if last_arg.nil? || last_arg.kind_of?(String) content_type = last_arg delegate = Class.new(Delegate, &block) delegate.content_type(content_type) if content_type && !delegate.content_type elsif last_arg.kind_of?(Class) && last_arg < Delegate delegate = block ? Class.new(last_arg, &block) : last_arg end extensions.each do |extension| @@delegates[extension] = delegate end @@glob = "**/*{#{@@delegates.keys.sort.join(',')}}" delegate end |
Instance Method Details
#asset(path) ⇒ Object
Public: Get an Asset object, given its external path. An external path includes any prefix and can be either the versioned or unversioned form (i.e. how an HTTP request for the asset comes in).
Examples
# Suppose the asset's internal path is '/js/app.js' and the prefix is '/assets'.
darkroom.asset('/assets/js/app-<hash>.js') # => #<Darkroom::Asset [...]>
darkroom.asset('/assets/js/app.js') # => #<Darkroom::Asset [...]>
path - String external path of the asset.
Returns the Asset object if it exists or nil otherwise.
224 225 226 |
# File 'lib/darkroom/darkroom.rb', line 224 def asset(path) @manifest_versioned[path] || @manifest_unversioned[path] end |
#asset_integrity(path, algorithm = nil) ⇒ Object
Public: Get an asset’s subresource integrity string.
path - String internal path of the asset. algorithm - Symbol hash algorithm name to use to generate the integrity string (must be one of
:sha256, :sha384, :sha512).
Returns the asset’s subresource integrity String. Raises AssetNotFoundError if the asset doesn’t exist.
263 264 265 266 267 |
# File 'lib/darkroom/darkroom.rb', line 263 def asset_integrity(path, algorithm = nil) asset = @manifest[path] or raise(AssetNotFoundError.new(path)) algorithm ? asset.integrity(algorithm) : asset.integrity end |
#asset_path(path, versioned: !@pristine.include?(path))) ⇒ Object
Public: Get the external asset path, given its internal path. An external path includes any prefix and can be either the versioned or unversioned form (i.e. how an HTTP request for the asset comes in).
path - String internal path of the asset. versioned: - Boolean specifying either the versioned or unversioned path to be returned.
Examples
# Suppose the asset's internal path is '/js/app.js' and the prefix is '/assets'.
darkroom.asset_path('/js/app.js') # => "/assets/js/app-<hash>.js"
darkroom.asset_path('/js/app.js', versioned: false) # => "/assets/js/app.js"
Returns the String external asset path. Raises AssetNotFoundError if the asset doesn’t exist.
242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/darkroom/darkroom.rb', line 242 def asset_path(path, versioned: !@pristine.include?(path)) asset = @manifest[path] or raise(AssetNotFoundError.new(path)) unless @hosts.empty? host_index = (Thread.current[:darkroom_host_index] + 1) % @hosts.size host = @hosts[host_index] Thread.current[:darkroom_host_index] = host_index end "#{host}#{versioned ? asset.path_versioned : asset.path_unversioned}" end |
#dump(dir, clear: false, include_pristine: true) ⇒ Object
Public: Write assets to disk. This is useful when deploying to a production environment where assets will be uploaded to and served from a CDN or proxy server. Note that #process must be called manually before calling this method.
dir - String directory path to write the assets to. clear: - Boolean indicating if the existing contents of the directory should be deleted
before writing files.
include_pristine: - Boolean indicating if pristine assets should be included (when dumping for the
purpose of uploading to a CDN, assets such as /robots.txt and /favicon.ico don't
need to be included).
Returns nothing. Raises ProcessingError if errors were encountered during the last #process run.
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
# File 'lib/darkroom/darkroom.rb', line 291 def dump(dir, clear: false, include_pristine: true) raise(@error) if @error require('fileutils') dir = File.(dir) FileUtils.mkdir_p(dir) Dir.each_child(dir) { |child| FileUtils.rm_rf(File.join(dir, child)) } if clear @manifest_versioned.each do |path, asset| next if @pristine.include?(asset.path) && !include_pristine file_path = File.join(dir, @pristine.include?(asset.path) ? asset.path_unversioned : path) FileUtils.mkdir_p(File.dirname(file_path)) File.write(file_path, asset.content) end end |
#error? ⇒ Boolean
Public: Check if there were any errors encountered the last time assets were processed.
Returns the boolean result.
208 209 210 |
# File 'lib/darkroom/darkroom.rb', line 208 def error? !!@error end |
#inspect ⇒ Object
Public: Get a high-level object info string about this Darkroom instance.
Returns the String.
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/darkroom/darkroom.rb', line 314 def inspect "#<#{self.class} " \ "@entries=#{@entries.inspect}, " \ "@errors=#{@errors.inspect}, " \ "@hosts=#{@hosts.inspect}, " \ "@last_processed_at=#{@last_processed_at.inspect}, " \ "@load_paths=#{@load_paths.inspect}, " \ "@min_process_interval=#{@min_process_interval.inspect}, " \ "@minified=#{@minified.inspect}, " \ "@minify=#{@minify.inspect}, " \ "@prefix=#{@prefix.inspect}, " \ "@pristine=#{@pristine.inspect}, " \ "@process_key=#{@process_key.inspect}" \ '>' end |
#manifest(path) ⇒ Object
Public: Get the Asset object from the manifest Hash associated with the given path.
path - String internal path of the asset.
Returns the Asset object if it exists or nil otherwise.
274 275 276 |
# File 'lib/darkroom/darkroom.rb', line 274 def manifest(path) @manifest[path] end |
#process ⇒ Object
Public: Walk all load paths and refresh any assets that have been modified on disk since the last call to this method. Processing is skipped if either a) a previous call to this method happened less than min_process_interval seconds ago or b) another thread is currently executing this method.
A mutex is used to ensure that, say, multiple web request threads do not trample each other. If the mutex is locked when this method is called, it will wait until the mutex is released to ensure that the caller does not then start working with stale / invalid Asset objects due to the work of the other thread’s active call to #process being incomplete.
If any errors are encountered during processing, they must be checked for manually afterward via #error or #errors. If a raise is preferred, use #process! instead.
Returns boolean indicating if processing actually happened (true) or was skipped (false).
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/darkroom/darkroom.rb', line 135 def process return false if Time.now.to_f - @last_processed_at < @min_process_interval if @mutex.locked? @mutex.synchronize {} # Wait until other #process call is done to avoid stale/invalid assets. return false end @mutex.synchronize do @process_key += 1 @errors.clear found = {} @load_paths.each do |load_path| Dir.glob(File.join(load_path, @@glob)).each do |file| path = file.sub(load_path, '') if (index = path.index(Asset::INVALID_PATH_REGEX)) @errors << InvalidPathError.new(path, index) elsif found.key?(path) @errors << DuplicateAssetError.new(path, found[path], load_path) else found[path] = load_path unless @manifest.key?(path) entry = entry?(path) @manifest[path] = Asset.new( path, file, self, prefix: (@prefix unless @pristine.include?(path)), entry: entry, minify: entry && @minify && !minified?(path), ) end end end end @manifest.select! { |path, _| found.key?(path) } @manifest_unversioned.clear @manifest_versioned.clear @manifest.each_value do |asset| asset.process if asset.entry? @manifest_unversioned[asset.path_unversioned] = asset @manifest_versioned[asset.path_versioned] = asset end @errors.concat(asset.errors) end true ensure @last_processed_at = Time.now.to_f @error = @errors.empty? ? nil : ProcessingError.new(@errors) end end |
#process! ⇒ Object
Public: Call #process but raise an error if there were errors.
Returns boolean indicating if processing actually happened (true) or was skipped (false). Raises ProcessingError if processing actually happened from this call and error(s) were encountered.
199 200 201 202 203 |
# File 'lib/darkroom/darkroom.rb', line 199 def process! result = process result && @error ? raise(@error) : result end |