Skip to content
This repository has been archived by the owner on Jun 10, 2018. It is now read-only.

Rake Task #256

Merged
merged 14 commits into from Dec 15, 2011
140 changes: 140 additions & 0 deletions lib/rake/sprocketstask.rb
@@ -0,0 +1,140 @@
require 'rake'
require 'rake/tasklib'

require 'sprockets'
require 'logger'

module Rake
# Simple Sprockets compilation Rake task macro.
#
# Rake::SprocketsTask.new do |t|
# t.environment = Sprockets::Environment.new
# t.bundle_dir = "./public/assets"
# t.bundles = %w( application.js application.css )
# end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm, bundle_dir and bundles?

#
class SprocketsTask < Rake::TaskLib
# Name of the task. Defaults to "bundle".
#
# The name will also be used to suffix the clean and clobber
# tasks, "clean_bundle" and "clobber_bundle".
attr_accessor :name

# `Environment` instance used for finding assets.
#
# You'll most likely want to reassign `environment` to your own.
#
# Rake::SprocketsTask.new do |t|
# t.environment = Foo::Assets
# end
#
def environment
if !@environment.is_a?(Sprockets::Base) && @environment.respond_to?(:call)
@environment = @environment.call
else
@environment
end
end
attr_writer :environment

# Directory to write compiled assets too. As well as the manifest file.
#
# t.bundle_dir = "./public/assets"
#
attr_accessor :bundle_dir

# Array of logical paths to compile.
#
# t.bundles = %w( application.js jquery.js application.css )
#
attr_accessor :bundles

# Logger to use during rake tasks. Defaults to using stderr.
#
# t.logger = Logger.new($stdout)
#
attr_accessor :logger

# Returns logger level Integer.
def log_level
@logger.level
end

# Set logger level with constant or symbol.
#
# t.log_level = Logger::INFO
# t.log_level = :debug
#
def log_level=(level)
if level.is_a?(Integer)
@logger.level = level
else
@logger.level = Logger.const_get(level.to_s.upcase)
end
end

def initialize(name = :bundle)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the default task name should be.

@name = name
@environment = lambda { Sprockets::Environment.new(Dir.pwd) }
@logger = Logger.new($stderr)
@logger.level = Logger::WARN

yield self if block_given?

define
end

# Define tasks
def define
bundles.each do |logical_path|
task "#{name}:#{logical_path}" do
with_logger do
manifest.compile logical_path
end
end
end

desc name == :bundle ? "Compile asset bundles" : "Compile #{name} bundles"
task name => bundles.map { |path| "#{name}:#{path}" }

desc name == :bundle ? "Remove all asset bundles" : "Remove all #{name} bundles"
task "clobber_#{name}" do
with_logger do
manifest.clobber
end
end

task :clobber => ["clobber_#{name}"]

desc name == :bundle ? "Clean old asset bundles" : "Clean old #{name} bundles"
task "clean_#{name}" do
with_logger do
manifest.clean
end
end

task :clean => ["clean_#{name}"]
end

private
# Returns cached indexed environment
def index
@index ||= environment.index
end

# Returns manifest for tasks
def manifest
@manifest ||= Sprockets::Manifest.new(index, "#{bundle_dir}/manifest.json")
end

# Sub out environment logger with our rake task logger that
# writes to stderr.
def with_logger
old_logger = index.logger
index.logger = @logger
yield
ensure
index.logger = old_logger
end
end
end
1 change: 1 addition & 0 deletions lib/sprockets.rb
Expand Up @@ -6,6 +6,7 @@ module Sprockets
autoload :Engines, "sprockets/engines"
autoload :Environment, "sprockets/environment"
autoload :Index, "sprockets/index"
autoload :Manifest, "sprockets/manifest"

# Assets
autoload :Asset, "sprockets/asset"
Expand Down
2 changes: 2 additions & 0 deletions lib/sprockets/asset.rb
Expand Up @@ -138,6 +138,8 @@ def write_to(filename, options = {})
# Gzip contents if filename has '.gz'
options[:compress] ||= File.extname(filename) == '.gz'

FileUtils.mkdir_p File.dirname(filename)

File.open("#{filename}+", 'wb') do |f|
if options[:compress]
# Run contents through `Zlib`
Expand Down
192 changes: 192 additions & 0 deletions lib/sprockets/manifest.rb
@@ -0,0 +1,192 @@
require 'json'
require 'time'

module Sprockets
# 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 with 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 infomation about the structure.
class Manifest
attr_reader :environment, :path, :dir

# Create new Manifest associated with an `environment`. `path` is
# a full path to the manifest json file. The file may or may not
# already exist. The dirname of the `path` will be used to write
# compiled assets to. Otherwise, if the path is a directory, the
# filename will default to "manifest.json" in that directory.
#
# Manifest.new(environment, "./public/assets/manifest.json")
#
def initialize(environment, path)
@environment = environment

if File.extname(path) == ""
@dir = File.expand_path(path)
@path = File.join(@dir, 'manifest.json')
else
@path = File.expand_path(path)
@dir = File.dirname(path)
end

if File.exist?(@path)
@data = JSON.parse(File.read(@path))
else
@data = {}
end
end

# 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" }
#
def assets
@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" } }
#
def files
@data['files'] ||= {}
end

# Compile and write 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")
#
def compile(logical_path)
if asset = find_asset(logical_path)
files[asset.digest_path] = {
'logical_path' => asset.logical_path,
'mtime' => asset.mtime.iso8601,
'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
end

save
asset
end
end

# Removes file from directory and from manifest. `filename` must
# be the name with any directory path.
#
# manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
#
def remove(filename)
path = File.join(dir, filename)
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)

save

logger.warn "Removed #{filename}"

nil
end

# Cleanup old assets in the compile directory. By default it will
# keep the latest version plus 2 backups.
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

# Wipe directive
def clobber
FileUtils.rm_r(@dir) if File.exist?(@dir)
logger.warn "Removed #{@dir}"
nil
end

protected
# Finds all the backup assets for a logical path. The latest
# version is always excluded. The return array is sorted by the
# assets mtime in descending order (Newest to oldest).
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

# Basic wrapper around Environment#find_asset. Logs compile time.
def find_asset(logical_path)
asset = nil
ms = benchmark do
asset = environment.find_asset(logical_path)
end
logger.warn "Compiled #{logical_path} (#{ms}ms)"
asset
end

# Persist manfiest back to FS
def save
FileUtils.mkdir_p dir
File.open(path, 'w') do |f|
f.write JSON.generate(@data)
end
end

private
def logger
environment.logger
end

def benchmark
start_time = Time.now.to_f
yield
((Time.now.to_f - start_time) * 1000).to_i
end
end
end
2 changes: 2 additions & 0 deletions lib/sprockets/static_asset.rb
Expand Up @@ -23,6 +23,8 @@ def write_to(filename, options = {})
# Gzip contents if filename has '.gz'
options[:compress] ||= File.extname(filename) == '.gz'

FileUtils.mkdir_p File.dirname(filename)

if options[:compress]
# Open file and run it through `Zlib`
pathname.open('rb') do |rd|
Expand Down