fewer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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