lash 0.1.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.autotest +10 -0
  2. data/.gitignore +2 -1
  3. data/.rspec +1 -0
  4. data/Gemfile +2 -1
  5. data/LICENSE.md +48 -0
  6. data/README.md +117 -5
  7. data/Rakefile +1 -0
  8. data/lib/lash.rb +17 -4
  9. data/lib/lash/assets_host.rb +74 -0
  10. data/lib/lash/bundle_helper.rb +6 -24
  11. data/lib/lash/capistrano.rb +2 -0
  12. data/lib/lash/closure_minifier.rb +20 -0
  13. data/lib/lash/files.rb +56 -0
  14. data/lib/lash/java_script_bundler.rb +117 -0
  15. data/lib/lash/java_script_minifier.rb +67 -0
  16. data/lib/lash/railtie.rb +4 -33
  17. data/lib/lash/sprite_bundler.rb +132 -0
  18. data/lib/lash/tasks/lash.rake +50 -181
  19. data/lib/lash/version.rb +1 -1
  20. data/spec/lib/assets_host_spec.rb +56 -0
  21. data/spec/lib/bundle_helper_spec.rb +99 -0
  22. data/spec/lib/closure_minifier_spec.rb +67 -0
  23. data/spec/lib/java_script_bundler_spec.rb +46 -0
  24. data/spec/lib/lash_files_spec.rb +68 -0
  25. data/spec/spec_helper.rb +15 -25
  26. data/spec/support/rails_fake.rb +31 -0
  27. data/spec/test_app/.gitignore +4 -0
  28. data/spec/test_app/Rakefile +8 -0
  29. data/spec/test_app/public/images/rails.png +0 -0
  30. data/spec/test_app/public/images/ui-sprite.png +0 -0
  31. data/spec/test_app/public/images/ui-sprite.png.sprite +3 -0
  32. data/spec/test_app/public/javascripts/application/README +0 -0
  33. data/spec/test_app/public/javascripts/application/application.bundleVersion.js +1 -0
  34. data/spec/test_app/public/javascripts/application/application.js +3 -0
  35. data/spec/test_app/public/javascripts/application/application2.js +3 -0
  36. data/spec/test_app/public/javascripts/application/tools/validate.js +3 -0
  37. data/spec/test_app/public/javascripts/bundle_application.js +1 -0
  38. data/spec/test_app/public/javascripts/bundle_application.js.gz +0 -0
  39. data/spec/test_app/public/javascripts/cdn/jquery.js +1 -0
  40. data/spec/test_app/public/javascripts/cdn/jquery.js.gz +0 -0
  41. data/spec/test_app/public/javascripts/cdn/jquery.min.js +1 -0
  42. data/spec/test_app/public/javascripts/cdn/jquery.min.js.gz +0 -0
  43. data/spec/test_app/public/javascripts/demand/huge.js +0 -0
  44. data/spec/test_app/public/javascripts/demand/huge.js.gz +0 -0
  45. data/spec/test_app/public/javascripts/demand/huge.min.js +1 -0
  46. data/spec/test_app/public/javascripts/demand/huge.min.js.gz +0 -0
  47. data/spec/test_app/public/sprites/ui/accept.png +0 -0
  48. data/spec/test_app/public/sprites/ui/add.png +0 -0
  49. data/spec/test_app/public/stylesheets/.gitkeep +0 -0
  50. data/spec/test_app/public/stylesheets/sass/_ui-sprite.scss +20 -0
  51. data/spec/test_app/public/stylesheets/sass/_version.scss +1 -0
  52. metadata +81 -10
