sprockets 3.0.0 → 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +426 -404
  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 +33 -24
  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 +61 -13
  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 +71 -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 +239 -70
  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,162 @@ 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(source)
199
+ metadata[:length] = source.bytesize
200
+ metadata[:environment_version] = version
119
201
  else
202
+ dependencies << build_file_digest_uri(unloaded.filename)
120
203
  metadata = {
121
- digest: file_digest(filename),
122
- length: self.stat(filename).size,
123
- dependencies: dependencies
204
+ digest: file_digest(unloaded.filename),
205
+ length: self.stat(unloaded.filename).size,
206
+ dependencies: dependencies,
207
+ environment_version: version,
124
208
  }
125
209
  end
126
210
 
127
211
  asset = {
128
- uri: uri,
212
+ uri: unloaded.uri,
129
213
  load_path: load_path,
130
- filename: filename,
214
+ filename: unloaded.filename,
131
215
  name: name,
132
216
  logical_path: logical_path,
133
217
  content_type: type,
134
218
  source: source,
135
219
  metadata: metadata,
136
- integrity: integrity_uri(metadata[:digest], type),
137
- dependencies_digest: digest(resolve_dependencies(metadata[:dependencies]))
220
+ dependencies_digest: DigestUtils.digest(resolve_dependencies(metadata[:dependencies]))
138
221
  }
139
222
 
140
- asset[:id] = pack_hexdigest(digest(asset))
141
- asset[:uri] = build_asset_uri(filename, params.merge(id: asset[:id]))
223
+ asset[:id] = hexdigest(asset)
224
+ asset[:uri] = build_asset_uri(unloaded.filename, unloaded.params.merge(id: asset[:id]))
142
225
 
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
226
+ store_asset(asset, unloaded)
227
+ asset
228
+ end
229
+
230
+ # Internal: Save a given asset to the cache
231
+ #
232
+ # asset - A hash containing values of loaded asset
233
+ # unloaded - The UnloadedAsset used to lookup the `asset`
234
+ #
235
+ # This method converts all absolute paths to "compressed" paths
236
+ # which are relative if they're in the root.
237
+ def store_asset(asset, unloaded)
238
+ # Save the asset in the cache under the new URI
239
+ cached_asset = asset.dup
240
+ cached_asset[:uri] = compress_from_root(asset[:uri])
241
+ cached_asset[:filename] = compress_from_root(asset[:filename])
242
+ cached_asset[:load_path] = compress_from_root(asset[:load_path])
243
+
244
+ if cached_asset[:metadata]
245
+ # Deep dup to avoid modifying `asset`
246
+ cached_asset[:metadata] = cached_asset[:metadata].dup
247
+ compress_key_from_hash(cached_asset[:metadata], :included)
248
+ compress_key_from_hash(cached_asset[:metadata], :links)
249
+ compress_key_from_hash(cached_asset[:metadata], :stubbed)
250
+ compress_key_from_hash(cached_asset[:metadata], :required)
251
+ compress_key_from_hash(cached_asset[:metadata], :to_load)
252
+ compress_key_from_hash(cached_asset[:metadata], :to_link)
253
+ compress_key_from_hash(cached_asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
254
+
255
+ cached_asset[:metadata].each do |key, value|
256
+ next unless key.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
257
+ compress_key_from_hash(cached_asset[:metadata], key)
150
258
  end
151
- }.max
259
+ end
152
260
 
153
- cache.set("asset-uri:#{VERSION}:#{asset[:uri]}", asset, true)
154
- cache.set("asset-uri-digest:#{VERSION}:#{uri}:#{asset[:dependencies_digest]}", asset[:uri], true)
261
+ # Unloaded asset and stored_asset now have a different URI
262
+ stored_asset = UnloadedAsset.new(asset[:uri], self)
263
+ cache.set(stored_asset.asset_key, cached_asset, true)
155
264
 
156
- asset
265
+ # Save the new relative path for the digest key of the unloaded asset
266
+ cache.set(unloaded.digest_key(asset[:dependencies_digest]), stored_asset.compressed_path, true)
157
267
  end
158
268
 
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
269
 
270
+ # Internal: Resolve set of dependency URIs.
271
+ #
272
+ # uris - An Array of "dependencies" for example:
273
+ # ["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
274
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
275
+ # "processors:type=text/css&file_type=text/css&pipeline=self",
276
+ # "file-digest:///Full/path/app/assets/stylesheets"]
277
+ #
278
+ # Returns back array of things that the given uri depends on
279
+ # For example the environment version, if you're using a different version of sprockets
280
+ # then the dependencies should be different, this is used only for generating cache key
281
+ # for example the "environment-version" may be resolved to "environment-1.0-3.2.0" for
282
+ # version "3.2.0" of sprockets.
283
+ #
284
+ # Any paths that are returned are converted to relative paths
285
+ #
286
+ # Returns array of resolved dependencies
287
+ def resolve_dependencies(uris)
288
+ uris.map { |uri| resolve_dependency(uri) }
289
+ end
290
+
291
+ # Internal: Retrieves an asset based on its digest
292
+ #
293
+ # unloaded - An UnloadedAsset
294
+ # limit - A Fixnum which sets the maximum number of versions of "histories"
295
+ # stored in the cache
296
+ #
297
+ # This method attempts to retrieve the last `limit` number of histories of an asset
298
+ # from the cache a "history" which is an array of unresolved "dependencies" that the asset needs
299
+ # to compile. In this case a dependency can refer to either an asset e.g. index.js
300
+ # may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
301
+ # compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
302
+ # are used.
303
+ #
304
+ # For example a history array may look something like this
305
+ #
306
+ # [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
307
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
308
+ # "processors:type=text/css&file_digesttype=text/css&pipeline=self",
309
+ # "file-digest:///Full/path/app/assets/stylesheets"]]
310
+ #
311
+ # Where the first entry is a Set of dependencies for last generated version of that asset.
312
+ # Multiple versions are stored since Sprockets keeps the last `limit` number of assets
313
+ # generated present in the system.
314
+ #
315
+ # If a "history" of dependencies is present in the cache, each version of "history" will be
316
+ # yielded to the passed block which is responsible for loading the asset. If found, the existing
317
+ # history will be saved with the dependency that found a valid asset moved to the front.
318
+ #
319
+ # If no history is present, or if none of the histories could be resolved to a valid asset then,
320
+ # the block is yielded to and expected to return a valid asset.
321
+ # When this happens the dependencies for the returned asset are added to the "history", and older
322
+ # entries are removed if the "history" is above `limit`.
323
+ def fetch_asset_from_dependency_cache(unloaded, limit = 3)
324
+ key = unloaded.dependency_history_key
325
+
326
+ history = cache.get(key) || []
163
327
  history.each_with_index do |deps, index|
164
- if asset = yield(deps)
328
+ expanded_deps = deps.map do |path|
329
+ path.start_with?("file-digest://") ? expand_from_root(path) : path
330
+ end
331
+ if asset = yield(expanded_deps)
165
332
  cache.set(key, history.rotate!(index)) if index > 0
166
333
  return asset
167
334
  end
168
335
  end
169
336
 
170
337
  asset = yield
171
- deps = asset[:metadata][:dependencies]
338
+ deps = asset[:metadata][:dependencies].dup.map! do |uri|
339
+ uri.start_with?("file-digest://") ? compress_from_root(uri) : uri
340
+ end
172
341
  cache.set(key, history.unshift(deps).take(limit))
173
342
  asset
174
343
  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