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,89 +1,19 @@
1
- require 'sprockets/asset_attributes'
2
- require 'sprockets/bundled_asset'
3
- require 'sprockets/caching'
1
+ require 'sprockets/asset'
2
+ require 'sprockets/bower'
4
3
  require 'sprockets/errors'
5
- require 'sprockets/processed_asset'
4
+ require 'sprockets/legacy'
5
+ require 'sprockets/resolve'
6
6
  require 'sprockets/server'
7
- require 'sprockets/static_asset'
8
- require 'multi_json'
9
- require 'pathname'
10
7
 
11
8
  module Sprockets
12
- # `Base` class for `Environment` and `Index`.
9
+ # `Base` class for `Environment` and `Cached`.
13
10
  class Base
14
- include Caching, Paths, Mime, Processing, Compressing, Engines, Server
15
-
16
- # Returns a `Digest` implementation class.
17
- #
18
- # Defaults to `Digest::MD5`.
19
- attr_reader :digest_class
20
-
21
- # Assign a `Digest` implementation class. This may be any Ruby
22
- # `Digest::` implementation such as `Digest::MD5` or
23
- # `Digest::SHA1`.
24
- #
25
- # environment.digest_class = Digest::SHA1
26
- #
27
- def digest_class=(klass)
28
- expire_index!
29
- @digest_class = klass
30
- end
31
-
32
- # The `Environment#version` is a custom value used for manually
33
- # expiring all asset caches.
34
- #
35
- # Sprockets is able to track most file and directory changes and
36
- # will take care of expiring the cache for you. However, its
37
- # impossible to know when any custom helpers change that you mix
38
- # into the `Context`.
39
- #
40
- # It would be wise to increment this value anytime you make a
41
- # configuration change to the `Environment` object.
42
- attr_reader :version
43
-
44
- # Assign an environment version.
45
- #
46
- # environment.version = '2.0'
47
- #
48
- def version=(version)
49
- expire_index!
50
- @version = version
51
- end
52
-
53
- # Returns a `Digest` instance for the `Environment`.
54
- #
55
- # This value serves two purposes. If two `Environment`s have the
56
- # same digest value they can be treated as equal. This is more
57
- # useful for comparing environment states between processes rather
58
- # than in the same. Two equal `Environment`s can share the same
59
- # cached assets.
60
- #
61
- # The value also provides a seed digest for all `Asset`
62
- # digests. Any change in the environment digest will affect all of
63
- # its assets.
64
- def digest
65
- # Compute the initial digest using the implementation class. The
66
- # Sprockets release version and custom environment version are
67
- # mixed in. So any new releases will affect all your assets.
68
- @digest ||= digest_class.new.update(VERSION).update(version.to_s)
69
-
70
- # Returned a dupped copy so the caller can safely mutate it with `.update`
71
- @digest.dup
72
- end
73
-
74
- # Get and set `Logger` instance.
75
- attr_accessor :logger
76
-
77
- # Get `Context` class.
78
- #
79
- # This class maybe mutated and mixed in with custom helpers.
80
- #
81
- # environment.context_class.instance_eval do
82
- # include MyHelpers
83
- # def asset_url; end
84
- # end
85
- #
86
- attr_reader :context_class
11
+ include PathUtils, HTTPUtils
12
+ include Configuration
13
+ include Server
14
+ include Resolve
15
+ include Bower
16
+ include Legacy
87
17
 
88
18
  # Get persistent cache store
89
19
  attr_reader :cache
@@ -94,197 +24,80 @@ module Sprockets
94
24
  # setters. Either `get(key)`/`set(key, value)`,
95
25
  # `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
96
26
  def cache=(cache)
97
- expire_index!
98
- @cache = cache
99
- end
100
-
101
- def prepend_path(path)
102
- # Overrides the global behavior to expire the index
103
- expire_index!
104
- super
27
+ @cache = Cache.new(cache, logger)
105
28
  end
106
29
 
