sprockets 3.0.0 → 3.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +49 -183
  4. data/bin/sprockets +1 -0
  5. data/lib/sprockets/asset.rb +3 -2
  6. data/lib/sprockets/base.rb +13 -2
  7. data/lib/sprockets/bundle.rb +5 -1
  8. data/lib/sprockets/cache/file_store.rb +7 -4
  9. data/lib/sprockets/cache.rb +6 -4
  10. data/lib/sprockets/closure_compressor.rb +5 -11
  11. data/lib/sprockets/coffee_script_processor.rb +2 -2
  12. data/lib/sprockets/coffee_script_template.rb +12 -1
  13. data/lib/sprockets/compressing.rb +20 -0
  14. data/lib/sprockets/context.rb +3 -1
  15. data/lib/sprockets/dependencies.rb +8 -8
  16. data/lib/sprockets/deprecation.rb +90 -0
  17. data/lib/sprockets/digest_utils.rb +81 -57
  18. data/lib/sprockets/eco_processor.rb +2 -2
  19. data/lib/sprockets/eco_template.rb +12 -1
  20. data/lib/sprockets/ejs_processor.rb +2 -2
  21. data/lib/sprockets/ejs_template.rb +12 -1
  22. data/lib/sprockets/encoding_utils.rb +7 -4
  23. data/lib/sprockets/engines.rb +11 -0
  24. data/lib/sprockets/erb_template.rb +6 -1
  25. data/lib/sprockets/errors.rb +0 -1
  26. data/lib/sprockets/http_utils.rb +3 -1
  27. data/lib/sprockets/legacy.rb +16 -0
  28. data/lib/sprockets/legacy_proc_processor.rb +1 -1
  29. data/lib/sprockets/legacy_tilt_processor.rb +2 -2
  30. data/lib/sprockets/loader.rb +208 -59
  31. data/lib/sprockets/manifest.rb +57 -6
  32. data/lib/sprockets/path_utils.rb +20 -15
  33. data/lib/sprockets/processing.rb +10 -0
  34. data/lib/sprockets/processor_utils.rb +77 -0
  35. data/lib/sprockets/resolve.rb +9 -6
  36. data/lib/sprockets/sass_cache_store.rb +6 -1
  37. data/lib/sprockets/sass_compressor.rb +9 -17
  38. data/lib/sprockets/sass_processor.rb +16 -9
  39. data/lib/sprockets/sass_template.rb +14 -2
  40. data/lib/sprockets/server.rb +32 -12
  41. data/lib/sprockets/uglifier_compressor.rb +6 -13
  42. data/lib/sprockets/unloaded_asset.rb +137 -0
  43. data/lib/sprockets/uri_tar.rb +98 -0
  44. data/lib/sprockets/uri_utils.rb +5 -7
  45. data/lib/sprockets/utils/gzip.rb +67 -0
  46. data/lib/sprockets/utils.rb +35 -18
  47. data/lib/sprockets/version.rb +1 -1
  48. data/lib/sprockets/yui_compressor.rb +4 -14
  49. data/lib/sprockets.rb +21 -11
  50. metadata +28 -3
@@ -53,7 +53,15 @@ module Sprockets
53
53
 
54
54
  unless uri
55
55
  message = "couldn't find file '#{path}'"
56
+
57
+ if relative_path?(path) && options[:base_path]
58
+ load_path, _ = paths_split(config[:paths], options[:base_path])
59
+ message << " under '#{load_path}'"
60
+ end
61
+
56
62
  message << " with type '#{options[:accept]}'" if options[:accept]
63
+ message << "\nChecked in these paths: \n #{ config[:paths].join("\n ") }"
64
+
57
65
  raise FileNotFound, message
58
66
  end
59
67
 
@@ -144,12 +152,6 @@ module Sprockets
144
152
  accepts
145
153
  end
146
154
 
147
- def normalize_logical_path(path)
148
- dirname, basename = File.split(path)
149
- path = dirname if basename == 'index'
150
- path
151
- end
152
-
153
155
  def path_matches(load_path, logical_name, logical_basename)
154
156
  dirname = File.dirname(File.join(load_path, logical_name))
155
157
  candidates = dirname_matches(dirname, logical_basename)
@@ -176,6 +178,7 @@ module Sprockets
176
178
  candidates = []
177
179
  entries = self.entries(dirname)
178
180
  entries.each do |entry|
