lash 0.1.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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