107
- def append_path(path)
108
- # Overrides the global behavior to expire the index
109
- expire_index!
110
- super
111
- end
112
-
113
- def clear_paths
114
- # Overrides the global behavior to expire the index
115
- expire_index!
116
- super
30
+ # Return an `Cached`. Must be implemented by the subclass.
31
+ def cached
32
+ raise NotImplementedError
117
33
  end
34
+ alias_method :index, :cached
118
35
 
119
- # Finds the expanded real path for a given logical path by
120
- # searching the environment's paths.
36
+ # Internal: Compute hexdigest for path.
121
37
  #
122
- # resolve("application.js")
123
- # # => "/path/to/app/javascripts/application.js.coffee"
38
+ # path - String filename or directory path.
124
39
  #
125
- # A `FileNotFound` exception is raised if the file does not exist.
126
- def resolve(logical_path, options = {})
127
- # If a block is given, preform an iterable search
128
- if block_given?
129
- args = attributes_for(logical_path).search_paths + [options]
130
- @trail.find(*args) do |path|
131
- pathname = Pathname.new(path)
132
- if %w( .bower.json bower.json component.json ).include?(pathname.basename.to_s)
133
- bower = json_decode(pathname.read)
134
- case bower['main']
135
- when String
136
- yield pathname.dirname.join(bower['main'])
137
- when Array
138
- extname = File.extname(logical_path)
139
- bower['main'].each do |fn|
140
- if extname == "" || extname == File.extname(fn)
141
- yield pathname.dirname.join(fn)
142
- end
143
- end
144
- end
145
- else
146
- yield pathname
40
+ # Returns a String SHA1 hexdigest or nil.
41
+ def file_hexdigest(path)
42
+ if stat = self.stat(path)
43
+ # Caveat: Digests are cached by the path's current mtime. Its possible
44
+ # for a files contents to have changed and its mtime to have been
45
+ # negligently reset thus appearing as if the file hasn't changed on
46
+ # disk. Also, the mtime is only read to the nearest second. Its
47
+ # also possible the file was updated more than once in a given second.
48
+ cache.fetch(['file_hexdigest', path, stat.mtime.to_i]) do
49
+ if stat.directory?
50
+ # If its a directive, digest the list of filenames
51
+ Digest::SHA1.hexdigest(self.entries(path).join(','))
52
+ elsif stat.file?
53
+ # If its a file, digest the contents
54
+ Digest::SHA1.file(path.to_s).hexdigest
147
55
  end
148
56
  end
149
- else
150
- resolve(logical_path, options) do |pathname|
151
- return pathname
152
- end
153
- raise FileNotFound, "couldn't find file '#{logical_path}'"
154
57
  end
155
58
  end
156
59
 
157
- # Register a new mime type.
158
- def register_mime_type(mime_type, ext)
159
- # Overrides the global behavior to expire the index
160
- expire_index!
161
- @trail.append_extension(ext)
162
- super
163
- end
164
-
165
- # Registers a new Engine `klass` for `ext`.
166
- def register_engine(ext, klass)
167
- # Overrides the global behavior to expire the index
168
- expire_index!
169
- add_engine_to_trail(ext, klass)
170
- super
171
- end
172
-
173
- def register_preprocessor(mime_type, klass, &block)
174
- # Overrides the global behavior to expire the index
175
- expire_index!
176
- super
177
- end
178
-
179
- def unregister_preprocessor(mime_type, klass)
180
- # Overrides the global behavior to expire the index
181
- expire_index!
182
- super
183
- end
184
-
185
- def register_postprocessor(mime_type, klass, &block)
186
- # Overrides the global behavior to expire the index
187
- expire_index!
188
- super
189
- end
190
-
191
- def unregister_postprocessor(mime_type, klass)
192
- # Overrides the global behavior to expire the index
193
- expire_index!
194
- super
195
- end
196
-
197
- def register_bundle_processor(mime_type, klass, &block)
198
- # Overrides the global behavior to expire the index
199
- expire_index!
200
- super
201
- end
202
-
203
- def unregister_bundle_processor(mime_type, klass)
204
- # Overrides the global behavior to expire the index
205
- expire_index!
206
- super
207
- end
208
-
209
- # Return an `Index`. Must be implemented by the subclass.
210
- def index
211
- raise NotImplementedError
212
- end
213
-
214
- if defined? Encoding.default_external
215
- # Define `default_external_encoding` accessor on 1.9.
216
- # Defaults to UTF-8.
217
- attr_accessor :default_external_encoding
218
- end
219
-
220
- # Works like `Dir.entries`.
60
+ # Internal: Compute hexdigest for a set of paths.
221
61
  #
