sprockets 2.12.5 → 3.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/LICENSE +2 -2
  4. data/README.md +235 -262
  5. data/bin/sprockets +1 -0
  6. data/lib/rake/sprocketstask.rb +5 -4
  7. data/lib/sprockets/asset.rb +143 -212
  8. data/lib/sprockets/autoload/closure.rb +7 -0
  9. data/lib/sprockets/autoload/coffee_script.rb +7 -0
  10. data/lib/sprockets/autoload/eco.rb +7 -0
  11. data/lib/sprockets/autoload/ejs.rb +7 -0
  12. data/lib/sprockets/autoload/sass.rb +7 -0
  13. data/lib/sprockets/autoload/uglifier.rb +7 -0
  14. data/lib/sprockets/autoload/yui.rb +7 -0
  15. data/lib/sprockets/autoload.rb +11 -0
  16. data/lib/sprockets/base.rb +56 -393
  17. data/lib/sprockets/bower.rb +58 -0
  18. data/lib/sprockets/bundle.rb +69 -0
  19. data/lib/sprockets/cache/file_store.rb +168 -14
  20. data/lib/sprockets/cache/memory_store.rb +66 -0
  21. data/lib/sprockets/cache/null_store.rb +46 -0
  22. data/lib/sprockets/cache.rb +236 -0
  23. data/lib/sprockets/cached_environment.rb +69 -0
  24. data/lib/sprockets/closure_compressor.rb +35 -10
  25. data/lib/sprockets/coffee_script_processor.rb +25 -0
  26. data/lib/sprockets/coffee_script_template.rb +17 -0
  27. data/lib/sprockets/compressing.rb +44 -23
  28. data/lib/sprockets/configuration.rb +83 -0
  29. data/lib/sprockets/context.rb +86 -144
  30. data/lib/sprockets/dependencies.rb +73 -0
  31. data/lib/sprockets/deprecation.rb +90 -0
  32. data/lib/sprockets/digest_utils.rb +180 -0
  33. data/lib/sprockets/directive_processor.rb +207 -211
  34. data/lib/sprockets/eco_processor.rb +32 -0
  35. data/lib/sprockets/eco_template.rb +9 -30
  36. data/lib/sprockets/ejs_processor.rb +31 -0
  37. data/lib/sprockets/ejs_template.rb +9 -29
  38. data/lib/sprockets/encoding_utils.rb +261 -0
  39. data/lib/sprockets/engines.rb +53 -35
  40. data/lib/sprockets/environment.rb +17 -64
  41. data/lib/sprockets/erb_processor.rb +30 -0
  42. data/lib/sprockets/erb_template.rb +11 -0
  43. data/lib/sprockets/errors.rb +4 -13
  44. data/lib/sprockets/file_reader.rb +15 -0
  45. data/lib/sprockets/http_utils.rb +117 -0
  46. data/lib/sprockets/jst_processor.rb +35 -15
  47. data/lib/sprockets/legacy.rb +330 -0
  48. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  49. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  50. data/lib/sprockets/loader.rb +325 -0
  51. data/lib/sprockets/manifest.rb +202 -127
  52. data/lib/sprockets/manifest_utils.rb +45 -0
  53. data/lib/sprockets/mime.rb +112 -31
  54. data/lib/sprockets/path_dependency_utils.rb +85 -0
  55. data/lib/sprockets/path_digest_utils.rb +47 -0
  56. data/lib/sprockets/path_utils.rb +287 -0
  57. data/lib/sprockets/paths.rb +42 -19
  58. data/lib/sprockets/processing.rb +178 -126
  59. data/lib/sprockets/processor_utils.rb +180 -0
  60. data/lib/sprockets/resolve.rb +211 -0
  61. data/lib/sprockets/sass_cache_store.rb +22 -17
  62. data/lib/sprockets/sass_compressor.rb +39 -15
  63. data/lib/sprockets/sass_functions.rb +2 -70
  64. data/lib/sprockets/sass_importer.rb +2 -30
  65. data/lib/sprockets/sass_processor.rb +292 -0
  66. data/lib/sprockets/sass_template.rb +12 -59
  67. data/lib/sprockets/server.rb +129 -84
  68. data/lib/sprockets/transformers.rb +145 -0
  69. data/lib/sprockets/uglifier_compressor.rb +39 -12
  70. data/lib/sprockets/unloaded_asset.rb +137 -0
  71. data/lib/sprockets/uri_tar.rb +98 -0
  72. data/lib/sprockets/uri_utils.rb +188 -0
  73. data/lib/sprockets/utils/gzip.rb +67 -0
  74. data/lib/sprockets/utils.rb +210 -44
  75. data/lib/sprockets/version.rb +1 -1
  76. data/lib/sprockets/yui_compressor.rb +39 -11
  77. data/lib/sprockets.rb +142 -81
  78. metadata +96 -90
  79. data/lib/sprockets/asset_attributes.rb +0 -137
  80. data/lib/sprockets/bundled_asset.rb +0 -78
  81. data/lib/sprockets/caching.rb +0 -96
  82. data/lib/sprockets/charset_normalizer.rb +0 -41
  83. data/lib/sprockets/index.rb +0 -100
  84. data/lib/sprockets/processed_asset.rb +0 -152
  85. data/lib/sprockets/processor.rb +0 -32
  86. data/lib/sprockets/safety_colons.rb +0 -28
  87. data/lib/sprockets/scss_template.rb +0 -13
  88. data/lib/sprockets/static_asset.rb +0 -60
