sprockets 2.12.5 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sprockets might be problematic. Click here for more details.

Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +2 -2
  3. data/README.md +61 -34
  4. data/lib/rake/sprocketstask.rb +5 -4
  5. data/lib/sprockets.rb +123 -85
  6. data/lib/sprockets/asset.rb +161 -200
  7. data/lib/sprockets/asset_uri.rb +64 -0
  8. data/lib/sprockets/base.rb +138 -373
  9. data/lib/sprockets/bower.rb +56 -0
  10. data/lib/sprockets/bundle.rb +32 -0
  11. data/lib/sprockets/cache.rb +220 -0
  12. data/lib/sprockets/cache/file_store.rb +145 -13
  13. data/lib/sprockets/cache/memory_store.rb +66 -0
  14. data/lib/sprockets/cache/null_store.rb +46 -0
  15. data/lib/sprockets/cached_environment.rb +103 -0
  16. data/lib/sprockets/closure_compressor.rb +30 -12
  17. data/lib/sprockets/coffee_script_template.rb +23 -0
  18. data/lib/sprockets/compressing.rb +20 -25
  19. data/lib/sprockets/configuration.rb +95 -0
  20. data/lib/sprockets/context.rb +68 -131
  21. data/lib/sprockets/directive_processor.rb +138 -179
  22. data/lib/sprockets/eco_template.rb +10 -19
  23. data/lib/sprockets/ejs_template.rb +10 -19
  24. data/lib/sprockets/encoding_utils.rb +246 -0
  25. data/lib/sprockets/engines.rb +40 -29
  26. data/lib/sprockets/environment.rb +10 -66
  27. data/lib/sprockets/erb_template.rb +23 -0
  28. data/lib/sprockets/errors.rb +5 -13
  29. data/lib/sprockets/http_utils.rb +97 -0
  30. data/lib/sprockets/jst_processor.rb +28 -15
  31. data/lib/sprockets/lazy_processor.rb +15 -0
  32. data/lib/sprockets/legacy.rb +23 -0
  33. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  34. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  35. data/lib/sprockets/manifest.rb +128 -99
  36. data/lib/sprockets/mime.rb +114 -33
  37. data/lib/sprockets/path_utils.rb +179 -0
  38. data/lib/sprockets/paths.rb +13 -26
  39. data/lib/sprockets/processing.rb +198 -107
  40. data/lib/sprockets/resolve.rb +289 -0
  41. data/lib/sprockets/sass_compressor.rb +36 -17
  42. data/lib/sprockets/sass_template.rb +269 -46
  43. data/lib/sprockets/server.rb +113 -83
  44. data/lib/sprockets/transformers.rb +69 -0
  45. data/lib/sprockets/uglifier_compressor.rb +36 -15
  46. data/lib/sprockets/utils.rb +161 -44
  47. data/lib/sprockets/version.rb +1 -1
  48. data/lib/sprockets/yui_compressor.rb +37 -12
  49. metadata +64 -106
  50. data/lib/sprockets/asset_attributes.rb +0 -137
  51. data/lib/sprockets/bundled_asset.rb +0 -78
  52. data/lib/sprockets/caching.rb +0 -96
  53. data/lib/sprockets/charset_normalizer.rb +0 -41
  54. data/lib/sprockets/index.rb +0 -100
  55. data/lib/sprockets/processed_asset.rb +0 -152
  56. data/lib/sprockets/processor.rb +0 -32
  57. data/lib/sprockets/safety_colons.rb +0 -28
  58. data/lib/sprockets/sass_cache_store.rb +0 -29
  59. data/lib/sprockets/sass_functions.rb +0 -70
  60. data/lib/sprockets/sass_importer.rb +0 -30
  61. data/lib/sprockets/scss_template.rb +0 -13
  62. data/lib/sprockets/static_asset.rb +0 -60
@@ -1,9 +1,9 @@
1
1
  require 'time'