@@ -0,0 +1,10 @@
1
+ Autotest.add_hook(:initialize) do |at|
2
+ at.clear_mappings
3
+ at.add_mapping(%r%^lib/(.*)\.rb$%) { |_, m|
4
+ ["spec/lib/#{m[1]}_spec.rb"]
5
+ }
6
+ at.add_mapping(%r%^spec/(models|controllers|routing|views|helpers|mailers|requests|lib)/.*rb$%) { |filename, _|
7
+ filename
8
+ }
9
+ nil
10
+ end
data/.gitignore CHANGED
@@ -4,4 +4,5 @@ Gemfile.lock
4
4
  pkg/*
5
5
  .yardoc
6
6
  doc/yard/
7
- .DS_Store
7
+ .DS_Store
8
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --debug
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in lash.gemspec
4
- gemspec
4
+ gemspec
5
+
@@ -0,0 +1,48 @@
1
+ # License Notices
2
+
3
+ ## Lash
4
+
5
+ Copyright (C) 2011 Apps In Your Pants.
6
+
7
+ Dual licensed under MIT and GPLv3
8
+
9
+
10
+ ## Google Closure Compiler
11
+
12
+ Copyright 2009 The Closure Compiler Authors.
13
+
14
+ Licensed under the Apache License, Version 2.0 (the "License");
15
+ you may not use this file except in compliance with the License.
16
+ You may obtain a copy of the License at
17
+
18
+ http://www.apache.org/licenses/LICENSE-2.0
19
+
20
+ Unless required by applicable law or agreed to in writing, software
21
+ distributed under the License is distributed on an "AS IS" BASIS,
22
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
+ See the License for the specific language governing permissions and
24
+ limitations under the License.
25
+
26
+
27
+
28
+ ## Optipng
29
+
30
+ Copyright (C) 2001-2011 Cosmin Truta.
31
+
32
+ This software is provided 'as-is', without any express or implied
33
+ warranty. In no event will the author(s) be held liable for any damages
34
+ arising from the use of this software.
35
+
36
+ Permission is granted to anyone to use this software for any purpose,
37
+ including commercial applications, and to alter it and redistribute it
38
+ freely, subject to the following restrictions:
39
+
40
+ 1. The origin of this software must not be misrepresented; you must not
41
+ claim that you wrote the original software. If you use this software
42
+ in a product, an acknowledgment in the product documentation would be
43
+ appreciated but is not required.
44
+
45
+ 2. Altered source versions must be plainly marked as such, and must not
46
+ be misrepresented as being the original software.
47
+
48
+ 3. This notice may not be removed or altered from any source distribution.
data/README.md CHANGED
@@ -15,10 +15,10 @@ Based on ["Optimizing asset bundling and serving with Rails"](https://github.com
15
15
 
16
16
  # Gemfile
17
17
  gem 'lash'
18
-
19
- or as a plugin
20
18
 
21
- rails plugin install git://github.com/appsinyourpants/lash.git
19
+
20
+
21
+
22
22
 
23
23
  ## Bundling Assets
24
24
 
@@ -29,6 +29,10 @@ Lash includes several rake tasks to bundle loose, development versions of your s
29
29
 
30
30
  Individual assets can be bundled on demand using their respective `lash:asset_type` tasks.
31
31
 
32
+
33
+
34
+
35
+
32
36
  ## JavaScript
33
37
 
34
38
  Lash expects to find JavaScript assets as subfolders of the public/javascripts folder like so
@@ -60,6 +64,19 @@ __public/javascripts/demand__
60
64
 
61
65
  ### CDN JavaScripts and Developer Mode
62
66
 
67
+ Shared CDN hosts are great for optimizing your site's user experience but there are two issues that developers regularly have to deal with - developing offline and accounting for CDN unavailability. Lash handles both of these cases with ease.
68
+
69
+ During development, Lash will use the copies of the libraries found in your public/javascripts/cdn directory. This makes it easy to work offline.
70
+
71
+ In production, Lash will use the CDN versions of the scripts and if a runtime test is provided, fallback to the local version if the CDN is unavailable.
72
+
73
+ <%= javascript_cdn 'jquery', '//ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js', 'jQuery' -%>
74
+
75
+ # => <script src="//ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js" type="text/javascript"></script>
76
+ # => <script type="text/javascript">
77
+ # => if(typeof jQuery === "undefined" )
78
+ # => document.write( unescape("<script src=\"/javascripts/cdn/jquery.min.js?1297459967\" type=\"text/javascript\"><\/script>") );
79
+ # => </script>
63
80
 
64
81
  ### Referencing JavaScript Bundles in Layouts
65
82
 
@@ -72,9 +89,15 @@ When {Lash::BundleHelper#bundle_files?} is true (on by default in production), l
72
89
 
73
90
  See {Lash::BundleHelper#javascript_bundle} for details.
74
91
 
92
+ ### application.bundleVersion.js
75
93
 
94
+ When referring to assets in your javascripts you loose the convenience of the rails cache busting asset tags. During bundling, lash will generate an `public/javascripts/application/application.bundleVersion.js` which gets bundled into the application script. This script simply declares a global variable `bundleVersion` which you can append to asset urls in your scripts.
76
95
 
77
- #### JavaScript bundling tasks
96
+ #### Example
97
+
98
+ $('#waiting-div').append( $('<img src="/images/wainting.gif?' + bundleVersion + '" />' ) )
99
+
100
+ ### JavaScript bundling tasks
78
101
 
79
102
  rake lash:js # Bundles and minifies javascripts
80
103
  rake lash:js_bundle # Bundles javascripts folders into single minified files
@@ -84,8 +107,97 @@ See {Lash::BundleHelper#javascript_bundle} for details.
84
107
 
85
108
 
86
109
 
110
+
111
+
87
112
  ## CSS Sprites
88
113
 
114
+ CSS sprites is are an effective optimization technique, but can be very time consuming to produce. Lash makes it easy. Just drop the desired images into a folder, run a rake task and you've got a highly optimized sprite image and accompanying CSS file.
115
+
116
+ Lash looks for sprite images in `public/sprites`. Each sub folder will be bundled into a single image and css file. Given the following folder structure:
117
+
118
+ * public/sprites/ui
119
+ * accept.png
120
+ * add.png
121
+
122
+ Lash will generate a sprite image `public/images/ui-sprite.png` and corresponding css file `public/stylesheets/sass/_ui-sprite.scss`.
123
+
124
+ #### To generate your apps sprites
125
+
126
+ rake lash:sprites
127
+
128
+
129
+ ### _version.scss
130
+
131
+ When referring to assets in your css scripts you loose the convenience of the rails cache busting asset tags. During bundling, lash will generate an `public/stylesheets/sass/_version.scss`. You can include this into any of your SASS scripts when you reference static assets like images.
132
+
133
+ #### Example
134
+
135
+ @import 'version';
136
+ .smiley { background-image: url(/images/smiley.png?#{$bundle-version})}
137
+
138
+ ### CSS bundling tasks
139
+
140
+ rake lash:css # Process CSS scripts
141
+ rake lash:css_gzip # Compresses stylesheets for use with nginx gzip_static
142
+ rake lash:sass # Pre-generate sass scripts
143
+ rake lash:sprites # Generate CSS sprites from the public/sprites folders
144
+
145
+
146
+
147
+
148
+
149
+ ## Optimizing PNG Images
150
+
151
+ Most image editors will compress PNG files using a very basic compression algorithm. However the PNG format
152
+ allows for much more aggressive optimization at the cost of speed. Since image resources are some of the
153
+ heaviest assets downloaded from your site, optimizing them is often worth the effort.
154
+
155
+ See [A guide to PNG optimization](http://optipng.sourceforge.net/pngtech/optipng.html) for a more detailed discussion.
156
+
157
+ #### To optimize your pngs
158
+
159
+ rake lash:png
160
+
161
+
162
+
163
+
164
+
89
165
  ## Integrating With Capistrano
90
166
 
91
-
167
+ Lash was designed to work with capistrano to easily run static asset bundling during the deployment process. This makes
168
+ sure that all assets are primed for static serving from your website without interfering with any existing requests
169
+ that are currently being served.
170
+
171
+ #### To run bundling tasks during deployment
172
+
173
+ # in config/deploy.rb
174
+ require 'lash/capistrano'
175
+
176
+ If you use capistrano to publish your app (and really who isn't?) you'll want to add some additional filters to your .gitignore file
177
+
178
+ # lash asset helpers
179
+ public/javascripts/common/application.bundleVersion.js
180
+ public/stylesheets/sass/_version.scss
181
+
182
+ # lash generated filed
183
+ public/javascripts/bundle_*.js
184
+ public/stylesheets/*.css
185
+ public/stylesheets/sass/_*-sprite.scss
186
+ public/javascripts/**/*.min.js
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+ ## License
196
+
197
+ ### Lash
198
+
199
+ Copyright (C) 2011 Apps In Your Pants.
200
+
201
+ Dual licensed under MIT and GPLv3
202
+
203
+ [Other License Notices](LICENSE.md)
data/Rakefile CHANGED
@@ -1,2 +1,3 @@
1
1
  require 'bundler'
