Minify all JS and CSS via middleware. This means JS or CSS anywhere in the site is minified, not just in the Sprockets-controlled directories. I've also added inline CSS compression, and handling of several common "guards" that get wrapped around JS. Also, leave alone non-JS script tags (which may contain something like jQuery templates). Sprockets no longer has anything to do with minification after this.

This commit is contained in:
Ben Hollis 2012-04-07 20:00:24 -07:00
parent b1ba94cf22
commit 192047d242
16 changed files with 347 additions and 80 deletions

View file

@ -68,6 +68,10 @@ Then /^I should see '([^\']*)'$/ do |expected|
@browser.last_response.body.should include(expected) @browser.last_response.body.should include(expected)
end end
Then /^I should see:$/ do |expected|
@browser.last_response.body.should include(expected)
end
Then /^I should not see "([^\"]*)"$/ do |expected| Then /^I should not see "([^\"]*)"$/ do |expected|
@browser.last_response.body.should_not include(expected) @browser.last_response.body.should_not include(expected)
end end

View file

@ -59,8 +59,9 @@ module Middleman::Templates
return unless options[:bundler] return unless options[:bundler]
template "shared/Gemfile.tt", File.join(location, "Gemfile") template "shared/Gemfile.tt", File.join(location, "Gemfile")
say_status :run, "bundle install" inside(location) do
print `cd #{location} && "#{Gem.ruby}" -rubygems "#{Gem.bin_path('bundler', 'bundle')}" install` run('bundle install', :capture => true)
end
end end
end end
end end

View file

@ -1,6 +1,9 @@
Feature: Minify CSS Feature: Minify CSS
In order reduce bytes sent to client and appease YSlow In order reduce bytes sent to client and appease YSlow
Background:
Given current environment is "build"
Scenario: Rendering external css with the feature disabled Scenario: Rendering external css with the feature disabled
Given "minify_css" feature is "disabled" Given "minify_css" feature is "disabled"
And the Server is running at "minify-css-app" And the Server is running at "minify-css-app"
@ -14,8 +17,51 @@ Feature: Minify CSS
When I go to "/stylesheets/site.css" When I go to "/stylesheets/site.css"
Then I should see "1" lines Then I should see "1" lines
And I should see "only screen and (device-width" And I should see "only screen and (device-width"
When I go to "/more-css/site.css"
Then I should see "1" lines
Scenario: Rendering external css with passthrough compressor Scenario: Rendering external css with passthrough compressor
Given the Server is running at "passthrough-app" Given the Server is running at "passthrough-app"
When I go to "/stylesheets/site.css" When I go to "/stylesheets/site.css"
Then I should see "55" lines Then I should see "55" lines
Scenario: Rendering inline css with the feature disabled
Given "minify_css" feature is "disabled"
And the Server is running at "minify-css-app"
When I go to "/inline-css.html"
Then I should see:
"""
<style type='text/css'>
/*<![CDATA[*/
body {
test: style;
good: deal;
}
/*]]>*/
</style>
"""
Scenario: Rendering inline css with a passthrough minifier
Given the Server is running at "passthrough-app"
When I go to "/inline-css.html"
Then I should see:
"""
<style type='text/css'>
body {
test: style;
good: deal; }
</style>
"""
Scenario: Rendering inline css with the feature enabled
Given "minify_css" feature is "enabled"
And the Server is running at "minify-css-app"
When I go to "/inline-css.html"
Then I should see:
"""
<style type='text/css'>
/*<![CDATA[*/
body{test:style;good:deal}
/*]]>*/
</style>
"""

View file