@@ -1,89 +1,26 @@
1
- require 'sprockets/asset_attributes'
2
- require 'sprockets/bundled_asset'
3
- require 'sprockets/caching'
1
+ require 'sprockets/asset'
2
+ require 'sprockets/bower'
3
+ require 'sprockets/cache'
4
+ require 'sprockets/configuration'
5
+ require 'sprockets/digest_utils'
4
6
  require 'sprockets/errors'
5
- require 'sprockets/processed_asset'
7
+ require 'sprockets/loader'
8
+ require 'sprockets/path_digest_utils'
9
+ require 'sprockets/path_dependency_utils'
10
+ require 'sprockets/path_utils'
11
+ require 'sprockets/resolve'
6
12
  require 'sprockets/server'
7
- require 'sprockets/static_asset'
8
- require 'multi_json'
9
- require 'pathname'
13
+ require 'sprockets/loader'
14
+ require 'sprockets/uri_tar'
10
15
 
11
16
  module Sprockets
12
- # `Base` class for `Environment` and `Index`.
17
+ # `Base` class for `Environment` and `Cached`.
13
18
  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
19
+ include PathUtils, PathDependencyUtils, PathDigestUtils, DigestUtils
20
+ include Configuration
21
+ include Server
22
+ include Resolve, Loader
23
+ include Bower
87
24
 
88
25
  # Get persistent cache store
89
26
  attr_reader :cache
@@ -94,197 +31,57 @@ module Sprockets
94
31
  # setters. Either `get(key)`/`set(key, value)`,
95
32
  # `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
96
33
  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
105
- end
106
-
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
34
+ @cache = Cache.new(cache, logger)
117
35
  end
118
36
 
119
- # Finds the expanded real path for a given logical path by
120
- # searching the environment's paths.
121
- #
122
- # resolve("application.js")
123
- # # => "/path/to/app/javascripts/application.js.coffee"
124
- #
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
147
- end
148
- 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
- end
155
- end
156
-
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
37
+ # Return an `Cached`. Must be implemented by the subclass.
38
+ def cached
211
39
  raise NotImplementedError
212
40
  end
