sprockets 2.6.0 → 4.2.2

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +118 -0
  3. data/{LICENSE → MIT-LICENSE} +2 -2
  4. data/README.md +541 -289
  5. data/bin/sprockets +20 -7
  6. data/lib/rake/sprocketstask.rb +34 -17
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +158 -210
  9. data/lib/sprockets/autoload/babel.rb +8 -0
  10. data/lib/sprockets/autoload/closure.rb +8 -0
  11. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  12. data/lib/sprockets/autoload/eco.rb +8 -0
  13. data/lib/sprockets/autoload/ejs.rb +8 -0
  14. data/lib/sprockets/autoload/jsminc.rb +8 -0
  15. data/lib/sprockets/autoload/sass.rb +8 -0
  16. data/lib/sprockets/autoload/sassc.rb +8 -0
  17. data/lib/sprockets/autoload/uglifier.rb +8 -0
  18. data/lib/sprockets/autoload/yui.rb +8 -0
  19. data/lib/sprockets/autoload/zopfli.rb +7 -0
  20. data/lib/sprockets/autoload.rb +16 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +89 -378
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache/file_store.rb +190 -14
  26. data/lib/sprockets/cache/memory_store.rb +84 -0
  27. data/lib/sprockets/cache/null_store.rb +54 -0
  28. data/lib/sprockets/cache.rb +271 -0
  29. data/lib/sprockets/cached_environment.rb +64 -0
  30. data/lib/sprockets/closure_compressor.rb +48 -0
  31. data/lib/sprockets/coffee_script_processor.rb +39 -0
  32. data/lib/sprockets/compressing.rb +134 -0
  33. data/lib/sprockets/configuration.rb +79 -0
  34. data/lib/sprockets/context.rb +166 -150
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +197 -0
  37. data/lib/sprockets/directive_processor.rb +241 -215
  38. data/lib/sprockets/eco_processor.rb +33 -0
  39. data/lib/sprockets/ejs_processor.rb +32 -0
  40. data/lib/sprockets/encoding_utils.rb +261 -0
  41. data/lib/sprockets/environment.rb +23 -64
  42. data/lib/sprockets/erb_processor.rb +43 -0
  43. data/lib/sprockets/errors.rb +5 -13
  44. data/lib/sprockets/exporters/base.rb +71 -0
  45. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  46. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  47. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  48. data/lib/sprockets/exporting.rb +73 -0
  49. data/lib/sprockets/file_reader.rb +16 -0
  50. data/lib/sprockets/http_utils.rb +135 -0
  51. data/lib/sprockets/jsminc_compressor.rb +32 -0
  52. data/lib/sprockets/jst_processor.rb +36 -19
  53. data/lib/sprockets/loader.rb +347 -0
  54. data/lib/sprockets/manifest.rb +228 -112
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +78 -31
  57. data/lib/sprockets/npm.rb +52 -0
  58. data/lib/sprockets/path_dependency_utils.rb +77 -0
  59. data/lib/sprockets/path_digest_utils.rb +48 -0
  60. data/lib/sprockets/path_utils.rb +367 -0
  61. data/lib/sprockets/paths.rb +43 -19
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +146 -164
  64. data/lib/sprockets/processor_utils.rb +170 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +20 -15
  67. data/lib/sprockets/sass_compressor.rb +55 -10
  68. data/lib/sprockets/sass_functions.rb +3 -70
  69. data/lib/sprockets/sass_importer.rb +3 -29
  70. data/lib/sprockets/sass_processor.rb +313 -0
  71. data/lib/sprockets/sassc_compressor.rb +56 -0
  72. data/lib/sprockets/sassc_processor.rb +297 -0
  73. data/lib/sprockets/server.rb +159 -91
  74. data/lib/sprockets/source_map_processor.rb +66 -0
  75. data/lib/sprockets/source_map_utils.rb +483 -0
  76. data/lib/sprockets/transformers.rb +173 -0
  77. data/lib/sprockets/uglifier_compressor.rb +66 -0
  78. data/lib/sprockets/unloaded_asset.rb +139 -0
  79. data/lib/sprockets/uri_tar.rb +99 -0
  80. data/lib/sprockets/uri_utils.rb +194 -0
  81. data/lib/sprockets/utils/gzip.rb +99 -0
  82. data/lib/sprockets/utils.rb +193 -52
  83. data/lib/sprockets/version.rb +2 -1
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. data/lib/sprockets.rb +217 -75
  86. metadata +272 -117
  87. data/lib/sprockets/asset_attributes.rb +0 -131
  88. data/lib/sprockets/bundled_asset.rb +0 -80
  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/sass_template.rb +0 -60
  99. data/lib/sprockets/scss_template.rb +0 -13
  100. data/lib/sprockets/static_asset.rb +0 -58
@@ -1,11 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
1
3
  require 'time'