2
- require 'uri'
2
+ require 'rack/utils'
3
3
 
4
4
  module Sprockets
5
5
  # `Server` is a concern mixed into `Environment` and
6
- # `Index` that provides a Rack compatible `call`
6
+ # `CachedEnvironment` that provides a Rack compatible `call`
7
7
  # interface and url generation helpers.
8
8
  module Server
9
9
  # `call` implements the Rack 1.x specification which accepts an
@@ -23,59 +23,84 @@ module Sprockets
23
23
  start_time = Time.now.to_f
24
24
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
25
 
26
- msg = "Served asset #{env['PATH_INFO']} -"
26
+ if env['REQUEST_METHOD'] != 'GET'
27
+ return method_not_allowed_response
28
+ end
27
29
 
28
- # Mark session as "skipped" so no `Set-Cookie` header is set
29
- env['rack.session.options'] ||= {}
30
- env['rack.session.options'][:defer] = true
31
- env['rack.session.options'][:skip] = true
30
+ msg = "Served asset #{env['PATH_INFO']} -"
32
31
 
33
32
  # Extract the path from everything after the leading slash
34
- path = unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
35
-
36
- # Strip fingerprint
37
- if fingerprint = path_fingerprint(path)
38
- path = path.sub("-#{fingerprint}", '')
39
- end
33
+ path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
40
34
 
41
35
  # URLs containing a `".."` are rejected for security reasons.
42
36
  if forbidden_request?(path)
43
37
  return forbidden_response
44
38
  end
45
39
 
40
+ # Strip fingerprint
41
+ if fingerprint = path_fingerprint(path)
42
+ path = path.sub("-#{fingerprint}", '')
43
+ end
44
+
46
45
  # Look up the asset.
47
- asset = find_asset(path, :bundle => !body_only?(env))
46
+ options = {}
47
+ options[:bundle] = !body_only?(env)
48
48
 
49
- # `find_asset` returns nil if the asset doesn't exist
50
- if asset.nil?
51
- logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
49
+ if fingerprint
50
+ if_match = fingerprint
51
+ elsif env['HTTP_IF_MATCH']
52
+ if_match = env['HTTP_IF_MATCH'][/^"(\w+)"$/, 1]
53
+ end
52
54
 
53
- # Return a 404 Not Found
54
- not_found_response
55
+ if env['HTTP_IF_NONE_MATCH']
56
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/^"(\w+)"$/, 1]
57
+ end
55
58
 
56
- # Check request headers `HTTP_IF_NONE_MATCH` against the asset digest
57
- elsif etag_match?(asset, env)
58
- logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
59
+ if !if_match && !if_none_match && env['HTTP_ACCEPT_ENCODING']
60
+ # Accept-Encoding negotiation is only enabled for non-fingerprinted
61
+ # assets. Avoids the "Apache ETag gzip" bug. Just Google it.
62
+ # https://issues.apache.org/bugzilla/show_bug.cgi?id=39727
63
+ options[:accept_encoding] = env['HTTP_ACCEPT_ENCODING']
64
+ end
59
65
 
60
- # Return a 304 Not Modified
61
- not_modified_response(asset, env)
66
+ asset = find_asset(path, options)
62
67
 
68
+ if asset.nil?
69
+ status = :not_found
70
+ elsif fingerprint && asset.digest != fingerprint
71
+ status = :not_found
72
+ elsif if_match && asset.digest != if_match
73
+ status = :precondition_failed
74
+ elsif if_none_match && asset.digest == if_none_match
75
+ status = :not_modified
63
76
  else
64
- logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
77
+ status = :ok
78
+ end
65
79
 
66
- # Return a 200 with the asset contents
80
+ case status
81
+ when :ok
82
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
67
83
  ok_response(asset, env)
84
+ when :not_modified
85
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
86
+ not_modified_response(env, if_none_match)
87
+ when :not_found
88
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
89
+ not_found_response
90
+ when :precondition_failed
91
+ logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
92
+ precondition_failed_response
68
93
  end
