sprockets 3.0.0 → 4.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 (95) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +68 -0
  3. data/README.md +397 -408
  4. data/bin/sprockets +12 -7
  5. data/lib/rake/sprocketstask.rb +3 -2
  6. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  7. data/lib/sprockets/asset.rb +19 -23
  8. data/lib/sprockets/autoload/babel.rb +8 -0
  9. data/lib/sprockets/autoload/closure.rb +1 -0
  10. data/lib/sprockets/autoload/coffee_script.rb +1 -0
  11. data/lib/sprockets/autoload/eco.rb +1 -0
  12. data/lib/sprockets/autoload/ejs.rb +1 -0
  13. data/lib/sprockets/autoload/jsminc.rb +8 -0
  14. data/lib/sprockets/autoload/sass.rb +1 -0
  15. data/lib/sprockets/autoload/sassc.rb +8 -0
  16. data/lib/sprockets/autoload/uglifier.rb +1 -0
  17. data/lib/sprockets/autoload/yui.rb +1 -0
  18. data/lib/sprockets/autoload/zopfli.rb +7 -0
  19. data/lib/sprockets/autoload.rb +5 -0
  20. data/lib/sprockets/babel_processor.rb +66 -0
  21. data/lib/sprockets/base.rb +59 -11
  22. data/lib/sprockets/bower.rb +5 -2
  23. data/lib/sprockets/bundle.rb +44 -4
  24. data/lib/sprockets/cache/file_store.rb +32 -7
  25. data/lib/sprockets/cache/memory_store.rb +9 -0
  26. data/lib/sprockets/cache/null_store.rb +8 -0
  27. data/lib/sprockets/cache.rb +42 -5
  28. data/lib/sprockets/cached_environment.rb +14 -19
  29. data/lib/sprockets/closure_compressor.rb +6 -11
  30. data/lib/sprockets/coffee_script_processor.rb +19 -5
  31. data/lib/sprockets/compressing.rb +62 -2
  32. data/lib/sprockets/configuration.rb +3 -7
  33. data/lib/sprockets/context.rb +98 -23
  34. data/lib/sprockets/dependencies.rb +9 -8
  35. data/lib/sprockets/digest_utils.rb +104 -60
  36. data/lib/sprockets/directive_processor.rb +45 -35
  37. data/lib/sprockets/eco_processor.rb +3 -2
  38. data/lib/sprockets/ejs_processor.rb +3 -2
  39. data/lib/sprockets/encoding_utils.rb +8 -4
  40. data/lib/sprockets/environment.rb +9 -4
  41. data/lib/sprockets/erb_processor.rb +28 -21
  42. data/lib/sprockets/errors.rb +1 -1
  43. data/lib/sprockets/exporters/base.rb +72 -0
  44. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  45. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  46. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  47. data/lib/sprockets/exporting.rb +73 -0
  48. data/lib/sprockets/file_reader.rb +1 -0
  49. data/lib/sprockets/http_utils.rb +26 -6
  50. data/lib/sprockets/jsminc_compressor.rb +32 -0
  51. data/lib/sprockets/jst_processor.rb +11 -10
  52. data/lib/sprockets/loader.rb +236 -69
  53. data/lib/sprockets/manifest.rb +97 -44
  54. data/lib/sprockets/manifest_utils.rb +9 -6
  55. data/lib/sprockets/mime.rb +8 -42
  56. data/lib/sprockets/npm.rb +52 -0
  57. data/lib/sprockets/path_dependency_utils.rb +3 -11
  58. data/lib/sprockets/path_digest_utils.rb +2 -1
  59. data/lib/sprockets/path_utils.rb +106 -21
  60. data/lib/sprockets/paths.rb +1 -0
  61. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  62. data/lib/sprockets/processing.rb +31 -51
  63. data/lib/sprockets/processor_utils.rb +81 -15
  64. data/lib/sprockets/resolve.rb +182 -95
  65. data/lib/sprockets/sass_cache_store.rb +1 -0
  66. data/lib/sprockets/sass_compressor.rb +21 -17
  67. data/lib/sprockets/sass_functions.rb +1 -0
  68. data/lib/sprockets/sass_importer.rb +1 -0
  69. data/lib/sprockets/sass_processor.rb +45 -17
  70. data/lib/sprockets/sassc_compressor.rb +56 -0
  71. data/lib/sprockets/sassc_processor.rb +297 -0
  72. data/lib/sprockets/server.rb +57 -34
  73. data/lib/sprockets/source_map_processor.rb +66 -0
  74. data/lib/sprockets/source_map_utils.rb +483 -0
  75. data/lib/sprockets/transformers.rb +63 -35
  76. data/lib/sprockets/uglifier_compressor.rb +23 -20
  77. data/lib/sprockets/unloaded_asset.rb +139 -0
  78. data/lib/sprockets/uri_tar.rb +99 -0
  79. data/lib/sprockets/uri_utils.rb +15 -14
  80. data/lib/sprockets/utils/gzip.rb +99 -0
  81. data/lib/sprockets/utils.rb +43 -59
  82. data/lib/sprockets/version.rb +2 -1
  83. data/lib/sprockets/yui_compressor.rb +5 -14
  84. data/lib/sprockets.rb +103 -33
  85. metadata +151 -22
  86. data/LICENSE +0 -21
  87. data/lib/sprockets/coffee_script_template.rb +0 -6
  88. data/lib/sprockets/eco_template.rb +0 -6
  89. data/lib/sprockets/ejs_template.rb +0 -6
  90. data/lib/sprockets/engines.rb +0 -81
  91. data/lib/sprockets/erb_template.rb +0 -6
  92. data/lib/sprockets/legacy.rb +0 -314
  93. data/lib/sprockets/legacy_proc_processor.rb +0 -35
  94. data/lib/sprockets/legacy_tilt_processor.rb +0 -29
  95. data/lib/sprockets/sass_template.rb +0 -7
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/asset'
2
3
  require 'sprockets/digest_utils'