2
2
  Bundler::GemHelper.install_tasks
3
+
@@ -3,17 +3,30 @@ module Lash
3
3
  # from the Lash::ViewHelpers.lash_options hash. You can write
4
4
  # to this hash to override default options on the global level:
5
5
  #
6
- # Lash::ViewHelpers.lash_options[:unicorns] = 'run free'
6
+ # Lash.lash_options[:unicorns] = 'run free'
7
7
  #
8
8
  def self.lash_options() @lash_options; end
9
+
9
10
  # Overrides the default {lash_options}
10
- def self.lash_options=(value) @lash_options = value; end
11
+ #
12
+ # @option options [String] :use_asset_servers Use asset server subdomain hack to allow multiple simultaenous connections. See {Lash::AssetsHost.resolve_static_asset_server_for_source}.
13
+ # @option options [String] :use_asset_servers_in_ssl Use asset server subdomain hack even over SSL. Requires a wildcard certificate.
14
+ # @option options [String] :use_sass Support SASS
15
+ # @option options [String] :use_git_asset_id Use GIT repro id for cachebusting asset id
16
+ # @option options [String] :closure_compiler Path to custom version of google's closure compiler.
17
+ def self.lash_options=(options) @lash_options = options; end
11
18
 
12
19
  self.lash_options = {
13
20
  :use_asset_servers => true,
14
21
  :use_asset_servers_in_ssl => true,
15
- :use_sass => Gem.available?('sass')
22
+ :use_sass => Gem.available?('sass'),
23
+ :use_git_asset_id => true,
24
+ :closure_compiler => File.expand_path( "../../bin/closure-compiler/compiler.jar", __FILE__ )
16
25
  }
