lib/sprockets/base.rb in sprockets-2.12.5 vs lib/sprockets/base.rb in sprockets-3.0.0.beta.1
- old
+ new
@@ -1,447 +1,212 @@
-require 'sprockets/asset_attributes'
-require 'sprockets/bundled_asset'
-require 'sprockets/caching'
+require 'sprockets/asset'
+require 'sprockets/bower'
require 'sprockets/errors'
-require 'sprockets/processed_asset'
+require 'sprockets/legacy'
+require 'sprockets/resolve'
require 'sprockets/server'
-require 'sprockets/static_asset'
-require 'multi_json'
-require 'pathname'
module Sprockets
- # `Base` class for `Environment` and `Index`.
+ # `Base` class for `Environment` and `Cached`.
class Base
- include Caching, Paths, Mime, Processing, Compressing, Engines, Server
+ include PathUtils, HTTPUtils
+ include Configuration
+ include Server
+ include Resolve
+ include Bower
+ include Legacy
- # Returns a `Digest` implementation class.
- #
- # Defaults to `Digest::MD5`.
- attr_reader :digest_class
-
- # Assign a `Digest` implementation class. This may be any Ruby
- # `Digest::` implementation such as `Digest::MD5` or
- # `Digest::SHA1`.
- #
- # environment.digest_class = Digest::SHA1
- #
- def digest_class=(klass)
- expire_index!
- @digest_class = klass
- end
-
- # The `Environment#version` is a custom value used for manually
- # expiring all asset caches.
- #
- # Sprockets is able to track most file and directory changes and
- # will take care of expiring the cache for you. However, its
- # impossible to know when any custom helpers change that you mix
- # into the `Context`.
- #
- # It would be wise to increment this value anytime you make a
- # configuration change to the `Environment` object.
- attr_reader :version
-
- # Assign an environment version.
- #
- # environment.version = '2.0'
- #
- def version=(version)
- expire_index!
- @version = version
- end
-
- # Returns a `Digest` instance for the `Environment`.
- #
- # This value serves two purposes. If two `Environment`s have the
- # same digest value they can be treated as equal. This is more
- # useful for comparing environment states between processes rather
- # than in the same. Two equal `Environment`s can share the same
- # cached assets.
- #
- # The value also provides a seed digest for all `Asset`
- # digests. Any change in the environment digest will affect all of
- # its assets.
- def digest
- # Compute the initial digest using the implementation class. The
- # Sprockets release version and custom environment version are
- # mixed in. So any new releases will affect all your assets.
- @digest ||= digest_class.new.update(VERSION).update(version.to_s)
-
- # Returned a dupped copy so the caller can safely mutate it with `.update`
- @digest.dup
- end
-
- # Get and set `Logger` instance.
- attr_accessor :logger
-
- # Get `Context` class.
- #
- # This class maybe mutated and mixed in with custom helpers.
- #
- # environment.context_class.instance_eval do
- # include MyHelpers
- # def asset_url; end
- # end
- #
- attr_reader :context_class
-
# Get persistent cache store
attr_reader :cache
# Set persistent cache store
#
# The cache store must implement a pair of getters and
# setters. Either `get(key)`/`set(key, value)`,
# `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
def cache=(cache)
- expire_index!
- @cache = cache
+ @cache = Cache.new(cache, logger)
end
- def prepend_path(path)
- # Overrides the global behavior to expire the index
- expire_index!
- super
+ # Return an `Cached`. Must be implemented by the subclass.
+ def cached
+ raise NotImplementedError
end
+ alias_method :index, :cached
- def append_path(path)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def clear_paths
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- # Finds the expanded real path for a given logical path by
- # searching the environment's paths.
+ # Internal: Compute hexdigest for path.
#
- # resolve("application.js")
- # # => "/path/to/app/javascripts/application.js.coffee"
+ # path - String filename or directory path.
#
- # A `FileNotFound` exception is raised if the file does not exist.
- def resolve(logical_path, options = {})
- # If a block is given, preform an iterable search
- if block_given?
- args = attributes_for(logical_path).search_paths + [options]
- @trail.find(*args) do |path|
- pathname = Pathname.new(path)
- if %w( .bower.json bower.json component.json ).include?(pathname.basename.to_s)
- bower = json_decode(pathname.read)
- case bower['main']
- when String
- yield pathname.dirname.join(bower['main'])
- when Array
- extname = File.extname(logical_path)
- bower['main'].each do |fn|
- if extname == "" || extname == File.extname(fn)
- yield pathname.dirname.join(fn)
- end
- end
- end
- else
- yield pathname
+ # Returns a String SHA1 hexdigest or nil.
+ def file_hexdigest(path)
+ if stat = self.stat(path)
+ # Caveat: Digests are cached by the path's current mtime. Its possible
+ # for a files contents to have changed and its mtime to have been
+ # negligently reset thus appearing as if the file hasn't changed on
+ # disk. Also, the mtime is only read to the nearest second. Its
+ # also possible the file was updated more than once in a given second.
+ cache.fetch(['file_hexdigest', path, stat.mtime.to_i]) do
+ if stat.directory?
+ # If its a directive, digest the list of filenames
+ Digest::SHA1.hexdigest(self.entries(path).join(','))
+ elsif stat.file?
+ # If its a file, digest the contents
+ Digest::SHA1.file(path.to_s).hexdigest
end
end
- else
- resolve(logical_path, options) do |pathname|
- return pathname
- end
- raise FileNotFound, "couldn't find file '#{logical_path}'"
end
end
- # Register a new mime type.
- def register_mime_type(mime_type, ext)
- # Overrides the global behavior to expire the index
- expire_index!
- @trail.append_extension(ext)
- super
- end
-
- # Registers a new Engine `klass` for `ext`.
- def register_engine(ext, klass)
- # Overrides the global behavior to expire the index
- expire_index!
- add_engine_to_trail(ext, klass)
- super
- end
-
- def register_preprocessor(mime_type, klass, &block)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def unregister_preprocessor(mime_type, klass)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def register_postprocessor(mime_type, klass, &block)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def unregister_postprocessor(mime_type, klass)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def register_bundle_processor(mime_type, klass, &block)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- def unregister_bundle_processor(mime_type, klass)
- # Overrides the global behavior to expire the index
- expire_index!
- super
- end
-
- # Return an `Index`. Must be implemented by the subclass.
- def index
- raise NotImplementedError
- end
-
- if defined? Encoding.default_external
- # Define `default_external_encoding` accessor on 1.9.
- # Defaults to UTF-8.
- attr_accessor :default_external_encoding
- end
-
- # Works like `Dir.entries`.
+ # Internal: Compute hexdigest for a set of paths.
#
- # Subclasses may cache this method.
- def entries(pathname)
- @trail.entries(pathname)
- end
-
- # Works like `File.stat`.
+ # paths - Array of filename or directory paths.
#
- # Subclasses may cache this method.
- def stat(path)
- @trail.stat(path)
+ # Returns a String SHA1 hexdigest.
+ def dependencies_hexdigest(paths)
+ digest = Digest::SHA1.new
+ paths.each { |path| digest.update(file_hexdigest(path).to_s) }
+ digest.hexdigest
end
- # Read and compute digest of filename.
- #
- # Subclasses may cache this method.
- def file_digest(path)
- if stat = self.stat(path)
- # If its a file, digest the contents
- if stat.file?
- digest.file(path.to_s)
-
- # If its a directive, digest the list of filenames
- elsif stat.directory?
- contents = self.entries(path).join(',')
- digest.update(contents)
- end
+ # Find asset by logical path or expanded path.
+ def find_asset(path, options = {})
+ if uri = resolve_asset_uri(path, options)
+ Asset.new(self, build_asset_by_uri(uri))
end
end
- # Internal. Return a `AssetAttributes` for `path`.
- def attributes_for(path)
- AssetAttributes.new(self, path)
+ def find_asset_by_uri(uri)
+ _, params = AssetURI.parse(uri)
+ asset = params.key?(:id) ?
+ build_asset_by_id_uri(uri) :
+ build_asset_by_uri(uri)
+ Asset.new(self, asset)
end
- # Internal. Return content type of `path`.
- def content_type_of(path)
- attributes_for(path).content_type
- end
+ def find_all_linked_assets(path, options = {})
+ return to_enum(__method__, path, options) unless block_given?
- # Find asset by logical path or expanded path.
- def find_asset(path, options = {})
- logical_path = path
- pathname = Pathname.new(path).cleanpath
+ asset = find_asset(path, options)
+ return unless asset
- if pathname.absolute?
- return unless stat(pathname)
- logical_path = attributes_for(pathname).logical_path
- else
- begin
- pathname = resolve(logical_path)
+ yield asset
+ stack = asset.links.to_a
- # If logical path is missing a mime type extension, append
- # the absolute path extname so it has one.
- #
- # Ensures some consistency between finding "foo/bar" vs
- # "foo/bar.js".
- if File.extname(logical_path) == ""
- expanded_logical_path = attributes_for(pathname).logical_path
- logical_path += File.extname(expanded_logical_path)
- end
- rescue FileNotFound
- return nil
- end
+ while uri = stack.shift
+ yield asset = find_asset_by_uri(uri)
+ stack = asset.links.to_a + stack
end
- build_asset(logical_path, pathname, options)
+ nil
end
# Preferred `find_asset` shorthand.
#
# environment['application.js']
#
def [](*args)
find_asset(*args)
end
- def each_entry(root, &block)
- return to_enum(__method__, root) unless block_given?
- root = Pathname.new(root) unless root.is_a?(Pathname)
-
- paths = []
- entries(root).sort.each do |filename|
- path = root.join(filename)
- paths << path
-
- if stat(path).directory?
- each_entry(path) do |subpath|
- paths << subpath
- end
- end
- end
-
- paths.sort_by(&:to_s).each(&block)
-
- nil
- end
-
- def each_file
- return to_enum(__method__) unless block_given?
- paths.each do |root|
- each_entry(root) do |path|
- if !stat(path).directory?
- yield path
- end
- end
- end
- nil
- end
-
- def each_logical_path(*args, &block)
- return to_enum(__method__, *args) unless block_given?
- filters = args.flatten
- files = {}
- each_file do |filename|
- if logical_path = logical_path_for_filename(filename, filters)
- unless files[logical_path]
- if block.arity == 2
- yield logical_path, filename.to_s
- else
- yield logical_path
- end
- end
-
- files[logical_path] = true
- end
- end
- nil
- end
-
# Pretty inspect
def inspect
"#<#{self.class}:0x#{object_id.to_s(16)} " +
"root=#{root.to_s.inspect}, " +
- "paths=#{paths.inspect}, " +
- "digest=#{digest.to_s.inspect}" +
- ">"
+ "paths=#{paths.inspect}>"
end
protected
- # Clear index after mutating state. Must be implemented by the subclass.
- def expire_index!
- raise NotImplementedError
- end
+ def build_asset_by_id_uri(uri)
+ path, params = AssetURI.parse(uri)
- def build_asset(logical_path, pathname, options)
- pathname = Pathname.new(pathname)
-
- # If there are any processors to run on the pathname, use
- # `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
- if attributes_for(pathname).processors.any?
- if options[:bundle] == false
- circular_call_protection(pathname.to_s) do
- ProcessedAsset.new(index, logical_path, pathname)
- end
- else
- BundledAsset.new(index, logical_path, pathname)
- end
- else
- StaticAsset.new(index, logical_path, pathname)
+ # Internal assertion, should be routed through build_asset_by_uri
+ unless id = params.delete(:id)
+ raise ArgumentError, "expected uri to have an id: #{uri}"
end
- end
- def cache_key_for(path, options)
- "#{path}:#{options[:bundle] ? '1' : '0'}"
- end
+ asset = build_asset_by_uri(AssetURI.build(path, params))
- def circular_call_protection(path)
- reset = Thread.current[:sprockets_circular_calls].nil?
- calls = Thread.current[:sprockets_circular_calls] ||= Set.new
- if calls.include?(path)
- raise CircularDependencyError, "#{path} has already been required"
+ if id && asset[:id] != id
+ raise VersionNotFound, "could not find specified id: #{id}"
end
- calls << path
- yield
- ensure
- Thread.current[:sprockets_circular_calls] = nil if reset
+
+ asset
end
- def logical_path_for_filename(filename, filters)
- logical_path = attributes_for(filename).logical_path.to_s
+ def build_asset_by_uri(uri)
+ filename, params = AssetURI.parse(uri)
- if matches_filter(filters, logical_path, filename)
- return logical_path
+ # Internal assertion, should be routed through build_asset_by_id_uri
+ if params.key?(:id)
+ raise ArgumentError, "expected uri to have no id: #{uri}"
end
- # If filename is an index file, retest with alias
- if File.basename(logical_path)[/[^\.]+/, 0] == 'index'
- path = logical_path.sub(/\/index\./, '.')
- if matches_filter(filters, path, filename)
- return path
- end
+ type = params[:type]
+ load_path, logical_path = paths_split(self.paths, filename)
+
+ if !file?(filename)
+ raise FileNotFound, "could not find file: #{filename}"
+ elsif type && !resolve_path_transform_type(filename, type)
+ raise ConversionError, "could not convert to type: #{type}"
+ elsif !load_path
+ raise FileOutsidePaths, "#{filename} is no longer under a load path: #{self.paths.join(', ')}"
end
- nil
- end
+ logical_path, file_type, engine_extnames = parse_path_extnames(logical_path)
+ logical_path = normalize_logical_path(logical_path)
- def matches_filter(filters, logical_path, filename)
- return true if filters.empty?
+ asset = {
+ uri: uri,
+ load_path: load_path,
+ filename: filename,
+ name: logical_path,
+ logical_path: logical_path
+ }
- filters.any? do |filter|
- if filter.is_a?(Regexp)
- filter.match(logical_path)
- elsif filter.respond_to?(:call)
- if filter.arity == 1
- filter.call(logical_path)
- else
- filter.call(logical_path, filename.to_s)
- end
- else
- File.fnmatch(filter.to_s, logical_path)
- end
+ if type
+ asset[:content_type] = type
+ asset[:logical_path] += mime_types[type][:extensions].first
end
- end
- # Feature detect newer MultiJson API
- if MultiJson.respond_to?(:dump)
- def json_decode(obj)
- MultiJson.load(obj)
+ processed_processors = unwrap_preprocessors(file_type) +
+ unwrap_engines(engine_extnames).reverse +
+ unwrap_transformer(file_type, type) +
+ unwrap_postprocessors(type)
+
+ bundled_processors = params[:skip_bundle] ? [] : unwrap_bundle_processors(type)
+
+ processors = bundled_processors.any? ? bundled_processors : processed_processors
+ processors += unwrap_encoding_processors(params[:encoding])
+
+ if processors.any?
+ asset.merge!(process(
+ [method(:read_input)] + processors,
+ asset[:uri],
+ asset[:filename],
+ asset[:load_path],
+ asset[:name],
+ asset[:content_type]
+ ))
+ else
+ asset.merge!({
+ encoding: Encoding::BINARY,
+ length: self.stat(asset[:filename]).size,
+ digest: digest_class.file(asset[:filename]).hexdigest,
+ metadata: {}
+ })
end
- else
- def json_decode(obj)
- MultiJson.decode(obj)
- end
+
+ metadata = asset[:metadata]
+ metadata[:dependency_paths] = Set.new(metadata[:dependency_paths]).merge([asset[:filename]])
+ metadata[:dependency_digest] = dependencies_hexdigest(metadata[:dependency_paths])
+
+ asset[:id] = Utils.hexdigest(asset)
+ asset[:uri] = AssetURI.build(filename, params.merge(id: asset[:id]))
+
+ # TODO: Avoid tracking Asset mtime
+ asset[:mtime] = metadata[:dependency_paths].map { |p| stat(p).mtime.to_i }.max
+
+ asset
end
end
end