sprockets 2.6.0 → 4.2.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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +118 -0
  3. data/{LICENSE → MIT-LICENSE} +2 -2
  4. data/README.md +541 -289
  5. data/bin/sprockets +20 -7
  6. data/lib/rake/sprocketstask.rb +34 -17
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +158 -210
  9. data/lib/sprockets/autoload/babel.rb +8 -0
  10. data/lib/sprockets/autoload/closure.rb +8 -0
  11. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  12. data/lib/sprockets/autoload/eco.rb +8 -0
  13. data/lib/sprockets/autoload/ejs.rb +8 -0
  14. data/lib/sprockets/autoload/jsminc.rb +8 -0
  15. data/lib/sprockets/autoload/sass.rb +8 -0
  16. data/lib/sprockets/autoload/sassc.rb +8 -0
  17. data/lib/sprockets/autoload/uglifier.rb +8 -0
  18. data/lib/sprockets/autoload/yui.rb +8 -0
  19. data/lib/sprockets/autoload/zopfli.rb +7 -0
  20. data/lib/sprockets/autoload.rb +16 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +89 -378
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache/file_store.rb +190 -14
  26. data/lib/sprockets/cache/memory_store.rb +84 -0
  27. data/lib/sprockets/cache/null_store.rb +54 -0
  28. data/lib/sprockets/cache.rb +271 -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 +166 -150
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +197 -0
  37. data/lib/sprockets/directive_processor.rb +241 -215
  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 +261 -0
  41. data/lib/sprockets/environment.rb +23 -64
  42. data/lib/sprockets/erb_processor.rb +43 -0
  43. data/lib/sprockets/errors.rb +5 -13
  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 +36 -19
  53. data/lib/sprockets/loader.rb +347 -0
  54. data/lib/sprockets/manifest.rb +228 -112
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +78 -31
  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 +43 -19
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +146 -164
  64. data/lib/sprockets/processor_utils.rb +170 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +20 -15
  67. data/lib/sprockets/sass_compressor.rb +55 -10
  68. data/lib/sprockets/sass_functions.rb +3 -70
  69. data/lib/sprockets/sass_importer.rb +3 -29
  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 +159 -91
  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 +194 -0
  81. data/lib/sprockets/utils/gzip.rb +99 -0
  82. data/lib/sprockets/utils.rb +193 -52
  83. data/lib/sprockets/version.rb +2 -1
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. data/lib/sprockets.rb +217 -75
  86. metadata +272 -117
  87. data/lib/sprockets/asset_attributes.rb +0 -131
  88. data/lib/sprockets/bundled_asset.rb +0 -80
  89. data/lib/sprockets/caching.rb +0 -96
  90. data/lib/sprockets/charset_normalizer.rb +0 -41
  91. data/lib/sprockets/eco_template.rb +0 -38
  92. data/lib/sprockets/ejs_template.rb +0 -37
  93. data/lib/sprockets/engines.rb +0 -74
  94. data/lib/sprockets/index.rb +0 -99
  95. data/lib/sprockets/processed_asset.rb +0 -152
  96. data/lib/sprockets/processor.rb +0 -32
  97. data/lib/sprockets/safety_colons.rb +0 -28
  98. data/lib/sprockets/sass_template.rb +0 -60
  99. data/lib/sprockets/scss_template.rb +0 -13
  100. data/lib/sprockets/static_asset.rb +0 -58
@@ -1,51 +1,85 @@
1
- require 'multi_json'
1
+ # frozen_string_literal: true
2
+ require 'json'
2
3
  require 'time'
3
4
 
5
+ require 'concurrent'
6
+
7
+ require 'sprockets/manifest_utils'
8
+
4
9
  module Sprockets
5
- # The Manifest logs the contents of assets compiled to a single
6
- # directory. It records basic attributes about the asset for fast
7
- # lookup without having to compile. A pointer from each logical path
8
- # indicates with fingerprinted asset is the current one.
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.
9
14
  #
