sprockets 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +665 -0
  4. data/bin/sprockets +93 -0
  5. data/lib/rake/sprocketstask.rb +153 -0
  6. data/lib/sprockets.rb +229 -0
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +202 -0
  9. data/lib/sprockets/autoload.rb +16 -0
  10. data/lib/sprockets/autoload/babel.rb +8 -0
  11. data/lib/sprockets/autoload/closure.rb +8 -0
  12. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  13. data/lib/sprockets/autoload/eco.rb +8 -0
  14. data/lib/sprockets/autoload/ejs.rb +8 -0
  15. data/lib/sprockets/autoload/jsminc.rb +8 -0
  16. data/lib/sprockets/autoload/sass.rb +8 -0
  17. data/lib/sprockets/autoload/sassc.rb +8 -0
  18. data/lib/sprockets/autoload/uglifier.rb +8 -0
  19. data/lib/sprockets/autoload/yui.rb +8 -0
  20. data/lib/sprockets/autoload/zopfli.rb +7 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +147 -0
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache.rb +271 -0
  26. data/lib/sprockets/cache/file_store.rb +208 -0
  27. data/lib/sprockets/cache/memory_store.rb +75 -0
  28. data/lib/sprockets/cache/null_store.rb +54 -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 +304 -0
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +200 -0
  37. data/lib/sprockets/directive_processor.rb +414 -0
  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 +262 -0
  41. data/lib/sprockets/environment.rb +46 -0
  42. data/lib/sprockets/erb_processor.rb +37 -0
  43. data/lib/sprockets/errors.rb +12 -0
  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 +50 -0
  53. data/lib/sprockets/loader.rb +345 -0
  54. data/lib/sprockets/manifest.rb +338 -0
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +96 -0
  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 +82 -0
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +228 -0
  64. data/lib/sprockets/processor_utils.rb +169 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +30 -0
  67. data/lib/sprockets/sass_compressor.rb +63 -0
  68. data/lib/sprockets/sass_functions.rb +3 -0
  69. data/lib/sprockets/sass_importer.rb +3 -0
  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 +295 -0
  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 +191 -0
  81. data/lib/sprockets/utils.rb +202 -0
  82. data/lib/sprockets/utils/gzip.rb +99 -0
  83. data/lib/sprockets/version.rb +4 -0
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. metadata +444 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/autoload'
3
+ require 'sprockets/source_map_utils'
4
+
5
+ module Sprockets
6
+ # Public: Sass CSS minifier.
7
+ #
8
+ # To accept the default options
9
+ #
10
+ # environment.register_bundle_processor 'text/css',
11
+ # Sprockets::SasscCompressor
12
+ #
13
+ # Or to pass options to the Sass::Engine class.
14
+ #
15
+ # environment.register_bundle_processor 'text/css',
16
+ # Sprockets::SasscCompressor.new({ ... })
17
+ #
18
+ class SasscCompressor
19
+ # Public: Return singleton instance with default options.
20
+ #
21
+ # Returns SasscCompressor object.
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ def self.call(input)
27
+ instance.call(input)
28
+ end
29
+
30
+ def initialize(options = {})
31
+ @options = {
32
+ syntax: :scss,
33
+ style: :compressed,
34
+ source_map_contents: false,
35
+ omit_source_map_url: true,
36
+ }.merge(options).freeze
37
+ end
38
+
39
+ def call(input)
40
+ # SassC requires the template to be modifiable
41
+ input_data = input[:data].frozen? ? input[:data].dup : input[:data]
42
+ engine = Autoload::SassC::Engine.new(input_data, @options.merge(filename: input[:filename], source_map_file: "#{input[:filename]}.map"))
43
+
44
+ css = engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
45
+
46
+ begin
47
+ map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
48
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
49
+ rescue SassC::NotRenderedError
50
+ map = input[:metadata][:map]
51
+ end
52
+
53
+ { data: css, map: map }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+ require 'rack/utils'
3
+ require 'sprockets/autoload'
4
+ require 'sprockets/source_map_utils'
5
+ require 'uri'
6
+
7
+ module Sprockets
8
+ # Processor engine class for the SASS/SCSS compiler. Depends on the `sassc` gem.
9
+ #
10
+ # For more infomation see:
11
+ #
12
+ # https://github.com/sass/sassc-ruby
13
+ # https://github.com/sass/sassc-rails
14
+ #
15
+ class SasscProcessor
16
+
17
+ # Internal: Defines default sass syntax to use. Exposed so the ScsscProcessor
18
+ # may override it.
19
+ def self.syntax
20
+ :sass
21
+ end
22
+
23
+ # Public: Return singleton instance with default options.
24
+ #
25
+ # Returns SasscProcessor object.
26
+ def self.instance
27
+ @instance ||= new
28
+ end
29
+
30
+ def self.call(input)
31
+ instance.call(input)
32
+ end
33
+
34
+ def self.cache_key
35
+ instance.cache_key
36
+ end
37
+
38
+ attr_reader :cache_key
39
+
40
+ def initialize(options = {}, &block)
41
+ @cache_version = options[:cache_version]
42
+ @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::SassC::VERSION}:#{@cache_version}".freeze
43
+ @importer_class = options[:importer]
44
+ @sass_config = options[:sass_config] || {}
45
+ @functions = Module.new do
46
+ include Functions
47
+ include options[:functions] if options[:functions]
48
+ class_eval(&block) if block_given?
49
+ end
50
+ end
51
+
52
+ def call(input)
53
+ context = input[:environment].context_class.new(input)
54
+
55
+ options = engine_options(input, context)
56
+ engine = Autoload::SassC::Engine.new(input[:data], options)
57
+
58
+ css = Utils.module_include(Autoload::SassC::Script::Functions, @functions) do
59
+ engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
60
+ end
61
+
62
+ begin
63
+ map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
64
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
65
+
66
+ engine.dependencies.each do |dependency|
67
+ context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.filename)
68
+ end
69
+ rescue SassC::NotRenderedError
70
+ map = input[:metadata][:map]
71
+ end
72
+
73
+ context.metadata.merge(data: css, map: map)
74
+ end
75
+
76
+ private
77
+
78
+ def merge_options(options)
79
+ defaults = @sass_config.dup
80
+
81
+ if load_paths = defaults.delete(:load_paths)
82
+ options[:load_paths] += load_paths
83
+ end
84
+
85
+ options.merge!(defaults)
86
+ options
87
+ end
88
+
89
+ # Public: Functions injected into Sass context during Sprockets evaluation.
90
+ #
91
+ # This module may be extended to add global functionality to all Sprockets
92
+ # Sass environments. Though, scoping your functions to just your environment
93
+ # is preferred.
94
+ #
95
+ # module Sprockets::SasscProcessor::Functions
96
+ # def asset_path(path, options = {})
97
+ # end
98
+ # end
99
+ #
100
+ module Functions
101
+ # Public: Generate a url for asset path.
102
+ #
103
+ # Default implementation is deprecated. Currently defaults to
104
+ # Context#asset_path.
105
+ #
106
+ # Will raise NotImplementedError in the future. Users should provide their
107
+ # own base implementation.
108
+ #
109
+ # Returns a SassC::Script::Value::String.
110
+ def asset_path(path, options = {})
111
+ path = path.value
112
+
113
+ path, _, query, fragment = URI.split(path)[5..8]
114
+ path = sprockets_context.asset_path(path, options)
115
+ query = "?#{query}" if query
116
+ fragment = "##{fragment}" if fragment
117
+
118
+ Autoload::SassC::Script::Value::String.new("#{path}#{query}#{fragment}", :string)
119
+ end
120
+
121
+ # Public: Generate a asset url() link.
122
+ #
123
+ # path - SassC::Script::Value::String URL path
124
+ #
125
+ # Returns a SassC::Script::Value::String.
126
+ def asset_url(path, options = {})
127
+ Autoload::SassC::Script::Value::String.new("url(#{asset_path(path, options).value})")
128
+ end
129
+
130
+ # Public: Generate url for image path.
131
+ #
132
+ # path - SassC::Script::Value::String URL path
133
+ #
134
+ # Returns a SassC::Script::Value::String.
135
+ def image_path(path)
136
+ asset_path(path, type: :image)
137
+ end
138
+
139
+ # Public: Generate a image url() link.
140
+ #
141
+ # path - SassC::Script::Value::String URL path
142
+ #
143
+ # Returns a SassC::Script::Value::String.
144
+ def image_url(path)
145
+ asset_url(path, type: :image)
146
+ end
147
+
148
+ # Public: Generate url for video path.
149
+ #
150
+ # path - SassC::Script::Value::String URL path
151
+ #
152
+ # Returns a SassC::Script::Value::String.
153
+ def video_path(path)
154
+ asset_path(path, type: :video)
155
+ end
156
+
157
+ # Public: Generate a video url() link.
158
+ #
159
+ # path - SassC::Script::Value::String URL path
160
+ #
161
+ # Returns a SassC::Script::Value::String.
162
+ def video_url(path)
163
+ asset_url(path, type: :video)
164
+ end
165
+
166
+ # Public: Generate url for audio path.
167
+ #
168
+ # path - SassC::Script::Value::String URL path
169
+ #
170
+ # Returns a SassC::Script::Value::String.
171
+ def audio_path(path)
172
+ asset_path(path, type: :audio)
173
+ end
174
+
175
+ # Public: Generate a audio url() link.
176
+ #
177
+ # path - SassC::Script::Value::String URL path
178
+ #
179
+ # Returns a SassC::Script::Value::String.
180
+ def audio_url(path)
181
+ asset_url(path, type: :audio)
182
+ end
183
+
184
+ # Public: Generate url for font path.
185
+ #
186
+ # path - SassC::Script::Value::String URL path
187
+ #
188
+ # Returns a SassC::Script::Value::String.
189
+ def font_path(path)
190
+ asset_path(path, type: :font)
191
+ end
192
+
193
+ # Public: Generate a font url() link.
194
+ #
195
+ # path - SassC::Script::Value::String URL path
196
+ #
197
+ # Returns a SassC::Script::Value::String.
198
+ def font_url(path)
199
+ asset_url(path, type: :font)
200
+ end
201
+
202
+ # Public: Generate url for javascript path.
203
+ #
204
+ # path - SassC::Script::Value::String URL path
205
+ #
206
+ # Returns a SassC::Script::Value::String.
207
+ def javascript_path(path)
208
+ asset_path(path, type: :javascript)
209
+ end
210
+
211
+ # Public: Generate a javascript url() link.
212
+ #
213
+ # path - SassC::Script::Value::String URL path
214
+ #
215
+ # Returns a SassC::Script::Value::String.
216
+ def javascript_url(path)
217
+ asset_url(path, type: :javascript)
218
+ end
219
+
220
+ # Public: Generate url for stylesheet path.
221
+ #
222
+ # path - SassC::Script::Value::String URL path
223
+ #
224
+ # Returns a SassC::Script::Value::String.
225
+ def stylesheet_path(path)
226
+ asset_path(path, type: :stylesheet)
227
+ end
228
+
229
+ # Public: Generate a stylesheet url() link.
230
+ #
231
+ # path - SassC::Script::Value::String URL path
232
+ #
233
+ # Returns a SassC::Script::Value::String.
234
+ def stylesheet_url(path)
235
+ asset_url(path, type: :stylesheet)
236
+ end
237
+
238
+ # Public: Generate a data URI for asset path.
239
+ #
240
+ # path - SassC::Script::Value::String logical asset path
241
+ #
242
+ # Returns a SassC::Script::Value::String.
243
+ def asset_data_url(path)
244
+ url = sprockets_context.asset_data_uri(path.value)
245
+ Autoload::SassC::Script::Value::String.new("url(" + url + ")")
246
+ end
247
+
248
+ protected
249
+ # Public: The Environment.
250
+ #
251
+ # Returns Sprockets::Environment.
252
+ def sprockets_environment
253
+ options[:sprockets][:environment]
254
+ end
255
+
256
+ # Public: Mutatable set of dependencies.
257
+ #
258
+ # Returns a Set.
259
+ def sprockets_dependencies
260
+ options[:sprockets][:dependencies]
261
+ end
262
+
263
+ # Deprecated: Get the Context instance. Use APIs on
264
+ # sprockets_environment or sprockets_dependencies directly.
265
+ #
266
+ # Returns a Context instance.
267
+ def sprockets_context
268
+ options[:sprockets][:context]
269
+ end
270
+
271
+ end
272
+
273
+ def engine_options(input, context)
274
+ merge_options({
275
+ filename: input[:filename],
276
+ syntax: self.class.syntax,
277
+ load_paths: input[:environment].paths,
278
+ importer: @importer_class,
279
+ source_map_contents: false,
280
+ source_map_file: "#{input[:filename]}.map",
281
+ omit_source_map_url: true,
282
+ sprockets: {
283
+ context: context,
284
+ environment: input[:environment],
285
+ dependencies: context.metadata[:dependencies]
286
+ }
287
+ })
288
+ end
289
+ end
290
+
291
+
292
+ class ScsscProcessor < SasscProcessor
293
+ def self.syntax
294
+ :scss
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+ require 'time'
4
+ require 'rack/utils'
5
+
6
+ module Sprockets
7
+ # `Server` is a concern mixed into `Environment` and
8
+ # `CachedEnvironment` that provides a Rack compatible `call`
9
+ # interface and url generation helpers.
10
+ module Server
11
+ # Supported HTTP request methods.
12
+ ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
13
+
14
+ # `call` implements the Rack 1.x specification which accepts an
15
+ # `env` Hash and returns a three item tuple with the status code,
16
+ # headers, and body.
17
+ #
18
+ # Mapping your environment at a url prefix will serve all assets
19
+ # in the path.
20
+ #
21
+ # map "/assets" do
22
+ # run Sprockets::Environment.new
23
+ # end
24
+ #
25
+ # A request for `"/assets/foo/bar.js"` will search your
26
+ # environment for `"foo/bar.js"`.
27
+ def call(env)
28
+ start_time = Time.now.to_f
29
+ time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
30
+
31
+ unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
32
+ return method_not_allowed_response
33
+ end
34
+
35
+ msg = "Served asset #{env['PATH_INFO']} -"
36
+
37
+ # Extract the path from everything after the leading slash
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
43
+
44
+ # Strip fingerprint
45
+ if fingerprint = path_fingerprint(path)
46
+ path = path.sub("-#{fingerprint}", '')
47
+ end
48
+
49
+ # URLs containing a `".."` are rejected for security reasons.
50
+ if forbidden_request?(path)
51
+ return forbidden_response(env)
52
+ end
53
+
54
+ if fingerprint
55
+ if_match = fingerprint
56
+ elsif env['HTTP_IF_MATCH']
57
+ if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
58
+ end
59
+
60
+ if env['HTTP_IF_NONE_MATCH']
61
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
62
+ end
63
+
64
+ # Look up the asset.
65
+ asset = find_asset(path)
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
75
+ else
76
+ status = :ok
77
+ end
78
+
79
+ case status
80
+ when :ok
81
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
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)
92
+ end
93
+ rescue Exception => e
94
+ logger.error "Error compiling asset #{path}:"
95
+ logger.error "#{e.class.name}: #{e.message}"
96
+
97
+ case File.extname(path)
98
+ when ".js"
99
+ # Re-throw JavaScript asset exceptions to the browser
100
+ logger.info "#{msg} 500 Internal Server Error\n\n"
101
+ return javascript_exception_response(e)
102
+ when ".css"
103
+ # Display CSS asset exceptions in the browser
104
+ logger.info "#{msg} 500 Internal Server Error\n\n"
105
+ return css_exception_response(e)
106
+ else
107
+ raise
108
+ end
109
+ end
110
+
111
+ private
112
+ def forbidden_request?(path)
113
+ # Prevent access to files elsewhere on the file system
114
+ #
115
+ # http://example.org/assets/../../../etc/passwd
116
+ #
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
145
+ end
146
+
147
+ # Returns a 403 Forbidden response tuple
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
154
+ end
155
+
156
+ # Returns a 404 Not Found response tuple
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
175
+ end
176
+
177
+ # Returns a JavaScript response that re-throws a Ruby exception
178
+ # in the browser
179
+ def javascript_exception_response(exception)
180
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
181
+ body = "throw Error(#{err.inspect})"
182
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
183
+ end
184
+
185
+ # Returns a CSS response that hides all elements on the page and
186
+ # displays the exception
187
+ def css_exception_response(exception)
188
+ message = "\n#{exception.class.name}: #{exception.message}"
189
+ backtrace = "\n #{exception.backtrace.first}"
190
+
191
+ body = <<-CSS
192
+ html {
193
+ padding: 18px 36px;
194
+ }
195
+
196
+ head {
197
+ display: block;
198
+ }
199
+
200
+ body {
201
+ margin: 0;
202
+ padding: 0;
203
+ }
204
+
205
+ body > * {
206
+ display: none !important;
207
+ }
208
+
209
+ head:after, body:before, body:after {
210
+ display: block !important;
211
+ }
212
+
213
+ head:after {
214
+ font-family: sans-serif;
215
+ font-size: large;
216
+ font-weight: bold;
217
+ content: "Error compiling CSS asset";
218
+ }
219
+
220
+ body:before, body:after {
221
+ font-family: monospace;
222
+ white-space: pre-wrap;
223
+ }
224
+
225
+ body:before {
226
+ font-weight: bold;
227
+ content: "#{escape_css_content(message)}";
228
+ }
229
+
230
+ body:after {
231
+ content: "#{escape_css_content(backtrace)}";
232
+ }
233
+ CSS
234
+
235
+ [ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
236
+ end
237
+
238
+ # Escape special characters for use inside a CSS content("...") string
239
+ def escape_css_content(content)
240
+ content.
241
+ gsub('\\', '\\\\005c ').
242
+ gsub("\n", '\\\\000a ').
243
+ gsub('"', '\\\\0022 ').
244
+ gsub('/', '\\\\002f ')
245
+ end
246
+
247
+ def cache_headers(env, etag)
248
+ headers = {}
249
+
250
+ # Set caching headers
251
+ headers["Cache-Control"] = +"public"
252
+ headers["ETag"] = %("#{etag}")
253
+
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"
258
+
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
266
+ end
267
+
268
+ def headers(env, asset, length)
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}"
279
+ end
280
+ headers["Content-Type"] = type
281
+ end
282
+
283
+ headers.merge(cache_headers(env, asset.etag))
284
+ end
285
+
286
+ # Gets ETag fingerprint.
287
+ #
288
+ # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
289
+ # # => "0aa2105d29558f3eb790d411d7d8fb66"
290
+ #
291
+ def path_fingerprint(path)
292
+ path[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
293
+ end
294
+ end
295
+ end