69
94
  rescue Exception => e
70
95
  logger.error "Error compiling asset #{path}:"
71
96
  logger.error "#{e.class.name}: #{e.message}"
72
97
 
73
- case content_type_of(path)
74
- when "application/javascript"
98
+ case File.extname(path)
99
+ when ".js"
75
100
  # Re-throw JavaScript asset exceptions to the browser
76
101
  logger.info "#{msg} 500 Internal Server Error\n\n"
77
102
  return javascript_exception_response(e)
78
- when "text/css"
103
+ when ".css"
79
104
  # Display CSS asset exceptions in the browser
80
105
  logger.info "#{msg} 500 Internal Server Error\n\n"
81
106
  return css_exception_response(e)
@@ -90,7 +115,17 @@ module Sprockets
90
115
  #
91
116
  # http://example.org/assets/../../../etc/passwd
92
117
  #
93
- path.include?("..") || Pathname.new(path).absolute? || path.include?("://")
118
+ path.include?("..")
119
+ end
120
+
121
+ # Returns a 200 OK response tuple
122
+ def ok_response(asset, env)
123
+ [ 200, headers(env, asset, asset.length), asset ]
124
+ end
125
+
126
+ # Returns a 304 Not Modified response tuple
127
+ def not_modified_response(env, digest)
128
+ [ 304, cache_headers(env, digest), [] ]
94
129
  end
95
130
 
96
131
  # Returns a 403 Forbidden response tuple
