sprockets 2.2.3 → 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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +68 -0
  3. data/README.md +482 -255
  4. data/bin/sprockets +20 -7
  5. data/lib/rake/sprocketstask.rb +28 -15
  6. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  7. data/lib/sprockets/asset.rb +142 -207
  8. data/lib/sprockets/autoload/babel.rb +8 -0
  9. data/lib/sprockets/autoload/closure.rb +8 -0
  10. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  11. data/lib/sprockets/autoload/eco.rb +8 -0
  12. data/lib/sprockets/autoload/ejs.rb +8 -0
  13. data/lib/sprockets/autoload/jsminc.rb +8 -0
  14. data/lib/sprockets/autoload/sass.rb +8 -0
  15. data/lib/sprockets/autoload/sassc.rb +8 -0
  16. data/lib/sprockets/autoload/uglifier.rb +8 -0
  17. data/lib/sprockets/autoload/yui.rb +8 -0
  18. data/lib/sprockets/autoload/zopfli.rb +7 -0
  19. data/lib/sprockets/autoload.rb +16 -0
  20. data/lib/sprockets/babel_processor.rb +66 -0
  21. data/lib/sprockets/base.rb +89 -249
  22. data/lib/sprockets/bower.rb +61 -0
  23. data/lib/sprockets/bundle.rb +105 -0
  24. data/lib/sprockets/cache/file_store.rb +190 -14
  25. data/lib/sprockets/cache/memory_store.rb +75 -0
  26. data/lib/sprockets/cache/null_store.rb +54 -0
  27. data/lib/sprockets/cache.rb +271 -0
  28. data/lib/sprockets/cached_environment.rb +64 -0
  29. data/lib/sprockets/closure_compressor.rb +48 -0
  30. data/lib/sprockets/coffee_script_processor.rb +39 -0
  31. data/lib/sprockets/compressing.rb +134 -0
  32. data/lib/sprockets/configuration.rb +79 -0
  33. data/lib/sprockets/context.rb +204 -135
  34. data/lib/sprockets/dependencies.rb +74 -0
  35. data/lib/sprockets/digest_utils.rb +200 -0
  36. data/lib/sprockets/directive_processor.rb +224 -216
  37. data/lib/sprockets/eco_processor.rb +33 -0
  38. data/lib/sprockets/ejs_processor.rb +32 -0
  39. data/lib/sprockets/encoding_utils.rb +262 -0
  40. data/lib/sprockets/environment.rb +23 -68
  41. data/lib/sprockets/erb_processor.rb +37 -0
  42. data/lib/sprockets/errors.rb +6 -13
  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 +16 -0
  49. data/lib/sprockets/http_utils.rb +135 -0
  50. data/lib/sprockets/jsminc_compressor.rb +32 -0
  51. data/lib/sprockets/jst_processor.rb +36 -19
  52. data/lib/sprockets/loader.rb +343 -0
  53. data/lib/sprockets/manifest.rb +231 -96
  54. data/lib/sprockets/manifest_utils.rb +48 -0
  55. data/lib/sprockets/mime.rb +80 -32
  56. data/lib/sprockets/npm.rb +52 -0
  57. data/lib/sprockets/path_dependency_utils.rb +77 -0
  58. data/lib/sprockets/path_digest_utils.rb +48 -0
  59. data/lib/sprockets/path_utils.rb +367 -0
  60. data/lib/sprockets/paths.rb +82 -0
  61. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  62. data/lib/sprockets/processing.rb +140 -192
  63. data/lib/sprockets/processor_utils.rb +169 -0
  64. data/lib/sprockets/resolve.rb +295 -0
  65. data/lib/sprockets/sass_cache_store.rb +30 -0
  66. data/lib/sprockets/sass_compressor.rb +63 -0
  67. data/lib/sprockets/sass_functions.rb +3 -0
  68. data/lib/sprockets/sass_importer.rb +3 -0
  69. data/lib/sprockets/sass_processor.rb +313 -0
  70. data/lib/sprockets/sassc_compressor.rb +56 -0
  71. data/lib/sprockets/sassc_processor.rb +297 -0
  72. data/lib/sprockets/server.rb +138 -90
  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 +173 -0
  76. data/lib/sprockets/uglifier_compressor.rb +66 -0
  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 +191 -0
  80. data/lib/sprockets/utils/gzip.rb +99 -0
  81. data/lib/sprockets/utils.rb +186 -53
  82. data/lib/sprockets/version.rb +2 -1
  83. data/lib/sprockets/yui_compressor.rb +56 -0
  84. data/lib/sprockets.rb +217 -52
  85. metadata +250 -59
  86. data/LICENSE +0 -21
  87. data/lib/sprockets/asset_attributes.rb +0 -126
  88. data/lib/sprockets/bundled_asset.rb +0 -79
  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/static_asset.rb +0 -57
  99. data/lib/sprockets/trail.rb +0 -90
@@ -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
+ # infomation 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
34
49
  end
35
50
 
36
- data = nil
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 = {}
37
64
 
38
65
  begin
39
- if File.exist?(@path)
40
- data = MultiJson.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,48 @@ 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)
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
79
154
  # fingerprinted filename like
80
155
  # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
81
156
  # also inserted into the manifest file.
@@ -83,31 +158,53 @@ module Sprockets
83
158
  # compile("application.js")
84
159
  #
85
160
  def compile(*args)
