class Sprockets::Manifest
information 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 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(count = 2, age = 3600)
age=600.
To only keep files created within the last 10 minutes, set count=0 and
To force only 1 backup to be kept, set count=1 and age=0.
Examples
keep the latest version, 2 backups and any created within the past hour.
Cleanup old assets in the compile directory. By default it will
def clean(count = 2, age = 3600) asset_versions = files.group_by { |_, attrs| attrs['logical_path'] } asset_versions.each do |logical_path, versions| current = assets[logical_path] versions.reject { |path, _| path == current }.sort_by { |_, attrs| # Sort by timestamp Time.parse(attrs['mtime']) }.reverse.each_with_index.drop_while { |(_, attrs), index| _age = [0, Time.now - Time.parse(attrs['mtime'])].max # Keep if under age or within the count limit _age < age || index < count }.each { |(path, _), _| # Remove old assets remove(path) } end end
def clobber
def clobber FileUtils.rm_r(directory) if File.exist?(directory) logger.info "Removed #{directory}" # if we have an environment clear the cache too environment.cache.clear if environment nil end
def compile(*args)
compile("application.js")
also inserted into the manifest file.
`application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
fingerprinted filename like
Compile asset to directory. The asset is written to a
def compile(*args) unless environment raise Error, "manifest requires environment for compilation" end filenames = [] concurrent_exporters = [] assets_to_export = Concurrent::Array.new find(*args) do |asset| assets_to_export << asset end assets_to_export.each do |asset| mtime = Time.now.iso8601 files[asset.digest_path] = { 'logical_path' => asset.logical_path, 'mtime' => mtime, 'size' => asset.bytesize, 'digest' => asset.hexdigest, # Deprecated: Remove beta integrity attribute in next release. # Callers should DigestUtils.hexdigest_integrity_uri to compute the # digest themselves. 'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest) } assets[asset.logical_path] = asset.digest_path filenames << asset.filename promise = nil exporters_for_asset(asset) do |exporter| next if exporter.skip?(logger) if promise.nil? promise = Concurrent::Promise.new(executor: executor) { exporter.call } concurrent_exporters << promise.execute else concurrent_exporters << promise.then { exporter.call } end end end # make sure all exporters have finished before returning the main thread concurrent_exporters.each(&:wait!) save filenames end
def executor
def executor @executor ||= environment.export_concurrent ? :fast : :immediate end
def exporters_for_asset(asset)
end
array << exporter
exporters_for_asset(asset) do |exporter|
puts asset.content_type # => "application/javascript"
array = []
Will yield each expoter to the passed in block.
match its mime-type.
Given an asset, finds all exporters that
def exporters_for_asset(asset) exporters = [Exporters::FileExporter] environment.exporters.each do |mime_type, exporter_list| next unless asset.content_type next unless environment.match_mime_type? asset.content_type, mime_type exporter_list.each do |exporter| exporters << exporter end end exporters.uniq! exporters.each do |exporter| yield exporter.new(asset: asset, environment: environment, directory: dir) 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(*args, &block)
Public: Find all assets matching pattern set in environment.
def find(*args, &block) unless environment raise Error, "manifest requires environment for compilation" end return to_enum(__method__, *args) unless block_given? environment = self.environment.cached promises = args.flatten.map do |path| Concurrent::Promise.execute(executor: executor) do environment.find_all_linked_assets(path).to_a end end promises.each do |promise| promise.value!.each(&block) end nil end
def find_sources(*args)
Public: Find the source of assets by paths.
def find_sources(*args) return to_enum(__method__, *args) unless block_given? if environment find(*args).each do |asset| yield asset.source end else args.each do |path| asset = assets[path] yield File.binread(File.join(dir, asset)) if asset end end end
def initialize(*args)
Manifest.new(environment, "./public/assets/manifest.json")
".sprockets-manifest-*.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] # Whether the manifest file is using the old manifest-*.json naming convention @legacy_manifest = false # 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 location if @directory && @filename.nil? @filename = find_directory_manifest(@directory, logger) 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) 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 data = json_encode(@data) FileUtils.mkdir_p File.dirname(@filename) PathUtils.atomic_write(@filename) do |f| f.write(data) end end