10
- # The JSON is part of the public API and should be considered
11
- # stable. This should make it easy to read from other programming
12
- # languages and processes that don't have sprockets loaded. See
13
- # `#assets` and `#files` for more infomation about the structure.
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
+ # information about the structure.
14
19
  class Manifest
15
- attr_reader :environment, :path, :dir
20
+ include ManifestUtils
21
+
22
+ attr_reader :environment
16
23
 
17
- # Create new Manifest associated with an `environment`. `path` is
18
- # a full path to the manifest json file. The file may or may not
19
- # already exist. The dirname of the `path` will be used to write
20
- # compiled assets to. Otherwise, if the path is a directory, the
21
- # filename will default to "manifest.json" in that directory.
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.
22
29
  #
23
30
  # Manifest.new(environment, "./public/assets/manifest.json")
24
31
  #
25
- def initialize(environment, path)
26
- @environment = environment
32
+ def initialize(*args)
33
+ if args.first.is_a?(Base) || args.first.nil?
34
+ @environment = args.shift
35
+ end
27
36
 
28
- if File.extname(path) == ""
29
- @dir = File.expand_path(path)
30
- @path = File.join(@dir, 'manifest.json')
31
- else
32
- @path = File.expand_path(path)
33
- @dir = File.dirname(path)
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)
34
57
  end
35
58
 
36
- data = nil
59
+ unless @directory && @filename
60
+ raise ArgumentError, "manifest requires output filename"
61
+ end
62
+
63
+ data = {}
37
64
 
38
65
  begin
39
- if File.exist?(@path)
40
- data = json_decode(File.read(@path))
66
+ if File.exist?(@filename)
67
+ data = json_decode(File.read(@filename))
41
68
  end
42
- rescue MultiJson::DecodeError => e
43
- logger.error "#{@path} is invalid: #{e.class} #{e.message}"
69
+ rescue JSON::ParserError => e
70
+ logger.error "#{@filename} is invalid: #{e.class} #{e.message}"
44
71
  end
45
72
 
46
- @data = data.is_a?(Hash) ? data : {}
73
+ @data = data
47
74
  end
48
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
+
49
83
  # Returns internal assets mapping. Keys are logical paths which
50
84
  # map to the latest fingerprinted filename.
51
85
  #
@@ -75,7 +109,49 @@ module Sprockets
75
109
  @data['files'] ||= {}
76
110
  end
77
111
 
78
- # Compile and write asset to directory. The asset is written to a
112
+ # Public: Find all assets matching pattern set in environment.
113
+ #
114
+ # Returns Enumerator of Assets.
115
+ def find(*args, &block)
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).to_a
126
+ end
127
+ end
128
+
129
+ promises.each do |promise|
130
+ promise.value!.each(&block)
131
+ end
132
+
133
+ nil
134
+ end
135
+
136
+ # Public: Find the source of assets by paths.
137
+ #
138
+ # Returns Enumerator of assets file content.
139
+ def find_sources(*args)
140
+ return to_enum(__method__, *args) unless block_given?
141
+
142
+ if environment
143
+ find(*args).each do |asset|
144
+ yield asset.source
145
+ end
146
+ else
147
+ args.each do |path|
148
+ asset = assets[path]
149
+ yield File.binread(File.join(dir, asset)) if asset
150
+ end
151
+ end
152
+ end
153
+
154
+ # Compile asset to directory. The asset is written to a
79
155
  # fingerprinted filename like
80
156
  # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
81
157
  # also inserted into the manifest file.
@@ -83,32 +159,53 @@ module Sprockets
83
159
  # compile("application.js")
84
160
  #
85
161
  def compile(*args)