@ -8,24 +8,106 @@ Feature: Minify Javascript
Given "minify_javascript" feature is "disabled" Given "minify_javascript" feature is "disabled"
And the Server is running at "minify-js-app" And the Server is running at "minify-js-app"
When I go to "/inline-js.html" When I go to "/inline-js.html"
Then I should see "10" lines Then I should see:
"""
<script type='text/javascript'>
//<![CDATA[
;(function() {
this;
should();
all.be();
on = { one: line };
})();
//]]>
</script>
<script>
;(function() {
this;
should();
too();
})();
</script>
<script type='text/javascript'>
//<!--
;(function() {
one;
line();
here();
})();
//-->
</script>
<script type='text/html'>
I'm a jQuery {{template}}.
</script>
"""
Scenario: Rendering inline js with a passthrough minifier Scenario: Rendering inline js with a passthrough minifier
Given the Server is running at "passthrough-app" Given the Server is running at "passthrough-app"
When I go to "/inline-js.html" When I go to "/inline-js.html"
Then I should see "11" lines Then I should see:
"""
<script type='text/javascript'>
//<![CDATA[
;(function() {
this;
should();
all.be();
on = { one: line };
})();
//]]>
</script>
<script>
;(function() {
this;
should();
too();
})();
</script>
<script type='text/javascript'>
//<!--
;(function() {
one;
line();
here();
})();
//-->
</script>
<script type='text/html'>
I'm a jQuery {{template}}.
</script>
"""
Scenario: Rendering inline js with the feature enabled Scenario: Rendering inline js with the feature enabled
Given "minify_javascript" feature is "enabled" Given "minify_javascript" feature is "enabled"
And the Server is running at "minify-js-app" And the Server is running at "minify-js-app"
When I go to "/inline-js.html" When I go to "/inline-js.html"
Then I should see "5" lines Then I should see:
"""
<script type='text/javascript'>
//<![CDATA[
(function(){this,should(),all.be(),on={one:line}})();
//]]>
</script>
<script>
(function(){this,should(),too()})();
</script>
<script type='text/javascript'>
//<!--
(function(){one,line(),here()})();
//-->
</script>
<script type='text/html'>
I'm a jQuery {{template}}.
</script>
"""
Scenario: Rendering external js with the feature enabled Scenario: Rendering external js with the feature enabled
Given "minify_javascript" feature is "enabled" Given "minify_javascript" feature is "enabled"
And the Server is running at "minify-js-app" And the Server is running at "minify-js-app"
When I go to "/javascripts/js_test.js" When I go to "/javascripts/js_test.js"
Then I should see "1" lines Then I should see "1" lines
When I go to "/more-js/other.js"
Then I should see "1" lines
Scenario: Rendering external js with a passthrough minifier Scenario: Rendering external js with a passthrough minifier
And the Server is running at "passthrough-app" And the Server is running at "passthrough-app"
@ -36,7 +118,7 @@ Feature: Minify Javascript
Given "minify_javascript" feature is "enabled" Given "minify_javascript" feature is "enabled"
And the Server is running at "minify-js-app" And the Server is running at "minify-js-app"
When I go to "/inline-coffeescript.html" When I go to "/inline-coffeescript.html"
Then I should see "5" lines Then I should see "6" lines
Scenario: Rendering external js (coffeescript) with the feature enabled Scenario: Rendering external js (coffeescript) with the feature enabled
Given "minify_javascript" feature is "enabled" Given "minify_javascript" feature is "enabled"
@ -47,9 +129,10 @@ Feature: Minify Javascript
Scenario: Rendering inline js (coffeescript) with a passthrough minifier Scenario: Rendering inline js (coffeescript) with a passthrough minifier
Given the Server is running at "passthrough-app" Given the Server is running at "passthrough-app"
When I go to "/inline-coffeescript.html" When I go to "/inline-coffeescript.html"
Then I should see "17" lines Then I should see "16" lines
Scenario: Rendering external js (coffeescript) with a passthrough minifier Scenario: Rendering external js (coffeescript) with a passthrough minifier
And the Server is running at "passthrough-app" And the Server is running at "passthrough-app"
When I go to "/javascripts/coffee_test.js" When I go to "/javascripts/coffee_test.js"
Then I should see "11" lines Then I should see "11" lines

View file

@ -0,0 +1,5 @@
:css
body {
test: style;
good: deal;
}

View file

@ -0,0 +1,3 @@
body {
display: block;
}

View file

