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,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
1
3
  require 'time'
2
- require 'uri'
4
+ require 'rack/utils'
3
5
 
4
6
  module Sprockets
5
7
  # `Server` is a concern mixed into `Environment` and
6
- # `Index` that provides a Rack compatible `call`
8
+ # `CachedEnvironment` that provides a Rack compatible `call`
7
9
  # interface and url generation helpers.
8
10
  module Server
11
+ # Supported HTTP request methods.
12
+ ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
13
+
9
14
  # `call` implements the Rack 1.x specification which accepts an
10
15
  # `env` Hash and returns a three item tuple with the status code,
11
16
  # headers, and body.
@@ -23,15 +28,18 @@ module Sprockets
23
28
  start_time = Time.now.to_f
24
29
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
30
 
26
- msg = "Served asset #{env['PATH_INFO']} -"
31
+ unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
32
+ return method_not_allowed_response
33
+ end
27
34
 
28
- # Mark session as "skipped" so no `Set-Cookie` header is set
29
- env['rack.session.options'] ||= {}
30
- env['rack.session.options'][:defer] = true
31
- env['rack.session.options'][:skip] = true
35
+ msg = "Served asset #{env['PATH_INFO']} -"
32
36
 
33
37
  # Extract the path from everything after the leading slash
34
- path = unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
38
+ path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
39
+
40
+ unless path.valid_encoding?
41
+ return bad_request_response(env)
42
+ end
35
43
 
36
44
  # Strip fingerprint
37
45
  if fingerprint = path_fingerprint(path)
@@ -40,42 +48,58 @@ module Sprockets
40
48
 
41
49
  # URLs containing a `".."` are rejected for security reasons.
42
50
  if forbidden_request?(path)
43
- return forbidden_response
51
+ return forbidden_response(env)
44
52
  end
45
53
 
46
- # Look up the asset.
47
- asset = find_asset(path, :bundle => !body_only?(env))
48
-
49
- # `find_asset` returns nil if the asset doesn't exist
50
- if asset.nil?
51
- logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
52
-
53
- # Return a 404 Not Found
54
- not_found_response
54
+ if fingerprint
55
+ if_match = fingerprint
56
+ elsif env['HTTP_IF_MATCH']
57
+ if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
58
+ end
55
59
 
56
- # Check request headers `HTTP_IF_NONE_MATCH` against the asset digest
57
- elsif etag_match?(asset, env)
58
- logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
60
+ if env['HTTP_IF_NONE_MATCH']
61
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
62
+ end
59
63
 
60
- # Return a 304 Not Modified
61
- not_modified_response(asset, env)
64
+ # Look up the asset.
65
+ asset = find_asset(path)
62
66
 
67
+ if asset.nil?
68
+ status = :not_found
69
+ elsif fingerprint && asset.etag != fingerprint
70
+ status = :not_found
71
+ elsif if_match && asset.etag != if_match
72
+ status = :precondition_failed
73
+ elsif if_none_match && asset.etag == if_none_match
74
+ status = :not_modified
63
75
  else
64
- logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
76
+ status = :ok
77
+ end
65
78
 
66
- # Return a 200 with the asset contents
79
+ case status
80
+ when :ok
81
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
67
82
  ok_response(asset, env)
83
+ when :not_modified
84
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
85
+ not_modified_response(env, if_none_match)
86
+ when :not_found
87
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
88
+ not_found_response(env)
89
+ when :precondition_failed
90
+ logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
91
+ precondition_failed_response(env)
68
92
  end
69
93
  rescue Exception => e
70
94
  logger.error "Error compiling asset #{path}:"
71
95
  logger.error "#{e.class.name}: #{e.message}"
72
96
 
73
- case content_type_of(path)
74
- when "application/javascript"
97
+ case File.extname(path)
98
+ when ".js"
75
99
  # Re-throw JavaScript asset exceptions to the browser
76
100
  logger.info "#{msg} 500 Internal Server Error\n\n"
77
101
  return javascript_exception_response(e)
78
- when "text/css"
102
+ when ".css"
79
103
  # Display CSS asset exceptions in the browser
80
104
  logger.info "#{msg} 500 Internal Server Error\n\n"
81
105
  return css_exception_response(e)
@@ -90,25 +114,72 @@ module Sprockets
90
114
  #
91
115
  # http://example.org/assets/../../../etc/passwd
92
116
  #
