From 3b646f8124e72fa282d3e34267c02a9ef6a7a1f1 Mon Sep 17 00:00:00 2001 From: Patrick Mulder Date: Mon, 8 Jul 2013 22:11:25 +0200 Subject: [PATCH 1/3] use rspec and basic request on /assets --- Gemfile | 6 +- jammit.gemspec | 1 + lib/jammit.rb | 4 +- lib/jammit/controller.rb | 152 +++++++++++++++++++++++++-------- spec/helpers/request_helper.rb | 20 +++++ spec/jammit/controller_spec.rb | 23 +++++ spec/spec_helper.rb | 24 ++++++ 7 files changed, 192 insertions(+), 38 deletions(-) create mode 100644 spec/helpers/request_helper.rb create mode 100644 spec/jammit/controller_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/Gemfile b/Gemfile index 03edd774..f8874dea 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,6 @@ gemspec group :development, :test do gem "rake", "0.9.2.2" - gem "rails", "2.3.14" gem "cssmin", "1.0.3" gem "jsmin", "1.0.1" gem "yui-compressor", "0.9.6" @@ -17,3 +16,8 @@ group :development do gem "RedCloth", "4.2.9" gem "redgreen", "1.2.2" end + +group :test do + gem 'rspec', '~>2' + gem 'rack-test' +end diff --git a/jammit.gemspec b/jammit.gemspec index 1618ec74..9af9040a 100644 --- a/jammit.gemspec +++ b/jammit.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |s| s.add_dependency 'cssmin', ['>= 1.0.3'] s.add_dependency 'jsmin', ['>= 1.0.1'] + s.add_dependency "rack", ">= 1.0" s.files = Dir['lib/**/*', 'bin/*', 'rails/*', 'jammit.gemspec', 'LICENSE', 'README'] end diff --git a/lib/jammit.rb b/lib/jammit.rb index c9d6d08e..e18c711b 100644 --- a/lib/jammit.rb +++ b/lib/jammit.rb @@ -1,5 +1,7 @@ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__)) +require 'jammit/controller' + # @Jammit@ is the central namespace for all Jammit classes, and provides access # to all of the configuration options. module Jammit @@ -55,7 +57,7 @@ class << self :embed_assets, :package_assets, :compress_assets, :gzip_assets, :package_path, :mhtml_enabled, :include_jst_script, :config_path, :javascript_compressor, :compressor_options, :css_compressor, - :css_compressor_options, :template_extension, + :css_compressor_options, # :template_extension, # <<--- TODO: activate later :template_extension_matcher, :allow_debugging, :rewrite_relative_paths, :public_root attr_accessor :javascript_compressors, :css_compressors diff --git a/lib/jammit/controller.rb b/lib/jammit/controller.rb index 0ebbd71c..4730a012 100644 --- a/lib/jammit/controller.rb +++ b/lib/jammit/controller.rb @@ -1,11 +1,19 @@ -require 'action_controller' +require 'rack' module Jammit - # The JammitController is added to your Rails application when the Gem is - # loaded. It takes responsibility for /assets, and dynamically packages any - # missing or uncached asset packages. - class Controller < ActionController::Base + # defined somewhere else + # but for temporarily development + def self.public_root + '/public' + end + + def self.template_extension + 'js' + end + + + class Request < Rack::Request VALID_FORMATS = [:css, :js] @@ -13,6 +21,47 @@ class Controller < ActionController::Base NOT_FOUND_PATH = "#{Jammit.public_root}/404.html" + def asset_path? + path_info =~ /^\/assets/ + end + + def asset_from_path + path_info =~ /^\/assets\/([\w\.]+)/ + $1 + end + + def extension_from_path + path_info =~ /\.([\w\.]+)$/ + $1 + end + + def path_info + @env['PATH_INFO'] + end + + # Extracts the package name, extension (:css, :js), and variant (:datauri, + # :mhtml) from the incoming URL. + def parse_request + pack = asset_from_path + @extension = extension_from_path.to_sym + puts @extension.inspect + raise PackageNotFound unless (VALID_FORMATS + [Jammit.template_extension.to_sym]).include?(@extension) + if Jammit.embed_assets + suffix_match = pack.match(SUFFIX_STRIPPER) + @variant = Jammit.embed_assets && suffix_match && suffix_match[1].to_sym + pack.sub!(SUFFIX_STRIPPER, '') + end + @package = pack.to_sym + end + + # Tells the Jammit::Packager to cache and gzip an asset package. We can't + # just use the built-in "cache_page" because we need to ensure that + # the timestamp that ends up in the MHTML is also on the cached file. + def cache_package + dir = File.join(page_cache_directory, Jammit.package_path) + Jammit.packager.cache(@package, @extension, @contents, dir, @variant, @mtime) + end + # The "package" action receives all requests for asset packages that haven't # yet been cached. The package will be built, cached, and gzipped. def package @@ -20,28 +69,52 @@ def package template_ext = Jammit.template_extension.to_sym case @extension when :js - render :js => (@contents = Jammit.packager.pack_javascripts(@package)) + # (@contents = Jammit.packager.pack_javascripts(@package)) + 'foo_case1.js' when template_ext - render :js => (@contents = Jammit.packager.pack_templates(@package)) + # (@contents = Jammit.packager.pack_templates(@package)) + 'foo_case2.js' when :css - render :text => generate_stylesheets, :content_type => 'text/css' + [generate_stylesheets, :content_type => 'text/css'] end - cache_package if perform_caching && (@extension != template_ext) + # cache_package if perform_caching && (@extension != template_ext) rescue Jammit::PackageNotFound package_not_found end + def for_jammit? + get? && # GET on js resource in :hosted_at (fast, check first) + asset_path? + end + end - private - - # Tells the Jammit::Packager to cache and gzip an asset package. We can't - # just use the built-in "cache_page" because we need to ensure that - # the timestamp that ends up in the MHTML is also on the cached file. - def cache_package - dir = File.join(page_cache_directory, Jammit.package_path) - Jammit.packager.cache(@package, @extension, @contents, dir, @variant, @mtime) + class Response < Rack::Response + + # Rack response tuple accessors. + attr_accessor :status, :headers, :body + + def initialize(env, asset) + @env = env + @body = asset + @status = 200 # OK + @headers = Rack::Utils::HeaderHash.new + + headers["Content-Length"] = self.class.content_length(body).to_s end + class << self + + # Calculate appropriate content_length + def content_length(body) + if body.respond_to?(:bytesize) + body.bytesize + else + body.size + end + end + + end + # Generate the complete, timestamped, MHTML url -- if we're rendering a # dynamic MHTML package, we'll need to put one URL in the response, and a # different one into the cached package. @@ -63,19 +136,6 @@ def generate_stylesheets css end - # Extracts the package name, extension (:css, :js), and variant (:datauri, - # :mhtml) from the incoming URL. - def parse_request - pack = params[:package] - @extension = params[:extension].to_sym - raise PackageNotFound unless (VALID_FORMATS + [Jammit.template_extension.to_sym]).include?(@extension) - if Jammit.embed_assets - suffix_match = pack.match(SUFFIX_STRIPPER) - @variant = Jammit.embed_assets && suffix_match && suffix_match[1].to_sym - pack.sub!(SUFFIX_STRIPPER, '') - end - @package = pack.to_sym - end # Render the 404 page, if one exists, for any packages that don't. def package_not_found @@ -83,15 +143,35 @@ def package_not_found render :text => "

