class Sprockets::Manifest
‘#assets` and `#files` for more infomation about the structure.
languages and processes that don’t have sprockets loaded. See
stable. This should make it easy to read from other programming
The JSON is part of the public API and should be considered
indicates with fingerprinted asset is the current one.
lookup without having to compile. A pointer from each logical path
directory. It records basic attributes about the asset for fast
The Manifest logs the contents of assets compiled to a single
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 backups_for(logical_path)
version is always excluded. The return array is sorted by the
Finds all the backup assets for a logical path. The latest
def backups_for(logical_path) files.select { |filename, attrs| # Matching logical paths attrs['logical_path'] == logical_path && # Excluding whatever asset is the current assets[logical_path] != filename }.sort_by { |filename, attrs| # Sort by timestamp Time.parse(attrs['mtime']) }.reverse end
def benchmark
def benchmark start_time = Time.now.to_f yield ((Time.now.to_f - start_time) * 1000).to_i end
def clean(keep = 2)
Cleanup old assets in the compile directory. By default it will
def clean(keep = 2) self.assets.keys.each do |logical_path| # Get assets sorted by ctime, newest first assets = backups_for(logical_path) # Keep the last N backups assets = assets[keep..-1] || [] # Remove old assets assets.each { |path, _| remove(path) } end end
def clobber
def clobber FileUtils.rm_r(@dir) if File.exist?(@dir) logger.info "Removed #{@dir}" 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 paths = environment.each_logical_path(*args).to_a + args.flatten.select { |fn| Pathname.new(fn).absolute? if fn.is_a?(String)} paths.each do |path| if asset = find_asset(path) 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 asset.write_to "#{target}.gz" if asset.is_a?(BundledAsset) end save asset end end 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 find_asset(logical_path)
def find_asset(logical_path) asset = nil ms = benchmark do asset = environment.find_asset(logical_path) end logger.debug "Compiled #{logical_path} (#{ms}ms)" asset end
def initialize(*args)
Manifest.new(environment, "./public/assets/manifest.json")
directory.
filename will default a random "manifest-123.json" file in that
compiled assets to. Otherwise, if the path is a directory, the
already exist. The dirname of the `path` will be used to write
a full path to the manifest json file. The file may or may not
Create new Manifest associated with an `environment`. `path` is
def initialize(*args) if args.first.is_a?(Base) || args.first.nil? @environment = args.shift end @dir, @path = args[0], args[1] # Expand paths @dir = File.expand_path(@dir) if @dir @path = File.expand_path(@path) if @path # If path is given as the second arg if @dir && File.extname(@dir) != "" @dir, @path = nil, @dir end # Default dir to the directory of the path @dir ||= File.dirname(@path) if @path # If directory is given w/o path, pick a random manifest.json location if @dir && @path.nil? # Find the first manifest.json in the directory paths = Dir[File.join(@dir, "manifest*.json")] if paths.any? @path = paths.first else @path = File.join(@dir, "manifest-#{SecureRandom.hex(16)}.json") end end unless @dir && @path raise ArgumentError, "manifest requires output path" end data = nil begin if File.exist?(@path) data = json_decode(File.read(@path)) end rescue MultiJson::DecodeError => e logger.error "#{@path} is invalid: #{e.class} #{e.message}" end @data = data.is_a?(Hash) ? data : {} end
def json_decode(obj)
def json_decode(obj) MultiJson.load(obj) end
def json_decode(obj)
def json_decode(obj) MultiJson.decode(obj) end
def json_encode(obj)
def json_encode(obj) MultiJson.dump(obj) end
def json_encode(obj)
def json_encode(obj) MultiJson.encode(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) gzip = "#{path}.gz" 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) FileUtils.rm(gzip) if File.exist?(gzip) save logger.info "Removed #{filename}" nil end
def save
def save FileUtils.mkdir_p dir File.open(path, 'w') do |f| f.write json_encode(@data) end end