181
+ next unless File.basename(entry).start_with?(basename)
179
182
  name, type, _, _ = parse_path_extnames(entry)
180
183
  if basename == name
181
184
  candidates << [File.join(dirname, entry), type]
@@ -25,5 +25,10 @@ module Sprockets
25
25
  end
26
26
 
27
27
  # Deprecated: Use Sprockets::SassProcessor::CacheStore instead.
28
- SassCacheStore = SassProcessor::CacheStore
28
+ class SassCacheStore < SassProcessor::CacheStore
29
+ def initialize(*args)
30
+ Deprecation.new.warn "SassCacheStore is deprecated please use SassProcessor::CacheStore instead"
31
+ super
32
+ end
33
+ end
29
34
  end
@@ -1,4 +1,5 @@
1
1
  require 'sprockets/autoload'
2
+ require 'sprockets/digest_utils'
2
3
 
3
4
  module Sprockets
4
5
  # Public: Sass CSS minifier.
@@ -34,26 +35,17 @@ module Sprockets
34
35
  attr_reader :cache_key
35
36
 
36
37
  def initialize(options = {})
37
- @options = options
38
- @cache_key = [
39
- self.class.name,
40
- Autoload::Sass::VERSION,
41
- VERSION,
42
- options
43
- ].freeze
38
+ @options = {
39
+ syntax: :scss,
40
+ cache: false,
41
+ read_cache: false,
42
+ style: :compressed
43
+ }.merge(options).freeze
44
+ @cache_key = "#{self.class.name}:#{Autoload::Sass::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
44
45
  end
45
46
 
46
47
  def call(input)
47
- data = input[:data]
48
- input[:cache].fetch(@cache_key + [data]) do
49
- options = {
50
- syntax: :scss,
51
- cache: false,
52
- read_cache: false,
53
- style: :compressed
54
- }.merge(@options)
55
- Autoload::Sass::Engine.new(data, options).render
56
- end
48
+ Autoload::Sass::Engine.new(input[:data], @options).render
57
49
  end
58
50
  end
59
51
  end
@@ -39,17 +39,12 @@ module Sprockets
39
39
  # Public: Initialize template with custom options.
40
40
  #
41
41
  # options - Hash
42
- # cache_version - String custom cache version. Used to force a cache
43
- # change after code changes are made to Sass Functions.
42
+ # cache_version - String custom cache version. Used to force a cache
43
+ # change after code changes are made to Sass Functions.
44
44
  #
45
45
  def initialize(options = {}, &block)
46
46
  @cache_version = options[:cache_version]
47
- @cache_key = [
48
- self.class.name,
49
- VERSION,
50
- Autoload::Sass::VERSION,
51
- @cache_version
52
- ].freeze
47
+ @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::Sass::VERSION}:#{@cache_version}".freeze
53
48
 
54
49
  @functions = Module.new do
55
50
  include Functions