404: \"#{@package}\" asset package not found.

", :status => 404 end + def to_rack + [status, headers.to_hash, [body]] + end end -end + # The JammitController is added to your Rails application when the Gem is + # loaded. It takes responsibility for /assets, and dynamically packages any + # missing or uncached asset packages. + class Controller -# Make the Jammit::Controller available to Rails as a top-level controller. -::JammitController = Jammit::Controller + def initialize(app, options={}) + @app = app + # yield self if block_given? + # validate_options + end -if defined?(Rails) && Rails.env.development? - ActionController::Base.class_eval do - append_before_filter { Jammit.reload! } + def call(env) + dup.call!(env) + end + + def call!(env) + env['jammit'] = self + + if (@request = Request.new(env.dup.freeze)).for_jammit? + Response.new(env.dup.freeze, @request.package).to_rack + else + @app.call(env) + end + end end + end diff --git a/spec/helpers/request_helper.rb b/spec/helpers/request_helper.rb new file mode 100644 index 00000000..895aeb67 --- /dev/null +++ b/spec/helpers/request_helper.rb @@ -0,0 +1,20 @@ +# encoding: utf-8 +module Jammit::Spec + module Helpers + def env_with_params(path = "/", params = {}, env = {}) + method = params.delete(:method) || "GET" + env = { 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => "#{method}" }.merge(env) + Rack::MockRequest.env_for("#{path}?#{Rack::Utils.build_query(params)}", env) + end + + def setup_rack(app = basic_app, opts = {}, &block) + app ||= block if block_given? + + Rack::Builder.new do + use Jammit::Controller, opts + run app + end + end + + end +end diff --git a/spec/jammit/controller_spec.rb b/spec/jammit/controller_spec.rb new file mode 100644 index 00000000..085a63ab --- /dev/null +++ b/spec/jammit/controller_spec.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 +require 'spec_helper' + +describe Jammit::Controller do + + def basic_app + lambda { |e| [200, {'Content-Type' => 'text/plain'}, '

