juicer 0.2.6 → 1.0.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/History.txt +28 -0
- data/Rakefile +84 -36
- data/Readme.rdoc +192 -23
- data/VERSION +1 -0
- data/bin/juicer +2 -4
- data/lib/juicer.rb +9 -10
- data/lib/juicer/asset/path.rb +275 -0
- data/lib/juicer/asset/path_resolver.rb +79 -0
- data/lib/juicer/binary.rb +3 -5
- data/lib/juicer/cache_buster.rb +112 -27
- data/lib/juicer/command/install.rb +4 -2
- data/lib/juicer/command/list.rb +16 -9
- data/lib/juicer/command/merge.rb +30 -14
- data/lib/juicer/command/verify.rb +1 -1
- data/lib/juicer/css_cache_buster.rb +31 -47
- data/lib/juicer/datafy/datafy.rb +20 -0
- data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
- data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
- data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
- data/lib/juicer/ext/logger.rb +5 -0
- data/lib/juicer/ext/string.rb +47 -0
- data/lib/juicer/ext/symbol.rb +15 -0
- data/lib/juicer/image_embed.rb +129 -0
- data/lib/juicer/install/base.rb +2 -2
- data/lib/juicer/install/closure_compiler_installer.rb +69 -0
- data/lib/juicer/install/jslint_installer.rb +3 -3
- data/lib/juicer/install/rhino_installer.rb +3 -2
- data/lib/juicer/install/yui_compressor_installer.rb +3 -2
- data/lib/juicer/jslint.rb +1 -1
- data/lib/juicer/merger/base.rb +1 -1
- data/lib/juicer/merger/javascript_merger.rb +3 -4
- data/lib/juicer/merger/stylesheet_merger.rb +13 -15
- data/lib/juicer/minifyer/closure_compiler.rb +90 -0
- data/lib/juicer/minifyer/java_base.rb +77 -0
- data/lib/juicer/minifyer/yui_compressor.rb +15 -48
- data/test/bin/jslint-1.0.js +523 -0
- data/test/bin/jslint.js +523 -0
- data/test/bin/rhino1_7R1.zip +0 -0
- data/test/bin/rhino1_7R2-RC1.jar +0 -0
- data/test/bin/rhino1_7R2-RC1.zip +0 -0
- data/test/bin/yuicompressor +0 -0
- data/test/bin/yuicompressor-2.3.5.zip +0 -0
- data/test/bin/yuicompressor-2.4.2.jar +0 -0
- data/test/bin/yuicompressor-2.4.2.zip +0 -0
- data/test/data/Changelog.txt +10 -0
- data/test/data/a.css +3 -0
- data/test/data/a.js +5 -0
- data/test/data/a1.css +5 -0
- data/test/data/b.css +1 -0
- data/test/data/b.js +5 -0
- data/test/data/b1.css +5 -0
- data/test/data/c1.css +3 -0
- data/test/data/css/2.gif +1 -0
- data/test/data/css/test.css +11 -0
- data/test/data/css/test2.css +1 -0
- data/test/data/d1.css +3 -0
- data/test/data/images/1.png +1 -0
- data/test/data/my_app.js +2 -0
- data/test/data/not-ok.js +2 -0
- data/test/data/ok.js +3 -0
- data/test/data/path_test.css +5 -0
- data/test/data/path_test2.css +14 -0
- data/test/data/pkg/module/moda.js +2 -0
- data/test/data/pkg/module/modb.js +3 -0
- data/test/data/pkg/pkg.js +1 -0
- data/test/fixtures/yui-download.html +425 -0
- data/test/test_helper.rb +36 -7
- data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
- data/test/unit/juicer/asset/path_test.rb +370 -0
- data/test/unit/juicer/cache_buster_test.rb +104 -0
- data/test/{juicer/test_chainable.rb → unit/juicer/chainable_test.rb} +1 -1
- data/test/unit/juicer/command/install_test.rb +58 -0
- data/test/{juicer/command/test_list.rb → unit/juicer/command/list_test.rb} +26 -14
- data/test/unit/juicer/command/merge_test.rb +162 -0
- data/test/{juicer/command/test_util.rb → unit/juicer/command/util_test.rb} +10 -6
- data/test/unit/juicer/command/verify_test.rb +48 -0
- data/test/{juicer/test_css_cache_buster.rb → unit/juicer/css_cache_buster_test.rb} +10 -30
- data/test/unit/juicer/datafy_test.rb +37 -0
- data/test/{juicer/merger/test_css_dependency_resolver.rb → unit/juicer/dependency_resolver/css_dependency_resolver_test.rb} +2 -2
- data/test/{juicer/merger/test_javascript_dependency_resolver.rb → unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb} +13 -2
- data/test/unit/juicer/ext/{#string_test.rb# → string_test.rb} +0 -7
- data/test/unit/juicer/ext/symbol_test.rb +27 -0
- data/test/unit/juicer/image_embed_test.rb +271 -0
- data/test/unit/juicer/install/installer_base_test.rb +214 -0
- data/test/{juicer/install/test_jslint_installer.rb → unit/juicer/install/jslint_installer_test.rb} +1 -1
- data/test/{juicer/install/test_rhino_installer.rb → unit/juicer/install/rhino_installer_test.rb} +1 -1
- data/test/{juicer/install/test_yui_compressor_installer.rb → unit/juicer/install/yui_compressor_test.rb} +16 -16
- data/test/unit/juicer/jslint_test.rb +60 -0
- data/test/{juicer/merger/test_base.rb → unit/juicer/merger/base_test.rb} +1 -1
- data/test/{juicer/merger/test_javascript_merger.rb → unit/juicer/merger/javascript_merger_test.rb} +2 -2
- data/test/{juicer/merger/test_stylesheet_merger.rb → unit/juicer/merger/stylesheet_merger_test.rb} +15 -13
- data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
- data/test/{integration → unit}/juicer/minifyer/yui_compressor_test.rb +30 -47
- data/test/unit/juicer_test.rb +1 -0
- metadata +207 -113
- data/lib/juicer/core.rb +0 -61
- data/lib/juicer/merger/css_dependency_resolver.rb +0 -25
- data/lib/juicer/merger/dependency_resolver.rb +0 -82
- data/lib/juicer/merger/javascript_dependency_resolver.rb +0 -21
- data/tasks/ann.rake +0 -80
- data/tasks/bones.rake +0 -20
- data/tasks/gem.rake +0 -201
- data/tasks/git.rake +0 -40
- data/tasks/notes.rake +0 -27
- data/tasks/post_load.rake +0 -34
- data/tasks/rdoc.rake +0 -51
- data/tasks/rubyforge.rake +0 -55
- data/tasks/setup.rb +0 -292
- data/tasks/spec.rake +0 -54
- data/tasks/svn.rake +0 -47
- data/tasks/test.rake +0 -40
- data/tasks/test/setup.rake +0 -35
- data/tasks/zentest.rake +0 -36
- data/test/juicer/command/test_install.rb +0 -53
- data/test/juicer/command/test_merge.rb +0 -160
- data/test/juicer/command/test_verify.rb +0 -33
- data/test/juicer/install/test_installer_base.rb +0 -195
- data/test/juicer/minifyer/test_yui_compressor.rb +0 -159
- data/test/juicer/test_cache_buster.rb +0 -58
- data/test/juicer/test_core.rb +0 -47
- data/test/juicer/test_jslint.rb +0 -33
- data/test/test_juicer.rb +0 -4
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
data/bin/juicer
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
require "rubygems"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
require base
|
|
6
|
-
require File.join(base, "cli")
|
|
3
|
+
require "juicer"
|
|
4
|
+
require "juicer/cli"
|
|
7
5
|
|
|
8
6
|
Juicer::Cli.run(ARGV)
|
data/lib/juicer.rb
CHANGED
|
@@ -3,7 +3,7 @@ require "logger"
|
|
|
3
3
|
module Juicer
|
|
4
4
|
|
|
5
5
|
# :stopdoc:
|
|
6
|
-
VERSION = '0.
|
|
6
|
+
VERSION = '1.0.0'
|
|
7
7
|
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
|
8
8
|
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
|
9
9
|
LOGGER = Logger.new(STDOUT)
|
|
@@ -51,20 +51,19 @@ module Juicer
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Utility method used to require all files ending in .rb that lie in the
|
|
54
|
-
# directory below this file
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def self.require_all_libs_relative_to( fname, dir = nil )
|
|
59
|
-
dir ||= ::File.basename(fname, '.*')
|
|
60
|
-
search_me = ::File.expand_path(::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
|
54
|
+
# directory below this file.
|
|
55
|
+
def self.require_all_libs
|
|
56
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
|
57
|
+
glob = File.join(dir, "juicer", '**', '*.rb')
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
# Unexpand paths (avoids requiring the same file twice)
|
|
60
|
+
paths = Dir.glob(glob).map { |path| path.sub("#{dir}/", '').sub(/\.rb$/, "") }
|
|
61
|
+
paths.each { |rb| require rb }
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
end
|
|
66
65
|
|
|
67
|
-
Juicer.
|
|
66
|
+
Juicer.require_all_libs
|
|
68
67
|
|
|
69
68
|
class FileNotFoundError < Exception
|
|
70
69
|
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "juicer/cache_buster"
|
|
5
|
+
|
|
6
|
+
module Juicer
|
|
7
|
+
module Asset
|
|
8
|
+
#
|
|
9
|
+
# Assets are files used by CSS and JavaScript files. The Asset class provides
|
|
10
|
+
# tools for manipulating asset paths, such as rebasing, adding cache busters,
|
|
11
|
+
# and cycling asset hosts.
|
|
12
|
+
#
|
|
13
|
+
# Asset::Path objects are most commonly created by <tt>Juicer::Asset::PathResolver#resolve</tt>
|
|
14
|
+
# which resolves include paths to file names. It is possible, however, to use
|
|
15
|
+
# the asset class directly:
|
|
16
|
+
#
|
|
17
|
+
# Dir.pwd
|
|
18
|
+
# #=> "/home/christian/projects/mysite/design/css"
|
|
19
|
+
#
|
|
20
|
+
# asset = Juicer::Asset::Path.new "../images/logo.png"
|
|
21
|
+
# asset.path
|
|
22
|
+
# #=> "../images/logo.png"
|
|
23
|
+
#
|
|
24
|
+
# asset.rebase("~/projects/mysite/design").path
|
|
25
|
+
# #=> "images/logo.png"
|
|
26
|
+
#
|
|
27
|
+
# asset.filename
|
|
28
|
+
# #=> "/home/christian/projects/mysite/design/images/logo.png"
|
|
29
|
+
#
|
|
30
|
+
# asset.path(:cache_buster_type => :soft)
|
|
31
|
+
# #=> "../images/logo.png?jcb=1234567890"
|
|
32
|
+
#
|
|
33
|
+
# asset.path(:cache_buster_type => :soft, :cache_buster => nil)
|
|
34
|
+
# #=> "../images/logo.png?1234567890"
|
|
35
|
+
#
|
|
36
|
+
# asset.path(:cache_buster => "bustIT")
|
|
37
|
+
# #=> "../images/logo.png?bustIT=1234567890"
|
|
38
|
+
#
|
|
39
|
+
# asset = Juicer::Asset::Path.new "../images/logo.png", :document_root
|
|
40
|
+
# #=> "/home/christian/projects/mysite"
|
|
41
|
+
#
|
|
42
|
+
# asset.absolute_path(:cache_buster_type => :hard)
|
|
43
|
+
# #=> "/images/logo-jcb1234567890.png"
|
|
44
|
+
#
|
|
45
|
+
# asset.absolute_path(:host => "http://localhost")
|
|
46
|
+
# #=> "http://localhost/images/logo.png"
|
|
47
|
+
#
|
|
48
|
+
# asset.absolute_path(:host => "http://localhost", :cache_buster_type => :hard)
|
|
49
|
+
# #=> "http://localhost/images/logo-jcb1234567890.png"
|
|
50
|
+
#
|
|
51
|
+
#
|
|
52
|
+
# Author:: Christian Johansen (christian@cjohansen.no)
|
|
53
|
+
# Copyright:: Copyright (c) 2009 Christian Johansen
|
|
54
|
+
# License:: BSD
|
|
55
|
+
#
|
|
56
|
+
class Path
|
|
57
|
+
# Base directory to resolve relative path from, see Juicer::Asset::Path#initialize
|
|
58
|
+
attr_reader :base
|
|
59
|
+
|
|
60
|
+
# Hosts served from <tt>:document_root</tt>, see Juicer::Asset::Path#initialize
|
|
61
|
+
attr_reader :hosts
|
|
62
|
+
|
|
63
|
+
# Directory served as root through a web server, see Juicer::Asset::Path#initialize
|
|
64
|
+
attr_reader :document_root
|
|
65
|
+
|
|
66
|
+
@@scheme_pattern = %r{^[a-zA-Z]{3,5}://}
|
|
67
|
+
|
|
68
|
+
#
|
|
69
|
+
# Initialize asset at <tt>path</tt>. Accepts an optional hash of options:
|
|
70
|
+
#
|
|
71
|
+
# [<tt>:base</tt>]
|
|
72
|
+
# Base context from which asset is required. Given a <tt>path</tt> of
|
|
73
|
+
# <tt>../images/logo.png</tt> and a <tt>:base</tt> of <tt>/project/design/css</tt>,
|
|
74
|
+
# the asset file will be assumed to live in <tt>/project/design/images/logo.png</tt>
|
|
75
|
+
# Defaults to the current directory.
|
|
76
|
+
# [<tt>:hosts</tt>]
|
|
77
|
+
# Array of host names that are served from <tt>:document_root</tt>. May also
|
|
78
|
+
# include scheme/protocol. If not, http is assumed.
|
|
79
|
+
# [<tt>:document_root</tt>]
|
|
80
|
+
# The root directory for absolute URLs (ie, the server's document root). This
|
|
81
|
+
# option is needed when resolving absolute URLs that include a hostname as well
|
|
82
|
+
# as when generating absolute paths.
|
|
83
|
+
#
|
|
84
|
+
def initialize(path, options = {})
|
|
85
|
+
@path = path
|
|
86
|
+
@filename = nil
|
|
87
|
+
@absolute_path = nil
|
|
88
|
+
@relative_path = nil
|
|
89
|
+
@path_has_host = @path =~ @@scheme_pattern
|
|
90
|
+
@path_is_absolute = @path_has_host || @path =~ /^\//
|
|
91
|
+
|
|
92
|
+
# Options
|
|
93
|
+
@base = options[:base] || Dir.pwd
|
|
94
|
+
@document_root = options[:document_root]
|
|
95
|
+
@hosts = Juicer::Asset::Path.hosts_with_scheme(options[:hosts])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# Returns absolute path calculated using the <tt>#document_root</tt>.
|
|
100
|
+
# Optionally accepts a hash of options:
|
|
101
|
+
#
|
|
102
|
+
# [<tt>:host</tt>] Return fully qualified URL with this host name. May include
|
|
103
|
+
# scheme/protocol. Default scheme is http.
|
|
104
|
+
# [<tt>:cache_buster</tt>] The parameter name for the cache buster.
|
|
105
|
+
# [<tt>:cache_buster_type</tt>] The kind of cache buster to add, <tt>:soft</tt>
|
|
106
|
+
# or <tt>:hard</tt>.
|
|
107
|
+
#
|
|
108
|
+
# A cache buster will be added if either (or both) of the <tt>:cache_buster</tt>
|
|
109
|
+
# or <tt>:cache_buster_type</tt> options are provided. The default cache buster
|
|
110
|
+
# type is <tt>:soft</tt>.
|
|
111
|
+
#
|
|
112
|
+
# Raises an ArgumentException if no <tt>document_root</tt> has been set.
|
|
113
|
+
#
|
|
114
|
+
def absolute_path(options = {})
|
|
115
|
+
if !@absolute_path
|
|
116
|
+
# Pre-conditions
|
|
117
|
+
raise ArgumentError.new("No document root set") if @document_root.nil?
|
|
118
|
+
|
|
119
|
+
@absolute_path = filename.sub(%r{^#@document_root}, '').sub(/^\/?/, '/')
|
|
120
|
+
@absolute_path = "#{Juicer::Asset::Path.host_with_scheme(options[:host])}#@absolute_path"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
path_with_cache_buster(@absolute_path, options)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
#
|
|
127
|
+
# Return path relative to <tt>#base</tt>
|
|
128
|
+
#
|
|
129
|
+
# Accepts an optional hash of options for cache busters:
|
|
130
|
+
#
|
|
131
|
+
# [<tt>:cache_buster</tt>] The parameter name for the cache buster.
|
|
132
|
+
# [<tt>:cache_buster_type</tt>] The kind of cache buster to add, <tt>:soft</tt>
|
|
133
|
+
# or <tt>:hard</tt>.
|
|
134
|
+
#
|
|
135
|
+
# A cache buster will be added if either (or both) of the <tt>:cache_buster</tt>
|
|
136
|
+
# or <tt>:cache_buster_type</tt> options are provided. The default cache buster
|
|
137
|
+
# type is <tt>:soft</tt>.
|
|
138
|
+
#
|
|
139
|
+
def relative_path(options = {})
|
|
140
|
+
@relative_path ||= Pathname.new(filename).relative_path_from(Pathname.new(base)).to_s
|
|
141
|
+
path_with_cache_buster(@relative_path, options)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# Returns the original path.
|
|
146
|
+
#
|
|
147
|
+
# Accepts an optional hash of options for cache busters:
|
|
148
|
+
#
|
|
149
|
+
# [<tt>:cache_buster</tt>] The parameter name for the cache buster.
|
|
150
|
+
# [<tt>:cache_buster_type</tt>] The kind of cache buster to add, <tt>:soft</tt>
|
|
151
|
+
# or <tt>:hard</tt>.
|
|
152
|
+
#
|
|
153
|
+
# A cache buster will be added if either (or both) of the <tt>:cache_buster</tt>
|
|
154
|
+
# or <tt>:cache_buster_type</tt> options are provided. The default cache buster
|
|
155
|
+
# type is <tt>:soft</tt>.
|
|
156
|
+
#
|
|
157
|
+
def path(options = {})
|
|
158
|
+
path_with_cache_buster(@path, options)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
#
|
|
162
|
+
# Return filename on disk. Requires the <tt>#document_root</tt> to be set if
|
|
163
|
+
# original path was an absolute one.
|
|
164
|
+
#
|
|
165
|
+
# If asset path includes scheme/protocol and host, it can only be resolved if
|
|
166
|
+
# a match is found in <tt>#hosts</tt>. Otherwise, an exeception is raised.
|
|
167
|
+
#
|
|
168
|
+
def filename
|
|
169
|
+
return @filename if @filename
|
|
170
|
+
|
|
171
|
+
# Pre-conditions
|
|
172
|
+
raise ArgumentError.new("No document root set") if @path_is_absolute && @document_root.nil?
|
|
173
|
+
raise ArgumentError.new("No hosts served from document root") if @path_has_host && @hosts.empty?
|
|
174
|
+
|
|
175
|
+
path = strip_host(@path)
|
|
176
|
+
raise ArgumentError.new("No matching host found for #{@path}") if path =~ @@scheme_pattern
|
|
177
|
+
|
|
178
|
+
dir = @path_is_absolute ? document_root : base
|
|
179
|
+
@filename = File.expand_path(File.join(dir, path))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#
|
|
183
|
+
# Rebase path and return a new Asset::Path object.
|
|
184
|
+
#
|
|
185
|
+
# asset = Juicer::Asset::Path.new "../images/logo.png", :base => "/var/www/public/stylesheets"
|
|
186
|
+
# asset2 = asset.rebase("/var/www/public")
|
|
187
|
+
# asset2.relative_path #=> "images/logo.png"
|
|
188
|
+
#
|
|
189
|
+
def rebase(base_path)
|
|
190
|
+
path = Pathname.new(filename).relative_path_from(Pathname.new(base_path)).to_s
|
|
191
|
+
|
|
192
|
+
Juicer::Asset::Path.new(path,
|
|
193
|
+
:base => base_path,
|
|
194
|
+
:hosts => hosts,
|
|
195
|
+
:document_root => document_root)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
#
|
|
199
|
+
# Returns basename of filename on disk
|
|
200
|
+
#
|
|
201
|
+
def basename
|
|
202
|
+
File.basename(filename)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#
|
|
206
|
+
# Returns basename of filename on disk
|
|
207
|
+
#
|
|
208
|
+
def dirname
|
|
209
|
+
File.dirname(filename)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
#
|
|
213
|
+
# Returns <tt>true</tt> if file exists on disk
|
|
214
|
+
#
|
|
215
|
+
def exists?
|
|
216
|
+
File.exists?(filename)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
#
|
|
220
|
+
# Accepts a single host, or an array of hosts and returns an array of hosts
|
|
221
|
+
# that include scheme/protocol, and don't have trailing slash.
|
|
222
|
+
#
|
|
223
|
+
def self.hosts_with_scheme(hosts)
|
|
224
|
+
hosts.nil? ? [] : [hosts].flatten.collect { |host| self.host_with_scheme(host) }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
#
|
|
228
|
+
# Assures that a host has scheme/protocol and no trailing slash
|
|
229
|
+
#
|
|
230
|
+
def self.host_with_scheme(host)
|
|
231
|
+
return host if host.nil?
|
|
232
|
+
(host !~ @@scheme_pattern ? "http://#{host}" : host).sub(/\/$/, '')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def <=>(other)
|
|
236
|
+
filename <=> other.filename
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
#
|
|
241
|
+
# Adds cache buster to paths if :cache_buster_type and :cache_buster indicates
|
|
242
|
+
# they should be added.
|
|
243
|
+
#
|
|
244
|
+
def path_with_cache_buster(path, options = {})
|
|
245
|
+
return path if !options.key?(:cache_buster) && options[:cache_buster_type].nil?
|
|
246
|
+
|
|
247
|
+
buster_path = nil
|
|
248
|
+
type = options[:cache_buster_type] || :soft
|
|
249
|
+
|
|
250
|
+
if options.key?(:cache_buster)
|
|
251
|
+
# Pass :cache_buster even if it's nil
|
|
252
|
+
buster_path = Juicer::CacheBuster.send(type, filename, options[:cache_buster])
|
|
253
|
+
else
|
|
254
|
+
# If :cache_buster wasn't specified, rely on default value
|
|
255
|
+
buster_path = Juicer::CacheBuster.send(type, filename)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
path.sub(File.basename(path), File.basename(buster_path))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
#
|
|
262
|
+
# Strip known hosts from path
|
|
263
|
+
#
|
|
264
|
+
def strip_host(path)
|
|
265
|
+
hosts.each do |host|
|
|
266
|
+
return path if path !~ @@scheme_pattern
|
|
267
|
+
|
|
268
|
+
path.sub!(%r{^#{host}}, '')
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
return path
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
require "juicer/asset/path"
|
|
4
|
+
|
|
5
|
+
module Juicer
|
|
6
|
+
module Asset
|
|
7
|
+
#
|
|
8
|
+
# Factory class that creates <tt>Juicer::Asset::Path</tt> objects from a common set of
|
|
9
|
+
# options. Also facilitates asset host cycling on a set of asset paths.
|
|
10
|
+
#
|
|
11
|
+
# path_resolver = Juicer::Asset::PathResolver.new(
|
|
12
|
+
# :document_root => "/var/www",
|
|
13
|
+
# :hosts => ["assets1.mysite.com", "assets2.mysite.com"]
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# asset = path_resolver.resolve("../images/logo.png")
|
|
17
|
+
# asset.document_root
|
|
18
|
+
# #=> "/var/www"
|
|
19
|
+
#
|
|
20
|
+
# asset.absolute_path(path_resolver.cycle_hosts)
|
|
21
|
+
# #=> "http://assets1.mysite.com/images/logo.png"
|
|
22
|
+
#
|
|
23
|
+
# asset = path_resolver.resolve("/favicon.ico")
|
|
24
|
+
# asset.absolute_path(path_resolver.cycle_hosts)
|
|
25
|
+
# #=> "http://assets2.mysite.com/favicon.ico"
|
|
26
|
+
#
|
|
27
|
+
# Author:: Christian Johansen (christian@cjohansen.no)
|
|
28
|
+
# Copyright:: Copyright (c) 2009 Christian Johansen
|
|
29
|
+
# License:: BSD
|
|
30
|
+
#
|
|
31
|
+
class PathResolver
|
|
32
|
+
attr_reader :hosts, :document_root, :base
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Initialize resolver. All options set on the resolver will be carried on to the
|
|
36
|
+
# resolved assets.
|
|
37
|
+
#
|
|
38
|
+
def initialize(options = {})
|
|
39
|
+
options[:base] ||= Dir.pwd
|
|
40
|
+
@options = options
|
|
41
|
+
@base = options[:base]
|
|
42
|
+
@hosts = Juicer::Asset::Path.hosts_with_scheme(options[:hosts]) || []
|
|
43
|
+
@current_host = 0
|
|
44
|
+
@document_root = @options[:document_root]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#
|
|
48
|
+
# Returns a <tt>Juicer::Asset::Path</tt> object for the given path, and the options
|
|
49
|
+
# set on the resolver.
|
|
50
|
+
#
|
|
51
|
+
def resolve(path)
|
|
52
|
+
Juicer::Asset::Path.new(path, @options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# Set new base directory. Will affect any assets resolved from here, but any
|
|
57
|
+
# assets previously resolved will not be changed
|
|
58
|
+
#
|
|
59
|
+
def base=(base)
|
|
60
|
+
@base = base
|
|
61
|
+
@options[:base] = base
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Cycle asset hosts. Returns an asset host
|
|
66
|
+
#
|
|
67
|
+
def cycle_hosts
|
|
68
|
+
return nil if @hosts.length == 0
|
|
69
|
+
|
|
70
|
+
host = @hosts[@current_host % @hosts.length]
|
|
71
|
+
@current_host += 1
|
|
72
|
+
|
|
73
|
+
host
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
alias host cycle_hosts
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/juicer/binary.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require
|
|
1
|
+
require "juicer/chainable"
|
|
2
2
|
|
|
3
3
|
module Juicer
|
|
4
4
|
|
|
@@ -12,12 +12,11 @@ module Juicer
|
|
|
12
12
|
# allowed to set on the binary.
|
|
13
13
|
#
|
|
14
14
|
module Binary
|
|
15
|
-
|
|
16
15
|
# Initialize binary with options
|
|
17
16
|
# options = Hash of options, optional
|
|
18
17
|
#
|
|
19
18
|
def initialize(binary, options = {})
|
|
20
|
-
@options = self.respond_to?(:
|
|
19
|
+
@options = self.respond_to?(:default_options) ? default_options.merge(options) : options
|
|
21
20
|
@opt_set = false
|
|
22
21
|
@command = nil
|
|
23
22
|
@binary = binary
|
|
@@ -31,7 +30,6 @@ module Juicer
|
|
|
31
30
|
# Run command
|
|
32
31
|
#
|
|
33
32
|
def execute(params = nil)
|
|
34
|
-
#puts "#{self.command} #{params}"
|
|
35
33
|
cmd = IO.popen("#{self.command} #{params}", "r")
|
|
36
34
|
results = cmd.gets(nil)
|
|
37
35
|
cmd.close
|
|
@@ -131,7 +129,7 @@ module Juicer
|
|
|
131
129
|
#
|
|
132
130
|
def locate(bin_glob, env = nil)
|
|
133
131
|
path << ENV[env] if env && ENV.key?(env) && File.exist?(ENV[env])
|
|
134
|
-
|
|
132
|
+
|
|
135
133
|
(path << Dir.pwd).each do |path|
|
|
136
134
|
files = Dir.glob(File.expand_path(File.join(path, bin_glob)))
|
|
137
135
|
return files unless files.empty?
|
data/lib/juicer/cache_buster.rb
CHANGED
|
@@ -1,45 +1,130 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
1
3
|
module Juicer
|
|
2
4
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# Assists in creating filenames that reflect the last change to the file. These
|
|
6
|
+
# kinds of filenames are useful when serving static content through a web server.
|
|
7
|
+
# If the filename changes everytime the file is modified, you can safely configure
|
|
8
|
+
# the web server to cache files indefinately, and know that the updated filename
|
|
9
|
+
# will cause the file to be downloaded again - only once - when it has changed.
|
|
10
|
+
#
|
|
11
|
+
# = Types of cache busters
|
|
8
12
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
13
|
+
# == Query string / "soft" cache busters
|
|
14
|
+
# Soft cache busters require no web server configuration. However, it is not
|
|
15
|
+
# guaranteed to work in all settings. For example, older default
|
|
16
|
+
# configurations for popular proxy server Squid does not consider a known URL
|
|
17
|
+
# with a new query string a new URL, and thus will not download the file over.
|
|
14
18
|
#
|
|
19
|
+
# The soft cache busters transforms
|
|
20
|
+
# <tt>/images/logo.png</tt> to <tt>/images/logo.png?cb=1232923789</tt>
|
|
21
|
+
#
|
|
22
|
+
# == Filename change / "hard" cache busters
|
|
15
23
|
# Hard cache busters change the file name itself, and thus requires either
|
|
16
24
|
# the web server to (internally) rewrite requests for these files to the
|
|
17
25
|
# original ones, or the file names to actually change. Hard cache busters
|
|
18
|
-
# transforms
|
|
26
|
+
# transforms <tt>/images/logo.png</tt> to <tt>/images/logo-1232923789.png</tt>
|
|
27
|
+
#
|
|
28
|
+
# Hard cache busters are guaranteed to work, and is the recommended variant.
|
|
29
|
+
# An example configuration for the Apache web server that does not require
|
|
30
|
+
# you to actually change the filenames can be seen below.
|
|
31
|
+
#
|
|
32
|
+
# <VirtualHost *>
|
|
33
|
+
# # Application/website configuration
|
|
34
|
+
#
|
|
35
|
+
# # Cache static resources for a year
|
|
36
|
+
# <FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
|
|
37
|
+
# ExpiresActive On
|
|
38
|
+
# ExpiresDefault "access plus 1 year"
|
|
39
|
+
# </FilesMatch>
|
|
40
|
+
#
|
|
41
|
+
# # Rewrite URLs like /images/logo-cb1234567890.png to /images/logo.png
|
|
42
|
+
# RewriteEngine On
|
|
43
|
+
# RewriteRule (.*)-cb\d+\.(.*)$ $1.$2 [L]
|
|
44
|
+
# </VirtualHost>])
|
|
45
|
+
#
|
|
46
|
+
# = Consecutive calls
|
|
47
|
+
#
|
|
48
|
+
# Consecutive calls to add a cache buster to a path will replace the existing
|
|
49
|
+
# cache buster *as long as the parameter name is the same*. Consider this:
|
|
50
|
+
#
|
|
51
|
+
# file = Juicer::CacheBuster.hard("/home/file.png") #=> "/home/file-cb1234567890.png"
|
|
52
|
+
# Juicer::CacheBuster.hard(file) #=> "/home/file-cb1234567891.png"
|
|
53
|
+
#
|
|
54
|
+
# # Changing the parameter name breaks this
|
|
55
|
+
# Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-cb1234567891-juicer1234567892.png"
|
|
56
|
+
#
|
|
57
|
+
# Avoid this type of trouble simply be cleaning the URL with the old name first:
|
|
58
|
+
#
|
|
59
|
+
# Juicer::CacheBuster.clean(file) #=> "/home/file.png"
|
|
60
|
+
# file = Juicer::CacheBuster.hard(file, :juicer) #=> "/home/file-juicer1234567892.png"
|
|
61
|
+
# Juicer::CacheBuster.clean(file, :juicer) #=> "/home/file.png"
|
|
62
|
+
#
|
|
63
|
+
# Author:: Christian Johansen (christian@cjohansen.no)
|
|
64
|
+
# Copyright:: Copyright (c) 2009 Christian Johansen
|
|
65
|
+
# License:: BSD
|
|
19
66
|
#
|
|
20
67
|
module CacheBuster
|
|
68
|
+
DEFAULT_PARAMETER = "jcb"
|
|
69
|
+
|
|
21
70
|
#
|
|
22
71
|
# Creates a unique file name for every revision to the files contents.
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
72
|
+
# Raises an <tt>ArgumentError</tt> if the file can not be found.
|
|
73
|
+
#
|
|
74
|
+
# The type indicates which type of cache buster you want, <tt>:soft</tt>
|
|
75
|
+
# or <tt>:hard</tt>. Default is <tt>:soft</tt>. If an unsupported value
|
|
76
|
+
# is specified, <tt>:soft</tt> will be used.
|
|
26
77
|
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
78
|
+
# See <tt>#hard</tt> and <tt>#soft</tt> for explanation of the parameter
|
|
79
|
+
# argument.
|
|
80
|
+
#
|
|
81
|
+
def self.path(file, type = :soft, parameter = DEFAULT_PARAMETER)
|
|
82
|
+
file = self.clean(file, parameter)
|
|
83
|
+
filename = file.split("?").first
|
|
84
|
+
raise ArgumentError.new("#{file} could not be found") unless File.exists?(filename)
|
|
85
|
+
mtime = File.mtime(filename).to_i
|
|
86
|
+
type = [:soft, :hard].include?(type) ? type : :soft
|
|
32
87
|
|
|
33
88
|
if type == :soft
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"#{file}#{file.index('?') ? '&' : '?'}#{param}#{mtime}"
|
|
37
|
-
else
|
|
38
|
-
parts = file.split(".")
|
|
39
|
-
suffix = parts.pop
|
|
40
|
-
file = parts.join.sub(/-#{param}\d+/, "")
|
|
41
|
-
"#{parts.join('.')}-#{param}#{mtime}.#{suffix}"
|
|
89
|
+
parameter = "#{parameter}=".sub(/^=$/, '')
|
|
90
|
+
return "#{file}#{file.index('?') ? '&' : '?'}#{parameter}#{mtime}"
|
|
42
91
|
end
|
|
92
|
+
|
|
93
|
+
file.sub(/(\.[^\.]+$)/, "-#{parameter}#{mtime}" + '\1')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#
|
|
97
|
+
# Add a hard cache buster to a filename. The parameter is an optional prefix
|
|
98
|
+
# that is added before the mtime timestamp. It results in filenames of the form:
|
|
99
|
+
# <tt>file-[parameter name][timestamp].suffix</tt>, ie
|
|
100
|
+
# <tt>images/logo-cb1234567890.png</tt> which is the case for the default
|
|
101
|
+
# parameter name "cb" (as in *c*ache *b*uster).
|
|
102
|
+
#
|
|
103
|
+
def self.hard(file, parameter = DEFAULT_PARAMETER)
|
|
104
|
+
self.path(file, :hard, parameter)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
#
|
|
108
|
+
# Add a soft cache buster to a filename. The parameter is an optional name
|
|
109
|
+
# for the mtime timestamp value. It results in filenames of the form:
|
|
110
|
+
# <tt>file.suffix?[parameter name]=[timestamp]</tt>, ie
|
|
111
|
+
# <tt>images/logo.png?cb=1234567890</tt> which is the case for the default
|
|
112
|
+
# parameter name "cb" (as in *c*ache *b*uster).
|
|
113
|
+
#
|
|
114
|
+
def self.soft(file, parameter = DEFAULT_PARAMETER)
|
|
115
|
+
self.path(file, :soft, parameter)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# Remove cache buster from a URL for a given parameter name. Parameter name is
|
|
120
|
+
# "cb" by default.
|
|
121
|
+
#
|
|
122
|
+
def self.clean(file, parameter = DEFAULT_PARAMETER)
|
|
123
|
+
query_param = "#{parameter}".length == 0 ? "" : "#{parameter}="
|
|
124
|
+
new_file = file.sub(/#{query_param}\d+&?/, "").sub(/(\?|&)$/, "")
|
|
125
|
+
return new_file unless new_file == file
|
|
126
|
+
|
|
127
|
+
file.sub(/-#{parameter}\d+(\.\w+)($|\?)/, '\1\2')
|
|
43
128
|
end
|
|
44
129
|
end
|
|
45
130
|
end
|