86
- paths = environment.each_logical_path(*args).to_a +
87
- args.flatten.select { |fn| Pathname.new(fn).absolute? if fn.is_a?(String)}
88
-
89
- paths.each do |path|
90
- if asset = find_asset(path)
91
- files[asset.digest_path] = {
92
- 'logical_path' => asset.logical_path,
93
- 'mtime' => asset.mtime.iso8601,
94
- 'size' => asset.bytesize,
95
- 'digest' => asset.digest
96
- }
97
- assets[asset.logical_path] = asset.digest_path
98
-
99
- target = File.join(dir, asset.digest_path)
100
-
101
- if File.exist?(target)
102
- logger.debug "Skipping #{target}, already exists"
162
+ unless environment
163
+ raise Error, "manifest requires environment for compilation"
164
+ end
165
+
166
+ filenames = []
167
+ concurrent_exporters = []
168
+
169
+ assets_to_export = Concurrent::Array.new
170
+ find(*args) do |asset|
171
+ assets_to_export << asset
172
+ end
173
+
174
+ assets_to_export.each do |asset|
175
+ mtime = Time.now.iso8601
176
+ files[asset.digest_path] = {
177
+ 'logical_path' => asset.logical_path,
178
+ 'mtime' => mtime,
179
+ 'size' => asset.bytesize,
180
+ 'digest' => asset.hexdigest,
181
+
182
+ # Deprecated: Remove beta integrity attribute in next release.
183
+ # Callers should DigestUtils.hexdigest_integrity_uri to compute the
184
+ # digest themselves.
185
+ 'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest)
186
+ }
187
+ assets[asset.logical_path] = asset.digest_path
188
+
189
+ filenames << asset.filename
190
+
191
+ promise = nil
192
+ exporters_for_asset(asset) do |exporter|
193
+ next if exporter.skip?(logger)
194
+
195
+ if promise.nil?
196
+ promise = Concurrent::Promise.new(executor: executor) { exporter.call }
197
+ concurrent_exporters << promise.execute
103
198
  else
104
- logger.info "Writing #{target}"
105
- asset.write_to target
199
+ concurrent_exporters << promise.then { exporter.call }
106
200
  end
107
-
108
- save
109
- asset
110
201
  end
111
202
  end
203
+
204
+ # make sure all exporters have finished before returning the main thread
205
+ concurrent_exporters.each(&:wait!)
206
+ save
207
+
208
+ filenames
112
209
  end
113
210
 
114
211
  # Removes file from directory and from manifest. `filename` must
@@ -118,6 +215,7 @@ module Sprockets
118
215
  #
119
216
  def remove(filename)
120
217
  path = File.join(dir, filename)
218
+ gzip = "#{path}.gz"
121
219
  logical_path = files[filename]['logical_path']
122
220
 
123
221
  if assets[logical_path] == filename
@@ -126,6 +224,7 @@ module Sprockets
126
224
 
127
225
  files.delete(filename)
128
226
  FileUtils.rm(path) if File.exist?(path)
227
+ FileUtils.rm(gzip) if File.exist?(gzip)
129
228
 
130
229
  save
131
230
 
@@ -135,89 +234,106 @@ module Sprockets
135
234
  end
136
235
 
137
236
  # Cleanup old assets in the compile directory. By default it will
138
- # keep the latest version plus 2 backups.
139
- def clean(keep = 2)
140
- self.assets.keys.each do |logical_path|
141
- # Get assets sorted by ctime, newest first
142
- assets = backups_for(logical_path)
237
+ # keep the latest version, 2 backups and any created within the past hour.
238
+ #
239
+ # Examples
240
+ #
241
+ # To force only 1 backup to be kept, set count=1 and age=0.
242
+ #
243
+ # To only keep files created within the last 10 minutes, set count=0 and
244
+ # age=600.
245
+ #
246
+ def clean(count = 2, age = 3600)
247
+ asset_versions = files.group_by { |_, attrs| attrs['logical_path'] }
143
248
 
144
- # Keep the last N backups
145
- assets = assets[keep..-1] || []
249
+ asset_versions.each do |logical_path, versions|
250
+ current = assets[logical_path]
146
251
 
147
- # Remove old assets
148
- assets.each { |path, _| remove(path) }
252
+ versions.reject { |path, _|
253
+ path == current
254
+ }.sort_by { |_, attrs|
255
+ # Sort by timestamp
256
+ Time.parse(attrs['mtime'])
257
+ }.reverse.each_with_index.drop_while { |(_, attrs), index|
258
+ _age = [0, Time.now - Time.parse(attrs['mtime'])].max
259
+ # Keep if under age or within the count limit
260
+ _age < age || index < count
261
+ }.each { |(path, _), _|
262
+ # Remove old assets
263
+ remove(path)
264
+ }
149
265
  end