3
- require 'sprockets/engines'
4
4
  require 'sprockets/errors'
5
5
  require 'sprockets/file_reader'
6
6
  require 'sprockets/mime'
@@ -10,82 +10,161 @@ require 'sprockets/processor_utils'
10
10
  require 'sprockets/resolve'
11
11
  require 'sprockets/transformers'
12
12
  require 'sprockets/uri_utils'
13
+ require 'sprockets/unloaded_asset'
13
14
 
14
15
  module Sprockets
16
+
15
17
  # The loader phase takes a asset URI location and returns a constructed Asset
16
18
  # object.
17
19
  module Loader
18
20
  include DigestUtils, PathUtils, ProcessorUtils, URIUtils
19
- include Engines, Mime, Processing, Resolve, Transformers
21
+ include Mime, Processing, Resolve, Transformers
22
+
20
23
 
21
- # Public: Load Asset by AssetURI.
24
+ # Public: Load Asset by Asset URI.
22
25
  #
23
- # uri - AssetURI
26
+ # uri - A String containing complete URI to a file including schema
27
+ # and full path such as:
28
+ # "file:///Path/app/assets/js/app.js?type=application/javascript"
24
29
  #
25
30
  # Returns Asset.
26
31
  def load(uri)
27
- filename, params = parse_asset_uri(uri)
28
- if params.key?(:id)
29
- asset = cache.fetch("asset-uri:#{VERSION}#{uri}") do
30
- load_asset_by_id_uri(uri, filename, params)
32
+ unloaded = UnloadedAsset.new(uri, self)
33
+ if unloaded.params.key?(:id)
34
+ unless asset = asset_from_cache(unloaded.asset_key)
35
+ id = unloaded.params.delete(:id)
36
+ uri_without_id = build_asset_uri(unloaded.filename, unloaded.params)
37
+ asset = load_from_unloaded(UnloadedAsset.new(uri_without_id, self))
38
+ if asset[:id] != id
39
+ @logger.warn "Sprockets load error: Tried to find #{uri}, but latest was id #{asset[:id]}"
40
+ end
31
41
  end
32
42
  else
33
- asset = fetch_asset_from_dependency_cache(uri, filename) do |paths|
43
+ asset = fetch_asset_from_dependency_cache(unloaded) do |paths|
44
+ # When asset is previously generated, its "dependencies" are stored in the cache.
45
+ # The presence of `paths` indicates dependencies were stored.
46
+ # We can check to see if the dependencies have not changed by "resolving" them and
47
+ # generating a digest key from the resolved entries. If this digest key has not
48
+ # changed, the asset will be pulled from cache.
49
+ #
50
+ # If this `paths` is present but the cache returns nothing then `fetch_asset_from_dependency_cache`
51
+ # will confusingly be called again with `paths` set to nil where the asset will be
52
+ # loaded from disk.
34
53
  if paths
