fewer 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
+ *.gem
2
+ .bundle
1
3
  /coverage
2
- /pkg
3
- /rdoc
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fewer.gemspec
4
+ gemspec
data/README.md CHANGED
@@ -22,8 +22,8 @@ Using Fewer in your Rails app is easy, just initialize your Fewer apps and add t
22
22
  )
23
23
 
24
24
  # config/routes.rb
25
- match '/javascripts/:data.js', :to => Fewer::App[:javascripts]
26
- match '/stylesheets/:data.css', :to => Fewer::App[:stylesheets]
25
+ match '/javascripts/*data.js', :to => Fewer::App[:javascripts]
26
+ match '/stylesheets/*data.css', :to => Fewer::App[:stylesheets]
27
27
 
28
28
  # app/helpers/application_helper.rb
29
29
  module ApplicationHelper
data/Rakefile CHANGED
@@ -1,22 +1,7 @@
1
- require 'rubygems'
2
- require 'rake'
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
3
 
4
- begin
5
- require 'jeweler'
6
- Jeweler::Tasks.new do |gem|
7
- gem.name = "fewer"
8
- gem.summary = 'Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests.'
9
- gem.description = 'Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests. Fewer extracts and combines a list of assets encoded in the URL and serves the response with far-future HTTP caching headers.'
10
- gem.email = 'spideryoung@gmail.com'
11
- gem.homepage = 'http://github.com/benpickles/fewer'
12
- gem.authors = ['Ben Pickles']
13
- gem.add_dependency('rack')
14
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
- end
16
- Jeweler::GemcutterTasks.new
17
- rescue LoadError
18
- puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler'
19
- end
4
+ task :default => :test
20
5
 
21
6
  require 'rake/testtask'
22
7
  Rake::TestTask.new(:test) do |test|
@@ -31,23 +16,10 @@ begin
31
16
  test.libs << 'test'
32
17
  test.pattern = 'test/**/*_test.rb'
33
18
  test.verbose = true
19
+ test.rcov_opts << '--exclude "gems/*"'
34
20
  end
35
21
  rescue LoadError
36
22
  task :rcov do
37
23
  abort 'RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov'
38
24
  end
39
25
  end