150
266
  end
151
267
 
152
268
  # Wipe directive
153
269
  def clobber
154
- FileUtils.rm_r(@dir) if File.exist?(@dir)
155
- logger.info "Removed #{@dir}"
270
+ FileUtils.rm_r(directory) if File.exist?(directory)
271
+ logger.info "Removed #{directory}"
272
+ # if we have an environment clear the cache too
273
+ environment.cache.clear if environment
156
274
  nil
157
275
  end
158
276
 
159
- protected
160
- # Finds all the backup assets for a logical path. The latest
161
- # version is always excluded. The return array is sorted by the
162
- # assets mtime in descending order (Newest to oldest).
163
- def backups_for(logical_path)
164
- files.select { |filename, attrs|
165
- # Matching logical paths
166
- attrs['logical_path'] == logical_path &&
167
- # Excluding whatever asset is the current
168
- assets[logical_path] != filename
169
- }.sort_by { |filename, attrs|
170
- # Sort by timestamp
171
- Time.parse(attrs['mtime'])
172
- }.reverse
277
+ # Persist manifest back to FS
278
+ def save
279
+ data = json_encode(@data)
280
+ FileUtils.mkdir_p File.dirname(@filename)
281
+ PathUtils.atomic_write(@filename) do |f|
282
+ f.write(data)
173
283
  end
284
+ end
174
285
 
175
- # Basic wrapper around Environment#find_asset. Logs compile time.
176
- def find_asset(logical_path)
177
- asset = nil
178
- ms = benchmark do
179
- asset = environment.find_asset(logical_path)
180
- end
181
- logger.debug "Compiled #{logical_path} (#{ms}ms)"
182
- asset
183
- end
286
+ private
184
287
 
185
- # Persist manfiest back to FS
186
- def save
187
- FileUtils.mkdir_p dir
188
- File.open(path, 'w') do |f|
189
- f.write json_encode(@data)
190
- end
191
- end
288
+ # Given an asset, finds all exporters that
289
+ # match its mime-type.
290
+ #
291
+ # Will yield each expoter to the passed in block.
292
+ #
293
+ # array = []
294
+ # puts asset.content_type # => "application/javascript"
295
+ # exporters_for_asset(asset) do |exporter|
296
+ # array << exporter
297
+ # end
298
+ # # puts array => [Exporters::FileExporter, Exporters::ZlibExporter]
299
+ def exporters_for_asset(asset)
300
+ exporters = [Exporters::FileExporter]
192
301
 
193
- private
194
- # Feature detect newer MultiJson API
195
- if MultiJson.respond_to?(:dump)
196
- def json_decode(obj)
197
- MultiJson.load(obj)
302
+ environment.exporters.each do |mime_type, exporter_list|
303
+ next unless asset.content_type
304
+ next unless environment.match_mime_type? asset.content_type, mime_type
305
+ exporter_list.each do |exporter|
306
+ exporters << exporter
307
+ end
198
308
  end
199
309
 
200
- def json_encode(obj)
201
- MultiJson.dump(obj)
202
- end
203
- else
204
- def json_decode(obj)
205
- MultiJson.decode(obj)
206
- end
310
+ exporters.uniq!
207
311
 
208
- def json_encode(obj)
209
- MultiJson.encode(obj)
312
+ exporters.each do |exporter|
313
+ yield exporter.new(asset: asset, environment: environment, directory: dir)
210
314
  end
211
315
  end
212
316
 
317
+ def json_decode(obj)
318
+ JSON.parse(obj, create_additions: false)
319
+ end
320
+
321
+ def json_encode(obj)
322
+ JSON.generate(obj)
323
+ end
324
+
213
325
  def logger
