Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #213 from sstephenson/2.1.x

Release 2.1.0
  • Loading branch information...
commit 20db9ed6cb67a4e923fb12ade5b864d9b9fcf2c2 2 parents ae7f3b7 + 6fd49f9
@josh josh authored
View
56 lib/sprockets.rb
@@ -1,31 +1,63 @@
require 'sprockets/version'
module Sprockets
- autoload :ArgumentError, "sprockets/errors"
+ # Environment
+ autoload :Base, "sprockets/base"
+ autoload :Engines, "sprockets/engines"
+ autoload :Environment, "sprockets/environment"
+ autoload :Index, "sprockets/index"
+
+ # Assets
autoload :Asset, "sprockets/asset"
- autoload :AssetAttributes, "sprockets/asset_attributes"
autoload :BundledAsset, "sprockets/bundled_asset"
+ autoload :ProcessedAsset, "sprockets/processed_asset"
+ autoload :StaticAsset, "sprockets/static_asset"
+
+ # Processing
autoload :CharsetNormalizer, "sprockets/charset_normalizer"
- autoload :CircularDependencyError, "sprockets/errors"
- autoload :ContentTypeMismatch, "sprockets/errors"
autoload :Context, "sprockets/context"
autoload :DirectiveProcessor, "sprockets/directive_processor"
autoload :EcoTemplate, "sprockets/eco_template"
autoload :EjsTemplate, "sprockets/ejs_template"
+ autoload :JstProcessor, "sprockets/jst_processor"
+ autoload :Processor, "sprockets/processor"
+ autoload :SafetyColons, "sprockets/safety_colons"
+
+ # Internal utilities
+ autoload :ArgumentError, "sprockets/errors"
+ autoload :AssetAttributes, "sprockets/asset_attributes"
+ autoload :CircularDependencyError, "sprockets/errors"
+ autoload :ContentTypeMismatch, "sprockets/errors"
autoload :EngineError, "sprockets/errors"
- autoload :Engines, "sprockets/engines"
- autoload :Environment, "sprockets/environment"
autoload :Error, "sprockets/errors"
autoload :FileNotFound, "sprockets/errors"
- autoload :Index, "sprockets/index"
- autoload :JstProcessor, "sprockets/jst_processor"
- autoload :Processing, "sprockets/processing"
- autoload :Processor, "sprockets/processor"
- autoload :Server, "sprockets/server"
- autoload :StaticAsset, "sprockets/static_asset"
autoload :Utils, "sprockets/utils"
module Cache
autoload :FileStore, "sprockets/cache/file_store"
end
+
+ # Extend Sprockets module to provide global registry
+ extend Engines
+ @engines = {}
+
+ # Cherry pick the default Tilt engines that make sense for
+ # Sprockets. We don't need ones that only generate html like HAML.
+
+ # Mmm, CoffeeScript
+ register_engine '.coffee', Tilt::CoffeeScriptTemplate
+
+ # JST engines
+ register_engine '.jst', JstProcessor
+ register_engine '.eco', EcoTemplate
+ register_engine '.ejs', EjsTemplate
+
+ # CSS engines
+ register_engine '.less', Tilt::LessTemplate
+ register_engine '.sass', Tilt::SassTemplate
+ register_engine '.scss', Tilt::ScssTemplate
+
+ # Other
+ register_engine '.erb', Tilt::ERBTemplate
+ register_engine '.str', Tilt::StringTemplate
end
View
182 lib/sprockets/asset.rb
@@ -1,90 +1,79 @@
require 'time'
+require 'set'
module Sprockets
# `Asset` is the base class for `BundledAsset` and `StaticAsset`.
class Asset
# Internal initializer to load `Asset` from serialized `Hash`.
def self.from_hash(environment, hash)
- asset = allocate
- asset.init_with(environment, hash)
- asset
- end
+ return unless hash.is_a?(Hash)
+
+ klass = case hash['class']
+ when 'BundledAsset'
+ BundledAsset
+ when 'ProcessedAsset'
+ ProcessedAsset
+ when 'StaticAsset'
+ StaticAsset
+ else
+ nil
+ end
- # Define base set of attributes to be serialized.
- def self.serialized_attributes
- %w( id logical_path pathname )
+ if klass
+ asset = klass.allocate
+ asset.init_with(environment, hash)
+ asset
+ end
+ rescue UnserializeError
+ nil
end
- attr_reader :environment
- attr_reader :id, :logical_path, :pathname
+ attr_reader :logical_path, :pathname
+ attr_reader :content_type, :mtime, :length, :digest
def initialize(environment, logical_path, pathname)
- @environment = environment
+ @root = environment.root
@logical_path = logical_path.to_s
@pathname = Pathname.new(pathname)
- @id = environment.digest.update(object_id.to_s).to_s
+ @content_type = environment.content_type_of(pathname)
+ @mtime = environment.stat(pathname).mtime
+ @length = environment.stat(pathname).size
+ @digest = environment.file_digest(pathname).hexdigest
end
# Initialize `Asset` from serialized `Hash`.
def init_with(environment, coder)
- @environment = environment
- @pathname = @mtime = @length = nil
+ @root = environment.root
- self.class.serialized_attributes.each do |attr|
- instance_variable_set("@#{attr}", coder[attr].to_s) if coder[attr]
- end
+ @logical_path = coder['logical_path']
+ @content_type = coder['content_type']
+ @digest = coder['digest']
- if @pathname && @pathname.is_a?(String)
+ if pathname = coder['pathname']
# Expand `$root` placeholder and wrapper string in a `Pathname`
- @pathname = Pathname.new(expand_root_path(@pathname))
+ @pathname = Pathname.new(expand_root_path(pathname))
end
- if @mtime && @mtime.is_a?(String)
+ if mtime = coder['mtime']
# Parse time string
- @mtime = Time.parse(@mtime)
+ @mtime = Time.parse(mtime)
end
- if @length && @length.is_a?(String)
+ if length = coder['length']
# Convert length to an `Integer`
- @length = Integer(@length)
+ @length = Integer(length)
end
end
# Copy serialized attributes to the coder object
def encode_with(coder)
- coder['class'] = self.class.name.sub(/Sprockets::/, '')
-
- self.class.serialized_attributes.each do |attr|
- value = send(attr)
- coder[attr] = case value
- when Time
- value.iso8601
- else
- value.to_s
- end
- end
-
- coder['pathname'] = relativize_root_path(coder['pathname'])
- end
-
- # Returns `Content-Type` from pathname.
- def content_type
- @content_type ||= environment.content_type_of(pathname)
- end
-
- # Get mtime at the time the `Asset` is built.
- def mtime
- @mtime ||= environment.stat(pathname).mtime
- end
-
- # Get length at the time the `Asset` is built.
- def length
- @length ||= environment.stat(pathname).size
- end
-
- # Get content digest at the time the `Asset` is built.
- def digest
- @digest ||= environment.file_digest(pathname).hexdigest
+ coder['class'] = self.class.name.sub(/Sprockets::/, '')
+ coder['logical_path'] = logical_path
+ coder['pathname'] = relativize_root_path(pathname).to_s
+ coder['content_type'] = content_type
+ coder['mtime'] = mtime.iso8601
+ coder['length'] = length
+ coder['digest'] = digest
end
# Return logical path with digest spliced in.
@@ -92,7 +81,7 @@ def digest
# "foo/bar-37b51d194a7513e45b56f6524f2d51f2.js"
#
def digest_path
- environment.attributes_for(logical_path).path_with_fingerprint(digest)
+ logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
end
# Return an `Array` of `Asset` files that are declared dependencies.
@@ -111,6 +100,16 @@ def to_a
[self]
end
+ # `body` is aliased to source by default if it can't have any dependencies.
+ def body
+ source
+ end
+
+ # Return `String` of concatenated source.
+ def to_s
+ source
+ end
+
# Add enumerator to allow `Asset` instances to be used as Rack
# compatible body objects.
def each
@@ -121,18 +120,47 @@ def each
# digest to the inmemory model.
#
# Used to test if cached models need to be rebuilt.
- #
- # Subclass must override `fresh?` or `stale?`.
- def fresh?
- !stale?
+ def fresh?(environment)
+ # Check current mtime and digest
+ dependency_fresh?(environment, self)
end
# Checks if Asset is stale by comparing the actual mtime and
# digest to the inmemory model.
#
# Subclass must override `fresh?` or `stale?`.
- def stale?
- !fresh?
+ def stale?(environment)
+ !fresh?(environment)
+ end
+
+ # Save asset to disk.
+ def write_to(filename, options = {})
+ # Gzip contents if filename has '.gz'
+ options[:compress] ||= File.extname(filename) == '.gz'
+
+ File.open("#{filename}+", 'wb') do |f|
+ if options[:compress]
+ # Run contents through `Zlib`
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
+ gz.write to_s
+ gz.close
+ else
+ # Write out as is
+ f.write to_s
+ f.close
+ end
+ end
+
+ # Atomic write
+ FileUtils.mv("#{filename}+", filename)
+
+ # Set mtime correctly
+ File.utime(mtime, mtime, filename)
+
+ nil
+ ensure
+ # Ensure tmp file gets cleaned up
+ FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
end
# Pretty inspect
@@ -144,29 +172,47 @@ def inspect
">"
end
+ def hash
+ digest.hash
+ end
+
# Assets are equal if they share the same path, mtime and digest.
def eql?(other)
other.class == self.class &&
- other.relative_pathname == self.relative_pathname &&
+ other.logical_path == self.logical_path &&
other.mtime.to_i == self.mtime.to_i &&
other.digest == self.digest
end
alias_method :==, :eql?
protected
+ # Internal: String paths that are marked as dependencies after processing.
+ #
+ # Default to an empty `Array`.
+ def dependency_paths
+ @dependency_paths ||= []
+ end
+
+ # Internal: `ProccessedAsset`s that are required after processing.
+ #
+ # Default to an empty `Array`.
+ def required_assets
+ @required_assets ||= []
+ end
+
# Get pathname with its root stripped.
def relative_pathname
- Pathname.new(relativize_root_path(pathname))
+ @relative_pathname ||= Pathname.new(relativize_root_path(pathname))
end
# Replace `$root` placeholder with actual environment root.
def expand_root_path(path)
- environment.attributes_for(path).expand_root
+ path.to_s.sub(/^\$root/, @root)
end
# Replace actual environment root with `$root` placeholder.
def relativize_root_path(path)
- environment.attributes_for(path).relativize_root
+ path.to_s.sub(/^#{Regexp.escape(@root)}/, '$root')
end
# Check if dependency is fresh.
@@ -175,8 +221,8 @@ def relativize_root_path(path)
#
# A `Hash` is used rather than other `Asset` object because we
# want to test non-asset files and directories.
- def dependency_fresh?(dep = {})
- path, mtime, hexdigest = dep.values_at('path', 'mtime', 'hexdigest')
+ def dependency_fresh?(environment, dep)
+ path, mtime, hexdigest = dep.pathname.to_s, dep.mtime, dep.digest
stat = environment.stat(path)
View
35 lib/sprockets/asset_attributes.rb
@@ -13,16 +13,6 @@ def initialize(environment, path)
@pathname = path.is_a?(Pathname) ? path : Pathname.new(path.to_s)
end
- # Replaces `$root` placeholder with actual environment root.
- def expand_root
- pathname.to_s.sub(/^\$root/, environment.root)
- end
-
- # Replaces environment root with `$root` placeholder.
- def relativize_root
- pathname.to_s.sub(/^#{Regexp.escape(environment.root)}/, '$root')
- end
-
# Returns paths search the load path for.
def search_paths
paths = [pathname.to_s]
@@ -42,9 +32,8 @@ def search_paths
# shaddowed in the path, but is required relatively, its logical
# path will be incorrect.
def logical_path
- raise ArgumentError unless pathname.absolute?
-
if root_path = environment.paths.detect { |path| pathname.to_s[path] }
+ path = pathname.to_s.sub("#{root_path}/", '')
path = pathname.relative_path_from(Pathname.new(root_path)).to_s
path = engine_extensions.inject(path) { |p, ext| p.sub(ext, '') }
path = "#{path}#{engine_format_extension}" unless format_extension
@@ -114,28 +103,6 @@ def content_type
end
end
- # Gets digest fingerprint.
- #
- # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
- # # => "0aa2105d29558f3eb790d411d7d8fb66"
- #
- def path_fingerprint
- pathname.basename(extensions.last.to_s).to_s =~ /-([0-9a-f]{7,40})$/ ? $1 : nil
- end
-
- # Injects digest fingerprint into path.
- #
- # "foo.js"
- # # => "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
- #
- def path_with_fingerprint(digest)
- if old_digest = path_fingerprint
- pathname.sub(old_digest, digest).to_s
- else
- pathname.to_s.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
- end
- end
-
private
# Returns implicit engine content type.
#
View
113 lib/sprockets/base.rb
@@ -1,7 +1,7 @@
require 'sprockets/asset_attributes'
require 'sprockets/bundled_asset'
require 'sprockets/caching'
-require 'sprockets/digest'
+require 'sprockets/processed_asset'
require 'sprockets/processing'
require 'sprockets/server'
require 'sprockets/static_asset'
@@ -11,9 +11,66 @@
module Sprockets
# `Base` class for `Environment` and `Index`.
class Base
- include Digest
include Caching, Processing, Server, Trail
+ # Returns a `Digest` implementation class.
+ #
+ # Defaults to `Digest::MD5`.
+ attr_reader :digest_class
+
+ # Assign a `Digest` implementation class. This maybe 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
@@ -63,14 +120,10 @@ def stat(path)
# Read and compute digest of filename.
#
# Subclasses may cache this method.
- def file_digest(path, data = nil)
+ def file_digest(path)
if stat = self.stat(path)
- # `data` maybe provided
- if data
- digest.update(data)
-
# If its a file, digest the contents
- elsif stat.file?
+ if stat.file?
digest.file(path.to_s)
# If its a directive, digest the list of filenames
@@ -93,13 +146,21 @@ def content_type_of(path)
# Find asset by logical path or expanded path.
def find_asset(path, options = {})
- pathname = Pathname.new(path)
+ logical_path = path
+ pathname = Pathname.new(path)
- if pathname.absolute?
- build_asset(attributes_for(pathname).logical_path, pathname, options)
+ if pathname.to_s =~ /^\//
+ return unless stat(pathname)
+ logical_path = attributes_for(pathname).logical_path
else
- find_asset_in_path(pathname, options)
+ begin
+ pathname = resolve(logical_path)
+ rescue FileNotFound
+ return nil
+ end
end
+
+ build_asset(logical_path, pathname, options)
end
# Preferred `find_asset` shorthand.
@@ -172,15 +233,35 @@ def expire_index!
def build_asset(logical_path, pathname, options)
pathname = Pathname.new(pathname)
- return unless stat(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?
- BundledAsset.new(self, logical_path, pathname, options)
+ 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(self, logical_path, pathname)
+ StaticAsset.new(index, logical_path, pathname)
+ end
+ end
+
+ def cache_key_for(path, options)
+ "#{path}:#{options[:bundle] ? '1' : '0'}"
+ end
+
+ 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"
end
+ calls << path
+ yield
+ ensure
+ Thread.current[:sprockets_circular_calls] = nil if reset
end
end
end
View
243 lib/sprockets/bundled_asset.rb
@@ -8,251 +8,72 @@ module Sprockets
# `BundledAsset`s are used for files that need to be processed and
# concatenated with other assets. Use for `.js` and `.css` files.
class BundledAsset < Asset
- # Define extra attributes to be serialized.
- def self.serialized_attributes
- super + %w( content_type mtime )
- end
+ attr_reader :source
- def initialize(environment, logical_path, pathname, options)
+ def initialize(environment, logical_path, pathname)
super(environment, logical_path, pathname)
- @options = options || {}
+
+ @processed_asset = environment.find_asset(pathname, :bundle => false)
+ @required_assets = @processed_asset.required_assets
+
+ @source = ""
+
+ # Explode Asset into parts and gather the dependency bodies
+ to_a.each { |dependency| @source << dependency.to_s }
+
+ # Run bundle processors on concatenated source
+ context = environment.context_class.new(environment, logical_path, pathname)
+ @source = context.evaluate(pathname, :data => @source,
+ :processors => environment.bundle_processors(content_type))
+
+ @mtime = to_a.map(&:mtime).max
+ @length = Rack::Utils.bytesize(source)
+ @digest = environment.digest.update(source).hexdigest
end
# Initialize `BundledAsset` from serialized `Hash`.
def init_with(environment, coder)
- @options = {}
-
super
- @body = coder['body']
- @assets = coder['asset_paths'].map { |p|
- p = expand_root_path(p)
- p == pathname.to_s ? self : environment[p, @options]
- }
+ @processed_asset = environment.find_asset(pathname, :bundle => false)
+ @required_assets = @processed_asset.required_assets
- @dependency_paths = coder['dependency_paths'].map { |h|
- h.merge('path' => expand_root_path(h['path']))
- }
- @dependency_paths.each do |dep|
- dep['mtime'] = Time.parse(dep['mtime']) if dep['mtime'].is_a?(String)
+ if @processed_asset.dependency_digest != coder['required_assets_digest']
+ raise UnserializeError, "processed asset belongs to a stale environment"
end
+
+ @source = coder['source']
end
# Serialize custom attributes in `BundledAsset`.
def encode_with(coder)
super
- coder['body'] = body
- coder['asset_paths'] = to_a.map { |a| relativize_root_path(a.pathname) }
- coder['dependency_paths'] = dependency_paths.map { |h|
- h.merge('path' => relativize_root_path(h['path']))
- }
+ coder['source'] = source
+ coder['required_assets_digest'] = @processed_asset.dependency_digest
end
# Get asset's own processed contents. Excludes any of its required
# dependencies but does run any processors or engines on the
# original file.
def body
- @body ||= build_dependency_context_and_body[1]
- end
-
- # Get latest mtime of all its dependencies.
- def mtime
- @mtime ||= dependency_paths.map { |h| h['mtime'] }.max
- end
-
- # Get size of concatenated source.
- def length
- @length ||= build_source['length']
- end
-
- # Compute digest of concatenated source.
- def digest
- @digest ||= build_source['digest']
+ @processed_asset.source
end
# Return an `Array` of `Asset` files that are declared dependencies.
def dependencies
- to_a - [self]
+ to_a.reject { |a| a.eql?(@processed_asset) }
end
# Expand asset into an `Array` of parts.
def to_a
- @assets ||= build_dependencies_paths_and_assets[1]
+ required_assets
end
# Checks if Asset is stale by comparing the actual mtime and
# digest to the inmemory model.
- def fresh?
- # Check freshness of all declared dependencies
- dependency_paths.all? { |h| dependency_fresh?(h) }
- end
-
- # Return `String` of concatenated source.
- def to_s
- @source ||= build_source['source']
- end
-
- # Save asset to disk.
- def write_to(filename, options = {})
- # Gzip contents if filename has '.gz'
- options[:compress] ||= File.extname(filename) == '.gz'
-
- File.open("#{filename}+", 'wb') do |f|
- if options[:compress]
- # Run contents through `Zlib`
- gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
- gz.write to_s
- gz.close
- else
- # Write out as is
- f.write to_s
- f.close
- end
- end
-
- # Atomic write
- FileUtils.mv("#{filename}+", filename)
-
- # Set mtime correctly
- File.utime(mtime, mtime, filename)
-
- nil
- ensure
- # Ensure tmp file gets cleaned up
- FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
+ def fresh?(environment)
+ @processed_asset.fresh?(environment)
end
-
- protected
- # Return new blank `Context` to evaluate processors in.
- def blank_context
- environment.context_class.new(environment, logical_path.to_s, pathname)
- end
-
- # Get `Context` after processors have been ran on it. This
- # trackes any dependencies that processors have added to it.
- def dependency_context
- @dependency_context ||= build_dependency_context_and_body[0]
- end
-
- # All paths that this asset depends on. This list may include
- # non-assets like directories.
- def dependency_paths
- @dependency_paths ||= build_dependencies_paths_and_assets[0]
- end
-
- private
- def logger
- environment.logger
- end
-
- # Check if self has already been required and raise a fast
- # error. Otherwise you end up with a StackOverflow error.
- def check_circular_dependency!
- requires = @options[:_requires] ||= []
- if requires.include?(pathname.to_s)
- raise CircularDependencyError, "#{pathname} has already been required"
- end
- requires << pathname.to_s
- end
-
- def build_dependency_context_and_body
- start_time = Time.now.to_f
-
- context = blank_context
-
- # Read original data once and pass it along to `Context`
- data = Sprockets::Utils.read_unicode(pathname)
-
- # Prime digest cache with data, since we happen to have it
- environment.file_digest(pathname, data)
-
- # Runs all processors on `Context`
- body = context.evaluate(pathname, :data => data)
-
- @dependency_context, @body = context, body
-
- elapsed_time = ((Time.now.to_f - start_time) * 1000).to_i
- logger.info "Compiled #{logical_path} (#{elapsed_time}ms) (pid #{Process.pid})"
-
- return context, body
- end
-
- def build_dependencies_paths_and_assets
- check_circular_dependency!
-
- paths, assets = {}, []
-
- # Define an `add_dependency` helper
- add_dependency = lambda do |asset|
- unless assets.any? { |a| a.pathname == asset.pathname }
- assets << asset
- end
- end
-
- # Iterate over all the declared require paths from the `Context`
- dependency_context._required_paths.each do |required_path|
- # Catch `require_self`
- if required_path == pathname.to_s
- add_dependency.call(self)
- else
- # Recursively lookup required asset
- environment[required_path, @options].to_a.each do |asset|
- add_dependency.call(asset)
- end
- end
- end
-
- # Ensure self is added to the dependency list
- add_dependency.call(self)
-
- dependency_context._dependency_paths.each do |path|
- paths[path] ||= {
- 'path' => path,
- 'mtime' => environment.stat(path).mtime,
- 'hexdigest' => environment.file_digest(path).hexdigest
- }
- end
-
- dependency_context._dependency_assets.each do |path|
- # Skip if depending on self
- next if path == pathname.to_s
-
- # Recursively lookup required asset
- environment[path, @options].to_a.each do |asset|
- asset.dependency_paths.each do |dep|
- paths[dep['path']] ||= dep
- end
- end
- end
-
- @dependency_paths, @assets = paths.values, assets
-
- return @dependency_paths, @assets
- end
-
- def build_source
- hash = environment.cache_hash("#{pathname}:source", id) do
- data = ""
-
- # Explode Asset into parts and gather the dependency bodies
- to_a.each { |dependency| data << dependency.body }
-
- # Run bundle processors on concatenated source
- data = blank_context.evaluate(pathname, :data => data,
- :processors => environment.bundle_processors(content_type))
-
- { 'length' => Rack::Utils.bytesize(data),
- 'digest' => environment.digest.update(data).hexdigest,
- 'source' => data }
- end
- hash['length'] = Integer(hash['length']) if hash['length'].is_a?(String)
-
- @length = hash['length']
- @digest = hash['digest']
- @source = hash['source']
-
- hash
- end
end
end
View
13 lib/sprockets/cache/file_store.rb
@@ -18,24 +18,15 @@ def initialize(root)
# Lookup value in cache
def [](key)
- pathname = path_for(key)
+ pathname = @root.join(key)
pathname.exist? ? pathname.open('rb') { |f| Marshal.load(f) } : nil
end
# Save value to cache
def []=(key, value)
- path_for(key).open('w') { |f| Marshal.dump(value, f)}
+ @root.join(key).open('w') { |f| Marshal.dump(value, f)}
value
end
-
- private
- # Returns path for cache key.
- #
- # The key may include some funky characters so hash it into
- # safe hex.
- def path_for(key)
- @root.join(::Digest::MD5.hexdigest(key))
- end
end
end
end
View
53 lib/sprockets/caching.rb
@@ -1,38 +1,7 @@
-require 'sprockets/bundled_asset'
-require 'sprockets/static_asset'
-
module Sprockets
# `Caching` is an internal mixin whose public methods are exposed on
# the `Environment` and `Index` classes.
module Caching
- # Return `Asset` instance for serialized `Hash`.
- def asset_from_hash(hash)
- return unless hash.is_a?(Hash)
- case hash['class']
- when 'BundledAsset'
- BundledAsset.from_hash(self, hash)
- when 'StaticAsset'
- StaticAsset.from_hash(self, hash)
- else
- nil
- end
- rescue Exception => e
- logger.debug "Cache for Asset (#{hash['logical_path']}) is stale"
- logger.debug e
- nil
- end
-
- def cache_hash(key, version)
- if cache.nil?
- yield
- elsif hash = cache_get_hash(key, version)
- hash
- elsif hash = yield
- cache_set_hash(key, version, hash)
- hash
- end
- end
-
protected
# Cache helper method. Takes a `path` argument which maybe a
# logical path or fully expanded path. The `&block` is passed
@@ -43,7 +12,7 @@ def cache_asset(path)
yield
# Check cache for `path`
- elsif (asset = asset_from_hash(cache_get_hash(path.to_s, digest.hexdigest))) && asset.fresh?
+ elsif (asset = Asset.from_hash(self, cache_get_hash(path.to_s))) && asset.fresh?(self)
asset
# Otherwise yield block that slowly finds and builds the asset
@@ -52,12 +21,12 @@ def cache_asset(path)
asset.encode_with(hash)
# Save the asset to its path
- cache_set_hash(path.to_s, digest.hexdigest, hash)
+ cache_set_hash(path.to_s, hash)
# Since path maybe a logical or full pathname, save the
# asset its its full path too
if path.to_s != asset.pathname.to_s
- cache_set_hash(asset.pathname.to_s, digest.hexdigest, hash)
+ cache_set_hash(asset.pathname.to_s, hash)
end
asset
@@ -68,20 +37,20 @@ def cache_asset(path)
# Strips `Environment#root` from key to make the key work
# consisently across different servers. The key is also hashed
# so it does not exceed 250 characters.
- def cache_key_for(key)
- File.join('sprockets', digest.hexdigest(key.sub(root, '')))
+ def expand_cache_key(key)
+ File.join('sprockets', digest_class.hexdigest(key.sub(root, '')))
end
- def cache_get_hash(key, version)
- hash = cache_get(cache_key_for(key))
- if hash.is_a?(Hash) && version == hash['_version']
+ def cache_get_hash(key)
+ hash = cache_get(expand_cache_key(key))
+ if hash.is_a?(Hash) && digest.hexdigest == hash['_version']
hash
end
end
- def cache_set_hash(key, version, hash)
- hash['_version'] = version
- cache_set(cache_key_for(key), hash)
+ def cache_set_hash(key, hash)
+ hash['_version'] = digest.hexdigest
+ cache_set(expand_cache_key(key), hash)
hash
end
View
12 lib/sprockets/context.rb
@@ -32,8 +32,8 @@ def initialize(environment, logical_path, pathname)
@__LINE__ = nil
@_required_paths = []
- @_dependency_paths = Set.new([pathname.to_s])
- @_dependency_assets = Set.new
+ @_dependency_paths = Set.new
+ @_dependency_assets = Set.new([pathname.to_s])
end
# Returns the environment path that contains the file.
@@ -78,7 +78,7 @@ def resolve(path, options = {}, &block)
pathname = Pathname.new(path)
attributes = environment.attributes_for(pathname)
- if pathname.absolute?
+ if pathname.to_s =~ /^\//
pathname
elsif content_type = options[:content_type]
@@ -148,7 +148,9 @@ def require_asset(path)
def asset_requirable?(path)
pathname = resolve(path)
content_type = environment.content_type_of(pathname)
- pathname.file? && (self.content_type.nil? || self.content_type == content_type)
+ stat = environment.stat(path)
+ return false unless stat && stat.file?
+ self.content_type.nil? || self.content_type == content_type
end
# Reads `path` and runs processors on the file.
@@ -193,7 +195,7 @@ def evaluate(path, options = {})
# $('<img>').attr('src', '<%= asset_data_uri 'avatar.jpg' %>')
#
def asset_data_uri(path)
- depend_on(path)
+ depend_on_asset(path)
asset = environment.find_asset(path)
base64 = Base64.encode64(asset.to_s).gsub(/\s+/, "")
"data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}"
View
67 lib/sprockets/digest.rb
@@ -1,67 +0,0 @@
-module Sprockets
- # `Digest` is an internal mixin whose public methods are exposed on
- # the `Environment` and `Index` classes.
- module Digest
- # Returns a `Digest` implementation class.
- #
- # Defaults to `Digest::MD5`.
- def digest_class
- @digest_class
- end
-
- # Assign a `Digest` implementation class. This maybe 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.
- def version
- @version
- end
-
- # 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
- end
-end
View
24 lib/sprockets/engines.rb
@@ -71,28 +71,4 @@ def deep_copy_hash(hash)
hash.inject(initial) { |h, (k, a)| h[k] = a.dup; h }
end
end
-
- # Extend Sprockets module to provide global registry
- extend Engines
- @engines = {}
-
- # Cherry pick the default Tilt engines that make sense for
- # Sprockets. We don't need ones that only generate html like HAML.
-
- # Mmm, CoffeeScript
- register_engine '.coffee', Tilt::CoffeeScriptTemplate
-
- # JST engines
- register_engine '.jst', JstProcessor
- register_engine '.eco', EcoTemplate
- register_engine '.ejs', EjsTemplate
-
- # CSS engines
- register_engine '.less', Tilt::LessTemplate
- register_engine '.sass', Tilt::SassTemplate
- register_engine '.scss', Tilt::ScssTemplate
-
- # Other
- register_engine '.erb', Tilt::ERBTemplate
- register_engine '.str', Tilt::StringTemplate
end
View
19 lib/sprockets/environment.rb
@@ -66,27 +66,18 @@ def index
# Cache `find_asset` calls
def find_asset(path, options = {})
+ options[:bundle] = true unless options.key?(:bundle)
+
# Ensure inmemory cached assets are still fresh on every lookup
- if (asset = @assets[path.to_s]) && asset.fresh?
+ if (asset = @assets[cache_key_for(path, options)]) && asset.fresh?(self)
asset
- elsif asset = super
- # Eager load asset to catch build errors
- asset.to_s
-
- @assets[path.to_s] = @assets[asset.pathname.to_s] = asset
+ elsif asset = index.find_asset(path, options)
+ # Cache is pushed upstream by Index#find_asset
asset
end
end
protected
- # Cache asset building in persisted cache.
- def build_asset(path, pathname, options)
- # Persisted cache
- cache_asset(pathname.to_s) do
- super
- end
- end
-
def expire_index!
# Clear digest to be recomputed
@digest = nil
View
1  lib/sprockets/errors.rb
@@ -7,6 +7,7 @@ class ContentTypeMismatch < Error; end
class EncodingError < Error; end
class FileNotFound < Error; end
class FileOutsidePaths < Error; end
+ class UnserializeError < Error; end
module EngineError
attr_accessor :sprockets_annotation
View
47 lib/sprockets/index.rb
@@ -12,6 +12,8 @@ module Sprockets
# `Environment#index`.
class Index < Base
def initialize(environment)
+ @environment = environment
+
# Copy environment attributes
@logger = environment.logger
@context_class = environment.context_class
@@ -37,20 +39,32 @@ def index
end
# Cache calls to `file_digest`
- def file_digest(pathname, data = nil)
- memoize(@digests, pathname.to_s) { super }
+ def file_digest(pathname)
+ key = pathname.to_s
+ if @digests.key?(key)
+ @digests[key]
+ else
+ @digests[key] = super
+ end
end
# Cache `find_asset` calls
def find_asset(path, options = {})
- if asset = @assets[path.to_s]
+ options[:bundle] = true unless options.key?(:bundle)
+ if asset = @assets[cache_key_for(path, options)]
asset
elsif asset = super
- # Eager load asset to catch build errors
- asset.to_s
+ logical_path_cache_key = cache_key_for(path, options)
+ full_path_cache_key = cache_key_for(asset.pathname, options)
+
+ # Cache on Index
+ @assets[logical_path_cache_key] = @assets[full_path_cache_key] = asset
+
+ # Push cache upstream to Environment
+ @environment.instance_eval do
+ @assets[logical_path_cache_key] = @assets[full_path_cache_key] = asset
+ end
- # Cache at logical path and expanded path
- @assets[path.to_s] = @assets[asset.pathname.to_s] = asset
asset
end
end
@@ -65,18 +79,17 @@ def expire_index!
# Cache asset building in memory and in persisted cache.
def build_asset(path, pathname, options)
# Memory cache
- memoize(@assets, pathname.to_s) do
- # Persisted cache
- cache_asset(pathname.to_s) do
- super
+ key = cache_key_for(pathname, options)
+ if @assets.key?(key)
+ @assets[key]
+ else
+ @assets[key] = begin
+ # Persisted cache
+ cache_asset(key) do
+ super
+ end
end
end
end
-
- private
- # Simple memoize helper that stores `nil` values
- def memoize(hash, key)
- hash.key?(key) ? hash[key] : hash[key] = yield
- end
end
end
View
148 lib/sprockets/processed_asset.rb
@@ -0,0 +1,148 @@
+require 'sprockets/asset'
+require 'sprockets/utils'
+
+module Sprockets
+ class ProcessedAsset < Asset
+ def initialize(environment, logical_path, pathname)
+ super
+
+ start_time = Time.now.to_f
+
+ context = environment.context_class.new(environment, logical_path, pathname)
+ @source = context.evaluate(pathname)
+ @length = Rack::Utils.bytesize(source)
+ @digest = environment.digest.update(source).hexdigest
+
+ build_required_assets(environment, context)
+ build_dependency_paths(environment, context)
+
+ @dependency_digest = compute_dependency_digest(environment)
+
+ elapsed_time = ((Time.now.to_f - start_time) * 1000).to_i
+ environment.logger.info "Compiled #{logical_path} (#{elapsed_time}ms) (pid #{Process.pid})"
+ end
+
+ # Interal: Used to check equality
+ attr_reader :dependency_digest
+
+ attr_reader :source
+
+ # Initialize `BundledAsset` from serialized `Hash`.
+ def init_with(environment, coder)
+ super
+
+ @source = coder['source']
+ @dependency_digest = coder['dependency_digest']
+
+ @required_assets = coder['required_paths'].map { |p|
+ p = expand_root_path(p)
+
+ unless environment.paths.detect { |path| p[path] }
+ raise UnserializeError, "#{p} isn't in paths"
+ end
+
+ p == pathname.to_s ? self : environment.find_asset(p, :bundle => false)
+ }
+ @dependency_paths = coder['dependency_paths'].map { |h|
+ DependencyFile.new(expand_root_path(h['path']), h['mtime'], h['digest'])
+ }
+ end
+
+ # Serialize custom attributes in `BundledAsset`.
+ def encode_with(coder)
+ super
+
+ coder['source'] = source
+ coder['dependency_digest'] = dependency_digest
+
+ coder['required_paths'] = required_assets.map { |a|
+ relativize_root_path(a.pathname).to_s
+ }
+ coder['dependency_paths'] = dependency_paths.map { |d|
+ { 'path' => relativize_root_path(d.pathname).to_s,
+ 'mtime' => d.mtime.iso8601,
+ 'digest' => d.digest }
+ }
+ end
+
+ # Checks if Asset is stale by comparing the actual mtime and
+ # digest to the inmemory model.
+ def fresh?(environment)
+ # Check freshness of all declared dependencies
+ @dependency_paths.all? { |dep| dependency_fresh?(environment, dep) }
+ end
+
+ protected
+ class DependencyFile < Struct.new(:pathname, :mtime, :digest)
+ def initialize(pathname, mtime, digest)
+ pathname = Pathname.new(pathname) unless pathname.is_a?(Pathname)
+ mtime = Time.parse(mtime) if mtime.is_a?(String)
+ super
+ end
+
+ def eql?(other)
+ other.is_a?(DependencyFile) &&
+ pathname.eql?(other.pathname) &&
+ mtime.eql?(other.mtime) &&
+ digest.eql?(other.digest)
+ end
+
+ def hash
+ pathname.to_s.hash
+ end
+ end
+
+ private
+ def build_required_assets(environment, context)
+ @required_assets = []
+ required_assets_cache = {}
+
+ (context._required_paths + [pathname.to_s]).each do |path|
+ if path == self.pathname.to_s
+ unless required_assets_cache[self]
+ required_assets_cache[self] = true
+ @required_assets << self
+ end
+ elsif asset = environment.find_asset(path, :bundle => false)
+ asset.required_assets.each do |asset_dependency|
+ unless required_assets_cache[asset_dependency]
+ required_assets_cache[asset_dependency] = true
+ @required_assets << asset_dependency
+ end
+ end
+ end
+ end
+
+ required_assets_cache.clear
+ required_assets_cache = nil
+ end
+
+ def build_dependency_paths(environment, context)
+ dependency_paths = {}
+
+ context._dependency_paths.each do |path|
+ dep = DependencyFile.new(path, environment.stat(path).mtime, environment.file_digest(path).hexdigest)
+ dependency_paths[dep] = true
+ end
+
+ context._dependency_assets.each do |path|
+ if path == self.pathname.to_s
+ dep = DependencyFile.new(pathname, environment.stat(path).mtime, environment.file_digest(path).hexdigest)
+ dependency_paths[dep] = true
+ elsif asset = environment.find_asset(path, :bundle => false)
+ asset.dependency_paths.each do |d|
+ dependency_paths[d] = true
+ end
+ end
+ end
+
+ @dependency_paths = dependency_paths.keys
+ end
+
+ def compute_dependency_digest(environment)
+ required_assets.inject(environment.digest) { |digest, asset|
+ digest.update asset.digest
+ }.hexdigest
+ end
+ end
+end
View
25 lib/sprockets/server.rb
@@ -38,9 +38,13 @@ def call(env)
# Extract the path from everything after the leading slash
path = unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
+ # Strip fingerprint
+ if fingerprint = path_fingerprint(path)
+ path = path.sub("-#{fingerprint}", '')
+ end
+
# Look up the asset.
- asset = find_asset(path)
- asset.to_a if asset
+ asset = find_asset(path, :bundle => !body_only?(env))
# `find_asset` returns nil if the asset doesn't exist
if asset.nil?
@@ -193,11 +197,7 @@ def not_modified_response(asset, env)
# Returns a 200 OK response tuple
def ok_response(asset, env)
- if body_only?(env)
- [ 200, headers(env, asset, Rack::Utils.bytesize(asset.body)), [asset.body] ]
- else
- [ 200, headers(env, asset, asset.length), asset ]
- end
+ [ 200, headers(env, asset, asset.length), asset ]
end
def headers(env, asset, length)
@@ -213,7 +213,7 @@ def headers(env, asset, length)
# If the request url contains a fingerprint, set a long
# expires on the response
- if attributes_for(env["PATH_INFO"]).path_fingerprint
+ if path_fingerprint(env["PATH_INFO"])
headers["Cache-Control"] << ", max-age=31536000"
# Otherwise set `must-revalidate` since the asset could be modified.
@@ -223,6 +223,15 @@ def headers(env, asset, length)
end
end
+ # Gets digest fingerprint.
+ #
+ # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
+ # # => "0aa2105d29558f3eb790d411d7d8fb66"
+ #
+ def path_fingerprint(path)
+ path[/-([0-9a-f]{7,40})\.[^.]+$/, 1]
+ end
+
# URI.unescape is deprecated on 1.9. We need to use URI::Parser
# if its available.
if defined? URI::DEFAULT_PARSER
View
35 lib/sprockets/static_asset.rb
@@ -7,40 +7,17 @@ module Sprockets
# any processing or concatenation. These are typical images and
# other binary files.
class StaticAsset < Asset
- # Define extra attributes to be serialized.
- def self.serialized_attributes
- super + %w( content_type mtime length digest )
- end
-
- def initialize(environment, logical_path, pathname, digest = nil)
- super(environment, logical_path, pathname)
- @digest = digest
- load!
- end
-
- # Returns file contents as its `body`.
- def body
+ # Returns file contents as its `source`.
+ def source
# File is read everytime to avoid memory bloat of large binary files
pathname.open('rb') { |f| f.read }
end
- # Checks if Asset is fresh by comparing the actual mtime and
- # digest to the inmemory model.
- def fresh?
- # Check current mtime and digest
- dependency_fresh?('path' => pathname, 'mtime' => mtime, 'hexdigest' => digest)
- end
-
# Implemented for Rack SendFile support.
def to_path
pathname.to_s
end
- # `to_s` is aliased to body since static assets can't have any dependencies.
- def to_s
- body
- end
-
# Save asset to disk.
def write_to(filename, options = {})
# Gzip contents if filename has '.gz'
@@ -74,13 +51,5 @@ def write_to(filename, options = {})
# Ensure tmp file gets cleaned up
FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
end
-
- private
- def load!
- content_type
- mtime
- length
- digest
- end
end
end
View
24 lib/sprockets/trail.rb
@@ -86,29 +86,5 @@ def resolve(logical_path, options = {})
def trail
@trail
end
-
- def find_asset_in_path(logical_path, options = {})
- # Strip fingerprint on logical path if there is one.
- # Not sure how valuable this feature is...
- if fingerprint = attributes_for(logical_path).path_fingerprint
- pathname = resolve(logical_path.to_s.sub("-#{fingerprint}", ''))
- else
- pathname = resolve(logical_path)
- end
- rescue FileNotFound
- nil
- else
- # Build the asset for the actual pathname
- asset = build_asset(logical_path, pathname, options)
-
- # Double check request fingerprint against actual digest
- # Again, not sure if this code path is even reachable
- if fingerprint && fingerprint != asset.digest
- logger.error "Nonexistent asset #{logical_path} @ #{fingerprint}"
- asset = nil
- end
-
- asset
- end
end
end
View
1  test/fixtures/context/environment-dep.js.erb
@@ -0,0 +1 @@
+<%= environment.class %>:<%= environment.object_id %>
View
2  test/fixtures/context/environment.js.erb
@@ -0,0 +1,2 @@
+//= require environment-dep
+<%= environment.class %>:<%= environment.object_id %>
View
638 test/test_asset.rb
@@ -10,24 +10,24 @@ def self.test(name, &block)
assert @asset.pathname.exist?
end
- test "logical path can find itself" do
- assert_equal @asset, @env[@asset.logical_path]
- end
-
test "mtime" do
assert @asset.mtime
end
- test "digest" do
- assert @asset.digest
+ test "digest is source digest" do
+ assert_equal @env.digest.update(@asset.to_s).hexdigest, @asset.digest
+ end
+
+ test "length is source length" do
+ assert_equal @asset.to_s.length, @asset.length
end
test "stale?" do
- assert !@asset.stale?
+ assert !@asset.stale?(@env)
end
test "fresh?" do
- assert @asset.fresh?
+ assert @asset.fresh?(@env)
end
test "dependencies are an Array" do
@@ -42,6 +42,14 @@ def self.test(name, &block)
assert_kind_of String, @asset.body
end
+ test "to_a body parts equals to_s" do
+ source = ""
+ @asset.to_a.each do |asset|
+ source << asset.body
+ end
+ assert_equal @asset.to_s, source
+ end
+
test "write to file" do
target = fixture_path('asset/tmp.js')
begin
@@ -67,6 +75,248 @@ def self.test(name, &block)
end
end
+module FreshnessTests
+ def self.test(name, &block)
+ define_method("test #{name.inspect}", &block)
+ end
+
+ test "asset is stale when its contents has changed" do
+ filename = fixture_path('asset/test.js')
+
+ sandbox filename do
+ File.open(filename, 'w') { |f| f.write "a;" }
+ asset = asset('test.js')
+
+ assert asset.fresh?(@env)
+
+ File.open(filename, 'w') { |f| f.write "b;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, filename)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale if the file is removed" do
+ filename = fixture_path('asset/test.js')
+
+ sandbox filename do
+ File.open(filename, 'w') { |f| f.write "a;" }
+ asset = asset('test.js')
+
+ assert asset.fresh?(@env)
+
+ File.unlink(filename)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its source files is modified" do
+ main = fixture_path('asset/test-main.js')
+ dep = fixture_path('asset/test-dep.js')
+
+ sandbox main, dep do
+ File.open(main, 'w') { |f| f.write "//= require test-dep\n" }
+ File.open(dep, 'w') { |f| f.write "a;" }
+ asset = asset('test-main.js')
+
+ assert asset.fresh?(@env)
+
+ File.open(dep, 'w') { |f| f.write "b;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, dep)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its dependencies is modified" do
+ main = fixture_path('asset/test-main.js')
+ dep = fixture_path('asset/test-dep.js')
+
+ sandbox main, dep do
+ File.open(main, 'w') { |f| f.write "//= depend_on test-dep\n" }
+ File.open(dep, 'w') { |f| f.write "a;" }
+ asset = asset('test-main.js')
+
+ assert asset.fresh?(@env)
+
+ File.open(dep, 'w') { |f| f.write "b;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, dep)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its asset dependencies is modified" do
+ main = fixture_path('asset/test-main.js')
+ dep = fixture_path('asset/test-dep.js')
+
+ sandbox main, dep do
+ File.open(main, 'w') { |f| f.write "//= depend_on_asset test-dep\n" }
+ File.open(dep, 'w') { |f| f.write "a;" }
+ asset = asset('test-main.js')
+
+ assert asset.fresh?(@env)
+
+ File.open(dep, 'w') { |f| f.write "b;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, dep)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its source files dependencies is modified" do
+ a = fixture_path('asset/test-a.js')
+ b = fixture_path('asset/test-b.js')
+ c = fixture_path('asset/test-c.js')
+
+ sandbox a, b, c do
+ File.open(a, 'w') { |f| f.write "//= require test-b\n" }
+ File.open(b, 'w') { |f| f.write "//= require test-c\n" }
+ File.open(c, 'w') { |f| f.write "c;" }
+ asset_a = asset('test-a.js')
+ asset_b = asset('test-b.js')
+ asset_c = asset('test-c.js')
+
+ assert asset_a.fresh?(@env)
+ assert asset_b.fresh?(@env)
+ assert asset_c.fresh?(@env)
+
+ File.open(c, 'w') { |f| f.write "x;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, c)
+
+ assert asset_a.stale?(@env)
+ assert asset_b.stale?(@env)
+ assert asset_c.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its dependency dependencies is modified" do
+ a = fixture_path('asset/test-a.js')
+ b = fixture_path('asset/test-b.js')
+ c = fixture_path('asset/test-c.js')
+
+ sandbox a, b, c do
+ File.open(a, 'w') { |f| f.write "//= require test-b\n" }
+ File.open(b, 'w') { |f| f.write "//= depend_on test-c\n" }
+ File.open(c, 'w') { |f| f.write "c;" }
+ asset_a = asset('test-a.js')
+ asset_b = asset('test-b.js')
+ asset_c = asset('test-c.js')
+
+ assert asset_a.fresh?(@env)
+ assert asset_b.fresh?(@env)
+ assert asset_c.fresh?(@env)
+
+ File.open(c, 'w') { |f| f.write "x;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, c)
+
+ assert asset_a.stale?(@env)
+ assert asset_b.stale?(@env)
+ assert asset_c.stale?(@env)
+ end
+ end
+
+ test "asset is stale when one of its asset dependency dependencies is modified" do
+ a = fixture_path('asset/test-a.js')
+ b = fixture_path('asset/test-b.js')
+ c = fixture_path('asset/test-c.js')
+
+ sandbox a, b, c do
+ File.open(a, 'w') { |f| f.write "//= depend_on_asset test-b\n" }
+ File.open(b, 'w') { |f| f.write "//= depend_on_asset test-c\n" }
+ File.open(c, 'w') { |f| f.write "c;" }
+ asset_a = asset('test-a.js')
+ asset_b = asset('test-b.js')
+ asset_c = asset('test-c.js')
+
+ assert asset_a.fresh?(@env)
+ assert asset_b.fresh?(@env)
+ assert asset_c.fresh?(@env)
+
+ File.open(c, 'w') { |f| f.write "x;" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, c)
+
+ assert asset_a.stale?(@env)
+ assert asset_b.stale?(@env)
+ assert asset_c.stale?(@env)
+ end
+ end
+
+ test "asset if stale if once of its source files is removed" do
+ main = fixture_path('asset/test-main.js')
+ dep = fixture_path('asset/test-dep.js')
+
+ sandbox main, dep do
+ File.open(main, 'w') { |f| f.write "//= require test-dep\n" }
+ File.open(dep, 'w') { |f| f.write "a;" }
+ asset = asset('test-main.js')
+
+ assert asset.fresh?(@env)
+
+ File.unlink(dep)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale if a file is added to its require directory" do
+ asset = asset("tree/all_with_require_directory.js")
+ assert asset.fresh?(@env)
+
+ dirname = File.join(fixture_path("asset"), "tree/all")
+ filename = File.join(dirname, "z.js")
+
+ sandbox filename do
+ File.open(filename, 'w') { |f| f.write "z" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, dirname)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale if a file is added to its require tree" do
+ asset = asset("tree/all_with_require_tree.js")
+ assert asset.fresh?(@env)
+
+ dirname = File.join(fixture_path("asset"), "tree/all/b/c")
+ filename = File.join(dirname, "z.js")
+
+ sandbox filename do
+ File.open(filename, 'w') { |f| f.write "z" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, dirname)
+
+ assert asset.stale?(@env)
+ end
+ end
+
+ test "asset is stale if its declared dependency changes" do
+ sprite = fixture_path('asset/sprite.css.erb')
+ image = fixture_path('asset/POW.png')
+
+ sandbox sprite, image do
+ asset = asset('sprite.css')
+
+ assert asset.fresh?(@env)
+
+ File.open(image, 'w') { |f| f.write "(change)" }
+ mtime = Time.now + 1
+ File.utime(mtime, mtime, image)
+
+ assert asset.stale?(@env)
+ end
+ end
+end
+
class StaticAssetTest < Sprockets::TestCase
def setup
@env = Sprockets::Environment.new
@@ -78,6 +328,10 @@ def setup
include AssetTests
+ test "logical path can find itself" do
+ assert_equal @asset, @env[@asset.logical_path]
+ end
+
test "class" do
assert_kind_of Sprockets::StaticAsset, @asset
end
@@ -107,7 +361,7 @@ def setup
end
test "asset is fresh if its mtime and contents are the same" do
- assert @asset.fresh?
+ assert @asset.fresh?(@env)
end
test "asset is fresh if its mtime is changed but its contents is the same" do
@@ -117,13 +371,13 @@ def setup
File.open(filename, 'w') { |f| f.write "a" }
asset = @env['test-POW.png']
- assert asset.fresh?
+ assert asset.fresh?(@env)
File.open(filename, 'w') { |f| f.write "a" }
mtime = Time.now + 1
File.utime(mtime, mtime, filename)
- assert asset.fresh?
+ assert asset.fresh?(@env)
end
end
@@ -134,13 +388,13 @@ def setup
File.open(filename, 'w') { |f| f.write "a" }
asset = @env['POW.png']
- assert asset.fresh?
+ assert asset.fresh?(@env)
File.open(filename, 'w') { |f| f.write "b" }
mtime = Time.now + 1
File.utime(mtime, mtime, filename)
- assert asset.stale?
+ assert asset.stale?(@env)
end
end
@@ -151,11 +405,11 @@ def setup
File.open(filename, 'w') { |f| f.write "a" }
asset = @env['POW.png']
- assert asset.fresh?
+ assert asset.fresh?(@env)
File.unlink(filename)
- assert asset.stale?
+ assert asset.stale?(@env)
end
end
@@ -163,7 +417,7 @@ def setup
expected = @asset
hash = {}
@asset.encode_with(hash)
- actual = @env.asset_from_hash(hash)
+ actual = Sprockets::Asset.from_hash(@env, hash)
assert_kind_of Sprockets::StaticAsset, actual
assert_equal expected.logical_path, actual.logical_path
@@ -171,6 +425,91 @@ def setup
assert_equal expected.content_type, actual.content_type
assert_equal expected.length, actual.length
assert_equal expected.digest, actual.digest
+ assert_equal expected.fresh?(@env), actual.fresh?(@env)
+
+ assert_equal expected.dependencies, actual.dependencies
+ assert_equal expected.to_a, actual.to_a
+ assert_equal expected.body, actual.body
+ assert_equal expected.to_s, actual.to_s
+
+ assert actual.eql?(expected)
+ assert expected.eql?(actual)
+ end
+end
+
+class ProcessedAssetTest < Sprockets::TestCase
+ include FreshnessTests
+
+ def setup
+ @env = Sprockets::Environment.new
+ @env.append_path(fixture_path('asset'))
+ @env.cache = {}
+
+ @asset = @env.find_asset('application.js', :bundle => false)
+ @bundle = false
+ end
+
+ include AssetTests
+
+ test "logical path can find itself" do
+ assert_equal @asset, @env.find_asset(@asset.logical_path, :bundle => false)
+ end
+
+ test "class" do
+ assert_kind_of Sprockets::ProcessedAsset, @asset
+ end
+
+ test "content type" do
+ assert_equal "application/javascript", @asset.content_type
+ end
+
+ test "length" do
+ assert_equal 67, @asset.length
+ end
+
+ test "splat" do
+ assert_equal [@asset], @asset.to_a
+ end
+
+ test "dependencies" do
+ assert_equal [], @asset.dependencies
+ end
+
+ test "to_s" do
+ assert_equal "\ndocument.on('dom:loaded', function() {\n $('search').focus();\n});\n", @asset.to_s
+ end
+
+ test "each" do
+ body = ""
+ @asset.each { |part| body << part }
+ assert_equal "\ndocument.on('dom:loaded', function() {\n $('search').focus();\n});\n", body
+ end
+
+ test "to_a" do
+ body = ""
+ @asset.to_a.each do |asset|
+ body << asset.body
+ end
+ assert_equal "\ndocument.on('dom:loaded', function() {\n $('search').focus();\n});\n", body
+ end
+
+ test "asset is fresh if its mtime and contents are the same" do
+ assert @asset.fresh?(@env)
+ end
+
+ test "serializing asset to and from hash" do
+ expected = @asset
+ hash = {}
+ @asset.encode_with(hash)
+ actual = Sprockets::Asset.from_hash(@env, hash)
+
+ assert_kind_of Sprockets::ProcessedAsset, actual
+ assert_equal expected.logical_path, actual.logical_path
+ assert_equal expected.pathname, actual.pathname
+ assert_equal expected.content_type, actual.content_type
+ assert_equal expected.length, actual.length
+ assert_equal expected.digest, actual.digest
+ assert_equal expected.fresh?(@env), actual.fresh?(@env)
assert_equal expected.dependencies, actual.dependencies
assert_equal expected.to_a, actual.to_a
@@ -180,19 +519,34 @@ def setup
assert actual.eql?(expected)
assert expected.eql?(actual)
end
+
+ def asset(logical_path)
+ @env.find_asset(logical_path, :bundle => @bundle)
+ end
+
+ def resolve(logical_path)
+ @env.resolve(logical_path)
+ end
end
class BundledAssetTest < Sprockets::TestCase
+ include FreshnessTests
+
def setup
@env = Sprockets::Environment.new
@env.append_path(fixture_path('asset'))
@env.cache = {}
@asset = @env['application.js']
+ @bundle = true
end
include AssetTests
+ test "logical path can find itself" do
+ assert_equal @asset, @env[@asset.logical_path]
+ end
+
test "class" do
assert_kind_of Sprockets::BundledAsset, @asset
end
@@ -229,7 +583,11 @@ def setup
assert_equal [resolve("project.js")], asset("project.js").to_a.map(&:pathname)
end
- test "asset includes self as dependency" do
+ test "splatted assets are processed assets" do
+ assert asset("project.js").to_a.all? { |a| a.is_a?(Sprockets::ProcessedAsset) }
+ end
+
+ test "asset doesn't include self as dependency" do
assert_equal [], asset("project.js").dependencies.map(&:pathname)
end
@@ -300,7 +658,7 @@ def setup
test "can't require absolute files outside the load path" do
assert_raise Sprockets::FileOutsidePaths do
- warn asset("absolute/require_outside_path.js").to_s
+ asset("absolute/require_outside_path.js").to_s
end
end
@@ -431,243 +789,7 @@ def setup
end
test "asset is fresh if its mtime and contents are the same" do
- assert asset("application.js").fresh?
- end
-
- test "asset is stale when its contents has changed" do
- filename = fixture_path('asset/test.js')
-
- sandbox filename do
- File.open(filename, 'w') { |f| f.write "a;" }
- asset = @env['test.js']
-
- assert asset.fresh?
-
- File.open(filename, 'w') { |f| f.write "b;" }
- mtime = Time.now + 1
- File.utime(mtime, mtime, filename)
-
- assert asset.stale?
- end
- end
-
- test "asset is stale if the file is removed" do
- filename = fixture_path('asset/test.js')
-
- sandbox filename do
- File.open(filename, 'w') { |f| f.write "a;" }
- asset = @env['test.js']
-
- assert asset.fresh?
-
- File.unlink(filename)
-
- assert asset.stale?
- end
- end
-
- test "asset is stale when one of its source files is modified" do
- main = fixture_path('asset/test-main.js')
- dep = fixture_path('asset/test-dep.js')
-
- sandbox main, dep do
- File.open(main, 'w') { |f| f.write "//= require test-dep\n" }
- File.open(dep, 'w') { |f| f.write "a;" }
- asset = @env['test-main.js']
-
- assert asset.fresh?
-
- File.open(dep, 'w') { |f| f.write "b;" }
- mtime = Time.now + 1
- File.utime(mtime, mtime, dep)
-
- assert asset.stale?
- end
- end
-
- test "asset is stale when one of its dependencies is modified" do
- main = fixture_path('asset/test-main.js')
- dep = fixture_path('asset/test-dep.js')
-
- sandbox main, dep do
- File.open(main, 'w') { |f| f.write "//= depend_on test-dep\n" }
- File.open(dep, 'w') { |f| f.write "a;" }
- asset = @env['test-main.js']
-
- assert asset.fresh?
-
- File.open(dep, 'w') { |f| f.write "b;" }
- mtime = Time.now + 1
- File.utime(mtime, mtime, dep)
-
- assert asset.stale?
- end
- end
-
- test "asset is stale when one of its asset dependencies is modified" do
- main = fixture_path('asset/test-main.js')
- dep = fixture_path('asset/test-dep.js')
-
- sandbox main, dep do
- File.open(main, 'w') { |f| f.write "//= depend_on_asset test-dep\n" }
- File.open(dep, 'w') { |f| f.write "a;" }
- asset = @env['test-main.js']
-
- assert asset.fresh?
-
- File.open(dep, 'w') { |f| f.write "b;" }
- mtime = Time.now + 1
- File.utime(mtime, mtime, dep)
-
- assert asset.stale?
- end
- end
-
- test "asset is stale when one of its source files dependencies is modified" do
- a = fixture_path('asset/test-a.js')
- b = fixture_path('asset/test-b.js')
- c = fixture_path('asset/test-c.js')
-
- sandbox a, b, c do
- File.open(a, 'w') { |f| f.write "//= require test-b\n" }
- File.open(b, 'w') { |f| f.write "//= require test-c\n" }
- File.open(c, 'w') { |f| f.write "c;" }
- asset_a = @env['test-a.js']
- asset_b = @env['test-b.js']
- asset_c = @env['test-c.js']
-
- assert asset_a.fresh?
- assert asset_b.fresh?
- assert asset_c.fresh?
-
- File.open(c, 'w') { |f| f.write "x;" }
- mtime = Time.now + 1
- File.utime(mtime, mtime, c)
-
- assert asset_a.stale?
- assert asset_b.stale?
- assert asset_c.stale?
- end
- end
-
- test "asset is stale when one of its dependency dependencies is modified" do
- a = fixture_path('asset/test-a.js')
- b = fixture_path('asset/test-b.js')
- c = fixture_path('asset/test-c.js')
-
- sandbox a, b, c do
- File.open(a, 'w') { |f| f.write "//= require test-b\n" }
- File.open(b, 'w') { |f| f.write "//= depend_on test-c\n" }
- File.open(c, 'w') { |f| f.write "c;" }
- asset_a = @env['test-a.js']
- asset_b = @env['test-b.js']
- asset_c = @env['test-c.js']
-
- assert asset_a.fresh?
- assert asset_b.fresh?