@ -5,3 +5,24 @@
all.be(); all.be();
on = { one: line }; on = { one: line };
})(); })();
%script
:plain
;(function() {
this;
should();
too();
})();
%script(type="text/javascript")
:plain
//<!--
;(function() {
one;
line();
here();
})();
//-->
%script(type="text/html")
I'm a jQuery {{template}}.

View file

@ -0,0 +1,8 @@
var race;
var __slice = Array.prototype.slice;
race = function() {
var runners, winner;
winner = arguments[0], runners = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
return print(winner, runners);
};

View file

@ -4,12 +4,12 @@ module ::PassThrough
end end
end end
set :js_compressor, ::PassThrough
set :css_compressor, ::PassThrough
activate :minify_javascript activate :minify_javascript
activate :minify_css activate :minify_css
set :js_compressor, ::PassThrough
set :css_compressor, ::PassThrough
with_layout false do with_layout false do
page "/inline-css.html" page "/inline-css.html"
page "/inline-js.html" page "/inline-js.html"

View file

@ -1,3 +1,4 @@
%style(type="text/css")
:sass :sass
body body
test: style test: style

View file

@ -5,3 +5,24 @@
all.be(); all.be();
on = { one: line }; on = { one: line };
})(); })();
%script
:plain
;(function() {
this;
should();
too();
})();
%script(type="text/javascript")
:plain
//<!--
;(function() {
one;
line();
here();
})();
//-->
%script(type="text/html")
I'm a jQuery {{template}}.

View file

@ -9,10 +9,6 @@ module Middleman::CoreExtensions::Sprockets
# Once registered # Once registered
def registered(app) def registered(app)
# Default compression to off
app.set :js_compressor, false
app.set :css_compressor, false
# Add class methods to context # Add class methods to context
app.send :include, InstanceMethods app.send :include, InstanceMethods
@ -70,28 +66,10 @@ module Middleman::CoreExtensions::Sprockets
end end
end end
# Remove old compressors # Remove compressors, we handle these with middleware
unregister_bundle_processor 'application/javascript', :js_compressor unregister_bundle_processor 'application/javascript', :js_compressor
unregister_bundle_processor 'text/css', :css_compressor unregister_bundle_processor 'text/css', :css_compressor
# Register compressor from config
register_bundle_processor 'application/javascript', :js_compressor do |context, data|
if context.pathname.to_s =~ /\.min\./
data
else
app.js_compressor.compress(data)
end
end if app.js_compressor
# Register compressor from config
register_bundle_processor 'text/css', :css_compressor do |context, data|
if context.pathname.to_s =~ /\.min\./
data
else
app.css_compressor.compress(data)
end
end if app.css_compressor
# configure search paths # configure search paths
append_path app.js_dir append_path app.js_dir
append_path app.css_dir append_path app.css_dir

View file

@ -53,18 +53,7 @@ module Middleman::Extensions
dirpath = Pathname.new(File.dirname(path)) dirpath = Pathname.new(File.dirname(path))
if path =~ /(^\/$)|(\.(htm|html|php|css|js)$)/ if path =~ /(^\/$)|(\.(htm|html|php|css|js)$)/
body = case(response) body = extract_response_text(response)
when String
response
when Array
response.join
when Rack::Response
response.body.join
when Rack::File
File.read(response.path)
else
response.to_s
end
if body if body
# TODO: This regex will change some paths in plan HTML (not in a tag) - is that OK? # TODO: This regex will change some paths in plan HTML (not in a tag) - is that OK?
@ -90,6 +79,23 @@ module Middleman::Extensions
end end
[status, headers, response] [status, headers, response]
end end
private
def extract_response_text(response)
case(response)
when String
response
when Array
response.join
when Rack::Response
response.body.join
when Rack::File
File.read(response.path)
else
response.to_s
end
end
end end
end end

View file

