class Sprockets::Manifest
infomation about the structure.
that don’t have sprockets loaded. See ‘#assets` and `#files` for more
should make it easy to read from other programming languages and processes
The JSON is part of the public API and should be considered stable. This
asset is the current one.
compile. A pointer from each logical path indicates which fingerprinted
records basic attributes about the asset for fast lookup without having to
The Manifest logs the contents of assets compiled to a single directory. It
def self.compile_match_filter(filter)
compile_match_filter("foo/*.js")
compile_match_filter(/application.js/)
})
File.extname(logical_path) == '.js'
compile_match_filter(proc { |logical_path|
passed to logical_paths.select(&proc).
Internal: Compile logical path matching filter into a proc that can be
def self.compile_match_filter(filter) # If the filter is already a proc, great nothing to do. if filter.respond_to?(:call) filter # If the filter is a regexp, wrap it in a proc that tests it against the # logical path. elsif filter.is_a?(Regexp) proc { |logical_path| filter.match(logical_path) } elsif filter.is_a?(String) # If its an absolute path, detect the matching full filename if PathUtils.absolute_path?(filter) proc { |logical_path, filename| filename == filter.to_s } else # Otherwise do an fnmatch against the logical path. proc { |logical_path| File.fnmatch(filter.to_s, logical_path) } end else raise TypeError, "unknown filter type: #{filter.inspect}" end end
def assets
"jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }
{ "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",
Logical path (String): Fingerprint path (String)
map to the latest fingerprinted filename.
Returns internal assets mapping. Keys are logical paths which
def assets @data['assets'] ||= {} end
def clean(keep = 2)
Cleanup old assets in the compile directory. By default it will
def clean(keep = 2) asset_versions = files.group_by { |_, attrs| attrs['logical_path'] } asset_versions.each do |logical_path, versions| current = assets[logical_path] backups = versions.reject { |path, _| path == current }.sort_by { |_, attrs| # Sort by timestamp Time.parse(attrs['mtime']) }.reverse # Keep the last N backups backups = backups[keep..-1] || [] # Remove old assets backups.each { |path, _| remove(path) } end end
def clobber
def clobber FileUtils.rm_r(directory) if File.exist?(directory) logger.info "Removed #{directory}" nil end
def compile(*args)
compile("application.js")
also inserted into the manifest file.
`application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
fingerprinted filename like
Compile and write asset to directory. The asset is written to a
def compile(*args) unless environment raise Error, "manifest requires environment for compilation" end filenames = [] filter_logical_paths(*args).each do |_, filename| find_assets(filename) do |asset| files[asset.digest_path] = { 'logical_path' => asset.logical_path, 'mtime' => asset.mtime.iso8601, 'size' => asset.bytesize, 'digest' => asset.digest } assets[asset.logical_path] = asset.digest_path target = File.join(dir, asset.digest_path) if File.exist?(target) logger.debug "Skipping #{target}, already exists" else logger.info "Writing #{target}" asset.write_to target end filenames << filename end end save filenames end
def files
'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }
'mtime' => "2011-12-13T21:47:08-06:00",
{ 'logical_path' => "application.js",
{ "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =>
digest: Base64 hex digest (String)
mtime: ISO8601 mtime (String)
logical_path: Logical path (String)
Fingerprint path (String):
which map to an attributes array.
Returns internal file directory listing. Keys are filenames
def files @data['files'] ||= {} end
def filter_logical_paths(*args)
files you want to compile.
Public: Filter logical paths in environment. Useful for selecting what
def filter_logical_paths(*args) filters = args.flatten.map { |arg| self.class.compile_match_filter(arg) } environment.logical_paths.select do |a, b| filters.any? { |f| f.call(a, b) } end end
def find_assets(path)
def find_assets(path) start = Utils.benchmark_start environment.find_all_linked_assets(path) do |asset| logger.debug do "Compiled #{asset.logical_path} (#{Utils.benchmark_end(start)}ms)" end yield asset start = Utils.benchmark_start end end
def initialize(*args)
Manifest.new(environment, "./public/assets/manifest.json")
"manifest-123.json" file in that directory.
Otherwise, if the path is a directory, the filename will default a random
dirname of the `filename` will be used to write compiled assets to.
path to the manifest json file. The file may or may not already exist. The
Create new Manifest associated with an `environment`. `filename` is a full
def initialize(*args) if args.first.is_a?(Base) || args.first.nil? @environment = args.shift end @directory, @filename = args[0], args[1] # Expand paths @directory = File.expand_path(@directory) if @directory @filename = File.expand_path(@filename) if @filename # If filename is given as the second arg if @directory && File.extname(@directory) != "" @directory, @filename = nil, @directory end # Default dir to the directory of the filename @directory ||= File.dirname(@filename) if @filename # If directory is given w/o filename, pick a random manifest.json location if @directory && @filename.nil? # Find the first manifest.json in the directory filenames = Dir[File.join(@directory, "manifest*.json")] if filenames.any? @filename = filenames.first else @filename = File.join(@directory, "manifest-#{SecureRandom.hex(16)}.json") end end unless @directory && @filename raise ArgumentError, "manifest requires output filename" end data = {} begin if File.exist?(@filename) data = json_decode(File.read(@filename)) end rescue JSON::ParserError => e logger.error "#{@filename} is invalid: #{e.class} #{e.message}" end @data = data end
def json_decode(obj)
def json_decode(obj) JSON.parse(obj, create_additions: false) end
def json_encode(obj)
def json_encode(obj) JSON.generate(obj) end
def logger
def logger if environment environment.logger else logger = Logger.new($stderr) logger.level = Logger::FATAL logger end end
def remove(filename)
manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
be the name with any directory path.
Removes file from directory and from manifest. `filename` must
def remove(filename) path = File.join(dir, filename) logical_path = files[filename]['logical_path'] if assets[logical_path] == filename assets.delete(logical_path) end files.delete(filename) FileUtils.rm(path) if File.exist?(path) save logger.info "Removed #{filename}" nil end
def save
def save FileUtils.mkdir_p File.dirname(filename) File.open(filename, 'w') do |f| f.write json_encode(@data) end end