214
- environment.logger
326
+ if environment
327
+ environment.logger
328
+ else
329
+ logger = Logger.new($stderr)
330
+ logger.level = Logger::FATAL
331
+ logger
332
+ end
215
333
  end
216
334
 
217
- def benchmark
218
- start_time = Time.now.to_f
219
- yield
220
- ((Time.now.to_f - start_time) * 1000).to_i
335
+ def executor
336
+ @executor ||= environment.export_concurrent ? :fast : :immediate
221
337
  end
222
338
  end
223
339
  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
@@ -1,48 +1,95 @@
1
- require 'rack/mime'
1
+ # frozen_string_literal: true
2
+ require 'sprockets/encoding_utils'
3
+ require 'sprockets/http_utils'
4
+ require 'sprockets/utils'
2
5
 
3
6
  module Sprockets
4
7
  module Mime
5
- # Returns a `Hash` of registered mime types registered on the
6
- # environment and those part of `Rack::Mime`.
8
+ include HTTPUtils, Utils
9
+
10
+ # Public: Mapping of MIME type Strings to properties Hash.
7
11
  #
8
- # If an `ext` is given, it will lookup the mime type for that extension.
9
- def mime_types(ext = nil)
10
- if ext.nil?
11
- Rack::Mime::MIME_TYPES.merge(@mime_types)
12
- else
13
- ext = Sprockets::Utils.normalize_extension(ext)
14
- @mime_types[ext] || Rack::Mime::MIME_TYPES[ext]
15
- end
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]
16
20
  end
17
21
 
18
- # Returns a `Hash` of explicitly registered mime types.
19
- def registered_mime_types
20
- @mime_types.dup
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]
21
36
  end
22
37
 
23
- if {}.respond_to?(:key)
24
- def extension_for_mime_type(type)
25
- mime_types.key(type)
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
26
57
  end
27
- else
28
- def extension_for_mime_type(type)
29
- mime_types.index(type)
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)
30
63
  end
31
64
  end
32
65
 
33
- # Register a new mime type.
34
- def register_mime_type(mime_type, ext)
35
- ext = Sprockets::Utils.normalize_extension(ext)
36
- @mime_types[ext] = mime_type
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
37
77
  end
38
78
 
39
- if defined? Encoding
40
- # Returns the correct encoding for a given mime type, while falling
41
- # back on the default external encoding, if it exists.
42
- def encoding_for_mime_type(type)
43
- encoding = Encoding::BINARY if type =~ %r{^(image|audio|video)/}
44
- encoding ||= default_external_encoding if respond_to?(:default_external_encoding)
45
- encoding
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
46
93
  end
47
94
  end
48
95
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module Sprockets
5
+ module Npm
6
+ # Internal: Override resolve_alternates to install package.json behavior.
7
+ #
8
+ # load_path - String environment path
9
+ # logical_path - String path relative to base
10
+ #
11
+ # Returns candidate filenames.
12
+ def resolve_alternates(load_path, logical_path)
13
+ candidates, deps = super
14
+
15
+ dirname = File.join(load_path, logical_path)
16
+
17
+ if directory?(dirname)
18
+ filename = File.join(dirname, 'package.json')
19
+
20
+ if self.file?(filename)
21
+ deps << build_file_digest_uri(filename)
22
+ read_package_directives(dirname, filename) do |path|
23
+ if file?(path)
24
+ candidates << path
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ return candidates, deps
31
+ end
32
+
33
+ # Internal: Read package.json's main and style directives.
34
+ #
35
+ # dirname - String path to component directory.
36
+ # filename - String path to package.json.
37
+ #
38
+ # Returns nothing.
39
+ def read_package_directives(dirname, filename)
40
+ package = JSON.parse(File.read(filename), create_additions: false)
41
+
42
+ case package['main']
43
+ when String
44
+ yield File.expand_path(package['main'], dirname)
45
+ when nil
46
+ yield File.expand_path('index.js', dirname)
47
+ end
48
+
49
+ yield File.expand_path(package['style'], dirname) if package['style']
50
+ end
51
+ end
52
+ end