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.
Files changed (122) hide show
  1. data/History.txt +28 -0
  2. data/Rakefile +84 -36
  3. data/Readme.rdoc +192 -23
  4. data/VERSION +1 -0
  5. data/bin/juicer +2 -4
  6. data/lib/juicer.rb +9 -10
  7. data/lib/juicer/asset/path.rb +275 -0
  8. data/lib/juicer/asset/path_resolver.rb +79 -0
  9. data/lib/juicer/binary.rb +3 -5
  10. data/lib/juicer/cache_buster.rb +112 -27
  11. data/lib/juicer/command/install.rb +4 -2
  12. data/lib/juicer/command/list.rb +16 -9
  13. data/lib/juicer/command/merge.rb +30 -14
  14. data/lib/juicer/command/verify.rb +1 -1
  15. data/lib/juicer/css_cache_buster.rb +31 -47
  16. data/lib/juicer/datafy/datafy.rb +20 -0
  17. data/lib/juicer/dependency_resolver/css_dependency_resolver.rb +29 -0
  18. data/lib/juicer/dependency_resolver/dependency_resolver.rb +101 -0
  19. data/lib/juicer/dependency_resolver/javascript_dependency_resolver.rb +23 -0
  20. data/lib/juicer/ext/logger.rb +5 -0
  21. data/lib/juicer/ext/string.rb +47 -0
  22. data/lib/juicer/ext/symbol.rb +15 -0
  23. data/lib/juicer/image_embed.rb +129 -0
  24. data/lib/juicer/install/base.rb +2 -2
  25. data/lib/juicer/install/closure_compiler_installer.rb +69 -0
  26. data/lib/juicer/install/jslint_installer.rb +3 -3
  27. data/lib/juicer/install/rhino_installer.rb +3 -2
  28. data/lib/juicer/install/yui_compressor_installer.rb +3 -2
  29. data/lib/juicer/jslint.rb +1 -1
  30. data/lib/juicer/merger/base.rb +1 -1
  31. data/lib/juicer/merger/javascript_merger.rb +3 -4
  32. data/lib/juicer/merger/stylesheet_merger.rb +13 -15
  33. data/lib/juicer/minifyer/closure_compiler.rb +90 -0
  34. data/lib/juicer/minifyer/java_base.rb +77 -0
  35. data/lib/juicer/minifyer/yui_compressor.rb +15 -48
  36. data/test/bin/jslint-1.0.js +523 -0
  37. data/test/bin/jslint.js +523 -0
  38. data/test/bin/rhino1_7R1.zip +0 -0
  39. data/test/bin/rhino1_7R2-RC1.jar +0 -0
  40. data/test/bin/rhino1_7R2-RC1.zip +0 -0
  41. data/test/bin/yuicompressor +0 -0
  42. data/test/bin/yuicompressor-2.3.5.zip +0 -0
  43. data/test/bin/yuicompressor-2.4.2.jar +0 -0
  44. data/test/bin/yuicompressor-2.4.2.zip +0 -0
  45. data/test/data/Changelog.txt +10 -0
  46. data/test/data/a.css +3 -0
  47. data/test/data/a.js +5 -0
  48. data/test/data/a1.css +5 -0
  49. data/test/data/b.css +1 -0
  50. data/test/data/b.js +5 -0
  51. data/test/data/b1.css +5 -0
  52. data/test/data/c1.css +3 -0
  53. data/test/data/css/2.gif +1 -0
  54. data/test/data/css/test.css +11 -0
  55. data/test/data/css/test2.css +1 -0
  56. data/test/data/d1.css +3 -0
  57. data/test/data/images/1.png +1 -0
  58. data/test/data/my_app.js +2 -0
  59. data/test/data/not-ok.js +2 -0
  60. data/test/data/ok.js +3 -0
  61. data/test/data/path_test.css +5 -0
  62. data/test/data/path_test2.css +14 -0
  63. data/test/data/pkg/module/moda.js +2 -0
  64. data/test/data/pkg/module/modb.js +3 -0
  65. data/test/data/pkg/pkg.js +1 -0
  66. data/test/fixtures/yui-download.html +425 -0
  67. data/test/test_helper.rb +36 -7
  68. data/test/unit/juicer/asset/path_resolver_test.rb +76 -0
  69. data/test/unit/juicer/asset/path_test.rb +370 -0
  70. data/test/unit/juicer/cache_buster_test.rb +104 -0
  71. data/test/{juicer/test_chainable.rb → unit/juicer/chainable_test.rb} +1 -1
  72. data/test/unit/juicer/command/install_test.rb +58 -0
  73. data/test/{juicer/command/test_list.rb → unit/juicer/command/list_test.rb} +26 -14
  74. data/test/unit/juicer/command/merge_test.rb +162 -0
  75. data/test/{juicer/command/test_util.rb → unit/juicer/command/util_test.rb} +10 -6
  76. data/test/unit/juicer/command/verify_test.rb +48 -0
  77. data/test/{juicer/test_css_cache_buster.rb → unit/juicer/css_cache_buster_test.rb} +10 -30
  78. data/test/unit/juicer/datafy_test.rb +37 -0
  79. data/test/{juicer/merger/test_css_dependency_resolver.rb → unit/juicer/dependency_resolver/css_dependency_resolver_test.rb} +2 -2
  80. data/test/{juicer/merger/test_javascript_dependency_resolver.rb → unit/juicer/dependency_resolver/javascript_dependency_resolver_test.rb} +13 -2
  81. data/test/unit/juicer/ext/{#string_test.rb# → string_test.rb} +0 -7
  82. data/test/unit/juicer/ext/symbol_test.rb +27 -0
  83. data/test/unit/juicer/image_embed_test.rb +271 -0
  84. data/test/unit/juicer/install/installer_base_test.rb +214 -0
  85. data/test/{juicer/install/test_jslint_installer.rb → unit/juicer/install/jslint_installer_test.rb} +1 -1
  86. data/test/{juicer/install/test_rhino_installer.rb → unit/juicer/install/rhino_installer_test.rb} +1 -1
  87. data/test/{juicer/install/test_yui_compressor_installer.rb → unit/juicer/install/yui_compressor_test.rb} +16 -16
  88. data/test/unit/juicer/jslint_test.rb +60 -0
  89. data/test/{juicer/merger/test_base.rb → unit/juicer/merger/base_test.rb} +1 -1
  90. data/test/{juicer/merger/test_javascript_merger.rb → unit/juicer/merger/javascript_merger_test.rb} +2 -2
  91. data/test/{juicer/merger/test_stylesheet_merger.rb → unit/juicer/merger/stylesheet_merger_test.rb} +15 -13
  92. data/test/unit/juicer/minifyer/closure_compressor_test.rb +107 -0
  93. data/test/{integration → unit}/juicer/minifyer/yui_compressor_test.rb +30 -47
  94. data/test/unit/juicer_test.rb +1 -0
  95. metadata +207 -113
  96. data/lib/juicer/core.rb +0 -61
  97. data/lib/juicer/merger/css_dependency_resolver.rb +0 -25
  98. data/lib/juicer/merger/dependency_resolver.rb +0 -82
  99. data/lib/juicer/merger/javascript_dependency_resolver.rb +0 -21
  100. data/tasks/ann.rake +0 -80
  101. data/tasks/bones.rake +0 -20
  102. data/tasks/gem.rake +0 -201
  103. data/tasks/git.rake +0 -40
  104. data/tasks/notes.rake +0 -27
  105. data/tasks/post_load.rake +0 -34
  106. data/tasks/rdoc.rake +0 -51
  107. data/tasks/rubyforge.rake +0 -55
  108. data/tasks/setup.rb +0 -292
  109. data/tasks/spec.rake +0 -54
  110. data/tasks/svn.rake +0 -47
  111. data/tasks/test.rake +0 -40
  112. data/tasks/test/setup.rake +0 -35
  113. data/tasks/zentest.rake +0 -36
  114. data/test/juicer/command/test_install.rb +0 -53
  115. data/test/juicer/command/test_merge.rb +0 -160
  116. data/test/juicer/command/test_verify.rb +0 -33
  117. data/test/juicer/install/test_installer_base.rb +0 -195
  118. data/test/juicer/minifyer/test_yui_compressor.rb +0 -159
  119. data/test/juicer/test_cache_buster.rb +0 -58
  120. data/test/juicer/test_core.rb +0 -47
  121. data/test/juicer/test_jslint.rb +0 -33
  122. 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
- dir = File.dirname(File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__)
4
- base = File.expand_path(File.join(dir, %w[.. lib juicer]))
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.2.6'
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 that has the same name as the filename passed
55
- # in. Optionally, a specific _directory_ name can be passed in such that
56
- # the _filename_ does not have to be equivalent to the directory.
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
- Dir.glob(search_me).sort.each { |rb| require rb }
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.require_all_libs_relative_to(__FILE__)
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 File.expand_path(File.join(File.dirname(__FILE__), "chainable"))
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?(:defualt_options) ? default_options.merge(options) : options
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?
@@ -1,45 +1,130 @@
1
+ # -*- coding: utf-8 -*-
2
+
1
3
  module Juicer
2
4
  #
3
- # Tool that assists in creating filenames that update everytime the file
4
- # contents change. There's two ways of generating filenames, soft and hard.
5
- # The point of all this is to facilitate configuring web servers to send
6
- # static assets with a far future expires header - improving end user
7
- # performance through caching.
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
- # Soft cache busters require no web server configuration, but will not work
10
- # as intended with older default configurations for popular proxy server
11
- # Squid. The soft busters use query parameters to create unique file names,
12
- # and these may not force an update in some cases. The soft cache busters
13
- # transforms /images/logo.png to /images/logo.png?cb=1232923789
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 /images/logo.png to /images/logo-1232923789.png
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
- # Default parameter name for soft cache busters is cb (ie ?cb=<timestamp>)
24
- # while default parameter names for hard cache busters is none (ie
25
- # file-<timestamp>.png).
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
- def self.path(file, type = :soft, param = :undef)
28
- param = (type == :soft ? "jcb" : nil) if param == :undef
29
- f = File.new(file.split("?").first)
30
- mtime = f.mtime.to_i
31
- f.close
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
- param = "#{param}".length == 0 ? "" : "#{param}="
35
- file = file.sub(/#{param}\d+/, "").sub(/(\?|\&)$/, "")
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