diff options
Diffstat (limited to 'actionpack/lib/sprockets')
-rw-r--r-- | actionpack/lib/sprockets/assets.rake | 95 | ||||
-rw-r--r-- | actionpack/lib/sprockets/bootstrap.rb | 37 | ||||
-rw-r--r-- | actionpack/lib/sprockets/compressors.rb | 83 | ||||
-rw-r--r-- | actionpack/lib/sprockets/helpers.rb | 6 | ||||
-rw-r--r-- | actionpack/lib/sprockets/helpers/isolated_helper.rb | 13 | ||||
-rw-r--r-- | actionpack/lib/sprockets/helpers/rails_helper.rb | 166 | ||||
-rw-r--r-- | actionpack/lib/sprockets/railtie.rb | 123 | ||||
-rw-r--r-- | actionpack/lib/sprockets/static_compiler.rb | 61 |
8 files changed, 503 insertions, 81 deletions
diff --git a/actionpack/lib/sprockets/assets.rake b/actionpack/lib/sprockets/assets.rake new file mode 100644 index 0000000000..a61a121d55 --- /dev/null +++ b/actionpack/lib/sprockets/assets.rake @@ -0,0 +1,95 @@ +require "fileutils" + +namespace :assets do + def ruby_rake_task(task) + env = ENV['RAILS_ENV'] || 'production' + groups = ENV['RAILS_GROUPS'] || 'assets' + args = [$0, task,"RAILS_ENV=#{env}","RAILS_GROUPS=#{groups}"] + args << "--trace" if Rake.application.options.trace + ruby(*args) + end + + # We are currently running with no explicit bundler group + # and/or no explicit environment - we have to reinvoke rake to + # execute this task. + def invoke_or_reboot_rake_task(task) + if ENV['RAILS_GROUPS'].to_s.empty? || ENV['RAILS_ENV'].to_s.empty? + ruby_rake_task task + else + Rake::Task[task].invoke + end + end + + desc "Compile all the assets named in config.assets.precompile" + task :precompile do + invoke_or_reboot_rake_task "assets:precompile:all" + end + + namespace :precompile do + def internal_precompile(digest=nil) + unless Rails.application.config.assets.enabled + warn "Cannot precompile assets if sprockets is disabled. Please set config.assets.enabled to true" + exit + end + + # Ensure that action view is loaded and the appropriate + # sprockets hooks get executed + _ = ActionView::Base + + config = Rails.application.config + config.assets.compile = true + config.assets.digest = digest unless digest.nil? + config.assets.digests = {} + + env = Rails.application.assets + target = File.join(Rails.public_path, config.assets.prefix) + compiler = Sprockets::StaticCompiler.new(env, + target, + config.assets.precompile, + :manifest_path => config.assets.manifest, + :digest => config.assets.digest, + :manifest => digest.nil?) + compiler.compile + end + + task :all do + Rake::Task["assets:precompile:primary"].invoke + # We need to reinvoke in order to run the secondary digestless + # asset compilation run - a fresh Sprockets environment is + # required in order to compile digestless assets as the + # environment has already cached the assets on the primary + # run. + ruby_rake_task "assets:precompile:nondigest" if Rails.application.config.assets.digest + end + + task :primary => ["assets:environment", "tmp:cache:clear"] do + internal_precompile + end + + task :nondigest => ["assets:environment", "tmp:cache:clear"] do + internal_precompile(false) + end + end + + desc "Remove compiled assets" + task :clean do + invoke_or_reboot_rake_task "assets:clean:all" + end + + namespace :clean do + task :all => ["assets:environment", "tmp:cache:clear"] do + config = Rails.application.config + public_asset_path = File.join(Rails.public_path, config.assets.prefix) + rm_rf public_asset_path, :secure => true + end + end + + task :environment do + if Rails.application.config.assets.initialize_on_precompile + Rake::Task["environment"].invoke + else + Rails.application.initialize!(:assets) + Sprockets::Bootstrap.new(Rails.application).run + end + end +end diff --git a/actionpack/lib/sprockets/bootstrap.rb b/actionpack/lib/sprockets/bootstrap.rb new file mode 100644 index 0000000000..395b264fe7 --- /dev/null +++ b/actionpack/lib/sprockets/bootstrap.rb @@ -0,0 +1,37 @@ +module Sprockets + class Bootstrap + def initialize(app) + @app = app + end + + # TODO: Get rid of config.assets.enabled + def run + app, config = @app, @app.config + return unless app.assets + + config.assets.paths.each { |path| app.assets.append_path(path) } + + if config.assets.compress + # temporarily hardcode default JS compressor to uglify. Soon, it will work + # the same as SCSS, where a default plugin sets the default. + unless config.assets.js_compressor == false + app.assets.js_compressor = LazyCompressor.new { Sprockets::Compressors.registered_js_compressor(config.assets.js_compressor || :uglifier) } + end + + unless config.assets.css_compressor == false + app.assets.css_compressor = LazyCompressor.new { Sprockets::Compressors.registered_css_compressor(config.assets.css_compressor) } + end + end + + if config.assets.compile + app.routes.prepend do + mount app.assets => config.assets.prefix + end + end + + if config.assets.digest + app.assets = app.assets.index + end + end + end +end diff --git a/actionpack/lib/sprockets/compressors.rb b/actionpack/lib/sprockets/compressors.rb new file mode 100644 index 0000000000..cb3e13314b --- /dev/null +++ b/actionpack/lib/sprockets/compressors.rb @@ -0,0 +1,83 @@ +module Sprockets + module Compressors + @@css_compressors = {} + @@js_compressors = {} + @@default_css_compressor = nil + @@default_js_compressor = nil + + def self.register_css_compressor(name, klass, options = {}) + @@default_css_compressor = name.to_sym if options[:default] || @@default_css_compressor.nil? + @@css_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.register_js_compressor(name, klass, options = {}) + @@default_js_compressor = name.to_sym if options[:default] || @@default_js_compressor.nil? + @@js_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.registered_css_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@css_compressors[name.to_sym] || @@css_compressors[@@default_css_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + def self.registered_js_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@js_compressors[name.to_sym] || @@js_compressors[@@default_js_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + # The default compressors must be registered in default plugins (ex. Sass-Rails) + register_css_compressor(:scss, 'Sass::Rails::Compressor', :require => 'sass/rails/compressor', :default => true) + register_js_compressor(:uglifier, 'Uglifier', :require => 'uglifier', :default => true) + + # Automaticaly register some compressors + register_css_compressor(:yui, 'YUI::CssCompressor', :require => 'yui/compressor') + register_js_compressor(:closure, 'Closure::Compiler', :require => 'closure-compiler') + register_js_compressor(:yui, 'YUI::JavaScriptCompressor', :require => 'yui/compressor') + end + + # An asset compressor which does nothing. + # + # This compressor simply returns the asset as-is, without any compression + # whatsoever. It is useful in development mode, when compression isn't + # needed but using the same asset pipeline as production is desired. + class NullCompressor #:nodoc: + def compress(content) + content + end + end + + # An asset compressor which only initializes the underlying compression + # engine when needed. + # + # This postpones the initialization of the compressor until + # <code>#compress</code> is called the first time. + class LazyCompressor #:nodoc: + # Initializes a new LazyCompressor. + # + # The block should return a compressor when called, i.e. an object + # which responds to <code>#compress</code>. + def initialize(&block) + @block = block + end + + def compress(content) + compressor.compress(content) + end + + private + + def compressor + @compressor ||= (@block.call || NullCompressor.new) + end + end +end diff --git a/actionpack/lib/sprockets/helpers.rb b/actionpack/lib/sprockets/helpers.rb new file mode 100644 index 0000000000..fee48386e0 --- /dev/null +++ b/actionpack/lib/sprockets/helpers.rb @@ -0,0 +1,6 @@ +module Sprockets + module Helpers + autoload :RailsHelper, "sprockets/helpers/rails_helper" + autoload :IsolatedHelper, "sprockets/helpers/isolated_helper" + end +end diff --git a/actionpack/lib/sprockets/helpers/isolated_helper.rb b/actionpack/lib/sprockets/helpers/isolated_helper.rb new file mode 100644 index 0000000000..3adb928c45 --- /dev/null +++ b/actionpack/lib/sprockets/helpers/isolated_helper.rb @@ -0,0 +1,13 @@ +module Sprockets + module Helpers + module IsolatedHelper + def controller + nil + end + + def config + Rails.application.config.action_controller + end + end + end +end diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb new file mode 100644 index 0000000000..ddf9b08b54 --- /dev/null +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -0,0 +1,166 @@ +require "action_view" + +module Sprockets + module Helpers + module RailsHelper + extend ActiveSupport::Concern + include ActionView::Helpers::AssetTagHelper + + def asset_paths + @asset_paths ||= begin + paths = RailsHelper::AssetPaths.new(config, controller) + paths.asset_environment = asset_environment + paths.asset_digests = asset_digests + paths.compile_assets = compile_assets? + paths.digest_assets = digest_assets? + paths + end + end + + def javascript_include_tag(*sources) + options = sources.extract_options! + debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? + body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? + + sources.collect do |source| + if debug && asset = asset_paths.asset_for(source, 'js') + asset.to_a.map { |dep| + super(dep.to_s, { :src => asset_path(dep, :ext => 'js', :body => true, :digest => digest) }.merge!(options)) + } + else + super(source.to_s, { :src => asset_path(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) + end + end.join("\n").html_safe + end + + def stylesheet_link_tag(*sources) + options = sources.extract_options! + debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? + body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? + + sources.collect do |source| + if debug && asset = asset_paths.asset_for(source, 'css') + asset.to_a.map { |dep| + super(dep.to_s, { :href => asset_path(dep, :ext => 'css', :body => true, :protocol => :request, :digest => digest) }.merge!(options)) + } + else + super(source.to_s, { :href => asset_path(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) + end + end.join("\n").html_safe + end + + def asset_path(source, options = {}) + source = source.logical_path if source.respond_to?(:logical_path) + path = asset_paths.compute_public_path(source, asset_prefix, options.merge(:body => true)) + options[:body] ? "#{path}?body=1" : path + end + + def image_path(source) + asset_path(source) + end + alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + + def javascript_path(source) + asset_path(source) + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with an javascript_path named route + + def stylesheet_path(source) + asset_path(source) + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with an stylesheet_path named route + + private + def debug_assets? + compile_assets? && (Rails.application.config.assets.debug || params[:debug_assets]) + rescue NoMethodError + false + end + + # Override to specify an alternative prefix for asset path generation. + # When combined with a custom +asset_environment+, this can be used to + # implement themes that can take advantage of the asset pipeline. + # + # If you only want to change where the assets are mounted, refer to + # +config.assets.prefix+ instead. + def asset_prefix + Rails.application.config.assets.prefix + end + + def asset_digests + Rails.application.config.assets.digests + end + + def compile_assets? + Rails.application.config.assets.compile + end + + def digest_assets? + Rails.application.config.assets.digest + end + + # Override to specify an alternative asset environment for asset + # path generation. The environment should already have been mounted + # at the prefix returned by +asset_prefix+. + def asset_environment + Rails.application.assets + end + + class AssetPaths < ::ActionView::AssetPaths #:nodoc: + attr_accessor :asset_environment, :asset_prefix, :asset_digests, :compile_assets, :digest_assets + + class AssetNotPrecompiledError < StandardError; end + + # Return the filesystem path for the source + def compute_source_path(source, ext) + asset_for(source, ext) + end + + def asset_for(source, ext) + source = source.to_s + return nil if is_uri?(source) + source = rewrite_extension(source, nil, ext) + asset_environment[source] + rescue Sprockets::FileOutsidePaths + nil + end + + def digest_for(logical_path) + if digest_assets && asset_digests && (digest = asset_digests[logical_path]) + return digest + end + + if compile_assets + if digest_assets && asset = asset_environment[logical_path] + return asset.digest_path + end + return logical_path + else + raise AssetNotPrecompiledError.new("#{logical_path} isn't precompiled") + end + end + + def rewrite_asset_path(source, dir, options = {}) + if source[0] == ?/ + source + else + source = digest_for(source) unless options[:digest] == false + source = File.join(dir, source) + source = "/#{source}" unless source =~ /^\// + source + end + end + + def rewrite_extension(source, dir, ext) + if ext && File.extname(source).empty? + "#{source}.#{ext}" + else + source + end + end + end + end + end +end diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 8cee3babe2..3d330bd91a 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -1,100 +1,61 @@ +require "action_controller/railtie" + module Sprockets - class Railtie < Rails::Railtie - def self.using_coffee? - require 'coffee-script' - defined?(CoffeeScript) - rescue LoadError - false + autoload :Bootstrap, "sprockets/bootstrap" + autoload :Helpers, "sprockets/helpers" + autoload :Compressors, "sprockets/compressors" + autoload :LazyCompressor, "sprockets/compressors" + autoload :NullCompressor, "sprockets/compressors" + autoload :StaticCompiler, "sprockets/static_compiler" + + # TODO: Get rid of config.assets.enabled + class Railtie < ::Rails::Railtie + config.action_controller.default_asset_host_protocol = :relative + + rake_tasks do + load "sprockets/assets.rake" end - def self.using_scss? - require 'sass' - defined?(Sass) - rescue LoadError - false - end + initializer "sprockets.environment", :group => :all do |app| + config = app.config + next unless config.assets.enabled - config.app_generators.javascript_engine :coffee if using_coffee? - config.app_generators.stylesheet_engine :scss if using_scss? + require 'sprockets' - # Configure ActionController to use sprockets. - initializer "sprockets.set_configs", :after => "action_controller.set_configs" do |app| - ActiveSupport.on_load(:action_controller) do - self.use_sprockets = app.config.assets.enabled - end - end + app.assets = Sprockets::Environment.new(app.root.to_s) do |env| + env.logger = ::Rails.logger + env.version = ::Rails.env + "-#{config.assets.version}" - # We need to configure this after initialization to ensure we collect - # paths from all engines. This hook is invoked exactly before routes - # are compiled. - config.after_initialize do |app| - assets = app.config.assets - next unless assets.enabled - - app.assets = asset_environment(app) - - ActiveSupport.on_load(:action_view) do - app.assets.context_class.instance_eval do - include ::ActionView::Helpers::SprocketsHelper + if config.assets.cache_store != false + env.cache = ActiveSupport::Cache.lookup_store(config.assets.cache_store) || ::Rails.cache end end - app.routes.prepend do - mount app.assets => assets.prefix + if config.assets.manifest + path = File.join(config.assets.manifest, "manifest.yml") + else + path = File.join(Rails.public_path, config.assets.prefix, "manifest.yml") end - if config.action_controller.perform_caching - app.assets = app.assets.index + if File.exist?(path) + config.assets.digests = YAML.load_file(path) end - end - protected - - def asset_environment(app) - require "sprockets" - assets = app.config.assets - env = Sprockets::Environment.new(app.root.to_s) - env.static_root = File.join(app.root.join("public"), assets.prefix) - env.paths.concat assets.paths - env.logger = Rails.logger - env.js_compressor = expand_js_compressor(assets.js_compressor) - env.css_compressor = expand_css_compressor(assets.css_compressor) - env - end - - def expand_js_compressor(sym) - case sym - when :closure - require 'closure-compiler' - Closure::Compiler.new - when :uglifier - require 'uglifier' - Uglifier.new - when :yui - require 'yui/compressor' - YUI::JavaScriptCompressor.new - else - sym + ActiveSupport.on_load(:action_view) do + include ::Sprockets::Helpers::RailsHelper + app.assets.context_class.instance_eval do + include ::Sprockets::Helpers::IsolatedHelper + include ::Sprockets::Helpers::RailsHelper + end end end - def expand_css_compressor(sym) - case sym - when :scss - require 'sass' - compressor = Object.new - def compressor.compress(source) - Sass::Engine.new(source, - :syntax => :scss, :style => :compressed - ).render - end - compressor - when :yui - require 'yui/compressor' - YUI::CssCompressor.new - else - sym - end + # We need to configure this after initialization to ensure we collect + # paths from all engines. This hook is invoked exactly before routes + # are compiled, and so that other Railties have an opportunity to + # register compressors. + config.after_initialize do |app| + Sprockets::Bootstrap.new(app).run end end end diff --git a/actionpack/lib/sprockets/static_compiler.rb b/actionpack/lib/sprockets/static_compiler.rb new file mode 100644 index 0000000000..32a9d66e6e --- /dev/null +++ b/actionpack/lib/sprockets/static_compiler.rb @@ -0,0 +1,61 @@ +require 'fileutils' + +module Sprockets + class StaticCompiler + attr_accessor :env, :target, :paths + + def initialize(env, target, paths, options = {}) + @env = env + @target = target + @paths = paths + @digest = options.key?(:digest) ? options.delete(:digest) : true + @manifest = options.key?(:manifest) ? options.delete(:manifest) : true + @manifest_path = options.delete(:manifest_path) || target + end + + def compile + manifest = {} + env.each_logical_path do |logical_path| + next unless compile_path?(logical_path) + if asset = env.find_asset(logical_path) + manifest[logical_path] = write_asset(asset) + end + end + write_manifest(manifest) if @manifest + end + + def write_manifest(manifest) + FileUtils.mkdir_p(@manifest_path) + File.open("#{@manifest_path}/manifest.yml", 'wb') do |f| + YAML.dump(manifest, f) + end + end + + def write_asset(asset) + path_for(asset).tap do |path| + filename = File.join(target, path) + FileUtils.mkdir_p File.dirname(filename) + asset.write_to(filename) + asset.write_to("#{filename}.gz") if filename.to_s =~ /\.(css|js)$/ + end + end + + def compile_path?(logical_path) + paths.each do |path| + case path + when Regexp + return true if path.match(logical_path) + when Proc + return true if path.call(logical_path) + else + return true if File.fnmatch(path.to_s, logical_path) + end + end + false + end + + def path_for(asset) + @digest ? asset.digest_path : asset.logical_path + end + end +end |