@@ -103,12 +138,20 @@ module Sprockets
103
138
  [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
104
139
  end
105
140
 
141
+ def method_not_allowed_response
142
+ [ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
143
+ end
144
+
145
+ def precondition_failed_response
146
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
147
+ end
148
+
106
149
  # Returns a JavaScript response that re-throws a Ruby exception
107
150
  # in the browser
108
151
  def javascript_exception_response(exception)
109
- err = "#{exception.class.name}: #{exception.message}"
152
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
110
153
  body = "throw Error(#{err.inspect})"
111
- [ 200, { "Content-Type" => "application/javascript", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
154
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
112
155
  end
113
156
 
114
157
  # Returns a CSS response that hides all elements on the page and
@@ -161,7 +204,7 @@ module Sprockets
161
204
  }
162
205
  CSS
163
206
 
164
- [ 200, { "Content-Type" => "text/css;charset=utf-8", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
207
+ [ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
165
208
  end
166
209
 
167
210
  # Escape special characters for use inside a CSS content("...") string
@@ -173,47 +216,53 @@ module Sprockets
173
216
  gsub('/', '\\\\002f ')
174
217
  end
175
218
 
176
- # Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
177
- def etag_match?(asset, env)
178
- env["HTTP_IF_NONE_MATCH"] == etag(asset)
179
- end
180
-
181
219
  # Test if `?body=1` or `body=true` query param is set
182
220
  def body_only?(env)
183
221
  env["QUERY_STRING"].to_s =~ /body=(1|t)/
184
222
  end
185
223
 
186
- # Returns a 304 Not Modified response tuple
187
- def not_modified_response(asset, env)
188
- [ 304, {}, [] ]
189
- end
224
+ def cache_headers(env, digest)
225
+ headers = {}
190
226
 
191
- # Returns a 200 OK response tuple
192
- def ok_response(asset, env)
193
- [ 200, headers(env, asset, asset.length), asset ]
227
+ # Set caching headers
228
+ headers["Cache-Control"] = "public"
229
+ headers["ETag"] = %("#{digest}")
230
+
231
+ # If the request url contains a fingerprint, set a long
232
+ # expires on the response
233
+ if path_fingerprint(env["PATH_INFO"])
234
+ headers["Cache-Control"] << ", max-age=31536000"
235
+
236
+ # Otherwise set `must-revalidate` since the asset could be modified.
237
+ else
238
+ headers["Cache-Control"] << ", must-revalidate"
239
+ headers["Vary"] = "Accept-Encoding"
240
+ end
241
+
242
+ headers
194
243
  end
195
244
 
196
245
  def headers(env, asset, length)
197
- Hash.new.tap do |headers|
198
- # Set content type and length headers
199
- headers["Content-Type"] = asset.content_type
200
- headers["Content-Length"] = length.to_s
201
-
202
- # Set caching headers
203
- headers["Cache-Control"] = "public"
204
- headers["Last-Modified"] = asset.mtime.httpdate
205
- headers["ETag"] = etag(asset)
206
-
207
- # If the request url contains a fingerprint, set a long
208
- # expires on the response
209
- if path_fingerprint(env["PATH_INFO"])
210
- headers["Cache-Control"] << ", max-age=31536000"
211
-
212
- # Otherwise set `must-revalidate` since the asset could be modified.
213
- else
214
- headers["Cache-Control"] << ", must-revalidate"
246
+ headers = {}
247
+
248
+ # Set content encoding
249
+ if asset.encoding
250
+ headers["Content-Encoding"] = asset.encoding
251
+ end
252
+
253
+ # Set content length header
254
+ headers["Content-Length"] = length.to_s
255
+
256
+ # Set content type header
257
+ if type = asset.content_type
258
+ # Set charset param for text/* mime types
259
+ if type.start_with?("text/") && asset.charset
260
+ type += "; charset=#{asset.charset}"
215
261
  end
262
+ headers["Content-Type"] = type
216
263
  end
264
+
265
+ headers.merge(cache_headers(env, asset.digest))
217
266
  end
218
267
 
219
268
  # Gets digest fingerprint.
@@ -222,26 +271,7 @@ module Sprockets
222
271
  # # => "0aa2105d29558f3eb790d411d7d8fb66"
223
272
  #
224
273
  def path_fingerprint(path)
225
- path[/-([0-9a-f]{7,40})\.[^.]+\z/, 1]
226
- end
227
-
228
- # URI.unescape is deprecated on 1.9. We need to use URI::Parser
229
- # if its available.
230
- if defined? URI::DEFAULT_PARSER
231
- def unescape(str)
232
- str = URI::DEFAULT_PARSER.unescape(str)
233
- str.force_encoding(Encoding.default_internal) if Encoding.default_internal
234
- str
235
- end
236
- else
237
- def unescape(str)
238
- URI.unescape(str)
239
- end
240
- end
241
-
242
- # Helper to quote the assets digest for use as an ETag.
243
- def etag(asset)
244
- %("#{asset.digest}")
274
+ path[/-([0-9a-f]{7,40})\.[^.]+$/, 1]
245
275
  end
246
276
  end
247
277
  end
@@ -0,0 +1,69 @@
1
+ module Sprockets
2
+ module Transformers
3
+ # Public: Two level mapping of a source mime type to a target mime type.
4
+ #
5
+ # environment.transformers
6
+ # # => { 'text/coffeescript' => {
7
+ # 'application/javascript' => ConvertCoffeeScriptToJavaScript
8
+ # }
9
+ # }
10
+ #
11
+ attr_reader :transformers
12
+
13
+ # Public: Register a transformer from and to a mime type.
14
+ #
15
+ # from - String mime type
16
+ # to - String mime type
17
+ # proc - Callable block that accepts an input Hash.
18
+ #
19
+ # Examples
20
+ #
21
+ # register_transformer 'text/coffeescript', 'application/javascript',
22
+ # ConvertCoffeeScriptToJavaScript
23
+ #
24
+ # register_transformer 'image/svg+xml', 'image/png', ConvertSvgToPng
25
+ #
26
+ # Returns nothing.
27
+ def register_transformer(from, to, proc)
28
+ mutate_hash_config(:transformers, from) do |transformers|
29
+ transformers.merge(to => proc)
30
+ end
31
+ end
32
+
33
+ # Public: Resolve target mime type that the source type should be
34
+ # transformed to.
35
+ #
36
+ # type - String from mime type
37
+ # accept - String accept type list (default: '*/*')
38
+ #
39
+ # Examples
40
+ #
41
+ # resolve_transform_type('text/plain', 'text/plain')
42
+ # # => 'text/plain'
43
+ #
44
+ # resolve_transform_type('image/svg+xml', 'image/png, image/*')
45
+ # # => 'image/png'
46
+ #
47
+ # resolve_transform_type('text/css', 'image/png')
48
+ # # => nil
49
+ #
50
+ # Returns String mime type or nil is no type satisfied the accept value.
51
+ def resolve_transform_type(type, accept = nil)
52
+ find_best_mime_type_match(accept || '*/*', [type].compact + transformers[type].keys)
53
+ end
54
+
55
+ # Internal: Find and load transformer by from and to mime type.
56
+ #
57
+ # from - String mime type
58
+ # to - String mime type
59
+ #
60
+ # Returns Array of Procs.
61
+ def unwrap_transformer(from, to)
62
+ if processor = transformers[from][to]
63
+ [unwrap_processor(processor)]
64
+ else
65
+ []
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,28 +1,49 @@
1
- require 'tilt'
1
+ require 'uglifier'
2
2
 
3
3
  module Sprockets
4
- class UglifierCompressor < Tilt::Template
5
- self.default_mime_type = 'application/javascript'
4
+ # Public: Uglifier/Uglify compressor.
5
+ #
6
+ # To accept the default options
7
+ #
8
+ # environment.register_bundle_processor 'application/javascript',
9
+ # Sprockets::UglifierCompressor
10
+ #
11
+ # Or to pass options to the Uglifier class.
12
+ #
13
+ # environment.register_bundle_processor 'application/javascript',
14
+ # Sprockets::UglifierCompressor.new(comments: :copyright)
15
+ #
16
+ class UglifierCompressor
17
+ VERSION = '1'
6
18
 
7
- def self.engine_initialized?
8
- defined?(::Uglifier)
19
+ def self.call(*args)
20
+ new.call(*args)
9
21
  end
10
22
 
11
- def initialize_engine
12
- require_template_library 'uglifier'
13
- end
14
-
15
- def prepare
16
- end
17
-
18
- def evaluate(context, locals, &block)
23
+ def initialize(options = {})
19
24
  # Feature detect Uglifier 2.0 option support
20
25
  if Uglifier::DEFAULTS[:copyright]
21
26
  # Uglifier < 2.x
22
- Uglifier.new(:copyright => false).compile(data)
27
+ options[:copyright] ||= false
23
28
  else
24
29
  # Uglifier >= 2.x
25
- Uglifier.new(:comments => :none).compile(data)
30
+ options[:copyright] ||= :none
31
+ end
32
+
33
+ @uglifier = ::Uglifier.new(options)
34
+
35
+ @cache_key = [
36
+ 'UglifierCompressor',
37
+ ::Uglifier::VERSION,
38
+ VERSION,
39
+ options
40
+ ]
41
+ end
42
+
43
+ def call(input)
44
+ data = input[:data]
45
+ input[:cache].fetch(@cache_key + [data]) do
46
+ @uglifier.compile(data)
26
47
  end
27
48
  end
28
49
  end
@@ -1,55 +1,48 @@
1
+ require 'digest/sha1'
2
+ require 'set'
3
+
1
4
  module Sprockets
2
5
  # `Utils`, we didn't know where else to put it!
3
6
  module Utils
4
- # If theres encoding support (aka Ruby 1.9)
5
- if "".respond_to?(:valid_encoding?)
6
- # Define UTF-8 BOM pattern matcher.
7
- # Avoid using a Regexp literal because it inheirts the files
8
- # encoding and we want to avoid syntax errors in other interpreters.
9
- UTF8_BOM_PATTERN = Regexp.new("\\A\uFEFF".encode('utf-8'))
10
-
11
- def self.read_unicode(pathname, external_encoding = Encoding.default_external)
12
- pathname.open("r:#{external_encoding}") do |f|
13
- f.read.tap do |data|
14
- # Eager validate the file's encoding. In most cases we
15
- # expect it to be UTF-8 unless `default_external` is set to
16
- # something else. An error is usually raised if the file is
17
- # saved as UTF-16 when we expected UTF-8.
18
- if !data.valid_encoding?
19
- raise EncodingError, "#{pathname} has a invalid " +
20
- "#{data.encoding} byte sequence"
21
-
22
- # If the file is UTF-8 and theres a BOM, strip it for safe concatenation.
23
- elsif data.encoding.name == "UTF-8" && data =~ UTF8_BOM_PATTERN
24
- data.sub!(UTF8_BOM_PATTERN, "")
25
- end
26
- end
7
+ extend self
8
+
9
+ # Internal: Check if string has a trailing semicolon.
10
+ #
11
+ # str - String
12
+ #
13
+ # Returns true or false.
14
+ def string_end_with_semicolon?(str)
15
+ i = str.size - 1
16
+ while i >= 0
17
+ c = str[i]
18
+ i -= 1
19
+ if c == "\n" || c == " " || c == "\t"
20
+ next
21
+ elsif c != ";"
22
+ return false
23
+ else
24
+ return true
27
25
  end
28
26
  end
27
+ true
28
+ end
29
29
 
30
- else
31
- # Define UTF-8 and UTF-16 BOM pattern matchers.
32
- # Avoid using a Regexp literal to prevent syntax errors in other interpreters.
33
- UTF8_BOM_PATTERN = Regexp.new("\\A\\xEF\\xBB\\xBF")
34
- UTF16_BOM_PATTERN = Regexp.new("\\A(\\xFE\\xFF|\\xFF\\xFE)")
35
-
36
- def self.read_unicode(pathname)
37
- pathname.read.tap do |data|
38
- # If the file is UTF-8 and theres a BOM, strip it for safe concatenation.
39
- if data =~ UTF8_BOM_PATTERN
40
- data.sub!(UTF8_BOM_PATTERN, "")
41
-
42
- # If we find a UTF-16 BOM, theres nothing we can do on
43
- # 1.8. Only UTF-8 is supported.
44
- elsif data =~ UTF16_BOM_PATTERN
45
- raise EncodingError, "#{pathname} has a UTF-16 BOM. " +
46
- "Resave the file as UTF-8 or upgrade to Ruby 1.9."
47
- end
48
- end
30
+ # Internal: Accumulate asset source to buffer and append a trailing
31
+ # semicolon if necessary.
32
+ #
33
+ # buf - String memo
34
+ # asset - Asset
35
+ #
36
+ # Returns appended buffer String.
37
+ def concat_javascript_sources(buf, source)
38
+ if string_end_with_semicolon?(buf)
39
+ buf + source
40
+ else
41
+ buf + ";\n" + source
49
42
  end
50
43
  end
51
44
 
52
- # Prepends a leading "." to an extension if its missing.
45
+ # Internal: Prepends a leading "." to an extension if its missing.
53
46
  #
54
47
  # normalize_extension("js")
55
48
  # # => ".js"
@@ -57,7 +50,7 @@ module Sprockets
57
50
  # normalize_extension(".css")
58
51
  # # => ".css"
59
52
  #
60
- def self.normalize_extension(extension)
53
+ def normalize_extension(extension)
61
54
  extension = extension.to_s
62
55
  if extension[/^\./]
63
56
  extension
@@ -65,5 +58,129 @@ module Sprockets
65
58
  ".#{extension}"
66
59
  end
67
60
  end
61
+
62
+ # Internal: Feature detect if UnboundMethods can #bind to any Object or
63
+ # just Objects that share the same super class.
64
+ # Basically if RUBY_VERSION >= 2.
65
+ UNBOUND_METHODS_BIND_TO_ANY_OBJECT = begin
66
+ foo = Module.new { def bar; end }
67
+ foo.instance_method(:bar).bind(Object.new)
68
+ true
69
+ rescue TypeError
70
+ false
71
+ end
72
+
73
+ # Internal: Inject into target module for the duration of the block.
74
+ #
75
+ # mod - Module
76
+ #
77
+ # Returns result of block.
78
+ def module_include(base, mod)
79
+ old_methods = {}
80
+
81
+ mod.instance_methods.each do |sym|
82
+ old_methods[sym] = base.instance_method(sym) if base.method_defined?(sym)
83
+ end
84
+
85
+ unless UNBOUND_METHODS_BIND_TO_ANY_OBJECT
86
+ base.send(:include, mod) unless base < mod
87
+ end
88
+
89
+ mod.instance_methods.each do |sym|
90
+ method = mod.instance_method(sym)
91
+ base.send(:define_method, sym, method)
92
+ end
93
+
94
+ yield
95
+ ensure
96
+ mod.instance_methods.each do |sym|
97
+ base.send(:undef_method, sym) if base.method_defined?(sym)
98
+ end
99
+ old_methods.each do |sym, method|
100
+ base.send(:define_method, sym, method)
101
+ end
102
+ end
103
+
104
+ # Internal: Generate a hexdigest for a nested JSON serializable object.
105
+ #
106
+ # obj - A JSON serializable object.
107
+ #
108
+ # Returns a String SHA1 digest of the object.
109
+ def hexdigest(obj)
110
+ digest = Digest::SHA1.new
111
+ queue = [obj]
112
+
113
+ while queue.length > 0
114
+ obj = queue.shift
115
+ klass = obj.class
116
+
117
+ if klass == String
118
+ digest << 'String'
119
+ digest << obj
120
+ elsif klass == Symbol
121
+ digest << 'Symbol'
122
+ digest << obj.to_s
123
+ elsif klass == Fixnum
124
+ digest << 'Fixnum'
125
+ digest << obj.to_s
126
+ elsif klass == TrueClass
127
+ digest << 'TrueClass'
128
+ elsif klass == FalseClass
129
+ digest << 'FalseClass'
130
+ elsif klass == NilClass
131
+ digest << 'NilClass'
132
+ elsif klass == Array
133
+ digest << 'Array'
134
+ queue.concat(obj)
135
+ elsif klass == Hash
136
+ digest << 'Hash'
137
+ queue.concat(obj.sort)
138
+ elsif klass == Set
139
+ digest << 'Set'
140
+ queue.concat(obj.to_a)
141
+ elsif klass == Encoding
142
+ digest << 'Encoding'
143
+ digest << obj.name
144
+ else
145
+ raise TypeError, "couldn't digest #{klass}"
146
+ end
147
+ end
148
+
149
+ digest.hexdigest
150
+ end
151
+
152
+ # Internal: Post-order Depth-First search algorithm.
153
+ #
154
+ # Used for resolving asset dependencies.
155
+ #
156
+ # initial - Initial Array of nodes to traverse.
157
+ # block -
158
+ # node - Current node to get children of
159
+ #
160
+ # Returns a Set of nodes.
161
+ def dfs(initial)
162
+ nodes, seen = Set.new, Set.new
163
+ stack = Array(initial).reverse
164
+
165
+ while node = stack.pop
166
+ if seen.include?(node)
167
+ nodes.add(node)
168
+ else
169
+ seen.add(node)
170
+ stack.push(node)
171
+ stack.concat(Array(yield node).reverse)
172
+ end
173
+ end
174
+
175
+ nodes
176
+ end
177
+
178
+ def benchmark_start
179
+ Time.now.to_f
180
+ end
181
+
182
+ def benchmark_end(start_time)
183
+ ((Time.now.to_f - start_time) * 1000).to_i
184
+ end
68
185
  end
69
186
  end