Merge pull request #1393 from AndrewKvalheim/minify-proxied

CSS/JS isn't minified when a proxy has removed the file extension.
This commit is contained in:
Thomas Reynolds 2015-05-02 14:28:00 -07:00
commit 2b1a4ed1b8
12 changed files with 276 additions and 46 deletions

View file

@ -12,6 +12,8 @@ Feature: Setting the right content type for files
Then the content type should be "text/css" Then the content type should be "text/css"
When I go to "/README" When I go to "/README"
Then the content type should be "text/plain" Then the content type should be "text/plain"
When I go to "/index.php"
Then the content type should be "text/php"
Scenario: Content type can be set explicitly via page or proxy or frontmatter Scenario: Content type can be set explicitly via page or proxy or frontmatter
Given a fixture app "content-type-app" Given a fixture app "content-type-app"
@ -31,6 +33,7 @@ Feature: Setting the right content type for files
When I go to "/override.html" When I go to "/override.html"
Then the content type should be "text/neato" Then the content type should be "text/neato"
@preserve_mime_types
Scenario: Content types can be overridden with mime_type Scenario: Content types can be overridden with mime_type
Given a fixture app "content-type-app" Given a fixture app "content-type-app"
And a file named "config.rb" with: And a file named "config.rb" with:

View file

@ -26,6 +26,18 @@ Feature: Minify CSS
When I go to "/stylesheets/report.css" When I go to "/stylesheets/report.css"
Then I should see "p{border:1px solid #ff6600}" Then I should see "p{border:1px solid #ff6600}"
Scenario: Rendering external css in a proxied resource
Given a fixture app "minify-css-app"
And a file named "config.rb" with:
"""
activate :minify_css
proxy '/css-proxy', '/stylesheets/site.css', ignore: true
"""
And the Server is running at "minify-css-app"
When I go to "/css-proxy"
Then I should see "1" lines
And I should see "only screen and (device-width"
Scenario: Rendering external css with passthrough compressor Scenario: Rendering external css with passthrough compressor
Given a fixture app "passthrough-app" Given a fixture app "passthrough-app"
And a file named "config.rb" with: And a file named "config.rb" with:
@ -125,3 +137,53 @@ Feature: Minify CSS
body{test:style;good:deal} body{test:style;good:deal}
</style> </style>
""" """
Scenario: Rendering inline css in a PHP document
Given a fixture app "minify-css-app"
And a file named "config.rb" with:
"""
activate :minify_css, :inline => true
"""
And the Server is running at "minify-css-app"
When I go to "/inline-css.php"
Then I should see:
"""
<?='Hello'?>
<style>
body{test:style;good:deal}
</style>
"""
Scenario: Rendering inline css in a proxied resource
Given a fixture app "minify-css-app"
And a file named "config.rb" with:
"""
activate :minify_css, :inline => true
proxy '/inline-css-proxy', '/inline-css.html', ignore: true
"""
And the Server is running at "minify-css-app"
When I go to "/inline-css-proxy"
Then I should see:
"""
<style>
body{test:style;good:deal}
</style>
"""
@preserve_mime_types
Scenario: Configuring content types of resources to be minified
Given a fixture app "minify-css-app"
And a file named "config.rb" with:
"""
mime_type('.xcss', 'text/x-css')
activate :minify_css, content_types: ['text/x-css'],
inline: true,
inline_content_types: ['text/html']
"""
And the Server is running at "minify-css-app"
When I go to "/stylesheets/site.xcss"
Then I should see "1" lines
And I should see "only screen and (device-width"
When I go to "/inline-css.php"
Then I should see "8" lines

View file