17
26
  end
18
27
 
19
- require 'lash/railtie' if defined?(::Rails::Railtie)
28
+ require 'lash/railtie' if defined?(Rails) && Rails === Class and defined?(::Rails::Railtie)
29
+ require 'lash/java_script_bundler'
30
+ require 'lash/sprite_bundler'
31
+ require 'lash/files'
32
+ require 'lash/assets_host'
@@ -0,0 +1,74 @@
1
+ require 'grit'
2
+ require 'lash'
3
+
4
+ module Lash
5
+ module AssetsHost
6
+
7
+ # Configures Rails to use the most recent GIT repo id for public/(javascripts|stylesheets|images) for the
8
+ # asset id used for cache busting.
9
+ def self.use_git_asset_id
10
+ return unless Lash.lash_options[:use_git_asset_id]
11
+
12
+ repo = Grit::Repo.new( Rails.root.to_s )
13
+
14
+ ENV['RAILS_ASSET_ID'] = \
15
+ [:javascripts, :stylesheets, :images] \
16
+ .map { |folder| repo.log( 'master', "spec/test_app/public/#{folder}", :max_count => 1 ).first } \
17
+ .max_by { |log| log && log.committed_date }
18
+ .id
19
+ end
20
+
21
+ # Generates an asset id for the given file used for cache-busting
22
+ def self.asset_id( file )
23
+ if Lash.lash_options[:use_git_asset_id]
24
+ repo = Grit::Repo.new( Rails.root.to_s )
25
+ [:javascripts, :stylesheets, :images] \
26
+ .map { |folder| repo.log( 'master', "spec/test_app/public/#{folder}", :max_count => 1 ).first } \
27
+ .max_by { |log| log && log.committed_date }
28
+ .id
29
+ elsif ::File.exist?( file )
30
+ File.mtime( file ).to_i.to_s
31
+ end
32
+ end
33
+
34
+ # Method used to map an asset to a static asset server. This method simply generates a semi-random domain
35
+ # prefix based on the filename of the source. The asset server should resolve to the same server as
36
+ # the rails app. This is a basic browser hack to allow more than 4 connections to the server so that the
37
+ # browser can download multiple assets simultaneously.
38
+ #
39
+ # @example
40
+ # request.host # => lvh.me
41
+ # resolve_static_asset_server_for_source "smiles", request
42
+ # # => assets1.lvh.me
43
+ #
44
+ # # from terminal
45
+ # nslookup lvh.me # => 127.0.0.1
46
+ # nslookup assets1.lvh.me # => 127.0.0.1
47
+
48
+ def self.resolve_static_asset_server_for_source( source, request )
49
+ if /\/\// =~ source
50
+ nil
51
+ elsif request.ssl? and ! Lash.lash_options[:use_asset_servers_in_ssl]
52
+ nil
53
+ elsif !Lash.lash_options[:use_asset_servers]
54
+ nil
55
+ else
56
+
57
+ # Change the host name to include a randomized asset name at the same domain
58
+ # level. This is required so that HTTPS requests can use a wildcard domain
59
+ # without using subject alt name.
60
+
61
+ host = request.host_with_port
62
+ parts = host.split( /\./ )
63
+ if parts.length > 2
64
+ parts[0] = "#{parts[0]}-assets#{source.hash % 4}"
65
+ else
66
+ parts.unshift "assets#{source.hash % 4}"
67
+ end
68
+
69
+ "http#{'s' if request.ssl?}://#{parts.join('.')}"
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -1,20 +1,19 @@
1
- require 'find'
1
+ require 'lash/files'
2
2
 
