Switch AssetHost to be a new-style extension. Setup extensions app scope earlier. Add supports_multiple_instances flag for extensions.

This commit is contained in:
Thomas Reynolds 2013-04-20 12:59:14 -07:00
parent c2e2839b79
commit b12a7bff3d
10 changed files with 197 additions and 146 deletions

3
.gitignore vendored
View file

@ -8,6 +8,7 @@ pkg
Gemfile.lock Gemfile.lock
docs docs
.rbenv-* .rbenv-*
.ruby-version
.*.swp .*.swp
build build
doc doc
@ -15,4 +16,4 @@ doc
tmp tmp
Makefile Makefile
.mm-pid-* .mm-pid-*
.idea .idea

View file

@ -37,6 +37,7 @@ module Middleman
class << self class << self
# @private # @private
def registered(app) def registered(app)
app.define_hook :initialized
app.define_hook :after_configuration app.define_hook :after_configuration
app.define_hook :before_configuration app.define_hook :before_configuration
app.define_hook :build_config app.define_hook :build_config
@ -68,19 +69,15 @@ module Middleman
# @param [Hash] options Per-extension options hash # @param [Hash] options Per-extension options hash
# @return [void] # @return [void]
def register(extension, options={}, &block) def register(extension, options={}, &block)
if extension.instance_of? Module extend extension
extend extension if extension.respond_to?(:registered)
if extension.respond_to?(:registered) if extension.method(:registered).arity === 1
if extension.method(:registered).arity === 1 extension.registered(self, &block)
extension.registered(self, &block) else
else extension.registered(self, options, &block)
extension.registered(self, options, &block)
end
end end
extension
elsif extension.instance_of?(Class) && extension.ancestors.include?(::Middleman::Extension)
extension.new(self, options, &block)
end end
extension
end end
end end
@ -105,7 +102,22 @@ module Middleman
logger.error "== Unknown Extension: #{ext}" logger.error "== Unknown Extension: #{ext}"
else else
logger.debug "== Activating: #{ext}" logger.debug "== Activating: #{ext}"
extensions[ext] = self.class.register(ext_module, options, &block)
if ext_module.instance_of? Module
extensions[ext] = self.class.register(ext_module, options, &block)
elsif ext_module.instance_of?(Class) && ext_module.ancestors.include?(::Middleman::Extension)
if ext_module.supports_multiple_instances?
extensions[ext] ||= {}
key = "instance_#{extensions[ext].keys.length}"
extensions[ext][key] = ext_module.new(self.class, options, &block)
else
if extensions[ext]
logger.error "== #{ext} already activated. Overwriting."
end
extensions[ext] = ext_module.new(self.class, options, &block)
end
end
end end
end end
@ -141,6 +153,8 @@ module Middleman
instance_eval File.read(local_config), local_config, 1 instance_eval File.read(local_config), local_config, 1
end end
run_hook :initialized
run_hook :build_config if build? run_hook :build_config if build?
run_hook :development_config if development? run_hook :development_config if development?
@ -148,7 +162,13 @@ module Middleman
logger.debug "Loaded extensions:" logger.debug "Loaded extensions:"
self.extensions.each do |ext,_| self.extensions.each do |ext,_|
logger.debug "== Extension: #{ext}" if ext.is_a?(Hash)
ext.each do |k,_|
logger.debug "== Extension: #{k}"
end
else
logger.debug "== Extension: #{ext}"
end
end end
end end
end end

View file

@ -1,3 +1,5 @@
require "active_support/core_ext/class/attribute"
module Middleman module Middleman
module Extensions module Extensions
@ -102,6 +104,8 @@ module Middleman
end end
class Extension class Extension
class_attribute :supports_multiple_instances, :instance_reader => false, :instance_writer => false
class << self class << self
def config def config
@_config ||= ::Middleman::Configuration::ConfigurationManager.new @_config ||= ::Middleman::Configuration::ConfigurationManager.new
@ -125,10 +129,11 @@ module Middleman
yield @options if block_given? yield @options if block_given?
ext = self ext = self
klass.after_configuration do klass.initialized do
ext.app = self ext.app = self
ext.after_configuration
end end
klass.after_configuration(&method(:after_configuration))
end end
def after_configuration def after_configuration