222
- # Subclasses may cache this method.
223
- def entries(pathname)
224
- @trail.entries(pathname)
225
- end
226
-
227
- # Works like `File.stat`.
62
+ # paths - Array of filename or directory paths.
228
63
  #
229
- # Subclasses may cache this method.
230
- def stat(path)
231
- @trail.stat(path)
64
+ # Returns a String SHA1 hexdigest.
65
+ def dependencies_hexdigest(paths)
66
+ digest = Digest::SHA1.new
67
+ paths.each { |path| digest.update(file_hexdigest(path).to_s) }
68
+ digest.hexdigest
232
69
  end
233
70
 
234
- # Read and compute digest of filename.
235
- #
236
- # Subclasses may cache this method.
237
- def file_digest(path)
238
- if stat = self.stat(path)
239
- # If its a file, digest the contents
240
- if stat.file?
241
- digest.file(path.to_s)
242
-
243
- # If its a directive, digest the list of filenames
244
- elsif stat.directory?
245
- contents = self.entries(path).join(',')
246
- digest.update(contents)
247
- end
71
+ # Find asset by logical path or expanded path.
72
+ def find_asset(path, options = {})
73
+ if uri = resolve_asset_uri(path, options)
74
+ Asset.new(self, build_asset_by_uri(uri))
248
75
  end
249
76
  end
250
77
 
251
- # Internal. Return a `AssetAttributes` for `path`.
252
- def attributes_for(path)
253
- AssetAttributes.new(self, path)
78
+ def find_asset_by_uri(uri)
79
+ _, params = AssetURI.parse(uri)
80
+ asset = params.key?(:id) ?
81
+ build_asset_by_id_uri(uri) :
82
+ build_asset_by_uri(uri)
83
+ Asset.new(self, asset)
254
84
  end
255
85
 
256
- # Internal. Return content type of `path`.
257
- def content_type_of(path)
258
- attributes_for(path).content_type
259
- end
86
+ def find_all_linked_assets(path, options = {})
87
+ return to_enum(__method__, path, options) unless block_given?
260
88
 
261
- # Find asset by logical path or expanded path.
262
- def find_asset(path, options = {})
263
- logical_path = path
264
- pathname = Pathname.new(path).cleanpath
265
-
266
- if pathname.absolute?
267
- return unless stat(pathname)
268
- logical_path = attributes_for(pathname).logical_path
269
- else
270
- begin
271
- pathname = resolve(logical_path)
272
-
273
- # If logical path is missing a mime type extension, append
274
- # the absolute path extname so it has one.
275
- #
276
- # Ensures some consistency between finding "foo/bar" vs
277
- # "foo/bar.js".
278
- if File.extname(logical_path) == ""
279
- expanded_logical_path = attributes_for(pathname).logical_path
280
- logical_path += File.extname(expanded_logical_path)
281
- end
282
- rescue FileNotFound
283
- return nil
284
- end
89
+ asset = find_asset(path, options)
90
+ return unless asset
91
+
92
+ yield asset
93
+ stack = asset.links.to_a
94
+
95
+ while uri = stack.shift
96
+ yield asset = find_asset_by_uri(uri)
97
+ stack = asset.links.to_a + stack
285
98
  end
286
99
 
287
- build_asset(logical_path, pathname, options)
100
+ nil
288
101
  end
289
102
 
290
103
  # Preferred `find_asset` shorthand.
@@ -295,153 +108,105 @@ module Sprockets
295
108
  find_asset(*args)
296
109
  end
297
110
 