3
3
  module Lash
4
4
  module BundleHelper
5
5
 
6
- unloadable if Rails.env.development?
7
-
8
6
  # Root folder where application javascript files can be found
9
7
  def javascript_root
10
8
  File.join( ::Rails.root, 'public', 'javascripts', '' )
11
9
  end
12
10
 
13
11
  # Determines if the processed bundle files should be used, or the loose files used to build those bundles.
14
- # By default bundled fies are used in production or if the request includes a `:bundle` param or cookie.
12
+ # By default bundled files are used in production or if the request includes a `:bundle` param or cookie.
13
+ # @return [Boolean] true if bundled assets should be used.
15
14
  def bundle_files?
16
15
  if params.has_key? :bundle
17
- return params[:bundle] =~ /^t(rue)?|y(es)?|1$/i
16
+ return /^t(rue)?|y(es)?|1$/i.match( params[:bundle] ) != nil
18
17
  end
19
18
  Rails.env.production? || cookies[:bundle] == "yes"
20
19
  end
@@ -71,28 +70,11 @@ module Lash
71
70
  end
72
71
 
73
72
  private
74
- # Collects an array of all files in `{basedir}` with the given `{ext}`.
75
- def recursive_file_list( basedir, ext )
76
- files = []
77
- return files unless File.exist? basedir
78
- Find.find( basedir ) do |path|
79
- if FileTest.directory?( path )
80
- if File.basename( path )[0] == ?. # Skip dot directories
81
- Find.prune
82
- else
83
- next
84
- end
85
- end
86
- files << path if File.extname( path ) == ext
87
- end
88
- files.sort
89
- end
90
-
91
73
  # Generates javscript include tags for the actual bundled javascript for each named bundle