93
- path.include?("..") || Pathname.new(path).absolute?
117
+ path.include?("..") || absolute_path?(path) || path.include?("://")
118
+ end
119
+
120
+ def head_request?(env)
121
+ env['REQUEST_METHOD'] == 'HEAD'
122
+ end
123
+
124
+ # Returns a 200 OK response tuple
125
+ def ok_response(asset, env)
126
+ if head_request?(env)
127
+ [ 200, headers(env, asset, 0), [] ]
128
+ else
129
+ [ 200, headers(env, asset, asset.length), asset ]
130
+ end
131
+ end
132
+
133
+ # Returns a 304 Not Modified response tuple
134
+ def not_modified_response(env, etag)
135
+ [ 304, cache_headers(env, etag), [] ]
136
+ end
137
+
138
+ # Returns a 400 Forbidden response tuple
139
+ def bad_request_response(env)
140
+ if head_request?(env)
141
+ [ 400, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
142
+ else
143
+ [ 400, { "Content-Type" => "text/plain", "Content-Length" => "11" }, [ "Bad Request" ] ]
144
+ end
94
145
  end
95
146
 
96
147
  # Returns a 403 Forbidden response tuple
97
- def forbidden_response
98
- [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
148
+ def forbidden_response(env)
149
+ if head_request?(env)
150
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
151
+ else
152
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
153
+ end
99
154
  end
100
155
 
101
156
  # Returns a 404 Not Found response tuple
102
- def not_found_response
103
- [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
157
+ def not_found_response(env)
158
+ if head_request?(env)
159
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
160
+ else
161
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
162
+ end
163
+ end
164
+
165
+ def method_not_allowed_response
166
+ [ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
167
+ end
168
+
169
+ def precondition_failed_response(env)
170
+ if head_request?(env)
171
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
172
+ else
173
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
174
+ end
104
175
  end
105
176
 
106
177
  # Returns a JavaScript response that re-throws a Ruby exception
107
178
  # in the browser
108
179
  def javascript_exception_response(exception)
109
- err = "#{exception.class.name}: #{exception.message}"
180
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
110
181
  body = "throw Error(#{err.inspect})"
111
- [ 200, { "Content-Type" => "application/javascript", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
182
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
112
183
  end
113
184
 
114
185
  # Returns a CSS response that hides all elements on the page and
@@ -161,7 +232,7 @@ module Sprockets
161
232
  }
162
233
  CSS
163
234
 
164
- [ 200, { "Content-Type" => "text/css;charset=utf-8", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
235
+ [ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
165
236
  end
166
237
 
167
238
  # Escape special characters for use inside a CSS content("...") string
@@ -173,75 +244,52 @@ module Sprockets
173
244
  gsub('/', '\\\\002f ')
174
245
  end
175
246
 
176
- # Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
177
- def etag_match?(asset, env)
178
- env["HTTP_IF_NONE_MATCH"] == etag(asset)
179
- end
247
+ def cache_headers(env, etag)
248
+ headers = {}
180
249
 
181
- # Test if `?body=1` or `body=true` query param is set
182
- def body_only?(env)
183
- env["QUERY_STRING"].to_s =~ /body=(1|t)/
184
- end
250
+ # Set caching headers
251
+ headers["Cache-Control"] = +"public"
252
+ headers["ETag"] = %("#{etag}")
185
253
 
186
- # Returns a 304 Not Modified response tuple
187
- def not_modified_response(asset, env)
188
- [ 304, {}, [] ]
189
- end
254
+ # If the request url contains a fingerprint, set a long
255
+ # expires on the response
256
+ if path_fingerprint(env["PATH_INFO"])
257
+ headers["Cache-Control"] << ", max-age=31536000, immutable"
190
258
 
191
- # Returns a 200 OK response tuple
192
- def ok_response(asset, env)
193
- [ 200, headers(env, asset, asset.length), asset ]
259
+ # Otherwise set `must-revalidate` since the asset could be modified.
260
+ else
261
+ headers["Cache-Control"] << ", must-revalidate"
262
+ headers["Vary"] = "Accept-Encoding"
263
+ end
264
+
265
+ headers
194
266
  end
195
267
 
196
268
  def headers(env, asset, length)
197
- Hash.new.tap do |headers|
198
- # Set content type and length headers
199
- headers["Content-Type"] = asset.content_type
200
- headers["Content-Length"] = length.to_s
201
-
202
- # Set caching headers
203
- headers["Cache-Control"] = "public"
204
- headers["Last-Modified"] = asset.mtime.httpdate
205
- headers["ETag"] = etag(asset)
206
-
207
- # If the request url contains a fingerprint, set a long
208
- # expires on the response
209
- if path_fingerprint(env["PATH_INFO"])
210
- headers["Cache-Control"] << ", max-age=31536000"
211
-
212
- # Otherwise set `must-revalidate` since the asset could be modified.
213
- else
214
- headers["Cache-Control"] << ", must-revalidate"
269
+ headers = {}
270
+
271
+ # Set content length header
272
+ headers["Content-Length"] = length.to_s
273
+
274
+ # Set content type header
275
+ if type = asset.content_type
276
+ # Set charset param for text/* mime types
277
+ if type.start_with?("text/") && asset.charset
278
+ type += "; charset=#{asset.charset}"
215
279
  end
280
+ headers["Content-Type"] = type
216
281
  end
282
+
283
+ headers.merge(cache_headers(env, asset.etag))
217
284
  end
218
285
 
219
- # Gets digest fingerprint.
286
+ # Gets ETag fingerprint.
220
287
  #
221
288
  # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
222
289
  # # => "0aa2105d29558f3eb790d411d7d8fb66"
223
290
  #
224
291
  def path_fingerprint(path)
225
- path[/-([0-9a-f]{7,40})\.[^.]+\z/, 1]
226
- end
227
-
228
- # URI.unescape is deprecated on 1.9. We need to use URI::Parser
229
- # if its available.
230
- if defined? URI::DEFAULT_PARSER
231
- def unescape(str)
232
- str = URI::DEFAULT_PARSER.unescape(str)
233
- str.force_encoding(Encoding.default_internal) if Encoding.default_internal
234
- str
235
- end
236
- else
237
- def unescape(str)
238
- URI.unescape(str)
239
- end
240
- end
241
-
242
- # Helper to quote the assets digest for use as an ETag.
243
- def etag(asset)
244
- %("#{asset.digest}")
292
+ path[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
245
293
  end
246
294
  end
247
295
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+
4
+ module Sprockets
5
+
6
+ # The purpose of this class is to generate a source map file
7
+ # that can be read and understood by browsers.
8
+ #
9
+ # When a file is passed in it will have a `application/js-sourcemap+json`
10
+ # or `application/css-sourcemap+json` mime type. The filename will be
11
+ # match the original asset. The original asset is loaded. As it
12
+ # gets processed by Sprockets it will aquire all information
13
+ # needed to build a source map file in the `asset.to_hash[:metadata][:map]`
14
+ # key.
15
+ #
16
+ # The output is an asset with a properly formatted source map file:
17
+ #
18
+ # {
19
+ # "version": 3,
20
+ # "sources": ["foo.js"],
21
+ # "names": [ ],
22
+ # "mappings": "AAAA,GAAIA"
23
+ # }
24
+ #
25
+ class SourceMapProcessor
26
+ def self.call(input)
27
+ links = Set.new(input[:metadata][:links])
28
+ env = input[:environment]
29
+
30
+ uri, _ = env.resolve!(input[:filename], accept: self.original_content_type(input[:content_type]))
31
+ asset = env.load(uri)
32
+ map = asset.metadata[:map]
33
+
34
+ # TODO: Because of the default piplene hack we have to apply dependencies
35
+ # from compiled asset to the source map, otherwise the source map cache
36
+ # will never detect the changes from directives
37
+ dependencies = Set.new(input[:metadata][:dependencies])
38
+ dependencies.merge(asset.metadata[:dependencies])
39
+
40
+ map["file"] = PathUtils.split_subpath(input[:load_path], input[:filename])
41
+ sources = map["sections"] ? map["sections"].map { |s| s["map"]["sources"] }.flatten : map["sources"]
42
+
43
+ sources.each do |source|
44
+ source = PathUtils.join(File.dirname(map["file"]), source)
45
+ uri, _ = env.resolve!(source)
46
+ links << uri
47
+ end
48
+
49
+ json = JSON.generate(map)
50
+
51
+ { data: json, links: links, dependencies: dependencies }
52
+ end
53
+
54
+ def self.original_content_type(source_map_content_type, error_when_not_found: true)
55
+ case source_map_content_type
56
+ when "application/js-sourcemap+json"
57
+ "application/javascript"
58
+ when "application/css-sourcemap+json"
59
+ "text/css"
60
+ else
61
+ fail(source_map_content_type) if error_when_not_found
62
+ source_map_content_type
63
+ end
64
+ end
65
+ end
66
+ end