41
+ alias_method :index, :cached
213
42
 
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`.
221
- #
222
- # Subclasses may cache this method.
223
- def entries(pathname)
224
- @trail.entries(pathname)
225
- end
226
-
227
- # Works like `File.stat`.
43
+ # Internal: Compute digest for path.
228
44
  #
229
- # Subclasses may cache this method.
230
- def stat(path)
231
- @trail.stat(path)
232
- end
233
-
234
- # Read and compute digest of filename.
45
+ # path - String filename or directory path.
235
46
  #
236
- # Subclasses may cache this method.
47
+ # Returns a String digest or nil.
237
48
  def file_digest(path)
238
49
  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)
50
+ # Caveat: Digests are cached by the path's current mtime. Its possible
51
+ # for a files contents to have changed and its mtime to have been
52
+ # negligently reset thus appearing as if the file hasn't changed on
53
+ # disk. Also, the mtime is only read to the nearest second. It's
54
+ # also possible the file was updated more than once in a given second.
55
+ key = UnloadedAsset.new(path, self).file_digest_key(stat.mtime.to_i)
56
+ cache.fetch(key) do
57
+ self.stat_digest(path, stat)
247
58
  end
248
59
  end
249
60
  end
250
61
 
251
- # Internal. Return a `AssetAttributes` for `path`.
252
- def attributes_for(path)
253
- AssetAttributes.new(self, path)
62
+ # Find asset by logical path or expanded path.
63
+ def find_asset(path, options = {})
64
+ uri, _ = resolve(path, options.merge(compat: false))
65
+ if uri
66
+ load(uri)
67
+ end
254
68
  end
255
69
 
256
- # Internal. Return content type of `path`.
257
- def content_type_of(path)
258
- attributes_for(path).content_type
259
- end
70
+ def find_all_linked_assets(path, options = {})
71
+ return to_enum(__method__, path, options) unless block_given?
260
72
 
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
73
+ asset = find_asset(path, options)
74
+ return unless asset
265
75
 
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)
76
+ yield asset
77
+ stack = asset.links.to_a
272
78
 
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
79
+ while uri = stack.shift
80
+ yield asset = load(uri)
81
+ stack = asset.links.to_a + stack
285
82
  end
286
83
 
287
- build_asset(logical_path, pathname, options)
84
+ nil
288
85
  end
289
86
 
290
87
  # Preferred `find_asset` shorthand.
@@ -295,153 +92,19 @@ module Sprockets
295
92
  find_asset(*args)
296
93
  end
297
94
 
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
95
  # Pretty inspect
352
96
  def inspect
353
97
  "#<#{self.class}:0x#{object_id.to_s(16)} " +
354
98
  "root=#{root.to_s.inspect}, " +
355
- "paths=#{paths.inspect}, " +
356
- "digest=#{digest.to_s.inspect}" +
357
- ">"
99
+ "paths=#{paths.inspect}>"
358
100
  end
359
101
 
360
- protected
361
- # Clear index after mutating state. Must be implemented by the subclass.
362
- def expire_index!
363
- raise NotImplementedError
364
- end
365
-
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)
381
- end
382
- end
383
-
384
- def cache_key_for(path, options)
385
- "#{path}:#{options[:bundle] ? '1' : '0'}"
386
- end
387
-
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"
393
- end
394
- calls << path
395
- yield
396
- ensure
397
- Thread.current[:sprockets_circular_calls] = nil if reset
398
- end
399
-
400
- def logical_path_for_filename(filename, filters)
401
- logical_path = attributes_for(filename).logical_path.to_s
402
-
403
- if matches_filter(filters, logical_path, filename)
404
- return logical_path
405
- end
406
-
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
413
- end
414
-
415
- nil
416
- end
417
-
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
102
+ def compress_from_root(uri)
103
+ URITar.new(uri, self).compress
104
+ end
435
105
 
436
- # Feature detect newer MultiJson API
437
- if MultiJson.respond_to?(:dump)
438
- def json_decode(obj)
439
- MultiJson.load(obj)
440
- end
441
- else
442
- def json_decode(obj)
443
- MultiJson.decode(obj)
444
- end
445
- end
106
+ def expand_from_root(uri)
107
+ URITar.new(uri, self).expand
108
+ end
446
109
  end
447
110
  end
@@ -0,0 +1,58 @@
1
+ require 'json'
2
+
3
+ module Sprockets
4
+ module Bower
5
+ # Internal: All supported bower.json files.
6
+ #
7
+ # https://github.com/bower/json/blob/0.4.0/lib/json.js#L7
8
+ POSSIBLE_BOWER_JSONS = ['bower.json', 'component.json', '.bower.json']
9
+
10
+ # Internal: Override resolve_alternates to install bower.json behavior.
11
+ #
12
+ # load_path - String environment path
13
+ # logical_path - String path relative to base
14
+ #
15
+ # Returns candiate filenames.
16
+ def resolve_alternates(load_path, logical_path)
17
+ candidates, deps = super
18
+
19
+ # bower.json can only be nested one level deep
20
+ if !logical_path.index('/')
21
+ dirname = File.join(load_path, logical_path)
22
+
23
+ if directory?(dirname)
24
+ filenames = POSSIBLE_BOWER_JSONS.map { |basename| File.join(dirname, basename) }
25
+ filename = filenames.detect { |fn| self.file?(fn) }
26
+
27
+ if filename
28
+ deps << build_file_digest_uri(filename)
29
+ read_bower_main(dirname, filename) do |path|
30
+ candidates << path
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ return candidates, deps
37
+ end
38
+
39
+ # Internal: Read bower.json's main directive.
40
+ #
41
+ # dirname - String path to component directory.
42
+ # filename - String path to bower.json.
43
+ #
44
+ # Returns nothing.
45
+ def read_bower_main(dirname, filename)
46
+ bower = JSON.parse(File.read(filename), create_additions: false)
47
+
48
+ case bower['main']
49
+ when String
50
+ yield File.expand_path(bower['main'], dirname)
51
+ when Array
52
+ bower['main'].each do |name|
53
+ yield File.expand_path(name, dirname)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,69 @@
1
+ require 'set'
2
+ require 'sprockets/utils'
3
+
4
+ module Sprockets
5
+ # Internal: Bundle processor takes a single file asset and prepends all the
6
+ # `:required` URIs to the contents.
7
+ #
8
+ # Uses pipeline metadata:
9
+ #
10
+ # :required - Ordered Set of asset URIs to prepend
11
+ # :stubbed - Set of asset URIs to substract from the required set.
12
+ #
13
+ # Also see DirectiveProcessor.
14
+ class Bundle
15
+ def self.call(input)
16
+ env = input[:environment]
17
+ type = input[:content_type]
18
+ dependencies = Set.new(input[:metadata][:dependencies])
19
+
20
+ processed_uri, deps = env.resolve(input[:filename], accept: type, pipeline: :self, compat: false)
21
+ dependencies.merge(deps)
22
+
23
+ find_required = proc { |uri| env.load(uri).metadata[:required] }
24
+ required = Utils.dfs(processed_uri, &find_required)
25
+ stubbed = Utils.dfs(env.load(processed_uri).metadata[:stubbed], &find_required)
26
+ required.subtract(stubbed)
27
+ assets = required.map { |uri| env.load(uri) }
28
+
29
+ (required + stubbed).each do |uri|
30
+ dependencies.merge(env.load(uri).metadata[:dependencies])
31
+ end
32
+
33
+ reducers = Hash[env.match_mime_type_keys(env.config[:bundle_reducers], type).flat_map(&:to_a)]
34
+ process_bundle_reducers(assets, reducers).merge(dependencies: dependencies, included: assets.map(&:uri))
35
+ end
36
+
37
+ # Internal: Run bundle reducers on set of Assets producing a reduced
38
+ # metadata Hash.
39
+ #
40
+ # assets - Array of Assets
41
+ # reducers - Array of [initial, reducer_proc] pairs
42
+ #
43
+ # Returns reduced asset metadata Hash.
44
+ def self.process_bundle_reducers(assets, reducers)
45
+ initial = {}
46
+ reducers.each do |k, (v, _)|
47
+ if v.respond_to?(:call)
48
+ initial[k] = v.call
49
+ elsif !v.nil?
50
+ initial[k] = v
51
+ end
52
+ end
53
+
54
+ assets.reduce(initial) do |h, asset|
55
+ reducers.each do |k, (_, block)|
56
+ value = k == :data ? asset.source : asset.metadata[k]
57
+ if h.key?(k)
58
+ if !value.nil?
59
+ h[k] = block.call(h[k], value)
60
+ end
61
+ else
62
+ h[k] = value
63
+ end
64
+ end
65
+ h
66
+ end
67
+ end
68
+ end
69
+ end