sprockets 2.2.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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