2
- require 'uri'
4
+ require 'rack'
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
+
14
+ # :stopdoc:
15
+ if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
16
+ X_CASCADE = "X-Cascade"
17
+ VARY = "Vary"
18
+ else
19
+ X_CASCADE = "x-cascade"
20
+ VARY = "vary"
21
+ end
22
+ # :startdoc:
23
+
9
24
  # `call` implements the Rack 1.x specification which accepts an
10
25
  # `env` Hash and returns a three item tuple with the status code,
11
26
  # headers, and body.
@@ -23,19 +38,18 @@ module Sprockets
23
38
  start_time = Time.now.to_f
24
39
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
40
 
26
- msg = "Served asset #{env['PATH_INFO']} -"
41
+ unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
42
+ return method_not_allowed_response
43
+ end
27
44
 
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
45
+ msg = "Served asset #{env['PATH_INFO']} -"
32
46
 
33
47
  # Extract the path from everything after the leading slash
34
- path = unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
48
+ full_path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
49
+ path = full_path
35
50
 
36
- # URLs containing a `".."` are rejected for security reasons.
37
- if forbidden_request?(path)
38
- return forbidden_response
51
+ unless path.valid_encoding?
52
+ return bad_request_response(env)
39
53
  end
40
54
 
41
55
  # Strip fingerprint
@@ -43,39 +57,69 @@ module Sprockets
43
57
  path = path.sub("-#{fingerprint}", '')
44
58
  end
45
59
 
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)"
60
+ # URLs containing a `".."` are rejected for security reasons.
61
+ if forbidden_request?(path)
62
+ return forbidden_response(env)
63
+ end
52
64
 
53
- # Return a 404 Not Found
54
- not_found_response
65
+ if fingerprint
66
+ if_match = fingerprint
67
+ elsif env['HTTP_IF_MATCH']
68
+ if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
69
+ end
55
70
 
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)"
71
+ if env['HTTP_IF_NONE_MATCH']
72
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
73
+ end
59
74
 
60
- # Return a 304 Not Modified
61
- not_modified_response(asset, env)
75
+ # Look up the asset.
76
+ asset = find_asset(path)
77
+
78
+ # Fallback to looking up the asset with the full path.
79
+ # This will make assets that are hashed with webpack or
80
+ # other js bundlers work consistently between production
81
+ # and development pipelines.
82
+ if asset.nil? && (asset = find_asset(full_path))
83
+ if_match = asset.etag if fingerprint
84
+ fingerprint = asset.etag
85
+ end
62
86
 
87
+ if asset.nil?
88
+ status = :not_found
89
+ elsif fingerprint && asset.etag != fingerprint
90
+ status = :not_found
91
+ elsif if_match && asset.etag != if_match
92
+ status = :precondition_failed
93
+ elsif if_none_match && asset.etag == if_none_match
94
+ status = :not_modified
63
95
  else
64
- logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
96
+ status = :ok
97
+ end
65
98
 
66
- # Return a 200 with the asset contents
99
+ case status
100
+ when :ok
101
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
67
102
  ok_response(asset, env)
103
+ when :not_modified
104
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
105
+ not_modified_response(env, if_none_match)
106
+ when :not_found
107
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
108
+ not_found_response(env)
109
+ when :precondition_failed
110
+ logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
111
+ precondition_failed_response(env)
68
112
  end
69
113
  rescue Exception => e
70
114
  logger.error "Error compiling asset #{path}:"
71
115
  logger.error "#{e.class.name}: #{e.message}"
72
116
 
73
- case content_type_of(path)
74
- when "application/javascript"
117
+ case File.extname(path)
118
+ when ".js"
75
119
  # Re-throw JavaScript asset exceptions to the browser
76
120
  logger.info "#{msg} 500 Internal Server Error\n\n"
77
121
  return javascript_exception_response(e)
78
- when "text/css"
122
+ when ".css"
79
123
  # Display CSS asset exceptions in the browser
80
124
  logger.info "#{msg} 500 Internal Server Error\n\n"
81
125
  return css_exception_response(e)
@@ -90,25 +134,72 @@ module Sprockets
90
134
  #
91
135
  # http://example.org/assets/../../../etc/passwd
92
136
  #
93
- path.include?("..")
137
+ path.include?("..") || absolute_path?(path) || path.include?("://")
138
+ end
139
+
140
+ def head_request?(env)
141
+ env['REQUEST_METHOD'] == 'HEAD'
142
+ end
143
+
144
+ # Returns a 200 OK response tuple
145
+ def ok_response(asset, env)
146
+ if head_request?(env)
147
+ [ 200, headers(env, asset, 0), [] ]
148
+ else
149
+ [ 200, headers(env, asset, asset.length), asset ]
150
+ end
151
+ end
152
+
153
+ # Returns a 304 Not Modified response tuple
154
+ def not_modified_response(env, etag)
155
+ [ 304, cache_headers(env, etag), [] ]
156
+ end
157
+
158
+ # Returns a 400 Forbidden response tuple
159
+ def bad_request_response(env)
160
+ if head_request?(env)
161
+ [ 400, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "0" }, [] ]
162
+ else
163
+ [ 400, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "11" }, [ "Bad Request" ] ]
164
+ end
94
165
  end