@ -88,7 +88,7 @@ Feature: Minify Javascript
</script> </script>
""" """
Scenario: Rendering inline css with a passthrough minifier using activate-style compressor Scenario: Rendering inline JS with a passthrough minifier using activate-style compressor
Given a fixture app "passthrough-app" Given a fixture app "passthrough-app"
And a file named "config.rb" with: And a file named "config.rb" with:
""" """
@ -148,6 +148,42 @@ Feature: Minify Javascript
</script> </script>
""" """
Scenario: Rendering inline js in a PHP document
Given a fixture app "minify-js-app"
And a file named "config.rb" with:
"""
activate :minify_javascript, :inline => true
"""
And the Server is running at "minify-js-app"
When I go to "/inline-js.php"
Then I should see:
"""
<?='Hello'?>
<script>
!function(){should(),all.be(),on={one:line}}();
</script>
<script type='text/javascript'>
//<!--
!function(){one,line(),here()}();
//-->
</script>
<script type='text/html'>
I'm a jQuery {{template}}.
</script>
"""
Scenario: Rendering inline js in a proxied resource
Given a fixture app "minify-js-app"
And a file named "config.rb" with:
"""
activate :minify_javascript, :inline => true
proxy '/inline-js-proxy', '/inline-js.html', ignore: true
"""
And the Server is running at "minify-js-app"
When I go to "/inline-js-proxy"
Then I should see "14" lines
Scenario: Rendering external js with the feature enabled Scenario: Rendering external js with the feature enabled
Given a fixture app "minify-js-app" Given a fixture app "minify-js-app"
And a file named "config.rb" with: And a file named "config.rb" with:
@ -160,6 +196,17 @@ Feature: Minify Javascript
When I go to "/more-js/other.js" When I go to "/more-js/other.js"
Then I should see "1" lines Then I should see "1" lines
Scenario: Rendering external js in a proxied resource
Given a fixture app "minify-js-app"
And a file named "config.rb" with:
"""
activate :minify_javascript
proxy '/js-proxy', '/javascripts/js_test.js', ignore: true
"""
And the Server is running at "minify-js-app"
When I go to "/js-proxy"
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"
When I go to "/javascripts/js_test.js" When I go to "/javascripts/js_test.js"

View file

@ -0,0 +1,7 @@
Around('@preserve_mime_types') do |_scenario, block|
mime_types = ::Rack::Mime::MIME_TYPES.clone
block.call
::Rack::Mime::MIME_TYPES.replace mime_types
end

View file

@ -0,0 +1 @@
<?="I'm a PHP file!"?>

View file

@ -0,0 +1,8 @@
<?='Hello'?>
<style>
body {
test: style;
good: deal;
}
</style>

View file

@ -0,0 +1,5 @@
@import "compass/reset"
@media handheld, only screen and (device-width: 768px)
body
display: block

View file

@ -0,0 +1,22 @@
<?='Hello'?>
<script>
;(function() {
this;
should();
all.be();
on = { one: line };
})();
</script>
<script type='text/javascript'>
//<!--
;(function() {
one;
line();
here();
})();
//-->
</script>
<script type='text/html'>
I'm a jQuery {{template}}.
</script>

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

@ -25,6 +25,9 @@ module Middleman
# Sourcemap format # Sourcemap format
::Rack::Mime::MIME_TYPES['.map'] = 'application/json; charset=utf-8' ::Rack::Mime::MIME_TYPES['.map'] = 'application/json; charset=utf-8'
# Create a MIME type for PHP files (for detection by extensions)
::Rack::Mime::MIME_TYPES['.php'] = 'text/php'
app.extend ClassMethods app.extend ClassMethods
app.extend ServerMethods app.extend ServerMethods

View file