40
-
41
- task :test => :check_dependencies
42
-
43
- task :default => :test
44
-
45
- require 'rake/rdoctask'
46
- Rake::RDocTask.new do |rdoc|
47
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
-
49
- rdoc.rdoc_dir = 'rdoc'
50
- rdoc.title = "fewer #{version}"
51
- rdoc.rdoc_files.include('README*')
52
- rdoc.rdoc_files.include('lib/**/*.rb')
53
- end
@@ -1,69 +1,28 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "fewer/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{fewer}
8
- s.version = "0.2.0"
6
+ s.name = "fewer"
7
+ s.version = Fewer::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ben Pickles"]
10
+ s.email = ["spideryoung@gmail.com"]
11
+ s.homepage = 'https://github.com/benpickles/fewer'
12
+ s.summary = 'Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests.'
13
+ s.description = 'Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests. Fewer extracts and combines a list of assets encoded in the URL and serves the response with far-future HTTP caching headers.'
9
14
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Ben Pickles"]
12
- s.date = %q{2010-08-17}
13
- s.description = %q{Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests. Fewer extracts and combines a list of assets encoded in the URL and serves the response with far-future HTTP caching headers.}
14
- s.email = %q{spideryoung@gmail.com}
15
- s.extra_rdoc_files = [
16
- "LICENSE",
17
- "README.md"
18
- ]
19
- s.files = [
20
- ".gitignore",
21
- "LICENSE",
22
- "README.md",
23
- "Rakefile",
24
- "VERSION",
25
- "fewer.gemspec",
26
- "lib/fewer.rb",
27
- "lib/fewer/app.rb",
28
- "lib/fewer/engines.rb",
29
- "lib/fewer/engines/abstract.rb",
30
- "lib/fewer/engines/css.rb",
31
- "lib/fewer/engines/js.rb",
32
- "lib/fewer/engines/less.rb",
33
- "lib/fewer/errors.rb",
34
- "lib/fewer/middleware.rb",
35
- "lib/fewer/rails_helpers.rb",
36
- "lib/fewer/serializer.rb",
37
- "test/app_test.rb",
38
- "test/engine_test.rb",
39
- "test/middleware_test.rb",
40
- "test/templates/rounded.less",
41
- "test/templates/style.less",
42
- "test/test_helper.rb"
43
- ]
44
- s.homepage = %q{http://github.com/benpickles/fewer}
45
- s.rdoc_options = ["--charset=UTF-8"]
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
46
18
  s.require_paths = ["lib"]
47
- s.rubygems_version = %q{1.3.7}
48
- s.summary = %q{Fewer is a Rack endpoint to bundle and cache assets and help you make fewer HTTP requests.}
49
- s.test_files = [
50
- "test/app_test.rb",
51
- "test/engine_test.rb",
52
- "test/middleware_test.rb",
53
- "test/test_helper.rb"
54
- ]
55
19
 
56
- if s.respond_to? :specification_version then
57
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
- s.specification_version = 3
20
+ s.add_runtime_dependency('rack', '>= 0')
59
21
 
60
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
61
- s.add_runtime_dependency(%q<rack>, [">= 0"])
62
- else
63
- s.add_dependency(%q<rack>, [">= 0"])
64
- end
65
- else
66
- s.add_dependency(%q<rack>, [">= 0"])
67
- end
22
+ s.add_development_dependency('activesupport')
23
+ s.add_development_dependency('leftright') if RUBY_VERSION < '1.9'
24
+ s.add_development_dependency('less')
25
+ s.add_development_dependency('mocha')
26
+ s.add_development_dependency('rack-test')
27
+ s.add_development_dependency('fakefs')
68
28
  end
69
-
@@ -1,6 +1,24 @@
1
+ module Fewer
2
+ class << self
3
+ attr_writer :logger
4
+
5
+ def logger
6
+ @logger ||= begin
7
+ defined?(Rails) ? Rails.logger : begin
8
+ require 'logger'
9
+ log = Logger.new(STDOUT)
10
+ log.level = Logger::INFO
11
+ log
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
1
18
  require 'fewer/app'
2
19
  require 'fewer/engines'
3
20
  require 'fewer/errors'
4
21
  require 'fewer/middleware'
5
22
  require 'fewer/rails_helpers'
6
23
  require 'fewer/serializer'
24
+ require 'fewer/version'
@@ -18,23 +18,30 @@ module Fewer
18
18
  @mount = options[:mount]
19
19
  @root = options[:root]
20
20
  @cache = options[:cache] || 3600 * 24 * 365
21
- raise 'You need to define an :engine class' unless @engine_klass
22
- raise 'You need to define a :root path' unless @root
21
+ raise ArgumentError.new('You need to define an :engine class') unless @engine_klass
22
+ raise ArgumentError.new('You need to define a :root path') unless @root
23
23
  self.class.apps[name] = self
24
24
  end
25
25
 
26
26
  def call(env)
27
- eng = engine(names_from_path(env['PATH_INFO']))
28
- headers = {
29
- 'Content-Type' => eng.content_type,
30
- 'Cache-Control' => "public, max-age=#{cache}",
31
- 'Last-Modified' => eng.mtime.rfc2822
32
- }
33
-
34
- [200, headers, [eng.read]]
27
+ eng = engine(sources_from_path(env['PATH_INFO']))
28
+
29
+ if env["HTTP_IF_NONE_MATCH"] && env["HTTP_IF_NONE_MATCH"] == eng.etag
30
+ Fewer.logger.debug "Fewer: returning 304 not modified"
31
+ [304, {}, []]
32
+ else
33
+ headers = {
34
+ 'Content-Type' => engine_klass.content_type || 'text/plain',
35
+ 'Cache-Control' => "public, max-age=#{cache}",
36
+ 'Last-Modified' => eng.mtime.rfc2822,
37
+ 'ETag' => eng.etag
38
+ }
39
+
40
+ [200, headers, [eng.read]]
41
+ end
35
42
  rescue Fewer::MissingSourceFileError => e
36
43
  [404, { 'Content-Type' => 'text/plain' }, [e.message]]
37
- rescue => e
44
+ rescue StandardError => e
38
45
  [500, { 'Content-Type' => 'text/plain' }, ["#{e.class}: #{e.message}"]]
39
46
  end
40
47
 
@@ -43,9 +50,9 @@ module Fewer
43
50
  end
44
51
 
45
52
  private
46
- def names_from_path(path)
47
- encoded = File.basename(path, '.*')
48
- Serializer.decode(encoded)
53
+ def sources_from_path(path)
54
+ encoded = File.basename(path, '.*').split('-').first
55
+ Serializer.decode(root, encoded)
49
56
  end
50
57
  end
51
58
  end
@@ -1,58 +1,66 @@
1
+ require 'digest/md5'
2
+
1
3
  module Fewer
2
4
  module Engines
3
5
  class Abstract
4
- SANITISE_REGEXP = /^#{File::Separator}|\.\.#{File::Separator}/
6
+ SANITISE_REGEXP = /\.?\.#{File::Separator}/
7
+
8
+ class << self
9
+ attr_accessor :content_type, :extension
10
+ end
5
11
 
6
- attr_reader :names, :options, :root
12
+ attr_reader :options, :paths, :root
7
13
 
8
- def initialize(root, names, options = {})
9
- @root = root
10
- @names = names.is_a?(Array) ? names : [names]
14
+ def initialize(root, paths, options = {})
15
+ @root = root.to_s
16
+ @paths = paths
11
17
  @options = options
12
- sanitise_names!
18
+ sanitise_paths!
13
19
  check_paths!
14
20
  end
15
21
 
16
- def content_type
17
- 'text/plain'
18
- end
19
-
20
22
  def encoded
21
- Serializer.encode(names)
22
- end
23
-
24
- def extension
23
+ Serializer.encode(root, paths)
25
24
  end
26
25
 
27
26
  def mtime
28
27
  paths.map { |path|
29
28
  File.mtime(path)
30
- }.max
29
+ }.max || Time.now
31
30
  end