35
- digest = digest(resolve_dependencies(paths))
36
- if id_uri = cache.get("asset-uri-digest:#{VERSION}:#{uri}:#{digest}", true)
37
- cache.get("asset-uri:#{VERSION}:#{id_uri}", true)
54
+ digest = DigestUtils.digest(resolve_dependencies(paths))
55
+ if uri_from_cache = cache.get(unloaded.digest_key(digest), true)
56
+ asset_from_cache(UnloadedAsset.new(uri_from_cache, self).asset_key)
38
57
  end
39
58
  else
40
- load_asset_by_uri(uri, filename, params)
59
+ load_from_unloaded(unloaded)
41
60
  end
42
61
  end
43
62
  end
44
- Asset.new(self, asset)
63
+ Asset.new(asset)
45
64
  end
46
65
 
47
66
  private
48
- def load_asset_by_id_uri(uri, filename, params)
49
- # Internal assertion, should be routed through load_asset_by_uri
50
- unless id = params.delete(:id)
51
- raise ArgumentError, "expected uri to have an id: #{uri}"
67
+ def compress_key_from_hash(hash, key)
68
+ return unless hash.key?(key)
69
+ value = hash[key].dup
70
+ return if !value
71
+
72
+ if block_given?
73
+ value.map! do |x|
74
+ if yield x
75
+ compress_from_root(x)
76
+ else
77
+ x
78
+ end
79
+ end
80
+ else
81
+ value.map! { |x| compress_from_root(x) }
52
82
  end
83
+ hash[key] = value
84
+ end
53
85
 
54
- uri = build_asset_uri(filename, params)
55
- asset = load_asset_by_uri(uri, filename, params)
56
86
 
57
- if id && asset[:id] != id
58
- raise VersionNotFound, "could not find specified id: #{uri}##{id}"
87
+ def expand_key_from_hash(hash, key)
88
+ return unless hash.key?(key)
89
+ value = hash[key].dup
90
+ return if !value
91
+ if block_given?
92
+ value.map! do |x|
93
+ if yield x
94
+ expand_from_root(x)
95
+ else
96
+ x
97
+ end
98
+ end
99
+ else
100
+ value.map! { |x| expand_from_root(x) }
59
101
  end