@ -3,6 +3,8 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
option :compressor, nil, 'Set the CSS compressor to use.' option :compressor, nil, 'Set the CSS compressor to use.'
option :inline, false, 'Whether to minify CSS inline within HTML files' option :inline, false, 'Whether to minify CSS inline within HTML files'
option :ignore, [], 'Patterns to avoid minifying' option :ignore, [], 'Patterns to avoid minifying'
option :content_types, %w(text/css), 'Content types of resources that contain CSS'
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline CSS'
def initialize(app, options_hash={}, &block) def initialize(app, options_hash={}, &block)
super super
@ -16,7 +18,9 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
# Setup Rack middleware to minify CSS # Setup Rack middleware to minify CSS
app.use Rack, compressor: chosen_compressor, app.use Rack, compressor: chosen_compressor,
ignore: Array(options[:ignore]) + [/\.min\./], ignore: Array(options[:ignore]) + [/\.min\./],
inline: options[:inline] inline: options[:inline],
content_types: options[:content_types],
inline_content_types: options[:inline_content_types]
end end
class SassCompressor class SassCompressor
@ -39,6 +43,8 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
@compressor = options[:compressor] @compressor = options[:compressor]
@ignore = options[:ignore] @ignore = options[:ignore]
@inline = options[:inline] @inline = options[:inline]
@content_types = options[:content_types]
@inline_content_types = options[:inline_content_types]
end end
# Rack interface # Rack interface
@ -47,19 +53,18 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
def call(env) def call(env)
status, headers, response = @app.call(env) status, headers, response = @app.call(env)
if inline_html_content?(env['PATH_INFO']) content_type = headers['Content-Type'].try(:slice, /^[^;]*/)
minified = ::Middleman::Util.extract_response_text(response) path = env['PATH_INFO']
minified.gsub!(INLINE_CSS_REGEX) do
$1 << @compressor.compress($2) << $3 minified = if @inline && minifiable_inline?(content_type)
minify_inline(::Middleman::Util.extract_response_text(response))
elsif minifiable?(content_type) && !ignore?(path)
minify(::Middleman::Util.extract_response_text(response))
end end
if minified
headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s
response = [minified] response = [minified]
elsif standalone_css_content?(env['PATH_INFO'])
minified_css = @compressor.compress(::Middleman::Util.extract_response_text(response))
headers['Content-Length'] = ::Rack::Utils.bytesize(minified_css).to_s
response = [minified_css]
end end
[status, headers, response] [status, headers, response]
@ -67,12 +72,41 @@ class Middleman::Extensions::MinifyCss < ::Middleman::Extension
private private
def inline_html_content?(path) # Whether the path should be ignored
(path.end_with?('.html') || path.end_with?('.php')) && @inline # @param [String] path
# @return [Boolean]
def ignore?(path)
@ignore.any? { |ignore| Middleman::Util.path_match(ignore, path) }
end end
def standalone_css_content?(path) # Whether this type of content can be minified
path.end_with?('.css') && @ignore.none? { |ignore| Middleman::Util.path_match(ignore, path) } # @param [String, nil] content_type
# @return [Boolean]
def minifiable?(content_type)
@content_types.include?(content_type)
end
# Whether this type of content contains inline content that can be minified
# @param [String, nil] content_type
# @return [Boolean]
def minifiable_inline?(content_type)
@inline_content_types.include?(content_type)
end
# Minify the content
# @param [String] content
# @return [String]
def minify(content)
@compressor.compress(content)
end
# Detect and minify inline content
# @param [String] content
# @return [String]
def minify_inline(content)
content.gsub(INLINE_CSS_REGEX) do
$1 + minify($2) + $3
end
end end
end end
end end

View file