FunkyBoss

'] } + end + + it "inserts Jammit into the rack env" do + env = env_with_params + setup_rack(basic_app).call(env) + env["jammit"].should be_an_instance_of(Jammit::Controller) + end + + describe "serves assets" do + it "responds with 200" do + env = env_with_params("/assets/app.js", {}) + result = setup_rack(basic_app).call(env) + result.last.should == ['var Foo = 1'] + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..7a62793d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,24 @@ +# encoding: utf-8 +$TESTING=true + +$:.unshift File.join(File.dirname(__FILE__), '..', 'lib') +$:.unshift File.expand_path(File.join(File.dirname(__FILE__))) +require 'jammit' + +require 'rubygems' +require 'rack' + +Dir[File.join(File.dirname(__FILE__), "helpers", "**/*.rb")].each do |f| + require f +end + +RSpec.configure do |config| + config.include(Jammit::Spec::Helpers) + # config.include(Warden::Test::Helpers) + + # def load_strategies + # Dir[File.join(File.dirname(__FILE__), "helpers", "strategies", "**/*.rb")].each do |f| + # load f + # end + # end +end From 08b5f6f3b76624a4838c9a0499323c8e76f5ceef Mon Sep 17 00:00:00 2001 From: Patrick Mulder Date: Thu, 11 Jul 2013 16:59:06 +0200 Subject: [PATCH 2/3] add spec for idea to insert assets with rack --- Gemfile | 1 + lib/jammit/controller.rb | 4 ++-- spec/jammit/controller_spec.rb | 33 +++++++++++++++++++++++++++------ spec/spec_helper.rb | 11 +++++++++++ 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Gemfile b/Gemfile index f8874dea..6874e690 100644 --- a/Gemfile +++ b/Gemfile @@ -19,5 +19,6 @@ end group :test do gem 'rspec', '~>2' + gem 'nokogiri' gem 'rack-test' end diff --git a/lib/jammit/controller.rb b/lib/jammit/controller.rb index 4730a012..345dfb98 100644 --- a/lib/jammit/controller.rb +++ b/lib/jammit/controller.rb @@ -69,8 +69,8 @@ def package template_ext = Jammit.template_extension.to_sym case @extension when :js - # (@contents = Jammit.packager.pack_javascripts(@package)) - 'foo_case1.js' + puts @package.inspect + (@contents = Jammit.packager.pack_javascripts(@package)) when template_ext # (@contents = Jammit.packager.pack_templates(@package)) 'foo_case2.js' diff --git a/spec/jammit/controller_spec.rb b/spec/jammit/controller_spec.rb index 085a63ab..11388a42 100644 --- a/spec/jammit/controller_spec.rb +++ b/spec/jammit/controller_spec.rb @@ -1,23 +1,44 @@ # encoding: utf-8 require 'spec_helper' +require 'nokogiri' describe Jammit::Controller do - def basic_app - lambda { |e| [200, {'Content-Type' => 'text/plain'}, '

FunkyBoss

'] } + def test_html + ' + + + test page + +

FunkyBoss