298
- def each_entry(root, &block)
299
- return to_enum(__method__, root) unless block_given?
300
- root = Pathname.new(root) unless root.is_a?(Pathname)
301
-
302
- paths = []
303
- entries(root).sort.each do |filename|
304
- path = root.join(filename)
305
- paths << path
306
-
307
- if stat(path).directory?
308
- each_entry(path) do |subpath|
309
- paths << subpath
310
- end
311
- end
312
- end
313
-
314
- paths.sort_by(&:to_s).each(&block)
315
-
316
- nil
317
- end
318
-
319
- def each_file
320
- return to_enum(__method__) unless block_given?
321
- paths.each do |root|
322
- each_entry(root) do |path|
323
- if !stat(path).directory?
324
- yield path
325
- end
326
- end
327
- end
328
- nil
329
- end
330
-
331
- def each_logical_path(*args, &block)
332
- return to_enum(__method__, *args) unless block_given?
333
- filters = args.flatten
334
- files = {}
335
- each_file do |filename|
336
- if logical_path = logical_path_for_filename(filename, filters)
337
- unless files[logical_path]
338
- if block.arity == 2
339
- yield logical_path, filename.to_s
340
- else
341
- yield logical_path
342
- end
343
- end
344
-
345
- files[logical_path] = true
346
- end
347
- end
348
- nil
349
- end
350
-
351
111
  # Pretty inspect
352
112
  def inspect
353
113
  "#<#{self.class}:0x#{object_id.to_s(16)} " +
354
114
  "root=#{root.to_s.inspect}, " +
355
- "paths=#{paths.inspect}, " +
356
- "digest=#{digest.to_s.inspect}" +
357
- ">"
115
+ "paths=#{paths.inspect}>"
358
116
  end
359
117
 
360
118
  protected
361
- # Clear index after mutating state. Must be implemented by the subclass.
362
- def expire_index!
363
- raise NotImplementedError
364
- end
119
+ def build_asset_by_id_uri(uri)
120
+ path, params = AssetURI.parse(uri)
365
121
 
366
- def build_asset(logical_path, pathname, options)
367
- pathname = Pathname.new(pathname)
368
-
369
- # If there are any processors to run on the pathname, use
370
- # `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
371
- if attributes_for(pathname).processors.any?
372
- if options[:bundle] == false
373
- circular_call_protection(pathname.to_s) do
374
- ProcessedAsset.new(index, logical_path, pathname)
375
- end
376
- else
377
- BundledAsset.new(index, logical_path, pathname)
378
- end
379
- else
380
- StaticAsset.new(index, logical_path, pathname)
122
+ # Internal assertion, should be routed through build_asset_by_uri
123
+ unless id = params.delete(:id)
124
+ raise ArgumentError, "expected uri to have an id: #{uri}"
381
125
  end
382
- end
383
126
 
384
- def cache_key_for(path, options)
385
- "#{path}:#{options[:bundle] ? '1' : '0'}"
386
- end
127
+ asset = build_asset_by_uri(AssetURI.build(path, params))
387
128
 
388
- def circular_call_protection(path)
389
- reset = Thread.current[:sprockets_circular_calls].nil?
390
- calls = Thread.current[:sprockets_circular_calls] ||= Set.new
391
- if calls.include?(path)
392
- raise CircularDependencyError, "#{path} has already been required"
129
+ if id && asset[:id] != id
130
+ raise VersionNotFound, "could not find specified id: #{id}"
393
131
  end
394
- calls << path
395
- yield
396
- ensure
397
- Thread.current[:sprockets_circular_calls] = nil if reset
132
+
133
+ asset
398
134
  end
399
135
 
400
- def logical_path_for_filename(filename, filters)
401
- logical_path = attributes_for(filename).logical_path.to_s
136
+ def build_asset_by_uri(uri)
137
+ filename, params = AssetURI.parse(uri)
402
138
 
403
- if matches_filter(filters, logical_path, filename)
404
- return logical_path
139
+ # Internal assertion, should be routed through build_asset_by_id_uri
140
+ if params.key?(:id)
141
+ raise ArgumentError, "expected uri to have no id: #{uri}"
405
142
  end