View file

@ -1,7 +1,56 @@
Feature: Alternate between multiple asset hosts Feature: Alternate between multiple asset hosts
In order to speed up page loading In order to speed up page loading
Scenario: Rendering html with the feature enabled Scenario: Set single host globally
Given the Server is running at "asset-host-app" Given a fixture app "asset-host-app"
And a file named "config.rb" with:
"""
activate :asset_host
set :asset_host, "http://assets1.example.com"
"""
And the Server is running
When I go to "/asset_host.html" When I go to "/asset_host.html"
Then I should see "http://assets1"
When I go to "/stylesheets/asset_host.css"
Then I should see "http://assets1"
Scenario: Set proc host globally
Given a fixture app "asset-host-app"
And a file named "config.rb" with:
"""
activate :asset_host
set :asset_host do |asset|
"http://assets%d.example.com" % (asset.hash % 4)
end
"""
And the Server is running
When I go to "/asset_host.html"
Then I should see "http://assets"
When I go to "/stylesheets/asset_host.css"
Then I should see "http://assets"
Scenario: Set single host with inline-option
Given a fixture app "asset-host-app"
And a file named "config.rb" with:
"""
activate :asset_host, :host => "http://assets1.example.com"
"""
And the Server is running
When I go to "/asset_host.html"
Then I should see "http://assets1"
When I go to "/stylesheets/asset_host.css"
Then I should see "http://assets1"
Scenario: Set proc host with inline-option
Given a fixture app "asset-host-app"
And a file named "config.rb" with:
"""
activate :asset_host, :host => Proc.new { |asset|
"http://assets%d.example.com" % (asset.hash % 4)
}
"""
And the Server is running
When I go to "/asset_host.html"
Then I should see "http://assets"
When I go to "/stylesheets/asset_host.css"
Then I should see "http://assets" Then I should see "http://assets"

View file

@ -1,7 +0,0 @@
Feature: Alternate between multiple asset hosts
In order to speed up page loading
Scenario: Rendering css with the feature enabled
Given the Server is running at "asset-host-app"
When I go to "/stylesheets/asset_host.css"
Then I should see "http://assets"

View file

@ -1,6 +0,0 @@
set :layout, false
activate :asset_host
set :asset_host do |asset|
"http://assets%d.example.com" % (asset.hash % 4)
end

View file

@ -83,7 +83,7 @@ module Middleman
# to avoid browser caches failing to update to your new content. # to avoid browser caches failing to update to your new content.
Middleman::Extensions.register(:asset_hash) do Middleman::Extensions.register(:asset_hash) do
require "middleman-more/extensions/asset_hash" require "middleman-more/extensions/asset_hash"
Middleman::Extensions::AssetHash::Extension Middleman::Extensions::AssetHash
end end
# AssetHost allows you to setup multiple domains to host your static # AssetHost allows you to setup multiple domains to host your static

View file

@ -52,8 +52,14 @@ module Middleman
# No line-comments in test mode (changing paths mess with sha1) # No line-comments in test mode (changing paths mess with sha1)
compass_config.line_comments = false if ENV["TEST"] compass_config.line_comments = false if ENV["TEST"]
if config.defines_setting?(:asset_host) && config[:asset_host].is_a?(Proc) if extensions[:asset_host] && asset_host = extensions[:asset_host].host
compass_config.asset_host(&config[:asset_host]) if asset_host.is_a?(Proc)
compass_config.asset_host(&asset_host)
else
compass_config.asset_host do |asset|
asset_host
end
end
end end
end end

View file