86
- paths = environment.each_logical_path(*args).to_a +
87
- args.flatten.select { |fn| Pathname.new(fn).absolute? }
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
- 'digest' => asset.digest
95
- }
96
- assets[asset.logical_path] = asset.digest_path
97
-
98
- target = File.join(dir, asset.digest_path)
99
-
100
- if File.exist?(target)
101
- logger.debug "Skipping #{target}, already exists"
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
102
197
  else
103
- logger.info "Writing #{target}"
104
- asset.write_to target
198
+ concurrent_exporters << promise.then { exporter.call }
105
199
  end
106
-
107
- save
108
- asset
109
200
  end
110
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
111
208
  end
112
209
 
113
210
  # Removes file from directory and from manifest. `filename` must
@@ -117,6 +214,7 @@ module Sprockets
117
214
  #
118
215
  def remove(filename)
119
216
  path = File.join(dir, filename)
217
+ gzip = "#{path}.gz"
120
218
  logical_path = files[filename]['logical_path']
121
219
 
122
220
  if assets[logical_path] == filename
@@ -125,79 +223,116 @@ module Sprockets
125
223
 
126
224
  files.delete(filename)
127
225
  FileUtils.rm(path) if File.exist?(path)
226
+ FileUtils.rm(gzip) if File.exist?(gzip)
128
227
 
129
228
  save
130
229
 
131
- logger.warn "Removed #{filename}"
230
+ logger.info "Removed #{filename}"
132
231
 
133
232
  nil
134
233
  end
135
234
 
136
235
  # Cleanup old assets in the compile directory. By default it will
137
- # keep the latest version plus 2 backups.
138
- def clean(keep = 2)
139
- self.assets.keys.each do |logical_path|
140
- # Get assets sorted by ctime, newest first
141
- assets = backups_for(logical_path)
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'] }
142
247
 
143
- # Keep the last N backups
144
- assets = assets[keep..-1] || []
248
+ asset_versions.each do |logical_path, versions|
249
+ current = assets[logical_path]
145
250
 
146
- # Remove old assets
147
- assets.each { |path, _| remove(path) }
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
+ }
148
264
  end
149
265
  end
150
266
 
151
267
  # Wipe directive
152
268
  def clobber
153
- FileUtils.rm_r(@dir) if File.exist?(@dir)
154
- logger.warn "Removed #{@dir}"
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
155
273
  nil
156
274
  end
157
275
 
158
- protected
159
- # Finds all the backup assets for a logical path. The latest
160
- # version is always excluded. The return array is sorted by the
161
- # assets mtime in descending order (Newest to oldest).
162
- def backups_for(logical_path)
163
- files.select { |filename, attrs|
164
- # Matching logical paths
165
- attrs['logical_path'] == logical_path &&
166
- # Excluding whatever asset is the current
167
- assets[logical_path] != filename
168
- }.sort_by { |filename, attrs|
169
- # Sort by timestamp
170
- Time.parse(attrs['mtime'])
171
- }.reverse
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)
172
282
  end
283
+ end
173
284
 
174
- # Basic wrapper around Environment#find_asset. Logs compile time.
175
- def find_asset(logical_path)
176
- asset = nil
177
- ms = benchmark do
178
- asset = environment.find_asset(logical_path)
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
179
307
  end
180
- logger.warn "Compiled #{logical_path} (#{ms}ms)"
181
- asset
182
- end
183
308
 
184
- # Persist manfiest back to FS
185
- def save
186
- FileUtils.mkdir_p dir
187
- File.open(path, 'w') do |f|
188
- f.write MultiJson.encode(@data)
309
+ exporters.uniq!
310
+
311
+ exporters.each do |exporter|
312
+ yield exporter.new(asset: asset, environment: environment, directory: dir)
189
313
  end
190
314
  end
191
315
 
192
- private
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
+
193
324
  def logger
194
- environment.logger
325
+ if environment
326
+ environment.logger
327
+ else
328
+ logger = Logger.new($stderr)
329
+ logger.level = Logger::FATAL
330
+ logger
331
+ end
195
332
  end
196
333
 
197
- def benchmark
198
- start_time = Time.now.to_f
199
- yield
200
- ((Time.now.to_f - start_time) * 1000).to_i
334
+ def executor
335
+ @executor ||= environment.export_concurrent ? :fast : :immediate
201
336
  end
202
337
  end
203
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
@@ -1,48 +1,96 @@
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]
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]
16
36
  end
17
37
 
18
- if {}.respond_to?(:key)
19
- def extension_for_mime_type(type)
20
- 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
21
57
  end
22
- else
23
- def extension_for_mime_type(type)
24
- 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)
25
63
  end
26
64
  end
27
65
 
28
- # Register a new mime type.
29
- def register_mime_type(mime_type, ext)
30
- ext = Sprockets::Utils.normalize_extension(ext)
31
- @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
32
77
  end
33
78
 
34
- if defined? Encoding
35
- # Returns the correct encoding for a given mime type, while falling
36
- # back on the default external encoding, if it exists.
37
- def encoding_for_mime_type(type)
38
- encoding = Encoding::BINARY if type =~ %r{^(image|audio|video)/}
39
- encoding ||= default_external_encoding if respond_to?(:default_external_encoding)
40
- 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
41
93
  end
42
94
  end
43
95
  end
44
-
45
- # Extend Sprockets module to provide global registry
46
- extend Mime
47
- @mime_types = {}
48
96
  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 candiate 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