@ -3,6 +3,8 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
option :compressor, nil, 'Set the JS compressor to use.' option :compressor, nil, 'Set the JS compressor to use.'
option :inline, false, 'Whether to minify JS inline within HTML files' option :inline, false, 'Whether to minify JS inline within HTML files'
option :ignore, [], 'Patterns to avoid minifying' option :ignore, [], 'Patterns to avoid minifying'
option :content_types, %w(application/javascript), 'Content types of resources that contain JS'
option :inline_content_types, %w(text/html text/php), 'Content types of resources that contain inline JS'
def initialize(app, options_hash={}, &block) def initialize(app, options_hash={}, &block)
super super
@ -16,14 +18,18 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
::Uglifier.new ::Uglifier.new
end end
# Setup Rack middleware to minify CSS # Setup Rack middleware to minify JS
app.use Rack, compressor: chosen_compressor, app.use Rack, compressor: chosen_compressor,
ignore: Array(options[:ignore]) + [/\.min\./], ignore: Array(options[:ignore]) + [/\.min\./],
inline: options[:inline] inline: options[:inline],
content_types: options[:content_types],
inline_content_types: options[:inline_content_types]
end end
# Rack middleware to look for JS and compress it # Rack middleware to look for JS and compress it
class Rack class Rack
INLINE_JS_REGEX = /(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m
# Init # Init
# @param [Class] app # @param [Class] app
# @param [Hash] options # @param [Hash] options
@ -32,6 +38,8 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
@compressor = options[:compressor] @compressor = options[:compressor]
@ignore = options[:ignore] @ignore = options[:ignore]
@inline = options[:inline] @inline = options[:inline]
@content_types = options[:content_types]
@inline_content_types = options[:inline_content_types]
end end
# Rack interface # Rack interface
@ -40,25 +48,18 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
def call(env) def call(env)
status, headers, response = @app.call(env) status, headers, response = @app.call(env)
type = headers['Content-Type'].try(:slice, /^[^;]*/)
path = env['PATH_INFO'] path = env['PATH_INFO']
begin minified = if @inline && minifiable_inline?(type)
if @inline && (path.end_with?('.html') || path.end_with?('.php')) minify_inline(::Middleman::Util.extract_response_text(response))
uncompressed_source = ::Middleman::Util.extract_response_text(response) elsif minifiable?(type) && !ignore?(path)
minify(::Middleman::Util.extract_response_text(response))
minified = minify_inline_content(uncompressed_source)
headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
elsif path.end_with?('.js') && @ignore.none? { |ignore| Middleman::Util.path_match(ignore, path) }
uncompressed_source = ::Middleman::Util.extract_response_text(response)
minified = @compressor.compress(uncompressed_source)
headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
end end
rescue ExecJS::ProgramError => e
warn "WARNING: Couldn't compress JavaScript in #{path}: #{e.message}" if minified
headers['Content-Length'] = ::Rack::Utils.bytesize(minified).to_s
response = [minified]
end end
[status, headers, response] [status, headers, response]
@ -66,19 +67,48 @@ class Middleman::Extensions::MinifyJavascript < ::Middleman::Extension
private private
def minify_inline_content(uncompressed_source) # Whether the path should be ignored
uncompressed_source.gsub(/(<script[^>]*>\s*(?:\/\/(?:(?:<!--)|(?:<!\[CDATA\[))\n)?)(.*?)((?:(?:\n\s*)?\/\/(?:(?:-->)|(?:\]\]>)))?\s*<\/script>)/m) do |match| # @param [String] path
first = $1 # @return [Boolean]
javascript = $2 def ignore?(path)
last = $3 @ignore.any? { |ignore| Middleman::Util.path_match(ignore, path) }
end
# Only compress script tags that contain JavaScript (as opposed # Whether this type of content can be minified
# to something like jQuery templates, identified with a "text/html" # @param [String, nil] content_type
# type. # @return [Boolean]
if first =~ /<script>/ || first.include?('text/javascript') def minifiable?(content_type)
minified_js = @compressor.compress(javascript) @content_types.include?(content_type)
end
first << minified_js << last # Whether this type of content contains inline content that can be minified
# @param [String, nil] content_type
# @return [Boolean]
def minifiable_inline?(content_type)
@inline_content_types.include?(content_type)
end
# Minify the content
# @param [String] content
# @return [String]
def minify(content)
@compressor.compress(content)
rescue ExecJS::ProgramError => e
warn "WARNING: Couldn't compress JavaScript in #{path}: #{e.message}"
content
end
# Detect and minify inline content
# @param [String] content
# @return [String]
def minify_inline(content)
content.gsub(INLINE_JS_REGEX) do |match|
first, inline_content, last = $1, $2, $3
# Only compress script tags that contain JavaScript (as opposed to
# something like jQuery templates, identified with a "text/html" type).
if first.include?('<script>') || first.include?('text/javascript')
first + minify(inline_content) + last
else else
match match
end end