@ -9,18 +9,84 @@ module Middleman::Extensions
# Once registered # Once registered
def registered(app) def registered(app)
# Tell Sprockets to use the built in CSSMin app.set :css_compressor, false
app.after_configuration do app.after_configuration do
if !css_compressor unless respond_to?(:css_compressor) && css_compressor
require "middleman-more/extensions/minify_css/rainpress" require "middleman-more/extensions/minify_css/rainpress"
set :css_compressor, ::Rainpress set :css_compressor, ::Rainpress
end end
# Setup Rack to watch for inline JS
use InlineCSSRack, :compressor => css_compressor
end end
end end
alias :included :registered alias :included :registered
end end
end end
# Rack middleware to look for JS in HTML and compress it
class InlineCSSRack
# Init
# @param [Class] app
# @param [Hash] options
def initialize(app, options={})
@app = app
@compressor = options[:compressor]
end
# Rack interface
# @param [Rack::Environmemt] env
# @return [Array]
def call(env)
status, headers, response = @app.call(env)
path = env["PATH_INFO"]
if path.end_with?('.html') || path.end_with?('.php')
uncompressed_source = extract_response_text(response)
minified = uncompressed_source.gsub(/(<style[^>]*?>\s*(?:\/\*<!\[CDATA\[\*\/\n)?)(.*?)((?:(?:\n\s*)?\/\*\]\]>\*\/)?\s*<\/style>)/m) do |match|
first = $1
css = $2
last = $3
minified_css = @compressor.compress(css)
first << minified_css << last
end
headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
elsif path.end_with?('.css') && path !~ /\.min\./
uncompressed_source = extract_response_text(response)
minified_css = @compressor.compress(uncompressed_source)
headers["Content-Length"] = ::Rack::Utils.bytesize(minified_css).to_s
response = [minified_css]
end
[status, headers, response]
end
private
def extract_response_text(response)
case(response)
when String
response
when Array
response.join
when Rack::Response
response.body.join
when Rack::File
File.read(response.path)
else
response.to_s
end
end
end
# Register extension # Register extension
# register :minify_css, MinifyCss # register :minify_css, MinifyCss
end end

View file

@ -9,12 +9,11 @@ module Middleman::Extensions
# Once registered # Once registered
def registered(app) def registered(app)
app.set :js_compressor, false
# Once config is parsed # Once config is parsed
app.after_configuration do app.after_configuration do
unless respond_to?(:js_compressor) && js_compressor
# Tell sprockets which compressor to use
if !js_compressor
require 'uglifier' require 'uglifier'
set :js_compressor, ::Uglifier.new set :js_compressor, ::Uglifier.new
end end
@ -43,8 +42,45 @@ module Middleman::Extensions
def call(env) def call(env)
status, headers, response = @app.call(env) status, headers, response = @app.call(env)
if env["PATH_INFO"].match(/\.html$/) path = env["PATH_INFO"]
uncompressed_source = case(response)
if path.end_with?('.html') || path.end_with?('.php')
uncompressed_source = extract_response_text(response)
minified = uncompressed_source.gsub(/(<script[^>]*?>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m) do |match|
first = $1
javascript = $2
last = $3
# Only compress script tags that contain JavaScript (as opposed
# to something like jQuery templates, identified with a "text/html"
# type.
if first =~ /<script>/ || first.include?('text/javascript')
minified_js = @compressor.compress(javascript)
first << minified_js << last
else
match
end
end
headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
elsif path.end_with?('.js') && path !~ /\.min\./
uncompressed_source = extract_response_text(response)
minified_js = @compressor.compress(uncompressed_source)
headers["Content-Length"] = ::Rack::Utils.bytesize(minified_js).to_s
response = [minified_js]
end
[status, headers, response]
end
private
def extract_response_text(response)
case(response)
when String when String
response response
when Array when Array
@ -53,21 +89,9 @@ module Middleman::Extensions
response.body.join response.body.join
when Rack::File when Rack::File
File.read(response.path) File.read(response.path)
else
response.to_s
end end
minified = uncompressed_source.gsub(/(<scri.*?\/\/<!\[CDATA\[\n)(.*?)(\/\/\]\].*?<\/script>)/m) do |m|
first = $1
uncompressed_source = $2
last = $3
minified_js = @compressor.compress(uncompressed_source)
first << minified_js << "\n" << last
end
headers["Content-Length"] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
end
[status, headers, response]
end end
end end
end end