406
143
 
407
- # If filename is an index file, retest with alias
408
- if File.basename(logical_path)[/[^\.]+/, 0] == 'index'
409
- path = logical_path.sub(/\/index\./, '.')
410
- if matches_filter(filters, path, filename)
411
- return path
412
- end
144
+ type = params[:type]
145
+ load_path, logical_path = paths_split(self.paths, filename)
146
+
147
+ if !file?(filename)
148
+ raise FileNotFound, "could not find file: #{filename}"
149
+ elsif type && !resolve_path_transform_type(filename, type)
150
+ raise ConversionError, "could not convert to type: #{type}"
151
+ elsif !load_path
152
+ raise FileOutsidePaths, "#{filename} is no longer under a load path: #{self.paths.join(', ')}"
413
153
  end
414
154
 
415
- nil
416
- end
155
+ logical_path, file_type, engine_extnames = parse_path_extnames(logical_path)
156
+ logical_path = normalize_logical_path(logical_path)
417
157
 
418
- def matches_filter(filters, logical_path, filename)
419
- return true if filters.empty?
420
-
421
- filters.any? do |filter|
422
- if filter.is_a?(Regexp)
423
- filter.match(logical_path)
424
- elsif filter.respond_to?(:call)
425
- if filter.arity == 1
426
- filter.call(logical_path)
427
- else
428
- filter.call(logical_path, filename.to_s)
429
- end
430
- else
431
- File.fnmatch(filter.to_s, logical_path)
432
- end
433
- end
434
- end
158
+ asset = {
159
+ uri: uri,
160
+ load_path: load_path,
161
+ filename: filename,
162
+ name: logical_path,
163
+ logical_path: logical_path
164
+ }
435
165
 
436
- # Feature detect newer MultiJson API
437
- if MultiJson.respond_to?(:dump)
438
- def json_decode(obj)
439
- MultiJson.load(obj)
166
+ if type
167
+ asset[:content_type] = type
168
+ asset[:logical_path] += mime_types[type][:extensions].first
440
169
  end
441
- else
442
- def json_decode(obj)
443
- MultiJson.decode(obj)
170
+
171
+ processed_processors = unwrap_preprocessors(file_type) +
172
+ unwrap_engines(engine_extnames).reverse +
173
+ unwrap_transformer(file_type, type) +
174
+ unwrap_postprocessors(type)
175
+
176
+ bundled_processors = params[:skip_bundle] ? [] : unwrap_bundle_processors(type)
177
+
178
+ processors = bundled_processors.any? ? bundled_processors : processed_processors
179
+ processors += unwrap_encoding_processors(params[:encoding])
180
+
181
+ if processors.any?
182
+ asset.merge!(process(
183
+ [method(:read_input)] + processors,
184
+ asset[:uri],
185
+ asset[:filename],
186
+ asset[:load_path],
187
+ asset[:name],
188
+ asset[:content_type]
189
+ ))
190
+ else
191
+ asset.merge!({
192
+ encoding: Encoding::BINARY,
193
+ length: self.stat(asset[:filename]).size,
194
+ digest: digest_class.file(asset[:filename]).hexdigest,
195
+ metadata: {}
196
+ })
444
197
  end
198
+
199
+ metadata = asset[:metadata]
200
+ metadata[:dependency_paths] = Set.new(metadata[:dependency_paths]).merge([asset[:filename]])
201
+ metadata[:dependency_digest] = dependencies_hexdigest(metadata[:dependency_paths])
202
+
203
+ asset[:id] = Utils.hexdigest(asset)
204
+ asset[:uri] = AssetURI.build(filename, params.merge(id: asset[:id]))
205
+
206
+ # TODO: Avoid tracking Asset mtime
207
+ asset[:mtime] = metadata[:dependency_paths].map { |p| stat(p).mtime.to_i }.max
208
+
209
+ asset
445
210
  end
446
211
  end
447
212
  end