Changeset 1136

Show
Ignore:
Timestamp:
12/28/07 21:18:32 (1 year ago)
Author:
coda.ha..@gmail.com
Message:

Added asset bundling for Javascript and stylesheet files with the :bundle option for js_include_tag, css_include_tag, include_required_js, and include_required_css.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/CHANGELOG

    r1107 r1136  
    11== 0.5.0 "The big cleanup." 2008-01-31 
     2* Added asset bundling for Javascript and stylesheet files 
    23 
    34 
  • trunk/app_generators/merb/templates/config/merb.yml

    r1087 r1136  
    3232# automatically in production mode. 
    3333#:cache_templates: true 
     34 
     35# Uncomment to bundle assets in dev mode. Assets are automatically bundled in 
     36# production mode. 
     37#:bundle_assets: true 
    3438 
    3539# this is true if you want mongrel to emulate the X-Sendfile header internally, 
  • trunk/lib/merb.rb

    r1087 r1136  
    6666  autoload :Plugins, 'merb/plugins' 
    6767  autoload :Rack,'merb/rack_adapter' 
     68  autoload :Assets, 'merb/assets' 
    6869 
    6970  # Set up Merb::Server.config[] as an accessor for @@merb_opts 
  • trunk/lib/merb/mixins/view_context.rb

    r1090 r1136  
    33  # linking to assets and other pages, dealing with JavaScript, and caching. 
    44  module ViewContextMixin 
     5     
     6    include Merb::Assets::AssetHelpers 
     7     
    58    # :section: Accessing Assets 
    69    # Merb provides views with convenience methods for links images and other assets. 
     
    163166    # See each method's documentation for more information. 
    164167     
     168    # :section: Bundling Asset Files 
     169    #  
     170    # The key to making a fast web application is to reduce both the amount of 
     171    # data transfered and the number of client-server interactions. While having 
     172    # many small, module Javascript or stylesheet files aids in the development 
     173    # process, your web application will benefit from bundling those assets in 
     174    # the production environment. 
     175    #  
     176    # An asset bundle is a set of asset files which are combined into a single 
     177    # file. This reduces the number of requests required to render a page, and 
     178    # can reduce the amount of data transfer required if you're using gzip 
     179    # encoding. 
     180    #  
     181    # Asset bundling is always enabled in production mode, and can be optionally 
     182    # enabled in all environments by setting the <tt>:bundle_assets</tt> value 
     183    # in <tt>config/merb.yml</tt> to +true+. 
     184    #  
     185    # ==== Examples 
     186    #  
     187    # In the development environment, this: 
     188    #  
     189    #   js_include_tag :prototype, :lowpro, :bundle => true 
     190    #  
     191    # will produce two <script> elements. In the production mode, however, the 
     192    # two files will be concatenated in the order given into a single file, 
     193    # <tt>all.js</tt>, in the <tt>public/javascripts</tt> directory. 
     194    #  
     195    # To specify a different bundle name: 
     196    #  
     197    #   css_include_tag :typography, :whitespace, :bundle => :base 
     198    #   css_include_tag :header, :footer, :bundle => "content" 
     199    #   css_include_tag :lightbox, :images, :bundle => "lb.css" 
     200    #  
     201    # (<tt>base.css</tt>, <tt>content.css</tt>, and <tt>lb.css</tt> will all be 
     202    # created in the <tt>public/stylesheets</tt> directory.) 
     203    #  
     204    # == Callbacks 
     205    #  
     206    # To use a Javascript or CSS compressor, like JSMin or YUI Compressor: 
     207    #  
     208    #   Merb::Assets::JavascriptAssetBundler.add_callback do |filename| 
     209    #     system("/usr/local/bin/yui-compress #{filename}") 
     210    #   end 
     211    #    
     212    #   Merb::Assets::StylesheetAssetBundler.add_callback do |filename| 
     213    #     system("/usr/local/bin/css-min #{filename}") 
     214    #   end 
     215    #  
     216    # These blocks will be run after a bundle is created. 
     217    #  
     218    # == Bundling Required Assets 
     219    #  
     220    # Combining the +require_css+ and +require_js+ helpers with bundling can be 
     221    # problematic. You may want to separate out the common assets for your 
     222    # application -- Javascript frameworks, common CSS, etc. -- and bundle those 
     223    # in a "base" bundle. Then, for each section of your site, bundle the 
     224    # required assets into a section-specific bundle. 
     225    #  
     226    # <b>N.B.: If you bundle an inconsistent set of assets with the same name, 
     227    # you will have inconsistent results. Be thorough and test often.</b> 
     228    #  
     229    # ==== Example 
     230    #  
     231    # In your application layout: 
     232    #  
     233    #   js_include_tag :prototype, :lowpro, :bundle => :base 
     234    #  
     235    # In your controller layout: 
     236    #  
     237    #   require_js :bundle => :posts 
     238     
    165239    # The require_js method can be used to require any JavaScript 
    166240    # file anywhere in your templates. Regardless of how many times 
     
    183257    end 
    184258     
    185     # The require_ccs method can be used to require any CSS 
     259    # The require_css method can be used to require any CSS 
    186260    # file anywhere in your templates. Regardless of how many times 
    187261    # a single stylesheet is included with require_css, Merb will only include 
     
    205279    # A method used in the layout of an application to create +<script>+ tags to include JavaScripts required in  
    206280    # in templates and subtemplates using require_js. 
    207     # 
     281    #  
     282    # ==== Options 
     283    # bundle::  The name of the bundle the scripts should be combined into. 
     284    #           If +nil+ or +false+, the bundle is not created. If +true+, a 
     285    #           bundle named <tt>all.js</tt> is created. Otherwise, 
     286    #           <tt>:bundle</tt> is treated as an asset name. 
     287    #  
    208288    # ==== Examples 
    209289    #   # my_action.herb has a call to require_js 'jquery' 
     
    219299    #   #    <script src="/javascripts/validation.js" type="text/javascript"></script> 
    220300    # 
    221     def include_required_js 
     301    def include_required_js(options = {}) 
    222302      return '' if @required_js.nil? 
    223       js_include_tag(*@required_js
     303      js_include_tag(*(@required_js + [options])
    224304    end 
    225305     
    226306    # A method used in the layout of an application to create +<link>+ tags for CSS stylesheets required in  
    227307    # in templates and subtemplates using require_css. 
    228     # 
     308    #  
     309    # ==== Options 
     310    # bundle::  The name of the bundle the stylesheets should be combined into. 
     311    #           If +nil+ or +false+, the bundle is not created. If +true+, a 
     312    #           bundle named <tt>all.css</tt> is created. Otherwise, 
     313    #           <tt>:bundle</tt> is treated as an asset name. 
     314    #  
    229315    # ==== Examples 
    230316    #   # my_action.herb has a call to require_css 'style' 
     
    239325    #   #    <link href="/stylesheets/ie-specific.css" media="all" rel="Stylesheet" type="text/css"/> 
    240326    # 
    241     def include_required_css 
     327    def include_required_css(options = {}) 
    242328      return '' if @required_css.nil? 
    243       css_include_tag(*@required_css
     329      css_include_tag(*(@required_css + [options])
    244330    end 
    245331     
     
    247333    # +<include>+ tag for each script named in the arguments, appending 
    248334    # '.js' if it is left out of the call. 
    249     # 
     335    #  
     336    # ==== Options 
     337    # bundle::  The name of the bundle the scripts should be combined into. 
     338    #           If +nil+ or +false+, the bundle is not created. If +true+, a 
     339    #           bundle named <tt>all.js</tt> is created. Otherwise, 
     340    #           <tt>:bundle</tt> is treated as an asset name. 
     341    #  
    250342    # ==== Examples 
    251343    #   js_include_tag 'jquery' 
    252     #   # => <script src="/javascripts/jquery.js" type="text/javascript"></script
     344    #   # => <script src="/javascripts/jquery.js" type="text/javascript" /
    253345    # 
    254346    #   js_include_tag 'moofx.js', 'upload' 
    255     #   # => <script src="/javascripts/moofx.js" type="text/javascript"></script
    256     #   #    <script src="/javascripts/upload.js" type="text/javascript"></script
     347    #   # => <script src="/javascripts/moofx.js" type="text/javascript" /
     348    #   #    <script src="/javascripts/upload.js" type="text/javascript" /
    257349    # 
    258350    #   js_include_tag :effects 
    259     #   # => <script src="/javascripts/effects.js" type="text/javascript"></script
     351    #   # => <script src="/javascripts/effects.js" type="text/javascript" /
    260352    # 
    261353    #   js_include_tag :jquery, :validation 
    262     #   # => <script src="/javascripts/jquery.js" type="text/javascript"></script
    263     #   #    <script src="/javascripts/validation.js" type="text/javascript"></script
     354    #   # => <script src="/javascripts/jquery.js" type="text/javascript" /
     355    #   #    <script src="/javascripts/validation.js" type="text/javascript" /
    264356    # 
    265357    def js_include_tag(*scripts) 
     358      options = scripts.last.is_a?(Hash) ? scripts.pop : {} 
    266359      return nil if scripts.empty? 
    267       include_tag = "" 
    268       scripts.each do |script| 
    269         script = script.to_s 
    270         url = "/javascripts/#{script =~ /\.js$/ ? script : script + '.js'}" 
    271         url = Merb::Server.config[:path_prefix] + url if Merb::Server.config[:path_prefix] 
    272         include_tag << %Q|<script src="#{url}" type="text/javascript">//</script>\n| 
     360       
     361      if (bundle_name = options[:bundle]) && Merb::Assets.bundle? && scripts.size > 1 
     362        bundler = Merb::Assets::JavascriptAssetBundler.new(bundle_name, *scripts) 
     363        bundled_asset = bundler.bundle! 
     364        return js_include_tag(bundled_asset) 
    273365      end 
    274       include_tag 
     366 
     367      tags = "" 
     368 
     369      for script in scripts 
     370        attrs = { 
     371          :src => asset_path(:javascript, script), 
     372          :type => "text/javascript" 
     373        } 
     374        tags << %Q{<script #{attrs.to_xml_attributes} />} 
     375      end 
     376 
     377      return tags 
    275378    end 
    276379     
     
    278381    # +<link>+ tag for each stylesheet named in the arguments, appending 
    279382    # '.css' if it is left out of the call. 
     383    #  
     384    # ==== Options 
     385    # bundle::  The name of the bundle the stylesheets should be combined into. 
     386    #           If +nil+ or +false+, the bundle is not created. If +true+, a 
     387    #           bundle named <tt>all.css</tt> is created. Otherwise, 
     388    #           <tt>:bundle</tt> is treated as an asset name. 
     389    # media::   The media attribute for the generated link element. Defaults 
     390    #           to <tt>:all</tt>. 
    280391    # 
    281392    # ==== Examples 
    282393    #   css_include_tag 'style' 
    283     #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css"/> 
     394    #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css" /> 
    284395    # 
    285396    #   css_include_tag 'style.css', 'layout' 
    286     #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css"/> 
    287     #   #    <link href="/stylesheets/layout.css" media="all" rel="Stylesheet" type="text/css"/> 
     397    #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css" /> 
     398    #   #    <link href="/stylesheets/layout.css" media="all" rel="Stylesheet" type="text/css" /> 
    288399    # 
    289400    #   css_include_tag :menu 
    290     #   # => <link href="/stylesheets/menu.css" media="all" rel="Stylesheet" type="text/css"/> 
     401    #   # => <link href="/stylesheets/menu.css" media="all" rel="Stylesheet" type="text/css" /> 
    291402    # 
    292403    #   css_include_tag :style, :screen 
    293     #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css"/> 
    294     #   #    <link href="/stylesheets/screen.css" media="all" rel="Stylesheet" type="text/css"/> 
    295     # 
    296     def css_include_tag(*scripts) 
    297       return nil if scripts.empty? 
    298       include_tag = "" 
    299       scripts.each do |script| 
    300         script = script.to_s 
    301         url = "/stylesheets/#{script =~ /\.css$/ ? script : script + '.css'}" 
    302         url = Merb::Server.config[:path_prefix] + url if Merb::Server.config[:path_prefix] 
    303         include_tag << %Q|<link href="#{url}" media="all" rel="Stylesheet" type="text/css"/>\n| 
     404    #   # => <link href="/stylesheets/style.css" media="all" rel="Stylesheet" type="text/css" /> 
     405    #   #    <link href="/stylesheets/screen.css" media="all" rel="Stylesheet" type="text/css" /> 
     406    #  
     407    #  css_include_tag :style, :media => :print 
     408    #  # => <link href="/stylesheets/style.css" media="print" rel="Stylesheet" type="text/css" /> 
     409    def css_include_tag(*stylesheets) 
     410      options = stylesheets.last.is_a?(Hash) ? stylesheets.pop : {} 
     411      return nil if stylesheets.empty? 
     412       
     413      if (bundle_name = options[:bundle]) && Merb::Assets.bundle? && stylesheets.size > 1 
     414        bundler = Merb::Assets::StylesheetAssetBundler.new(bundle_name, *stylesheets) 
     415        bundled_asset = bundler.bundle! 
     416        return css_include_tag(bundled_asset) 
    304417      end 
    305       include_tag 
     418 
     419      tags = "" 
     420 
     421      for stylesheet in stylesheets 
     422        attrs = { 
     423          :href => asset_path(:stylesheet, stylesheet), 
     424          :type => "text/css", 
     425          :rel => "Stylesheet", 
     426          :media => options[:media] || :all 
     427        } 
     428        tags << %Q{<link #{attrs.to_xml_attributes} />} 
     429      end 
     430 
     431      return tags 
    306432    end 
    307433     
  • trunk/spec/merb/view_context_spec.rb

    r1118 r1136  
    5757                                :media => "all", 
    5858                                :rel => "Stylesheet", 
    59                                 :type => "text/css") 
     59                                :type => "text/css", 
     60                                :content => nil) 
    6061 
    6162    css_include_tag('foo').should == css_include_tag('foo.css') 
     
    6364      css_include_tag('foo') + css_include_tag('bar') 
    6465  end 
     66   
     67  it "should alter the 'media' attribute based on the provided options" do 
     68    tag = css_include_tag('foo.css', :media => :screen) 
     69    tag.should match_tag(:link, :href => "/stylesheets/foo.css", 
     70                                :media => "screen", 
     71                                :rel => "Stylesheet", 
     72                                :type => "text/css", 
     73                                :content => nil) 
     74  end 
    6575 
    6676  it "should render a link tag with a path_prefix" do 
     
    7181                                :media => "all", 
    7282                                :rel => "Stylesheet", 
    73                                 :type => "text/css") 
     83                                :type => "text/css", 
     84                                :content => nil) 
    7485 
    7586    Merb::Server.config.delete(:path_prefix) 
     
    8798end 
    8899 
     100describe "View Context", "css tag with bundles" do 
     101 
     102  include Merb::ViewContextMixin 
     103   
     104  before(:each) do 
     105    @bundler = mock(:bundler) 
     106    Merb::Assets.stub!(:bundle?).and_return(true) 
     107    Merb::Assets::StylesheetAssetBundler.stub!(:new).and_return(@bundler) 
     108    @bundler.stub!(:bundle!).and_return(:all) 
     109  end 
     110   
     111  it "should not bundle stylesheets if asset bundling is disabled" do 
     112    Merb::Assets.should_receive(:bundle?).and_return(false) 
     113     
     114    tag = css_include_tag(:fonts, :colors, :bundle => true) 
     115    tag.should == css_include_tag(:fonts) + css_include_tag(:colors) 
     116  end 
     117   
     118  it "should not bundle stylesheets if only a single stylesheet was provided" do 
     119    css_include_tag(:fonts, :bundle => true).should == css_include_tag(:fonts) 
     120  end 
     121   
     122  it "should bundle all stylesheets as 'all.css' if no bundle name is provided" do 
     123    Merb::Assets.should_receive(:bundle?).and_return(true) 
     124    Merb::Assets::StylesheetAssetBundler.should_receive(:new).with(true, :fonts, :colors).and_return(@bundler) 
     125    @bundler.should_receive(:bundle!).and_return(:all) 
     126     
     127    tag = css_include_tag(:fonts, :colors, :bundle => true) 
     128    tag.should match_tag(:link, :href => "/stylesheets/all.css", 
     129                                :media => "all", 
     130                                :rel => "Stylesheet", 
     131                                :type => "text/css", 
     132                                :content => nil) 
     133  end 
     134   
     135  it "should bundle all stylesheets whatever bundle name is provided" do 
     136    Merb::Assets.should_receive(:bundle?).and_return(true) 
     137    Merb::Assets::StylesheetAssetBundler.should_receive(:new).with(:base, :fonts, :colors).and_return(@bundler) 
     138    @bundler.should_receive(:bundle!).and_return(:base) 
     139     
     140    tag = css_include_tag(:fonts, :colors, :bundle => :base) 
     141    tag.should match_tag(:link, :href => "/stylesheets/base.css", 
     142                                :media => "all", 
     143                                :rel => "Stylesheet", 
     144                                :type => "text/css", 
     145                                :content => nil) 
     146  end 
     147   
     148  it "should not generate a stylesheet tag with the include_required_css" do 
     149    include_required_css(:bundle => true).clean.should == '' 
     150  end 
     151 
     152  it "should generate stylesheet tags with the include_required_css" do 
     153    Merb::Assets.should_receive(:bundle?).and_return(true) 
     154    Merb::Assets::StylesheetAssetBundler.should_receive(:new).with(true, :fonts, :colors).and_return(@bundler) 
     155    @bundler.should_receive(:bundle!).and_return(:all) 
     156     
     157    require_css(:fonts) 
     158    require_css(:colors) 
     159    tag = include_required_css(:bundle => true) 
     160     
     161    tag.should match_tag(:link, :href => "/stylesheets/all.css", 
     162                                :media => "all", 
     163                                :rel => "Stylesheet", 
     164                                :type => "text/css", 
     165                                :content => nil) 
     166  end 
     167end 
     168 
    89169describe "View Context", "script tag" do 
    90170 
     
    95175    tag.should match_tag(:script, :src => "/javascripts/foo.js", 
    96176                                  :type => "text/javascript", 
    97                                   :content => '//'
     177                                  :content => nil
    98178 
    99179    js_include_tag('foo').should == js_include_tag('foo.js') 
     
    108188    tag.should match_tag(:script, :src => "/inky/javascripts/foo.js", 
    109189                                  :type => "text/javascript", 
    110                                   :content => '//'
     190                                  :content => nil
    111191 
    112192    Merb::Server.config.delete(:path_prefix) 
     
    124204end 
    125205 
     206describe "View Context", "script tag with bundles" do 
     207 
     208  include Merb::ViewContextMixin 
     209   
     210  before(:each) do 
     211    @bundler = mock(:bundler) 
     212    Merb::Assets.stub!(:bundle?).and_return(true) 
     213    Merb::Assets::JavascriptAssetBundler.stub!(:new).and_return(@bundler) 
     214    @bundler.stub!(:bundle!).and_return(:all) 
     215  end 
     216   
     217  it "should not bundle scripts if asset bundling is disabled" do 
     218    Merb::Assets.should_receive(:bundle?).and_return(false) 
     219     
     220    tag = js_include_tag(:prototype, :lowpro, :bundle => true) 
     221    tag.should == js_include_tag(:prototype) + js_include_tag(:lowpro) 
     222  end 
     223   
     224  it "should not bundle scripts if only a single script was provided" do 
     225    css_include_tag(:prototype, :bundle => true).should == css_include_tag(:prototype) 
     226  end 
     227   
     228  it "should bundle all scripts as 'all.js' if no bundle name is provided" do 
     229    Merb::Assets.should_receive(:bundle?).and_return(true) 
     230    Merb::Assets::JavascriptAssetBundler.should_receive(:new).with(true, :prototype, :lowpro).and_return(@bundler) 
     231    @bundler.should_receive(:bundle!).and_return(:all) 
     232     
     233    tag = js_include_tag(:prototype, :lowpro, :bundle => true) 
     234    tag.should match_tag(:script, :src => "/javascripts/all.js", 
     235                                  :type => "text/javascript", 
     236                                  :content => nil) 
     237  end 
     238   
     239  it "should bundle all stylesheets whatever bundle name is provided" do 
     240    Merb::Assets.should_receive(:bundle?).and_return(true) 
     241    Merb::Assets::JavascriptAssetBundler.should_receive(:new).with(:base, :prototype, :lowpro).and_return(@bundler) 
     242    @bundler.should_receive(:bundle!).and_return(:base) 
     243 
     244    tag = js_include_tag(:prototype, :lowpro, :bundle => :base) 
     245    tag.should match_tag(:script, :src => "/javascripts/base.js", 
     246                                  :type => "text/javascript", 
     247                                  :content => nil) 
     248  end 
     249   
     250  it "should not generate a script tag with the include_required_js" do 
     251    include_required_js(:bundle => true).clean.should == '' 
     252  end 
     253 
     254  it "should generate script tags with the include_required_js" do 
     255    Merb::Assets.should_receive(:bundle?).and_return(true) 
     256    Merb::Assets::JavascriptAssetBundler.should_receive(:new).with(true, :prototype, :lowpro).and_return(@bundler) 
     257    @bundler.should_receive(:bundle!).and_return(:all) 
     258     
     259    require_js(:prototype) 
     260    require_js(:lowpro) 
     261     
     262    tag = include_required_js(:bundle => true) 
     263    tag.should match_tag(:script, :src => "/javascripts/all.js", 
     264                                  :type => "text/javascript", 
     265                                  :content => nil) 
     266  end 
     267end 
     268 
    126269describe "View Context", "throw_content, catch_content" do 
    127270