@@ -64,7 +59,7 @@ module Sprockets
64
59
  options = {
65
60
  filename: input[:filename],
66
61
  syntax: self.class.syntax,
67
- cache_store: CacheStore.new(input[:cache], @cache_version),
62
+ cache_store: build_cache_store(input, @cache_version),
68
63
  load_paths: input[:environment].paths,
69
64
  sprockets: {
70
65
  context: context,
@@ -89,6 +84,18 @@ module Sprockets
89
84
  context.metadata.merge(data: css, sass_dependencies: sass_dependencies)
90
85
  end
91
86
 
87
+ # Public: Build the cache store to be used by the Sass engine.
88
+ #
89
+ # input - the input hash.
90
+ # version - the cache version.
91
+ #
92
+ # Override this method if you need to use a different cache than the
93
+ # Sprockets cache.
94
+ def build_cache_store(input, version)
95
+ CacheStore.new(input[:cache], version)
96
+ end
97
+ private :build_cache_store
98
+
92
99
  # Public: Functions injected into Sass context during Sprockets evaluation.
93
100
  #
94
101
  # This module may be extended to add global functionality to all Sprockets
@@ -2,6 +2,18 @@ require 'sprockets/sass_processor'
2
2
 
3
3
  module Sprockets
4
4
  # Deprecated
5
- SassTemplate = SassProcessor
6
- ScssTemplate = ScssProcessor
5
+ class SassTemplate < SassProcessor
6
+ def self.call(*args)
7
+ Deprecation.new.warn "SassTemplate is deprecated please use SassProcessor instead"
8
+ super
9
+ end
10
+ end
11
+
12
+ # Deprecated
13
+ class ScssTemplate < ScssProcessor
14
+ def self.call(*args)
15
+ Deprecation.new.warn "ScssTemplate is deprecated please use ScssProcessor instead"
16
+ super
17
+ end
18
+ end
7
19
  end
@@ -23,7 +23,7 @@ module Sprockets
23
23
  start_time = Time.now.to_f
24
24
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
25
 
26
- if env['REQUEST_METHOD'] != 'GET'
26
+ if !['GET', 'HEAD'].include?(env['REQUEST_METHOD'])
27
27
  return method_not_allowed_response
28
28
  end
29
29
 
@@ -39,7 +39,7 @@ module Sprockets
39
39
 
40
40
  # URLs containing a `".."` are rejected for security reasons.
41
41
  if forbidden_request?(path)
42
- return forbidden_response
42
+ return forbidden_response(env)
43
43
  end
44
44
 
45
45
  # Look up the asset.
@@ -86,10 +86,10 @@ module Sprockets
86
86
  not_modified_response(env, if_none_match)
87
87
  when :not_found
88
88
  logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
89
- not_found_response
89
+ not_found_response(env)
90
90
  when :precondition_failed
91
91
  logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
92
- precondition_failed_response
92
+ precondition_failed_response(env)
93
93
  end
94
94
  rescue Exception => e
95
95
  logger.error "Error compiling asset #{path}:"
@@ -115,12 +115,20 @@ module Sprockets
115
115
  #
116
116
  # http://example.org/assets/../../../etc/passwd
117
117
  #
118
- path.include?("..") || absolute_path?(path)
118
+ path.include?("..") || absolute_path?(path) || path.include?("://")
119
+ end
120
+
121
+ def head_request?(env)
122
+ env['REQUEST_METHOD'] == 'HEAD'
119
123
  end
120
124
 
121
125
  # Returns a 200 OK response tuple
122
126
  def ok_response(asset, env)
123
- [ 200, headers(env, asset, asset.length), asset ]
127
+ if head_request?(env)
128
+ [ 200, headers(env, asset, 0), [] ]
129
+ else
130
+ [ 200, headers(env, asset, asset.length), asset ]
131
+ end
124
132
  end
125
133
 
126
134
  # Returns a 304 Not Modified response tuple
@@ -129,21 +137,33 @@ module Sprockets
129
137
  end
130
138
 
131
139
  # Returns a 403 Forbidden response tuple
132
- def forbidden_response
133
- [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
140
+ def forbidden_response(env)
141
+ if head_request?(env)
142
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
143
+ else
144
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
145
+ end
134
146
  end
135
147
 
136
148
  # Returns a 404 Not Found response tuple
137
- def not_found_response
138
- [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
149
+ def not_found_response(env)
150
+ if head_request?(env)
151
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
152
+ else
153
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
154
+ end
139
155
  end
140
156
 
141
157
  def method_not_allowed_response
142
158
  [ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
143
159
  end
144
160
 
145
- def precondition_failed_response
146
- [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
161
+ def precondition_failed_response(env)
162
+ if head_request?(env)
163
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
164
+ else
165
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
166
+ end
147
167
  end
148
168
 
149
169
  # Returns a JavaScript response that re-throws a Ruby exception
@@ -1,4 +1,5 @@
1
1
  require 'sprockets/autoload'
2
+ require 'sprockets/digest_utils'
2
3
 
3
4
  module Sprockets
4
5
  # Public: Uglifier/Uglify compressor.
@@ -40,24 +41,16 @@ module Sprockets
40
41
  options[:copyright] ||= false
41
42
  else
42
43
  # Uglifier >= 2.x
43
- options[:copyright] ||= :none
44
+ options[:comments] ||= :none
44
45
  end
45
46
 
46
- @uglifier = Autoload::Uglifier.new(options)
47
-
48
- @cache_key = [
49
- self.class.name,
50
- Autoload::Uglifier::VERSION,
51
- VERSION,
52
- options
53
- ].freeze
47
+ @options = options
48
+ @cache_key = "#{self.class.name}:#{Autoload::Uglifier::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
54
49
  end
55
50
 
56
51
  def call(input)
57
- data = input[:data]
58
- input[:cache].fetch(@cache_key + [data]) do
59
- @uglifier.compile(data)
60
- end
52
+ @uglifier ||= Autoload::Uglifier.new(@options)
53
+ @uglifier.compile(input[:data])
61
54
  end
62
55
  end
63
56
  end
@@ -0,0 +1,137 @@
1
+ require 'sprockets/uri_utils'
2
+ require 'sprockets/uri_tar'
3
+
4
+ module Sprockets
5
+ # Internal: Used to parse and store the URI to an unloaded asset
6
+ # Generates keys used to store and retrieve items from cache
7
+ class UnloadedAsset
8
+
9
+ # Internal: Initialize object for generating cache keys
10
+ #
11
+ # uri - A String containing complete URI to a file including scheme
12
+ # and full path such as
13
+ # "file:///Path/app/assets/js/app.js?type=application/javascript"
14
+ # env - The current "environment" that assets are being loaded into.
15
+ # We need it so we know where the +root+ (directory where sprockets
16
+ # is being invoked). We also need for the `file_digest` method,
17
+ # since, for some strange reason, memoization is provided by
18
+ # overriding methods such as `stat` in the `PathUtils` module.
19
+ #
20
+ # Returns UnloadedAsset.
21
+ def initialize(uri, env)
22
+ @uri = uri.to_s
23
+ @env = env
24
+ @compressed_path = URITar.new(uri, env).compressed_path
25
+ @params = nil # lazy loaded
26
+ @filename = nil # lazy loaded
27
+ end
28
+ attr_reader :compressed_path, :uri
29
+
30
+ # Internal: Full file path without schema
31
+ #
32
+ # This returns a string containing the full path to the asset without the schema.
33
+ # Information is loaded lazilly since we want `UnloadedAsset.new(dep, self).relative_path`
34
+ # to be fast. Calling this method the first time allocates an array and a hash.
35
+ #
36
+ # Example
37
+ #
38
+ # If the URI is `file:///Full/path/app/assets/javascripts/application.js"` then the
39
+ # filename would be `"/Full/path/app/assets/javascripts/application.js"`
40
+ #
41
+ # Returns a String.
42
+ def filename
43
+ unless @filename
44
+ load_file_params
45
+ end
46
+ @filename
47
+ end
48
+
49
+ # Internal: Hash of param values
50
+ #
51
+ # This information is generated and used internally by sprockets.
52
+ # Known keys include `:type` which store the asset's mime-type, `:id` which is a fully resolved
53
+ # digest for the asset (includes dependency digest as opposed to a digest of only file contents)
54
+ # and `:pipeline`. Hash may be empty.
55
+ #
56
+ # Example
57
+ #
58
+ # If the URI is `file:///Full/path/app/assets/javascripts/application.js"type=application/javascript`
59
+ # Then the params would be `{type: "application/javascript"}`
60
+ #
61
+ # Returns a Hash.
62
+ def params
63
+ unless @params
64
+ load_file_params
65
+ end
66
+ @params
67
+ end
68
+
69
+ # Internal: Key of asset
70
+ #
71
+ # Used to retrieve an asset from the cache based on "compressed" path to asset.
72
+ # A "compressed" path can either be relative to the root of the project or an
73
+ # absolute path.
74
+ #
75
+ # Returns a String.
76
+ def asset_key
77
+ "asset-uri:#{compressed_path}"
78
+ end
79
+
80
+ # Public: Dependency History key
81
+ #
82
+ # Used to retrieve an array of "histories" each of which contain a set of stored dependencies
83
+ # for a given asset path and filename digest.
84
+ #
85
+ # A dependency can refer to either an asset i.e. index.js
86
+ # may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
87
+ # compilation, such as the VERSION of sprockets (i.e. the environment) and what "processors"
88
+ # are used.
89
+ #
90
+ # For example a history array with one Set of dependencies may look like:
91
+ #
92
+ # [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
93
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
94
+ # "processors:type=text/css&file_type=text/css&pipeline=self",
95
+ # "file-digest:///Full/path/app/assets/stylesheets"]]
96
+ #
97
+ # This method of asset lookup is used to ensure that none of the dependencies have been modified
98
+ # since last lookup. If one of them has, the key will be different and a new entry must be stored.
99
+ #
100
+ # URI depndencies are later converted to "compressed" paths
101
+ #
102
+ # Returns a String.
103
+ def dependency_history_key
104
+ "asset-uri-cache-dependencies:#{compressed_path}:#{ @env.file_digest(filename) }"
105
+ end
106
+
107
+ # Internal: Digest key
108
+ #
109
+ # Used to retrieve a string containing the "compressed" path to an asset based on
110
+ # a digest. The digest is generated from dependencies stored via information stored in
111
+ # the `dependency_history_key` after each of the "dependencies" is "resolved" for example
112
+ # "environment-version" may be resolved to "environment-1.0-3.2.0" for version "3.2.0" of sprockets
113
+ #
114
+ # Returns a String.
115
+ def digest_key(digest)
116
+ "asset-uri-digest:#{compressed_path}:#{digest}"
117
+ end
118
+
119
+ # Internal: File digest key
120
+ #
121
+ # The digest for a given file won't change if the path and the stat time hasn't changed
122
+ # We can save time by not re-computing this information and storing it in the cache
123
+ #
124
+ # Returns a String.
125
+ def file_digest_key(stat)
126
+ "file_digest:#{compressed_path}:#{stat}"
127
+ end
128
+
129
+ private
130
+ # Internal: Parses uri into filename and params hash
131
+ #
132
+ # Returns Array with filename and params hash
133
+ def load_file_params
134
+ @filename, @params = URIUtils.parse_asset_uri(uri)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,98 @@
1
+ require 'sprockets/path_utils'
2
+
3
+ module Sprockets
4
+ # Internal: used to "expand" and "compress" values for storage
5
+ class URITar
6
+ attr_reader :scheme, :root, :path
7
+
8
+ # Internal: Initialize object for compression or expansion
9
+ #
10
+ # uri - A String containing URI that may or may not contain the scheme
11
+ # env - The current "environment" that assets are being loaded into.
12
+ def initialize(uri, env)
13
+ @root = env.root
14
+ @env = env
15
+ uri = uri.to_s
16
+ if uri.include?("://".freeze)
17
+ @scheme, _, @path = uri.partition("://".freeze)
18
+ @scheme << "://".freeze
19
+ else
20
+ @scheme = "".freeze
21
+ @path = uri
22
+ end
23
+ end
24
+
25
+ # Internal: Converts full uri to a "compressed" uri
26
+ #
27
+ # If a uri is inside of an environment's root it will
28
+ # be shortened to be a relative path.
29
+ #
30
+ # If a uri is outside of the environment's root the original
31
+ # uri will be returned.
32
+ #
33
+ # Returns String
34
+ def compress
35
+ scheme + compressed_path
36
+ end
37
+
38
+ # Internal: Tells us if we are using an absolute path
39
+ #
40
+ # Nix* systems start with a `/` like /Users/schneems.
41
+ # Windows systems start with a drive letter than colon and slash
42
+ # like C:/Schneems.
43
+ def absolute_path?
44
+ PathUtils.absolute_path?(path)
45
+ end
46
+
47
+ # Internal: Convert a "compressed" uri to an absolute path
48
+ #
49
+ # If a uri is inside of the environment's root it will not
50
+ # start with a slash for example:
51
+ #
52
+ # file://this/is/a/relative/path
53
+ #
54
+ # If a uri is outside the root, it will start with a slash:
55
+ #
56
+ # file:///This/is/an/absolute/path
57
+ #
58
+ # Returns String
59
+ def expand
60
+ if absolute_path?
61
+ # Stored path was absolute, don't add root
62
+ scheme + path
63
+ else
64
+ if scheme.empty?
65
+ File.join(root, path)
66
+ else
67
+ # We always want to return an absolute uri,
68
+ # make sure the path starts with a slash.
69
+ scheme + File.join("/".freeze, root, path)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Internal: Returns "compressed" path
75
+ #
76
+ # If the input uri is relative to the environment root
77
+ # it will return a path relative to the environment root.
78
+ # Otherwise an absolute path will be returned.
79
+ #
80
+ # Only path information is returned, and not scheme.
81
+ #
82
+ # Returns String
83
+ def compressed_path
84
+ # windows
85
+ if !@root.start_with?("/".freeze) && path.start_with?("/".freeze)
86
+ consistent_root = "/".freeze + @root
87
+ else
88
+ consistent_root = @root
89
+ end
90
+
91
+ if compressed_path = PathUtils.split_subpath(consistent_root, path)
92
+ compressed_path
93
+ else
94
+ path
95
+ end
96
+ end
97
+ end
98
+ end
@@ -17,9 +17,6 @@ module Sprockets
17
17
  #
18
18
  # pipeline - String name of pipeline.
19
19
  #
20
- # encoding - A content encoding such as "gzip" or "deflate". NOT a charset
21
- # like "utf-8".
22
- #
23
20
  module URIUtils
24
21
  extend self
25
22
 
@@ -51,7 +48,7 @@ module Sprockets
51
48
  path.force_encoding(Encoding::UTF_8)
52
49
 
53
50
  # Hack for parsing Windows "file:///C:/Users/IEUser" paths
54
- path = path.gsub(/^\/([a-zA-Z]:)/, '\1')
51
+ path.gsub!(/^\/([a-zA-Z]:)/, '\1'.freeze)
55
52
 
56
53
  [scheme, host, path, query]
57
54
  end
@@ -128,7 +125,7 @@ module Sprockets
128
125
  def parse_file_digest_uri(uri)
129
126
  scheme, _, path, _ = split_file_uri(uri)
130
127
 
131
- unless scheme == 'file-digest'
128
+ unless scheme == 'file-digest'.freeze
132
129
  raise URI::InvalidURIError, "expected file-digest scheme: #{uri}"
133
130
  end
134
131
 
@@ -146,7 +143,7 @@ module Sprockets
146
143
  #
147
144
  # Returns String URI.
148
145
  def build_file_digest_uri(path)
149
- join_file_uri("file-digest", nil, path, nil)
146
+ join_file_uri('file-digest'.freeze, nil, path, nil)
150
147
  end
151
148
 
152
149
  # Internal: Serialize hash of params into query string.
@@ -183,7 +180,8 @@ module Sprockets
183
180
  query.to_s.split('&').reduce({}) do |h, p|
184
181
  k, v = p.split('=', 2)
185
182
  v = URI::Generic::DEFAULT_PARSER.unescape(v) if v
186
- h.merge(k.to_sym => v || true)
183
+ h[k.to_sym] = v || true
184
+ h
187
185
  end
188
186
  end
189
187
  end
@@ -0,0 +1,67 @@
1
+ module Sprockets
2
+ module Utils
3
+ class Gzip
4
+ # Private: Generates a gzipped file based off of reference file.
5
+ def initialize(asset)
6
+ @content_type = asset.content_type
7
+ @source = asset.source
8
+ @charset = asset.charset
9
+ end
10
+
11
+ # What non-text mime types should we compress? This list comes from:
12
+ # https://www.fastly.com/blog/new-gzip-settings-and-deciding-what-compress
13
+ COMPRESSABLE_MIME_TYPES = {
14
+ "application/vnd.ms-fontobject" => true,
15
+ "application/x-font-opentype" => true,
16
+ "application/x-font-ttf" => true,
17
+ "image/x-icon" => true,
18
+ "image/svg+xml" => true
19
+ }
20
+
21
+ # Private: Returns whether or not an asset can be compressed.
22
+ #
23
+ # We want to compress any file that is text based.
24
+ # You do not want to compress binary
25
+ # files as they may already be compressed and running them
26
+ # through a compression algorithm would make them larger.
27
+ #
28
+ # Return Boolean.
29
+ def can_compress?(mime_types)
30
+ # The "charset" of a mime type is present if the value is
31
+ # encoded text. We can check this value to see if the asset
32
+ # can be compressed.
33
+ #
34
+ # We also check against our list of non-text compressible mime types
35
+ @charset || COMPRESSABLE_MIME_TYPES.include?(@content_type)
36
+ end
37
+
38
+ # Private: Opposite of `can_compress?`.
39
+ #
40
+ # Returns Boolean.
41
+ def cannot_compress?(mime_types)
42
+ !can_compress?(mime_types)
43
+ end
44
+
45
+ # Private: Generates a gzipped file based off of reference asset.
46
+ #
47
+ # Compresses the target asset's contents and puts it into a file with
48
+ # the same name plus a `.gz` extension in the same folder as the original.
49
+ # Does not modify the target asset.
50
+ #
51
+ # Returns nothing.
52
+ def compress(target)
53
+ mtime = PathUtils.stat(target).mtime
54
+ PathUtils.atomic_write("#{target}.gz") do |f|
55
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
56
+ gz.mtime = mtime
57
+ gz.write(@source)
58
+ gz.close
59
+
60
+ File.utime(mtime, mtime, f.path)
61
+ end
62
+
63
+ nil
64
+ end
65
+ end
66
+ end
67
+ end