92
74
  def javascript_include_bundles( bundles )
93
75
  output = ""
94
76
  bundles.each do |bundle|
95
- output << javascript_src_tag( "bundle_#{bundle}", {} )
77
+ output << javascript_src_tag( "bundle_#{bundle}.js", {} )
96
78
  end
97
79
  output.html_safe
98
80
  end
@@ -101,7 +83,7 @@ module Lash
101
83
  def javascript_include_files( bundles )
102
84
  output = "\n"
103
85
  bundles.each do |bundle|
104
- files = recursive_file_list( File.join( javascript_root, bundle ), '.js' )
86
+ files = Lash::Files.recursive_file_list( File.join( javascript_root, bundle ), '.js' )
105
87
  files.each do |file|
106
88
  file = file.gsub( javascript_root, '' )
107
89
  output << javascript_src_tag( file, {} ) + "\n"
@@ -1,3 +1,5 @@
1
+ # Adds a deployment task to automatically generate bundled assetes on the server during deployment
2
+
1
3
  Capistrano::Configuration.instance.load do
2
4
  namespace :deploy do
3
5
  task :lash, :roles => [:app], :except => { :no_release => true } do
@@ -0,0 +1,20 @@
1
+ require 'lash/java_script_minifier'
2
+
3
+ module Lash
4
+ # Minifies a collection of scripts using Google's closure compiler.
5
+ class ClosureMinifier < JavaScriptMinifier
6
+
7
+ private
8
+ def minify_scripts( files, target )
9
+ tmp = "#{target}.tmp"
10
+ `java -jar \"#{Lash.lash_options[:closure_compiler]}\" #{command_options} --warning_level QUIET --js \"#{files.join("\" --js \"")}\" --js_output_file \"#{tmp}\"`
11
+ if $?.exitstatus == 0
12
+ File.delete target if File.exist? target
13
+ File.rename tmp, target
14
+ end
15
+ ensure
16
+ File.delete tmp if File.exist? tmp
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ require 'find'
2
+
3
+ module Lash
4
+ module Files
5
+
6
+ # Collects an array of all files in {basedir} with the given ext.
7
+ #
8
+ # @param [String] basedir the root directory to search
9
+ # @param [String] ext extension to filter results by
10
+ # @return [Array] an array of expanded paths for each file in {basedir} and any of it's sub directories
11
+ def self.recursive_file_list( basedir, ext )
12
+ unless ext.is_a? Regexp
13
+ ext = ".#{ext}" if ext && ext[0] != ?.
14
+ ext ||= ""
15
+ end
16
+
17
+ files = []
18
+ return files unless File.exist? basedir
19
+ Find.find( basedir ) do |path|
20
+ if FileTest.directory?( path )
21
+ if File.basename( path )[0] == ?. # Skip dot directories
22
+ Find.prune
23
+ else
24
+ next
25
+ end
26
+ end
27
+ if ext.is_a? Regexp
28
+ files << path if ext.match( path )
29
+ else
30
+ files << path if File.extname( path ) == ext
31
+ end
32
+ end
33
+ files.sort
34
+ end
35
+
36
+ # Gets all the top level directories in the given base_path
37
+ #
38
+ # @param [String] basedir the root directory to search
39
+ # @return [Array] an array of expanded paths for all directories found
40
+ def self.get_top_level_directories( basedir )
41
+ Dir.entries( basedir ).collect do |path|
42
+ path = File.join( basedir, path )
43
+ File.basename( path )[0] == ?. || !File.directory?( path ) ? nil : path # not dot directories or files
44
+ end - [nil]
45
+ end
46
+
47
+ # Gets the relative path from root to path of path is a subdirectory of root, otherwise returns path
48
+ def self.relative_to( path, root )
49
+ path = File.expand_path( path )
50
+ root = File.expand_path( root )
51
+ return path unless path.start_with? root
52
+ path[ root.length + 1, path.length ]
53
+ end
54
+
55
+ end
56
+ end