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,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