sprockets 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +665 -0
  4. data/bin/sprockets +93 -0
  5. data/lib/rake/sprocketstask.rb +153 -0
  6. data/lib/sprockets.rb +229 -0
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +202 -0
  9. data/lib/sprockets/autoload.rb +16 -0
  10. data/lib/sprockets/autoload/babel.rb +8 -0
  11. data/lib/sprockets/autoload/closure.rb +8 -0
  12. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  13. data/lib/sprockets/autoload/eco.rb +8 -0
  14. data/lib/sprockets/autoload/ejs.rb +8 -0
  15. data/lib/sprockets/autoload/jsminc.rb +8 -0
  16. data/lib/sprockets/autoload/sass.rb +8 -0
  17. data/lib/sprockets/autoload/sassc.rb +8 -0
  18. data/lib/sprockets/autoload/uglifier.rb +8 -0
  19. data/lib/sprockets/autoload/yui.rb +8 -0
  20. data/lib/sprockets/autoload/zopfli.rb +7 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +147 -0
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache.rb +271 -0
  26. data/lib/sprockets/cache/file_store.rb +208 -0
  27. data/lib/sprockets/cache/memory_store.rb +75 -0
  28. data/lib/sprockets/cache/null_store.rb +54 -0
  29. data/lib/sprockets/cached_environment.rb +64 -0
  30. data/lib/sprockets/closure_compressor.rb +48 -0
  31. data/lib/sprockets/coffee_script_processor.rb +39 -0
  32. data/lib/sprockets/compressing.rb +134 -0
  33. data/lib/sprockets/configuration.rb +79 -0
  34. data/lib/sprockets/context.rb +304 -0
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +200 -0
  37. data/lib/sprockets/directive_processor.rb +414 -0
  38. data/lib/sprockets/eco_processor.rb +33 -0
  39. data/lib/sprockets/ejs_processor.rb +32 -0
  40. data/lib/sprockets/encoding_utils.rb +262 -0
  41. data/lib/sprockets/environment.rb +46 -0
  42. data/lib/sprockets/erb_processor.rb +37 -0
  43. data/lib/sprockets/errors.rb +12 -0
  44. data/lib/sprockets/exporters/base.rb +71 -0
  45. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  46. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  47. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  48. data/lib/sprockets/exporting.rb +73 -0
  49. data/lib/sprockets/file_reader.rb +16 -0
  50. data/lib/sprockets/http_utils.rb +135 -0
  51. data/lib/sprockets/jsminc_compressor.rb +32 -0
  52. data/lib/sprockets/jst_processor.rb +50 -0
  53. data/lib/sprockets/loader.rb +345 -0
  54. data/lib/sprockets/manifest.rb +338 -0
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +96 -0
  57. data/lib/sprockets/npm.rb +52 -0
  58. data/lib/sprockets/path_dependency_utils.rb +77 -0
  59. data/lib/sprockets/path_digest_utils.rb +48 -0
  60. data/lib/sprockets/path_utils.rb +367 -0
  61. data/lib/sprockets/paths.rb +82 -0
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +228 -0
  64. data/lib/sprockets/processor_utils.rb +169 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +30 -0
  67. data/lib/sprockets/sass_compressor.rb +63 -0
  68. data/lib/sprockets/sass_functions.rb +3 -0
  69. data/lib/sprockets/sass_importer.rb +3 -0
  70. data/lib/sprockets/sass_processor.rb +313 -0
  71. data/lib/sprockets/sassc_compressor.rb +56 -0
  72. data/lib/sprockets/sassc_processor.rb +297 -0
  73. data/lib/sprockets/server.rb +295 -0
  74. data/lib/sprockets/source_map_processor.rb +66 -0
  75. data/lib/sprockets/source_map_utils.rb +483 -0
  76. data/lib/sprockets/transformers.rb +173 -0
  77. data/lib/sprockets/uglifier_compressor.rb +66 -0
  78. data/lib/sprockets/unloaded_asset.rb +139 -0
  79. data/lib/sprockets/uri_tar.rb +99 -0
  80. data/lib/sprockets/uri_utils.rb +191 -0
  81. data/lib/sprockets/utils.rb +202 -0
  82. data/lib/sprockets/utils/gzip.rb +99 -0
  83. data/lib/sprockets/version.rb +4 -0
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. metadata +444 -0
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+ require 'time'
4
+
5
+ require 'concurrent'
6
+
7
+ require 'sprockets/manifest_utils'
8
+
9
+ module Sprockets
10
+ # The Manifest logs the contents of assets compiled to a single directory. It
11
+ # records basic attributes about the asset for fast lookup without having to
12
+ # compile. A pointer from each logical path indicates which fingerprinted
13
+ # asset is the current one.
14
+ #
15
+ # The JSON is part of the public API and should be considered stable. This
16
+ # should make it easy to read from other programming languages and processes
17
+ # that don't have sprockets loaded. See `#assets` and `#files` for more
18
+ # infomation about the structure.
19
+ class Manifest
20
+ include ManifestUtils
21
+
22
+ attr_reader :environment
23
+
24
+ # Create new Manifest associated with an `environment`. `filename` is a full
25
+ # path to the manifest json file. The file may or may not already exist. The
26
+ # dirname of the `filename` will be used to write compiled assets to.
27
+ # Otherwise, if the path is a directory, the filename will default a random
28
+ # ".sprockets-manifest-*.json" file in that directory.
29
+ #
30
+ # Manifest.new(environment, "./public/assets/manifest.json")
31
+ #
32
+ def initialize(*args)
33
+ if args.first.is_a?(Base) || args.first.nil?
34
+ @environment = args.shift
35
+ end
36
+
37
+ @directory, @filename = args[0], args[1]
38
+
39
+ # Whether the manifest file is using the old manifest-*.json naming convention
40
+ @legacy_manifest = false
41
+
42
+ # Expand paths
43
+ @directory = File.expand_path(@directory) if @directory
44
+ @filename = File.expand_path(@filename) if @filename
45
+
46
+ # If filename is given as the second arg
47
+ if @directory && File.extname(@directory) != ""
48
+ @directory, @filename = nil, @directory
49
+ end
50
+
51
+ # Default dir to the directory of the filename
52
+ @directory ||= File.dirname(@filename) if @filename
53
+
54
+ # If directory is given w/o filename, pick a random manifest location
55
+ if @directory && @filename.nil?
56
+ @filename = find_directory_manifest(@directory, logger)
57
+ end
58
+
59
+ unless @directory && @filename
60
+ raise ArgumentError, "manifest requires output filename"
61
+ end
62
+
63
+ data = {}
64
+
65
+ begin
66
+ if File.exist?(@filename)
67
+ data = json_decode(File.read(@filename))
68
+ end
69
+ rescue JSON::ParserError => e
70
+ logger.error "#{@filename} is invalid: #{e.class} #{e.message}"
71
+ end
72
+
73
+ @data = data
74
+ end
75
+
76
+ # Returns String path to manifest.json file.
77
+ attr_reader :filename
78
+ alias_method :path, :filename
79
+
80
+ attr_reader :directory
81
+ alias_method :dir, :directory
82
+
83
+ # Returns internal assets mapping. Keys are logical paths which
84
+ # map to the latest fingerprinted filename.
85
+ #
86
+ # Logical path (String): Fingerprint path (String)
87
+ #
88
+ # { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",
89
+ # "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }
90
+ #
91
+ def assets
92
+ @data['assets'] ||= {}
93
+ end
94
+
95
+ # Returns internal file directory listing. Keys are filenames
96
+ # which map to an attributes array.
97
+ #
98
+ # Fingerprint path (String):
99
+ # logical_path: Logical path (String)
100
+ # mtime: ISO8601 mtime (String)
101
+ # digest: Base64 hex digest (String)
102
+ #
103
+ # { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =>
104
+ # { 'logical_path' => "application.js",
105
+ # 'mtime' => "2011-12-13T21:47:08-06:00",
106
+ # 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }
107
+ #
108
+ def files
109
+ @data['files'] ||= {}
110
+ end
111
+
112
+ # Public: Find all assets matching pattern set in environment.
113
+ #
114
+ # Returns Enumerator of Assets.
115
+ def find(*args)
116
+ unless environment
117
+ raise Error, "manifest requires environment for compilation"
118
+ end
119
+
120
+ return to_enum(__method__, *args) unless block_given?
121
+
122
+ environment = self.environment.cached
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
127
+ end
128
+ end
129
+ end
130
+ promises.each(&:wait!)
131
+
132
+ nil
133
+ end
134
+
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
154
+ # fingerprinted filename like
155
+ # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
156
+ # also inserted into the manifest file.
157
+ #
158
+ # compile("application.js")
159
+ #
160
+ def compile(*args)
161
+ unless environment
162
+ raise Error, "manifest requires environment for compilation"
163
+ end
164
+
165
+ filenames = []
166
+ concurrent_exporters = []
167
+
168
+ assets_to_export = Concurrent::Array.new
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
175
+ files[asset.digest_path] = {
176
+ 'logical_path' => asset.logical_path,
177
+ 'mtime' => mtime,
178
+ 'size' => asset.bytesize,
179
+ 'digest' => asset.hexdigest,
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)
185
+ }
186
+ assets[asset.logical_path] = asset.digest_path
187
+
188
+ filenames << asset.filename
189
+
190
+ promise = nil
191
+ exporters_for_asset(asset) do |exporter|
192
+ next if exporter.skip?(logger)
193
+
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
201
+ end
202
+
203
+ # make sure all exporters have finished before returning the main thread
204
+ concurrent_exporters.each(&:wait!)
205
+ save
206
+
207
+ filenames
208
+ end
209
+
210
+ # Removes file from directory and from manifest. `filename` must
211
+ # be the name with any directory path.
212
+ #
213
+ # manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
214
+ #
215
+ def remove(filename)
216
+ path = File.join(dir, filename)
217
+ gzip = "#{path}.gz"
218
+ logical_path = files[filename]['logical_path']
219
+
220
+ if assets[logical_path] == filename
221
+ assets.delete(logical_path)
222
+ end
223
+
224
+ files.delete(filename)
225
+ FileUtils.rm(path) if File.exist?(path)
226
+ FileUtils.rm(gzip) if File.exist?(gzip)
227
+
228
+ save
229
+
230
+ logger.info "Removed #{filename}"
231
+
232
+ nil
233
+ end
234
+
235
+ # Cleanup old assets in the compile directory. By default it will
236
+ # keep the latest version, 2 backups and any created within the past hour.
237
+ #
238
+ # Examples
239
+ #
240
+ # To force only 1 backup to be kept, set count=1 and age=0.
241
+ #
242
+ # To only keep files created within the last 10 minutes, set count=0 and
243
+ # age=600.
244
+ #
245
+ def clean(count = 2, age = 3600)
246
+ asset_versions = files.group_by { |_, attrs| attrs['logical_path'] }
247
+
248
+ asset_versions.each do |logical_path, versions|
249
+ current = assets[logical_path]
250
+
251
+ versions.reject { |path, _|
252
+ path == current
253
+ }.sort_by { |_, attrs|
254
+ # Sort by timestamp
255
+ Time.parse(attrs['mtime'])
256
+ }.reverse.each_with_index.drop_while { |(_, attrs), index|
257
+ _age = [0, Time.now - Time.parse(attrs['mtime'])].max
258
+ # Keep if under age or within the count limit
259
+ _age < age || index < count
260
+ }.each { |(path, _), _|
261
+ # Remove old assets
262
+ remove(path)
263
+ }
264
+ end
265
+ end
266
+
267
+ # Wipe directive
268
+ def clobber
269
+ FileUtils.rm_r(directory) if File.exist?(directory)
270
+ logger.info "Removed #{directory}"
271
+ # if we have an environment clear the cache too
272
+ environment.cache.clear if environment
273
+ nil
274
+ end
275
+
276
+ # Persist manfiest back to FS
277
+ def save
278
+ data = json_encode(@data)
279
+ FileUtils.mkdir_p File.dirname(@filename)
280
+ PathUtils.atomic_write(@filename) do |f|
281
+ f.write(data)
282
+ end
283
+ end
284
+
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
+
316
+ def json_decode(obj)
317
+ JSON.parse(obj, create_additions: false)
318
+ end
319
+
320
+ def json_encode(obj)
321
+ JSON.generate(obj)
322
+ end
323
+
324
+ def logger
325
+ if environment
326
+ environment.logger
327
+ else
328
+ logger = Logger.new($stderr)
329
+ logger.level = Logger::FATAL
330
+ logger
331
+ end
332
+ end
333
+
334
+ def executor
335
+ @executor ||= environment.export_concurrent ? :fast : :immediate
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ require 'securerandom'
3
+ require 'logger'
4
+
5
+ module Sprockets
6
+ # Public: Manifest utilities.
7
+ module ManifestUtils
8
+ extend self
9
+
10
+ MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/
11
+
12
+ # Public: Generate a new random manifest path.
13
+ #
14
+ # Manifests are not intended to be accessed publicly, but typically live
15
+ # alongside public assets for convenience. To avoid being served, the
16
+ # filename is prefixed with a "." which is usually hidden by web servers
17
+ # like Apache. To help in other environments that may not control this,
18
+ # a random hex string is appended to the filename to prevent people from
19
+ # guessing the location. If directory indexes are enabled on the server,
20
+ # all bets are off.
21
+ #
22
+ # Return String path.
23
+ def generate_manifest_path
24
+ ".sprockets-manifest-#{SecureRandom.hex(16)}.json"
25
+ end
26
+
27
+ # Public: Find or pick a new manifest filename for target build directory.
28
+ #
29
+ # dirname - String dirname
30
+ #
31
+ # Examples
32
+ #
33
+ # find_directory_manifest("/app/public/assets")
34
+ # # => "/app/public/assets/.sprockets-manifest-abc123.json"
35
+ #
36
+ # Returns String filename.
37
+ def find_directory_manifest(dirname, logger = Logger.new($stderr))
38
+ entries = File.directory?(dirname) ? Dir.entries(dirname) : []
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
45
+ File.join(dirname, entry)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/encoding_utils'
3
+ require 'sprockets/http_utils'
4
+ require 'sprockets/utils'
5
+
6
+ module Sprockets
7
+ module Mime
8
+ include HTTPUtils, Utils
9
+
10
+ # Public: Mapping of MIME type Strings to properties Hash.
11
+ #
12
+ # key - MIME Type String
13
+ # value - Hash
14
+ # extensions - Array of extnames
15
+ # charset - Default Encoding or function to detect encoding
16
+ #
17
+ # Returns Hash.
18
+ def mime_types
19
+ config[:mime_types]
20
+ end
21
+
22
+ # Internal: Mapping of MIME extension Strings to MIME type Strings.
23
+ #
24
+ # Used for internal fast lookup purposes.
25
+ #
26
+ # Examples:
27
+ #
28
+ # mime_exts['.js'] #=> 'application/javascript'
29
+ #
30
+ # key - MIME extension String
31
+ # value - MIME Type String
32
+ #
33
+ # Returns Hash.
34
+ def mime_exts
35
+ config[:mime_exts]
36
+ end
37
+
38
+ # Public: Register a new mime type.
39
+ #
40
+ # mime_type - String MIME Type
41
+ # extensions - Array of String extnames
42
+ # charset - Proc/Method that detects the charset of a file.
43
+ # See EncodingUtils.
44
+ #
45
+ # Returns nothing.
46
+ def register_mime_type(mime_type, extensions: [], charset: nil)
47
+ extnames = Array(extensions)
48
+
49
+ charset ||= :default if mime_type.start_with?('text/')
50
+ charset = EncodingUtils::CHARSET_DETECT[charset] if charset.is_a?(Symbol)
51
+
52
+ self.config = hash_reassoc(config, :mime_exts) do |mime_exts|
53
+ extnames.each do |extname|
54
+ mime_exts[extname] = mime_type
55
+ end
56
+ mime_exts
57
+ end
58
+
59
+ self.config = hash_reassoc(config, :mime_types) do |mime_types|
60
+ type = { extensions: extnames }
61
+ type[:charset] = charset if charset
62
+ mime_types.merge(mime_type => type)
63
+ end
64
+ end
65
+
66
+ # Internal: Get detecter function for MIME type.
67
+ #
68
+ # mime_type - String MIME type
69
+ #
70
+ # Returns Proc detector or nil if none is available.
71
+ def mime_type_charset_detecter(mime_type)
72
+ if type = config[:mime_types][mime_type]
73
+ if detect = type[:charset]
74
+ return detect
75
+ end
76
+ end
77
+ end
78
+
79
+ # Public: Read file on disk with MIME type specific encoding.
80
+ #
81
+ # filename - String path
82
+ # content_type - String MIME type
83
+ #
84
+ # Returns String file contents transcoded to UTF-8 or in its external
85
+ # encoding.
86
+ def read_file(filename, content_type = nil)
87
+ data = File.binread(filename)
88
+
89
+ if detect = mime_type_charset_detecter(content_type)
90
+ detect.call(data).encode(Encoding::UTF_8, universal_newline: true)
91
+ else
92
+ data
93
+ end
94
+ end
95
+ end
96
+ end