@ -1,119 +1,105 @@
module Middleman module Middleman
module Extensions module Extensions
module AssetHash class AssetHash < ::Middleman::Extension
class Extension < ::Middleman::Extension option :exts, %w(.jpg .jpeg .png .gif .js .css .otf .woff .eot .ttf .svg), "List of extensions that get asset hashes appended to them."
option :exts, %w(.jpg .jpeg .png .gif .js .css .otf .woff .eot .ttf .svg), "List of extensions that get asset hashes appended to them." option :ignore, [], "Regexes of filenames to skip adding asset hashes to"
option :ignore, [], "Regexes of filenames to skip adding asset hashes to"
def initialize(app, options_hash={}) def initialize(app, options_hash={})
super super
require 'digest/sha1' require 'digest/sha1'
require 'rack/test' require 'rack/test'
require 'uri' require 'uri'
end
def after_configuration
# Allow specifying regexes to ignore, plus always ignore apple touch icons
ignore = Array(options.ignore) + [/^apple-touch-icon/]
app.sitemap.register_resource_list_manipulator(
:asset_hash,
AssetHashManager.new(app, options.exts, ignore)
)
app.use Middleware, :exts => options.exts, :middleman_app => app, :ignore => ignore
end
end end
# Central class for managing asset_hash extension def after_configuration
class AssetHashManager # Allow specifying regexes to ignore, plus always ignore apple touch icons
def initialize(app, exts, ignore) @ignore = Array(options.ignore) + [/^apple-touch-icon/]
@app = app
@exts = exts
@ignore = ignore
end
# Update the main sitemap resource list app.sitemap.register_resource_list_manipulator(:asset_hash, self)
# @return [void]
def manipulate_resource_list(resources)
# Process resources in order: binary images and fonts, then SVG, then JS/CSS.
# This is so by the time we get around to the text files (which may reference
# images and fonts) the static assets' hashes are already calculated.
rack_client = ::Rack::Test::Session.new(@app.class.to_rack_app)
resources.sort_by do |a|
if %w(.svg).include? a.ext
0
elsif %w(.js .css).include? a.ext
1
else
-1
end
end.each do |resource|
next unless @exts.include? resource.ext
next if @ignore.any? { |ignore| Middleman::Util.path_match(ignore, resource.destination_path) }
# Render through the Rack interface so middleware and mounted apps get a shot app.use Middleware, :exts => options.exts, :middleman_app => app, :ignore => @ignore
response = rack_client.get(URI.escape(resource.destination_path), {}, { "bypass_asset_hash" => "true" }) end
raise "#{resource.path} should be in the sitemap!" unless response.status == 200
digest = Digest::SHA1.hexdigest(response.body)[0..7] # Update the main sitemap resource list
# @return [void]
resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" } def manipulate_resource_list(resources)
# Process resources in order: binary images and fonts, then SVG, then JS/CSS.
# This is so by the time we get around to the text files (which may reference
# images and fonts) the static assets' hashes are already calculated.
rack_client = ::Rack::Test::Session.new(@app.class.to_rack_app)
resources.sort_by do |a|
if %w(.svg).include? a.ext
0
elsif %w(.js .css).include? a.ext
1
else
-1
end end
end.each do |resource|
next unless options.exts.include? resource.ext
next if @ignore.any? { |ignore| Middleman::Util.path_match(ignore, resource.destination_path) }
# Render through the Rack interface so middleware and mounted apps get a shot
response = rack_client.get(URI.escape(resource.destination_path), {}, { "bypass_asset_hash" => "true" })
raise "#{resource.path} should be in the sitemap!" unless response.status == 200
digest = Digest::SHA1.hexdigest(response.body)[0..7]
resource.destination_path = resource.destination_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
end end
end end
end
# The asset hash middleware is responsible for rewriting references to # The asset hash middleware is responsible for rewriting references to
# assets to include their new, hashed name. # assets to include their new, hashed name.
class Middleware class Middleware
def initialize(app, options={}) def initialize(app, options={})
@rack_app = app @rack_app = app
@exts = options[:exts] @exts = options[:exts]
@ignore = options[:ignore] @ignore = options[:ignore]
@exts_regex_text = @exts.map {|e| Regexp.escape(e) }.join('|') @exts_regex_text = @exts.map {|e| Regexp.escape(e) }.join('|')
@middleman_app = options[:middleman_app] @middleman_app = options[:middleman_app]
end end
def call(env) def call(env)
status, headers, response = @rack_app.call(env) status, headers, response = @rack_app.call(env)
# We don't want to use this middleware when rendering files to figure out their hash! # We don't want to use this middleware when rendering files to figure out their hash!
return [status, headers, response] if env["bypass_asset_hash"] == 'true' return [status, headers, response] if env["bypass_asset_hash"] == 'true'
path = @middleman_app.full_path(env["PATH_INFO"]) path = @middleman_app.full_path(env["PATH_INFO"])
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 = ::Middleman::Util.extract_response_text(response) body = ::Middleman::Util.extract_response_text(response)
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?
body.gsub! /([=\'\"\(]\s*)([^\s\'\"\)]+(#{@exts_regex_text}))/ do |match| body.gsub!(/([=\'\"\(]\s*)([^\s\'\"\)]+(#{@exts_regex_text}))/) do |match|
opening_character = $1 opening_character = $1
asset_path = $2 asset_path = $2
relative_path = Pathname.new(asset_path).relative? relative_path = Pathname.new(asset_path).relative?
asset_path = dirpath.join(asset_path).to_s if relative_path asset_path = dirpath.join(asset_path).to_s if relative_path
if @ignore.any? { |r| asset_path.match(r) } if @ignore.any? { |r| asset_path.match(r) }
match match
elsif asset_page = @middleman_app.sitemap.find_resource_by_path(asset_path) elsif asset_page = @middleman_app.sitemap.find_resource_by_path(asset_path)
replacement_path = "/#{asset_page.destination_path}" replacement_path = "/#{asset_page.destination_path}"
replacement_path = Pathname.new(replacement_path).relative_path_from(dirpath).to_s if relative_path replacement_path = Pathname.new(replacement_path).relative_path_from(dirpath).to_s if relative_path
"#{opening_character}#{replacement_path}" "#{opening_character}#{replacement_path}"
else else
match match
end
end end
status, headers, response = Rack::Response.new(body, status, headers).finish
end end
status, headers, response = Rack::Response.new(body, status, headers).finish
end end
[status, headers, response]
end end
[status, headers, response]
end end
end end
end end

View file

@ -3,24 +3,19 @@ module Middleman
module Extensions module Extensions
# Asset Host module # Asset Host module
module AssetHost class AssetHost < ::Middleman::Extension
option :host, nil, 'The asset host to use, or false for no asset host, or a Proc to determine asset host'
# Setup extension def initialize(app, options_hash={}, &block)
class << self super
# Once registered # Backwards compatible API
def registered(app, options={}) app.config.define_setting :asset_host, nil, 'The asset host to use, or false for no asset host, or a Proc to determine asset host'
app.config.define_setting :asset_host, false, 'The asset host to use, or false for no asset host, or a Proc to determine asset host' app.send :include, InstanceMethods
end
if options[:host] def host
config[:asset_host] = options[:host] app.config[:asset_host] || options[:host]
end
# Include methods
app.send :include, InstanceMethods
end
alias :included :registered
end end
# Asset Host Instance Methods # Asset Host Instance Methods
@ -32,13 +27,15 @@ module Middleman
# @param [String] prefix # @param [String] prefix
# @return [String] # @return [String]
def asset_url(path, prefix="") def asset_url(path, prefix="")
original_output = super controller = extensions[:asset_host]
return original_output unless config[:asset_host]
asset_prefix = if config[:asset_host].is_a?(Proc) original_output = super
config[:asset_host].call(original_output) return original_output unless controller.host
elsif config[:asset_host].is_a?(String)
config[:asset_host] asset_prefix = if controller.host.is_a?(Proc)
controller.host.call(original_output)
elsif controller.host.is_a?(String)
controller.host
end end
File.join(asset_prefix, original_output) File.join(asset_prefix, original_output)