95
166
 
96
167
  # Returns a 403 Forbidden response tuple
97
- def forbidden_response
98
- [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
168
+ def forbidden_response(env)
169
+ if head_request?(env)
170
+ [ 403, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "0" }, [] ]
171
+ else
172
+ [ 403, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9" }, [ "Forbidden" ] ]
173
+ end
99
174
  end
100
175
 
101
176
  # 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" ] ]
177
+ def not_found_response(env)
178
+ if head_request?(env)
179
+ [ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "0", X_CASCADE => "pass" }, [] ]
180
+ else
181
+ [ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9", X_CASCADE => "pass" }, [ "Not found" ] ]
182
+ end
183
+ end
184
+
185
+ def method_not_allowed_response
186
+ [ 405, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "18" }, [ "Method Not Allowed" ] ]
187
+ end
188
+
189
+ def precondition_failed_response(env)
190
+ if head_request?(env)
191
+ [ 412, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "0", X_CASCADE => "pass" }, [] ]
192
+ else
193
+ [ 412, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "19", X_CASCADE => "pass" }, [ "Precondition Failed" ] ]
194
+ end
104
195
  end
105
196
 
106
197
  # Returns a JavaScript response that re-throws a Ruby exception
107
198
  # in the browser
108
199
  def javascript_exception_response(exception)
109
- err = "#{exception.class.name}: #{exception.message}"
200
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
110
201
  body = "throw Error(#{err.inspect})"
111
- [ 200, { "Content-Type" => "application/javascript", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
202
+ [ 200, { Rack::CONTENT_TYPE => "application/javascript", Rack::CONTENT_LENGTH => body.bytesize.to_s }, [ body ] ]
112
203
  end
113
204
 
114
205
  # Returns a CSS response that hides all elements on the page and
@@ -161,7 +252,7 @@ module Sprockets
161
252
  }
162
253
  CSS
163
254
 
164
- [ 200, { "Content-Type" => "text/css;charset=utf-8", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
255
+ [ 200, { Rack::CONTENT_TYPE => "text/css; charset=utf-8", Rack::CONTENT_LENGTH => body.bytesize.to_s }, [ body ] ]
165
256
  end
166
257
 
167
258
  # Escape special characters for use inside a CSS content("...") string
@@ -173,75 +264,52 @@ module Sprockets
173
264
  gsub('/', '\\\\002f ')
174
265
  end
175
266
 
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
267
+ def cache_headers(env, etag)
268
+ headers = {}
180
269
 
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
270
+ # Set caching headers
271
+ headers[Rack::CACHE_CONTROL] = +"public"
272
+ headers[Rack::ETAG] = %("#{etag}")
185
273
 
186
- # Returns a 304 Not Modified response tuple
187
- def not_modified_response(asset, env)
188
- [ 304, {}, [] ]
189
- end
274
+ # If the request url contains a fingerprint, set a long
275
+ # expires on the response
276
+ if path_fingerprint(env["PATH_INFO"])
277
+ headers[Rack::CACHE_CONTROL] << ", max-age=31536000, immutable"
190
278
 
191
- # Returns a 200 OK response tuple
192
- def ok_response(asset, env)
193
- [ 200, headers(env, asset, asset.length), asset ]
279
+ # Otherwise set `must-revalidate` since the asset could be modified.
280
+ else
281
+ headers[Rack::CACHE_CONTROL] << ", must-revalidate"
282
+ headers[VARY] = "Accept-Encoding"
283
+ end
284
+
285
+ headers
194
286
  end
195
287
 
196
288
  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"
289
+ headers = {}
290
+
291
+ # Set content length header
292
+ headers[Rack::CONTENT_LENGTH] = length.to_s
293
+
294
+ # Set content type header
295
+ if type = asset.content_type
296
+ # Set charset param for text/* mime types
297
+ if type.start_with?("text/") && asset.charset
298
+ type += "; charset=#{asset.charset}"
215
299
  end
300
+ headers[Rack::CONTENT_TYPE] = type
216
301
  end
302
+
303
+ headers.merge(cache_headers(env, asset.etag))
217
304
  end
218
305
 
219
- # Gets digest fingerprint.
306
+ # Gets ETag fingerprint.
220
307
  #
221
308
  # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
222
309
  # # => "0aa2105d29558f3eb790d411d7d8fb66"
223
310
  #
224
311
  def path_fingerprint(path)
225
- path[/-([0-9a-f]{7,40})\.[^.]+$/, 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}")
312
+ path[/-([0-9a-zA-Z]{7,128})\.[^.]+\z/, 1]
245
313
  end
246
314
  end
247
315
  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 acquire 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