102
+ hash[key] = value
103
+ end
104
+
105
+ # Internal: Load asset hash from cache
106
+ #
107
+ # key - A String containing lookup information for an asset
108
+ #
109
+ # This method converts all "compressed" paths to absolute paths.
110
+ # Returns a hash of values representing an asset
111
+ def asset_from_cache(key)
112
+ asset = cache.get(key, true)
113
+ if asset
114
+ asset[:uri] = expand_from_root(asset[:uri])
115
+ asset[:load_path] = expand_from_root(asset[:load_path])
116
+ asset[:filename] = expand_from_root(asset[:filename])
117
+ expand_key_from_hash(asset[:metadata], :included)
118
+ expand_key_from_hash(asset[:metadata], :links)
119
+ expand_key_from_hash(asset[:metadata], :stubbed)
120
+ expand_key_from_hash(asset[:metadata], :required)
121
+ expand_key_from_hash(asset[:metadata], :to_load)
122
+ expand_key_from_hash(asset[:metadata], :to_link)
123
+ expand_key_from_hash(asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
60
124
 
125
+ asset[:metadata].each_key do |k|
126
+ next unless k.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
127
+ expand_key_from_hash(asset[:metadata], k)
128
+ end
129
+ end
61
130
  asset
62
131
  end
63
132
 
64
- def load_asset_by_uri(uri, filename, params)
65
- # Internal assertion, should be routed through load_asset_by_id_uri
66
- if params.key?(:id)
67
- raise ArgumentError, "expected uri to have no id: #{uri}"
133
+ # Internal: Loads an asset and saves it to cache
134
+ #
135
+ # unloaded - An UnloadedAsset
136
+ #
137
+ # This method is only called when the given unloaded asset could not be
138
+ # successfully pulled from cache.
139
+ def load_from_unloaded(unloaded)
140
+ unless file?(unloaded.filename)
141
+ raise FileNotFound, "could not find file: #{unloaded.filename}"
68
142
  end
69
143
 
70
- unless file?(filename)
71
- raise FileNotFound, "could not find file: #{filename}"
72
- end
144
+ path_to_split =
145
+ if index_alias = unloaded.params[:index_alias]
146
+ expand_from_root index_alias
147
+ else
148
+ unloaded.filename
149
+ end
73
150
 
74
- load_path, logical_path = paths_split(config[:paths], filename)
151
+ load_path, logical_path = paths_split(config[:paths], path_to_split)
75
152
 
76
153
  unless load_path
77
- raise FileOutsidePaths, "#{filename} is no longer under a load path: #{self.paths.join(', ')}"
154
+ target = path_to_split
155
+ target += " (index alias of #{unloaded.filename})" if unloaded.params[:index_alias]
156
+ raise FileOutsidePaths, "#{target} is no longer under a load path: #{self.paths.join(', ')}"
78
157
  end
79
158
 
80
- logical_path, file_type, engine_extnames, _ = parse_path_extnames(logical_path)
81
- logical_path = normalize_logical_path(logical_path)
159
+ extname, file_type = match_path_extname(logical_path, mime_exts)
160
+ logical_path = logical_path.chomp(extname)
82
161
  name = logical_path
83
162
 
84
- if pipeline = params[:pipeline]
163
+ if pipeline = unloaded.params[:pipeline]
85
164
  logical_path += ".#{pipeline}"
86
165
  end
87
166
 
88
- if type = params[:type]
167
+ if type = unloaded.params[:type]
89
168
  logical_path += config[:mime_types][type][:extensions].first
90
169
  end
91
170
 
@@ -93,9 +172,9 @@ module Sprockets
93
172
  raise ConversionError, "could not convert #{file_type.inspect} to #{type.inspect}"
94
173
  end
95
174
 
96
- processors = processors_for(type, file_type, engine_extnames, pipeline)
175
+ processors = processors_for(type, file_type, pipeline)
97
176
 
98
- processors_dep_uri = build_processors_uri(type, file_type, engine_extnames, pipeline)
177
+ processors_dep_uri = build_processors_uri(type, file_type, pipeline)
99
178
  dependencies = config[:dependencies] + [processors_dep_uri]
100
179
 
101
180
  # Read into memory and process if theres a processor pipeline
@@ -103,72 +182,160 @@ module Sprockets
103
182
  result = call_processors(processors, {
104
183
  environment: self,
105
184
  cache: self.cache,
106
- uri: uri,
107
- filename: filename,
185
+ uri: unloaded.uri,
186
+ filename: unloaded.filename,
108
187
  load_path: load_path,
109
188
  name: name,
110
189
  content_type: type,
111
- metadata: { dependencies: dependencies }
190
+ metadata: {
191
+ dependencies: dependencies
192
+ }
112
193
  })
194
+ validate_processor_result!(result)
113
195
  source = result.delete(:data)
114
- metadata = result.merge!(
115
- charset: source.encoding.name.downcase,
116
- digest: digest(source),
117
- length: source.bytesize
118
- )
196
+ metadata = result
197
+ metadata[:charset] = source.encoding.name.downcase unless metadata.key?(:charset)
198
+ metadata[:digest] = digest(self.version + source)
199
+ metadata[:length] = source.bytesize
119
200
  else
201
+ dependencies << build_file_digest_uri(unloaded.filename)
120
202
  metadata = {
121
- digest: file_digest(filename),
122
- length: self.stat(filename).size,
203
+ digest: file_digest(unloaded.filename),
204
+ length: self.stat(unloaded.filename).size,
123
205
  dependencies: dependencies
124
206
  }
125
207
  end
126
208
 
127
209
  asset = {
128
- uri: uri,
210
+ uri: unloaded.uri,
129
211
  load_path: load_path,
130
- filename: filename,
212
+ filename: unloaded.filename,
131
213
  name: name,
132
214
  logical_path: logical_path,
133
215
  content_type: type,
134
216
  source: source,
135
217
  metadata: metadata,
136
- integrity: integrity_uri(metadata[:digest], type),
137
- dependencies_digest: digest(resolve_dependencies(metadata[:dependencies]))
218
+ dependencies_digest: DigestUtils.digest(resolve_dependencies(metadata[:dependencies]))
138
219
  }
139
220
 
140
- asset[:id] = pack_hexdigest(digest(asset))
141
- asset[:uri] = build_asset_uri(filename, params.merge(id: asset[:id]))
221
+ asset[:id] = hexdigest(asset)
222
+ asset[:uri] = build_asset_uri(unloaded.filename, unloaded.params.merge(id: asset[:id]))
142
223
 
143
- # Deprecated: Avoid tracking Asset mtime
144
- asset[:mtime] = metadata[:dependencies].map { |u|
145
- if u.start_with?("file-digest:")
146
- s = self.stat(parse_file_digest_uri(u))
147
- s ? s.mtime.to_i : 0
148
- else
149
- 0
224
+ store_asset(asset, unloaded)
225
+ asset
226
+ end
227
+
228
+ # Internal: Save a given asset to the cache
229
+ #
230
+ # asset - A hash containing values of loaded asset
231
+ # unloaded - The UnloadedAsset used to lookup the `asset`
232
+ #
233
+ # This method converts all absolute paths to "compressed" paths
234
+ # which are relative if they're in the root.
235
+ def store_asset(asset, unloaded)
236
+ # Save the asset in the cache under the new URI
237
+ cached_asset = asset.dup
238
+ cached_asset[:uri] = compress_from_root(asset[:uri])
239
+ cached_asset[:filename] = compress_from_root(asset[:filename])
240
+ cached_asset[:load_path] = compress_from_root(asset[:load_path])
241
+
242
+ if cached_asset[:metadata]
243
+ # Deep dup to avoid modifying `asset`
244
+ cached_asset[:metadata] = cached_asset[:metadata].dup
245
+ compress_key_from_hash(cached_asset[:metadata], :included)
246
+ compress_key_from_hash(cached_asset[:metadata], :links)
247
+ compress_key_from_hash(cached_asset[:metadata], :stubbed)
248
+ compress_key_from_hash(cached_asset[:metadata], :required)
249
+ compress_key_from_hash(cached_asset[:metadata], :to_load)
250
+ compress_key_from_hash(cached_asset[:metadata], :to_link)
251
+ compress_key_from_hash(cached_asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
252
+
253
+ cached_asset[:metadata].each do |key, value|
254
+ next unless key.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
255
+ compress_key_from_hash(cached_asset[:metadata], key)
150
256
  end
151
- }.max
257
+ end
152
258
 
153
- cache.set("asset-uri:#{VERSION}:#{asset[:uri]}", asset, true)
154
- cache.set("asset-uri-digest:#{VERSION}:#{uri}:#{asset[:dependencies_digest]}", asset[:uri], true)
259
+ # Unloaded asset and stored_asset now have a different URI
260
+ stored_asset = UnloadedAsset.new(asset[:uri], self)
261
+ cache.set(stored_asset.asset_key, cached_asset, true)
155
262
 
156
- asset
263
+ # Save the new relative path for the digest key of the unloaded asset
264
+ cache.set(unloaded.digest_key(asset[:dependencies_digest]), stored_asset.compressed_path, true)
157
265
  end
158
266
 
159
- def fetch_asset_from_dependency_cache(uri, filename, limit = 3)
160
- key = "asset-uri-cache-dependencies:#{VERSION}:#{uri}:#{file_digest(filename)}"
161
- history = cache.get(key) || []
162
267
 
268
+ # Internal: Resolve set of dependency URIs.
269
+ #
270
+ # uris - An Array of "dependencies" for example:
271
+ # ["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
272
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
273
+ # "processors:type=text/css&file_type=text/css&pipeline=self",
274
+ # "file-digest:///Full/path/app/assets/stylesheets"]
275
+ #
276
+ # Returns back array of things that the given uri depends on
277
+ # For example the environment version, if you're using a different version of sprockets
278
+ # then the dependencies should be different, this is used only for generating cache key
279
+ # for example the "environment-version" may be resolved to "environment-1.0-3.2.0" for
280
+ # version "3.2.0" of sprockets.
281
+ #
282
+ # Any paths that are returned are converted to relative paths
283
+ #
284
+ # Returns array of resolved dependencies
285
+ def resolve_dependencies(uris)
286
+ uris.map { |uri| resolve_dependency(uri) }
287
+ end
288
+
289
+ # Internal: Retrieves an asset based on its digest
290
+ #
291
+ # unloaded - An UnloadedAsset
292
+ # limit - A Fixnum which sets the maximum number of versions of "histories"
293
+ # stored in the cache
294
+ #
295
+ # This method attempts to retrieve the last `limit` number of histories of an asset
296
+ # from the cache a "history" which is an array of unresolved "dependencies" that the asset needs
297
+ # to compile. In this case a dependency can refer to either an asset e.g. index.js
298
+ # may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
299
+ # compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
300
+ # are used.
301
+ #
302
+ # For example a history array may look something like this
303
+ #
304
+ # [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
305
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
306
+ # "processors:type=text/css&file_digesttype=text/css&pipeline=self",
307
+ # "file-digest:///Full/path/app/assets/stylesheets"]]
308
+ #
309
+ # Where the first entry is a Set of dependencies for last generated version of that asset.
310
+ # Multiple versions are stored since Sprockets keeps the last `limit` number of assets
311
+ # generated present in the system.
312
+ #
313
+ # If a "history" of dependencies is present in the cache, each version of "history" will be
314
+ # yielded to the passed block which is responsible for loading the asset. If found, the existing
315
+ # history will be saved with the dependency that found a valid asset moved to the front.
316
+ #
317
+ # If no history is present, or if none of the histories could be resolved to a valid asset then,
318
+ # the block is yielded to and expected to return a valid asset.
319
+ # When this happens the dependencies for the returned asset are added to the "history", and older
320
+ # entries are removed if the "history" is above `limit`.
321
+ def fetch_asset_from_dependency_cache(unloaded, limit = 3)
322
+ key = unloaded.dependency_history_key
323
+
324
+ history = cache.get(key) || []
163
325
  history.each_with_index do |deps, index|
164
- if asset = yield(deps)
326
+ expanded_deps = deps.map do |path|
327
+ path.start_with?("file-digest://") ? expand_from_root(path) : path
328
+ end
329
+ if asset = yield(expanded_deps)
165
330
  cache.set(key, history.rotate!(index)) if index > 0
166
331
  return asset
167
332
  end
168
333
  end
169
334
 
170
335
  asset = yield
171
- deps = asset[:metadata][:dependencies]
336
+ deps = asset[:metadata][:dependencies].dup.map! do |uri|
337
+ uri.start_with?("file-digest://") ? compress_from_root(uri) : uri
338
+ end
172
339
  cache.set(key, history.unshift(deps).take(limit))
173
340
  asset
174
341
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'time'
4
+
5
+ require 'concurrent'
6
+
3
7
  require 'sprockets/manifest_utils'
4
8
 
5
9
  module Sprockets
@@ -48,14 +52,8 @@ module Sprockets
48
52
  @directory ||= File.dirname(@filename) if @filename
49
53
 
50
54
  # If directory is given w/o filename, pick a random manifest location
51
- @rename_filename = nil
52
55
  if @directory && @filename.nil?
53
- @filename = find_directory_manifest(@directory)
54
-
55
- # If legacy manifest name autodetected, mark to rename on save
56
- if File.basename(@filename).start_with?("manifest")
57
- @rename_filename = File.join(@directory, generate_manifest_path)
58
- end
56
+ @filename = find_directory_manifest(@directory, logger)
59
57
  end
60
58
 
61
59
  unless @directory && @filename
@@ -121,31 +119,38 @@ module Sprockets
121
119
 
122
120
  return to_enum(__method__, *args) unless block_given?
123
121
 
124
- paths, filters = args.flatten.partition { |arg| self.class.simple_logical_path?(arg) }
125
- filters = filters.map { |arg| self.class.compile_match_filter(arg) }
126
-
127
122
  environment = self.environment.cached
128
-
129
- paths.each do |path|
130
- environment.find_all_linked_assets(path) do |asset|
131
- yield asset
132
- end
133
- end
134
-
135
- if filters.any?
136
- environment.logical_paths do |logical_path, filename|
137
- if filters.any? { |f| f.call(logical_path, filename) }
138
- environment.find_all_linked_assets(filename) do |asset|
139
- yield asset
140
- end
123
+ promises = args.flatten.map do |path|
124
+ Concurrent::Promise.execute(executor: executor) do
125
+ environment.find_all_linked_assets(path) do |asset|
126
+ yield asset
141
127
  end
142
128
  end
143
129
  end
130
+ promises.each(&:wait!)
144
131
 
145
132
  nil
146
133
  end
147
134
 
148
- # Compile and write asset to directory. The asset is written to a
135
+ # Public: Find the source of assets by paths.
136
+ #
137
+ # Returns Enumerator of assets file content.
138
+ def find_sources(*args)
139
+ return to_enum(__method__, *args) unless block_given?
140
+
141
+ if environment
142
+ find(*args).each do |asset|
143
+ yield asset.source
144
+ end
145
+ else
146
+ args.each do |path|
147
+ asset = assets[path]
148
+ yield File.binread(File.join(dir, asset)) if asset
149
+ end
150
+ end
151
+ end
152
+
153
+ # Compile asset to directory. The asset is written to a
149
154
  # fingerprinted filename like
150
155
  # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
151
156
  # also inserted into the manifest file.
@@ -157,29 +162,46 @@ module Sprockets
157
162
  raise Error, "manifest requires environment for compilation"
158
163
  end
159
164
 
160
- filenames = []
165
+ filenames = []
166
+ concurrent_exporters = []
161
167
 
168
+ assets_to_export = Concurrent::Array.new
162
169
  find(*args) do |asset|
170
+ assets_to_export << asset
171
+ end
172
+
173
+ assets_to_export.each do |asset|
174
+ mtime = Time.now.iso8601
163
175
  files[asset.digest_path] = {
164
176
  'logical_path' => asset.logical_path,
165
- 'mtime' => asset.mtime.iso8601,
177
+ 'mtime' => mtime,
166
178
  'size' => asset.bytesize,
167
179
  'digest' => asset.hexdigest,
168
- 'integrity' => asset.integrity
180
+
181
+ # Deprecated: Remove beta integrity attribute in next release.
182
+ # Callers should DigestUtils.hexdigest_integrity_uri to compute the
183
+ # digest themselves.
184
+ 'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest)
169
185
  }
170
186
  assets[asset.logical_path] = asset.digest_path
171
187
 
172
- target = File.join(dir, asset.digest_path)
188
+ filenames << asset.filename
173
189
 
174
- if File.exist?(target)
175
- logger.debug "Skipping #{target}, already exists"
176
- else
177
- logger.info "Writing #{target}"
178
- asset.write_to target
179
- end
190
+ promise = nil
191
+ exporters_for_asset(asset) do |exporter|
192
+ next if exporter.skip?(logger)
180
193
 
181
- filenames << asset.filename
194
+ if promise.nil?
195
+ promise = Concurrent::Promise.new(executor: executor) { exporter.call }
196
+ concurrent_exporters << promise.execute
197
+ else
198
+ concurrent_exporters << promise.then { exporter.call }
199
+ end
200
+ end
182
201
  end
202
+
203
+ # make sure all exporters have finished before returning the main thread
204
+ concurrent_exporters.each(&:wait!)
183
205
  save
184
206
 
185
207
  filenames
@@ -192,6 +214,7 @@ module Sprockets
192
214
  #
193
215
  def remove(filename)
194
216
  path = File.join(dir, filename)
217
+ gzip = "#{path}.gz"
195
218
  logical_path = files[filename]['logical_path']
196
219
 
197
220
  if assets[logical_path] == filename
@@ -200,6 +223,7 @@ module Sprockets
200
223
 
201
224
  files.delete(filename)
202
225
  FileUtils.rm(path) if File.exist?(path)
226
+ FileUtils.rm(gzip) if File.exist?(gzip)
203
227
 
204
228
  save
205
229
 
@@ -230,9 +254,9 @@ module Sprockets
230
254
  # Sort by timestamp
231
255
  Time.parse(attrs['mtime'])
232
256
  }.reverse.each_with_index.drop_while { |(_, attrs), index|
233
- age = [0, Time.now - Time.parse(attrs['mtime'])].max
257
+ _age = [0, Time.now - Time.parse(attrs['mtime'])].max
234
258
  # Keep if under age or within the count limit
235
- age < age || index < count
259
+ _age < age || index < count
236
260
  }.each { |(path, _), _|
237
261
  # Remove old assets
238
262
  remove(path)
@@ -244,18 +268,13 @@ module Sprockets
244
268
  def clobber
245
269
  FileUtils.rm_r(directory) if File.exist?(directory)
246
270
  logger.info "Removed #{directory}"
271
+ # if we have an environment clear the cache too
272
+ environment.cache.clear if environment
247
273
  nil
248
274
  end
249
275
 
250
276
  # Persist manfiest back to FS
251
277
  def save
252
- if @rename_filename
253
- logger.info "Renaming #{@filename} to #{@rename_filename}"
254
- FileUtils.mv(@filename, @rename_filename)
255
- @filename = @rename_filename
256
- @rename_filename = nil
257
- end
258
-
259
278
  data = json_encode(@data)
260
279
  FileUtils.mkdir_p File.dirname(@filename)
261
280
  PathUtils.atomic_write(@filename) do |f|
@@ -264,6 +283,36 @@ module Sprockets
264
283
  end
265
284
 
266
285
  private
286
+
287
+ # Given an asset, finds all exporters that
288
+ # match its mime-type.
289
+ #
290
+ # Will yield each expoter to the passed in block.
291
+ #
292
+ # array = []
293
+ # puts asset.content_type # => "application/javascript"
294
+ # exporters_for_asset(asset) do |exporter|
295
+ # array << exporter
296
+ # end
297
+ # # puts array => [Exporters::FileExporter, Exporters::ZlibExporter]
298
+ def exporters_for_asset(asset)
299
+ exporters = [Exporters::FileExporter]
300
+
301
+ environment.exporters.each do |mime_type, exporter_list|
302
+ next unless asset.content_type
303
+ next unless environment.match_mime_type? asset.content_type, mime_type
304
+ exporter_list.each do |exporter|
305
+ exporters << exporter
306
+ end
307
+ end
308
+
309
+ exporters.uniq!
310
+
311
+ exporters.each do |exporter|
312
+ yield exporter.new(asset: asset, environment: environment, directory: dir)
313
+ end
314
+ end
315
+
267
316
  def json_decode(obj)
268
317
  JSON.parse(obj, create_additions: false)
269
318
  end
@@ -281,5 +330,9 @@ module Sprockets
281
330
  logger
282
331
  end
283
332
  end
333
+
334
+ def executor
335
+ @executor ||= environment.export_concurrent ? :fast : :immediate
336
+ end
284
337
  end
285
338
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'securerandom'
3
+ require 'logger'
2
4
 
3
5
  module Sprockets
4
6
  # Public: Manifest utilities.
@@ -6,7 +8,6 @@ module Sprockets
6
8
  extend self
7
9
 
8
10
  MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/
9
- LEGACY_MANIFEST_RE = /^manifest(-[0-9a-f]{32})?.json$/
10
11
 
11
12
  # Public: Generate a new random manifest path.
12
13
  #
@@ -33,12 +34,14 @@ module Sprockets
33
34
  # # => "/app/public/assets/.sprockets-manifest-abc123.json"
34
35
  #
35
36
  # Returns String filename.
36
- def find_directory_manifest(dirname)
37
+ def find_directory_manifest(dirname, logger = Logger.new($stderr))
37
38
  entries = File.directory?(dirname) ? Dir.entries(dirname) : []
38
- entry = entries.find { |e| e =~ MANIFEST_RE } ||
39
- # Deprecated: Will be removed in 4.x
40
- entries.find { |e| e =~ LEGACY_MANIFEST_RE } ||
41
- generate_manifest_path
39
+ manifest_entries = entries.select { |e| e =~ MANIFEST_RE }
40
+ if manifest_entries.length > 1
41
+ manifest_entries.sort!
42
+ logger.warn("Found multiple manifests: #{manifest_entries}. Choosing the first alphabetically: #{manifest_entries.first}")
43
+ end
44
+ entry = manifest_entries.first || generate_manifest_path
42
45
  File.join(dirname, entry)
43
46
  end
44
47
  end