+ ' + end + + def index_app + lambda { |e| [200, {'Content-Type' => 'text/plain'}, test_html] } end it "inserts Jammit into the rack env" do env = env_with_params - setup_rack(basic_app).call(env) + setup_rack(index_app).call(env) env["jammit"].should be_an_instance_of(Jammit::Controller) end - describe "serves assets" do - it "responds with 200" do + describe "serving assets" do + it "responds with" do env = env_with_params("/assets/app.js", {}) - result = setup_rack(basic_app).call(env) + result = setup_rack(index_app).call(env) result.last.should == ['var Foo = 1'] end end + + describe "serving non-assets" do + it "includes script tag" do + env = env_with_params("/index.html", {}) + result = setup_rack(index_app).call(env) + test_html_head(result.last, 'title', 'test page') + test_html_head_attributes(result.last, 'meta', 'charset=utf-8') + end + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7a62793d..0fef5624 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,3 +22,14 @@ # end # end end + +def test_html_head(body, tag, keys) + html = Nokogiri::HTML(body) + html.css("head #{tag}").text.should == keys +end + +def test_html_head_attributes(body, tag, keys) + html = Nokogiri::HTML(body) + attributes = html.css("head #{tag}").map(&:attributes) + # .... should +end From a6382c279c37b1f8b29687e351f06f27ece3f7c2 Mon Sep 17 00:00:00 2001 From: Patrick Mulder Date: Fri, 12 Jul 2013 09:19:49 +0200 Subject: [PATCH 3/3] add inserter for dependencies --- lib/jammit.rb | 1 + lib/jammit/controller.rb | 9 +++++++-- lib/jammit/header_processor.rb | 37 ++++++++++++++++++++++++++++++++++ spec/jammit/controller_spec.rb | 6 ++---- spec/spec_helper.rb | 6 +++--- 5 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 lib/jammit/header_processor.rb diff --git a/lib/jammit.rb b/lib/jammit.rb index e18c711b..527e5718 100644 --- a/lib/jammit.rb +++ b/lib/jammit.rb @@ -1,5 +1,6 @@ $LOAD_PATH.push File.expand_path(File.dirname(__FILE__)) +require 'jammit/header_processor' require 'jammit/controller' # @Jammit@ is the central namespace for all Jammit classes, and provides access diff --git a/lib/jammit/controller.rb b/lib/jammit/controller.rb index 345dfb98..46a0bd50 100644 --- a/lib/jammit/controller.rb +++ b/lib/jammit/controller.rb @@ -73,7 +73,7 @@ def package (@contents = Jammit.packager.pack_javascripts(@package)) when template_ext # (@contents = Jammit.packager.pack_templates(@package)) - 'foo_case2.js' + 'foo_case2.jst' when :css [generate_stylesheets, :content_type => 'text/css'] end @@ -169,7 +169,12 @@ def call!(env) if (@request = Request.new(env.dup.freeze)).for_jammit? Response.new(env.dup.freeze, @request.package).to_rack else - @app.call(env) + status, headers, body = @app.call(env) + + processor = HeaderProcessor.new(body) + processor.process!(env) + + [ status, headers, processor.new_body ] end end end diff --git a/lib/jammit/header_processor.rb b/lib/jammit/header_processor.rb new file mode 100644 index 00000000..01bb3788 --- /dev/null +++ b/lib/jammit/header_processor.rb @@ -0,0 +1,37 @@ +module Jammit + + class HeaderProcessor + + attr_reader :content_length, :new_body + + HEAD_TAG_REGEX = /<\/head>|<\/head[^(er)][^<]*>/ + + def initialize(body) + @body = body + end + + # Add the tags for script and stylesheets to the response + def process!(env) + @env = env + + @new_body = @body.map(&:to_s) + @livereload_added = false + + @new_body.each_with_index do |line, pos| + if line =~ HEAD_TAG_REGEX + @pos = pos + break + end + end + @new_body.insert(@pos, dependencies) + end + + def dependencies + ['', + ''].join + end + + + end + +end diff --git a/spec/jammit/controller_spec.rb b/spec/jammit/controller_spec.rb index 11388a42..a0d1a648 100644 --- a/spec/jammit/controller_spec.rb +++ b/spec/jammit/controller_spec.rb @@ -7,7 +7,6 @@ def test_html ' - test page

FunkyBoss

@@ -15,7 +14,7 @@ def test_html end def index_app - lambda { |e| [200, {'Content-Type' => 'text/plain'}, test_html] } + lambda { |e| [200, {'Content-Type' => 'text/html'}, test_html.split("\n")] } end it "inserts Jammit into the rack env" do @@ -36,8 +35,7 @@ def index_app it "includes script tag" do env = env_with_params("/index.html", {}) result = setup_rack(index_app).call(env) - test_html_head(result.last, 'title', 'test page') - test_html_head_attributes(result.last, 'meta', 'charset=utf-8') + test_html_head_script(result.last.join, '/assets/app.js') end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0fef5624..a19e6588 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -28,8 +28,8 @@ def test_html_head(body, tag, keys) html.css("head #{tag}").text.should == keys end -def test_html_head_attributes(body, tag, keys) +def test_html_head_script(body, str) html = Nokogiri::HTML(body) - attributes = html.css("head #{tag}").map(&:attributes) - # .... should + script = html.at_css("script") + script.attributes.first.last.text.should == str end