sprockets 2.12.5 → 3.7.2

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 (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