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 +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
|