32
31
 
33
- def paths
34
- @paths ||= names.map { |name|
35
- File.join(root, "#{name}#{extension}")
36
- }
32
+ def etag
33
+ # MD5 for concatenation of all files
34
+ Digest::MD5.hexdigest(source)
37
35
  end
38
36
 
39
37
  def read
40
- paths.map { |path|
38
+ source
39
+ end
40
+
41
+ def source
42
+ @source ||= paths.map { |path|
41
43
  File.read(path)
42
44
  }.join("\n")
43
45
  end
44
46
 
45
47
  private
46
48
  def check_paths!
47
- if (missing = paths.reject { |path| File.exist?(path) }).any?
48
- files = missing.map { |path| path.to_s }.join("\n")
49
- raise Fewer::MissingSourceFileError.new("Missing source file#{'s' if missing.size > 1}:\n#{files}")
49
+ missing = paths.reject { |path| File.exist?(path) }
50
+
51
+ if missing.any?
52
+ raise Fewer::MissingSourceFileError.new(
53
+ "Missing source file#{'s' if missing.size > 1}:\n" +
54
+ missing.join("\n"))
50
55
  end
51
56
  end
52
57
 
53
- def sanitise_names!
54
- names.map! { |name|
55
- name.to_s.gsub(SANITISE_REGEXP, '')
58
+ def sanitise_paths!
59
+ paths.map! { |path|
60
+ path = path.to_s
61
+ path.gsub!(SANITISE_REGEXP, '')
62
+ path.replace(File.join(root, path)) if path[0, root.length] != root
63
+ path
56
64
  }
57
65
  end
58
66
  end
@@ -1,13 +1,8 @@
1
1
  module Fewer
2
2
  module Engines
3
3
  class Css < Abstract
4
- def content_type
5
- 'text/css'
6
- end
7
-
8
- def extension
9
- '.css'
10
- end
4
+ self.content_type = 'text/css'
5
+ self.extension = '.css'
11
6
  end
12
7
  end
13
8
  end
@@ -3,13 +3,8 @@ autoload :Closure, 'closure-compiler'
3
3
  module Fewer
4
4
  module Engines
5
5
  class Js < Abstract
6
- def content_type
7
- 'application/x-javascript'
8
- end
9
-
10
- def extension
11
- '.js'
12
- end
6
+ self.content_type = 'application/x-javascript'
7
+ self.extension = '.js'
13
8
 
14
9
  def read
15
10
  if options[:min]
@@ -3,17 +3,12 @@ autoload :Less, 'less'
3
3
  module Fewer
4
4
  module Engines
5
5
  class Less < Abstract
6
- def content_type
7
- 'text/css'
8
- end
9
-
10
- def extension
11
- '.less'
12
- end
6
+ self.content_type = 'text/css'
7
+ self.extension = '.less'
13
8
 
14
9
  def read
15
10
  Dir.chdir root do
16
- ::Less::Engine.new(super).to_css
11
+ ::Less::Engine.new(source).to_css
17
12
  end
18
13
  end
19
14
  end
@@ -16,4 +16,3 @@ module Fewer
16
16
  end
17
17
  end
18
18
  end
19
-
@@ -1,13 +1,35 @@
1
1
  module Fewer
2
2
  module RailsHelpers
3
- def fewer_javascripts_tag(*names)
4
- engine = Fewer::App[:javascripts].engine(names)
5
- javascript_include_tag "#{engine.encoded}.js?#{engine.mtime.to_i}"
3
+ def fewer_encode_sources(app, sources, friendly_ext = nil)
4
+ ext = app.engine_klass.extension
5
+ sources.map! { |source|
6
+ ext && source[-ext.length, ext.length] != ext ? "#{source}#{ext}" : source
7
+ }
8
+
9
+ if config.perform_caching
10
+ engine = app.engine(sources)
11
+ ["#{engine.mtime.to_i.to_s(36)}/#{engine.encoded}#{friendly_ext}"]
12
+ else
13
+ sources.map { |source|
14
+ engine = app.engine([source])
15
+ friendly_name = File.basename(source, '.*')
16
+ "#{engine.mtime.to_i.to_s(36)}/#{engine.encoded}-#{friendly_name}#{friendly_ext}"
17
+ }
18
+ end
19
+ end
20
+
21
+ def fewer_javascripts_tag(*sources)
22
+ options = sources.extract_options!
23
+ options.delete(:cache)
24
+ app = Fewer::App[:javascripts]
25
+ javascript_include_tag fewer_encode_sources(app, sources, '.js'), options
6
26
  end
7
27
 
8
- def fewer_stylesheets_tag(*names)
9
- engine = Fewer::App[:stylesheets].engine(names)
10
- stylesheet_link_tag "#{engine.encoded}.css?#{engine.mtime.to_i}"
28
+ def fewer_stylesheets_tag(*sources)
29
+ options = sources.extract_options!
30
+ options.delete(:cache)
31
+ app = Fewer::App[:stylesheets]
32
+ stylesheet_link_tag fewer_encode_sources(app, sources, '.css'), options
11
33
  end
12
34
  end
13
35
  end
@@ -1,30 +1,33 @@
1
- require 'base64'
2
-
3
1
  module Fewer
4
2
  module Serializer
5
- class BadString < RuntimeError; end
6
-
7
3
  class << self
8
- def b64_decode(string)
9
- padding_length = string.length % 4
10
- Base64.decode64(string + '=' * padding_length)
11
- end
4
+ def decode(root, encoded)
5
+ files = ls(root)
6
+ delimeter = files.length > 36 ? ',' : ''
12
7
 
13
- def b64_encode(string)
14
- Base64.encode64(string).tr("\n=",'')
8
+ encoded.split(delimeter).map { |char|
9
+ files[char.to_i(36)]
10
+ }.compact
15
11
  end
16
12
 
17
- def marshal_decode(string)
18
- Marshal.load(b64_decode(string))
19
- rescue TypeError, ArgumentError => e
20
- raise BadString, "couldn't decode #{string} - got #{e}"
21
- end
22
- alias_method :decode, :marshal_decode
13
+ def encode(root, paths)
14
+ files = ls(root)
15
+ delimeter = files.length > 36 ? ',' : ''
23
16
 
24
- def marshal_encode(object)
25
- b64_encode(Marshal.dump(object))
17
+ paths.map { |path|
18
+ index = files.index(path)
19
+ index ? index.to_s(36) : nil
20
+ }.compact.join(delimeter)
26
21
  end
27
- alias_method :encode, :marshal_encode
22
+
23
+ private
24
+ def ls(root)
25
+ pattern = File.join(root, '**', '*.*')
26
+
27
+ Dir.glob(pattern).sort.delete_if { |path|
28
+ File.directory?(path)
29
+ }
30
+ end
28
31
  end
29
32
  end
30
33
  end