# frozen_string_literal: truerequire'json'require'time'require'concurrent'require'sprockets/manifest_utils'moduleSprockets# The Manifest logs the contents of assets compiled to a single directory. It# records basic attributes about the asset for fast lookup without having to# compile. A pointer from each logical path indicates which fingerprinted# asset is the current one.## The JSON is part of the public API and should be considered stable. This# should make it easy to read from other programming languages and processes# that don't have sprockets loaded. See `#assets` and `#files` for more# information about the structure.classManifestincludeManifestUtilsattr_reader:environment# Create new Manifest associated with an `environment`. `filename` is a full# path to the manifest json file. The file may or may not already exist. The# dirname of the `filename` will be used to write compiled assets to.# Otherwise, if the path is a directory, the filename will default a random# ".sprockets-manifest-*.json" file in that directory.## Manifest.new(environment, "./public/assets/manifest.json")#definitialize(*args)ifargs.first.is_a?(Base)||args.first.nil?@environment=args.shiftend@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 argif@directory&&File.extname(@directory)!=""@directory,@filename=nil,@directoryend# 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 locationif@directory&&@filename.nil?@filename=find_directory_manifest(@directory,logger)endunless@directory&&@filenameraiseArgumentError,"manifest requires output filename"enddata={}beginifFile.exist?(@filename)data=json_decode(File.read(@filename))endrescueJSON::ParserError=>elogger.error"#{@filename} is invalid: #{e.class}#{e.message}"end@data=dataend# Returns String path to manifest.json file.attr_reader:filenamealias_method:path,:filenameattr_reader:directoryalias_method:dir,:directory# Returns internal assets mapping. Keys are logical paths which# map to the latest fingerprinted filename.## Logical path (String): Fingerprint path (String)## { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",# "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }#defassets@data['assets']||={}end# Returns internal file directory listing. Keys are filenames# which map to an attributes array.## Fingerprint path (String):# logical_path: Logical path (String)# mtime: ISO8601 mtime (String)# digest: Base64 hex digest (String)## { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =># { 'logical_path' => "application.js",# 'mtime' => "2011-12-13T21:47:08-06:00",# 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }#deffiles@data['files']||={}end# Public: Find all assets matching pattern set in environment.## Returns Enumerator of Assets.deffind(*args,&block)unlessenvironmentraiseError,"manifest requires environment for compilation"endreturnto_enum(__method__,*args)unlessblock_given?environment=self.environment.cachedpromises=args.flatten.mapdo|path|Concurrent::Promise.execute(executor: executor)doenvironment.find_all_linked_assets(path).to_aendendpromises.eachdo|promise|promise.value!.each(&block)endnilend# Public: Find the source of assets by paths.## Returns Enumerator of assets file content.deffind_sources(*args)returnto_enum(__method__,*args)unlessblock_given?ifenvironmentfind(*args).eachdo|asset|yieldasset.sourceendelseargs.eachdo|path|asset=assets[path]yieldFile.binread(File.join(dir,asset))ifassetendendend# Compile asset to directory. The asset is written to a# fingerprinted filename like# `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is# also inserted into the manifest file.## compile("application.js")#defcompile(*args)unlessenvironmentraiseError,"manifest requires environment for compilation"endfilenames=[]concurrent_exporters=[]assets_to_export=Concurrent::Array.newfind(*args)do|asset|assets_to_export<<assetendassets_to_export.eachdo|asset|mtime=Time.now.iso8601files[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_pathfilenames<<asset.filenamepromise=nilexporters_for_asset(asset)do|exporter|nextifexporter.skip?(logger)ifpromise.nil?promise=Concurrent::Promise.new(executor: executor){exporter.call}concurrent_exporters<<promise.executeelseconcurrent_exporters<<promise.then{exporter.call}endendend# make sure all exporters have finished before returning the main threadconcurrent_exporters.each(&:wait!)savefilenamesend# Removes file from directory and from manifest. `filename` must# be the name with any directory path.## manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")#defremove(filename)path=File.join(dir,filename)gzip="#{path}.gz"logical_path=files[filename]['logical_path']ifassets[logical_path]==filenameassets.delete(logical_path)endfiles.delete(filename)FileUtils.rm(path)ifFile.exist?(path)FileUtils.rm(gzip)ifFile.exist?(gzip)savelogger.info"Removed #{filename}"nilend# Cleanup old assets in the compile directory. By default it will# keep the latest version, 2 backups and any created within the past hour.## Examples## To force only 1 backup to be kept, set count=1 and age=0.## To only keep files created within the last 10 minutes, set count=0 and# age=600.#defclean(count=2,age=3600)asset_versions=files.group_by{|_,attrs|attrs['logical_path']}asset_versions.eachdo|logical_path,versions|current=assets[logical_path]versions.reject{|path,_|path==current}.sort_by{|_,attrs|# Sort by timestampTime.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 assetsremove(path)}endend# Wipe directivedefclobberFileUtils.rm_r(directory)ifFile.exist?(directory)logger.info"Removed #{directory}"# if we have an environment clear the cache tooenvironment.cache.clearifenvironmentnilend# Persist manifest back to FSdefsavedata=json_encode(@data)FileUtils.mkdir_pFile.dirname(@filename)PathUtils.atomic_write(@filename)do|f|f.write(data)endendprivate# Given an asset, finds all exporters that# match its mime-type.## Will yield each expoter to the passed in block.## array = []# puts asset.content_type # => "application/javascript"# exporters_for_asset(asset) do |exporter|# array << exporter# end# # puts array => [Exporters::FileExporter, Exporters::ZlibExporter]defexporters_for_asset(asset)exporters=[Exporters::FileExporter]environment.exporters.eachdo|mime_type,exporter_list|nextunlessasset.content_typenextunlessenvironment.match_mime_type?asset.content_type,mime_typeexporter_list.eachdo|exporter|exporters<<exporterendendexporters.uniq!exporters.eachdo|exporter|yieldexporter.new(asset: asset,environment: environment,directory: dir)endenddefjson_decode(obj)JSON.parse(obj,create_additions: false)enddefjson_encode(obj)JSON.generate(obj)enddefloggerifenvironmentenvironment.loggerelselogger=Logger.new($stderr)logger.level=Logger::FATALloggerendenddefexecutor@executor||=environment.export_concurrent?:fast::immediateendendend