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 +4 -2
- data/Gemfile +4 -0
- data/README.md +2 -2
- data/Rakefile +4 -32
- data/fewer.gemspec +20 -61
- data/lib/fewer.rb +18 -0
- data/lib/fewer/app.rb +21 -14
- data/lib/fewer/engines/abstract.rb +34 -26
- data/lib/fewer/engines/css.rb +2 -7
- data/lib/fewer/engines/js.rb +2 -7
- data/lib/fewer/engines/less.rb +3 -8
- data/lib/fewer/middleware.rb +0 -1
- data/lib/fewer/rails_helpers.rb +28 -6
- data/lib/fewer/serializer.rb +22 -19
- data/lib/fewer/version.rb +3 -0
- data/test/app_test.rb +15 -65
- data/test/engine_test.rb +55 -25
- data/test/integration_test.rb +103 -0
- data/test/middleware_test.rb +5 -4
- data/test/rails_helpers_test.rb +60 -0
- data/test/serializer_test.rb +159 -0
- data/test/test_helper.rb +26 -13
- metadata +105 -16
- data/VERSION +0 -1
- data/test/templates/rounded.less +0 -5
- data/test/templates/style.less +0 -5
data/.gitignore
CHANGED
data/Gemfile
ADDED
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
|
26
|
-
match '/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 '
|
2
|
-
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
3
|
|
4
|
-
|
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
|
data/fewer.gemspec
CHANGED
@@ -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
|
8
|
-
s.version
|
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.
|
11
|
-
s.
|
12
|
-
s.
|
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
|
-
|
57
|
-
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
58
|
-
s.specification_version = 3
|
20
|
+
s.add_runtime_dependency('rack', '>= 0')
|
59
21
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
data/lib/fewer.rb
CHANGED
@@ -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'
|
data/lib/fewer/app.rb
CHANGED
@@ -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(
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
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 =
|
6
|
+
SANITISE_REGEXP = /\.?\.#{File::Separator}/
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :content_type, :extension
|
10
|
+
end
|
5
11
|
|
6
|
-
attr_reader :
|
12
|
+
attr_reader :options, :paths, :root
|
7
13
|
|
8
|
-
def initialize(root,
|
9
|
-
@root = root
|
10
|
-
@
|
14
|
+
def initialize(root, paths, options = {})
|
15
|
+
@root = root.to_s
|
16
|
+
@paths = paths
|
11
17
|
@options = options
|
12
|
-
|
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(
|
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
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
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
|
54
|
-
|
55
|
-
|
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
|
data/lib/fewer/engines/css.rb
CHANGED
data/lib/fewer/engines/js.rb
CHANGED
@@ -3,13 +3,8 @@ autoload :Closure, 'closure-compiler'
|
|
3
3
|
module Fewer
|
4
4
|
module Engines
|
5
5
|
class Js < Abstract
|
6
|
-
|
7
|
-
|
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]
|
data/lib/fewer/engines/less.rb
CHANGED
@@ -3,17 +3,12 @@ autoload :Less, 'less'
|
|
3
3
|
module Fewer
|
4
4
|
module Engines
|
5
5
|
class Less < Abstract
|
6
|
-
|
7
|
-
|
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(
|
11
|
+
::Less::Engine.new(source).to_css
|
17
12
|
end
|
18
13
|
end
|
19
14
|
end
|
data/lib/fewer/middleware.rb
CHANGED
data/lib/fewer/rails_helpers.rb
CHANGED
@@ -1,13 +1,35 @@
|
|
1
1
|
module Fewer
|
2
2
|
module RailsHelpers
|
3
|
-
def
|
4
|
-
|
5
|
-
|
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(*
|
9
|
-
|
10
|
-
|
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
|
data/lib/fewer/serializer.rb
CHANGED
@@ -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
|
9
|
-
|
10
|
-
|
11
|
-
end
|
4
|
+
def decode(root, encoded)
|
5
|
+
files = ls(root)
|
6
|
+
delimeter = files.length > 36 ? ',' : ''
|
12
7
|
|
13
|
-
|
14
|
-
|
8
|
+
encoded.split(delimeter).map { |char|
|
9
|
+
files[char.to_i(36)]
|
10
|
+
}.compact
|
15
11
|
end
|
16
12
|
|
17
|
-
def
|
18
|
-
|
19
|
-
|
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
|
-
|
25
|
-
|
17
|
+
paths.map { |path|
|
18
|
+
index = files.index(path)
|
19
|
+
index ? index.to_s(36) : nil
|
20
|
+
}.compact.join(delimeter)
|
26
21
|
end
|
27
|
-
|
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
|