sprockets 4.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +72 -0
- data/README.md +665 -0
- data/bin/sprockets +93 -0
- data/lib/rake/sprocketstask.rb +153 -0
- data/lib/sprockets.rb +229 -0
- data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
- data/lib/sprockets/asset.rb +202 -0
- data/lib/sprockets/autoload.rb +16 -0
- data/lib/sprockets/autoload/babel.rb +8 -0
- data/lib/sprockets/autoload/closure.rb +8 -0
- data/lib/sprockets/autoload/coffee_script.rb +8 -0
- data/lib/sprockets/autoload/eco.rb +8 -0
- data/lib/sprockets/autoload/ejs.rb +8 -0
- data/lib/sprockets/autoload/jsminc.rb +8 -0
- data/lib/sprockets/autoload/sass.rb +8 -0
- data/lib/sprockets/autoload/sassc.rb +8 -0
- data/lib/sprockets/autoload/uglifier.rb +8 -0
- data/lib/sprockets/autoload/yui.rb +8 -0
- data/lib/sprockets/autoload/zopfli.rb +7 -0
- data/lib/sprockets/babel_processor.rb +66 -0
- data/lib/sprockets/base.rb +147 -0
- data/lib/sprockets/bower.rb +61 -0
- data/lib/sprockets/bundle.rb +105 -0
- data/lib/sprockets/cache.rb +271 -0
- data/lib/sprockets/cache/file_store.rb +208 -0
- data/lib/sprockets/cache/memory_store.rb +75 -0
- data/lib/sprockets/cache/null_store.rb +54 -0
- data/lib/sprockets/cached_environment.rb +64 -0
- data/lib/sprockets/closure_compressor.rb +48 -0
- data/lib/sprockets/coffee_script_processor.rb +39 -0
- data/lib/sprockets/compressing.rb +134 -0
- data/lib/sprockets/configuration.rb +79 -0
- data/lib/sprockets/context.rb +304 -0
- data/lib/sprockets/dependencies.rb +74 -0
- data/lib/sprockets/digest_utils.rb +200 -0
- data/lib/sprockets/directive_processor.rb +414 -0
- data/lib/sprockets/eco_processor.rb +33 -0
- data/lib/sprockets/ejs_processor.rb +32 -0
- data/lib/sprockets/encoding_utils.rb +262 -0
- data/lib/sprockets/environment.rb +46 -0
- data/lib/sprockets/erb_processor.rb +37 -0
- data/lib/sprockets/errors.rb +12 -0
- data/lib/sprockets/exporters/base.rb +71 -0
- data/lib/sprockets/exporters/file_exporter.rb +24 -0
- data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
- data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
- data/lib/sprockets/exporting.rb +73 -0
- data/lib/sprockets/file_reader.rb +16 -0
- data/lib/sprockets/http_utils.rb +135 -0
- data/lib/sprockets/jsminc_compressor.rb +32 -0
- data/lib/sprockets/jst_processor.rb +50 -0
- data/lib/sprockets/loader.rb +345 -0
- data/lib/sprockets/manifest.rb +338 -0
- data/lib/sprockets/manifest_utils.rb +48 -0
- data/lib/sprockets/mime.rb +96 -0
- data/lib/sprockets/npm.rb +52 -0
- data/lib/sprockets/path_dependency_utils.rb +77 -0
- data/lib/sprockets/path_digest_utils.rb +48 -0
- data/lib/sprockets/path_utils.rb +367 -0
- data/lib/sprockets/paths.rb +82 -0
- data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
- data/lib/sprockets/processing.rb +228 -0
- data/lib/sprockets/processor_utils.rb +169 -0
- data/lib/sprockets/resolve.rb +295 -0
- data/lib/sprockets/sass_cache_store.rb +30 -0
- data/lib/sprockets/sass_compressor.rb +63 -0
- data/lib/sprockets/sass_functions.rb +3 -0
- data/lib/sprockets/sass_importer.rb +3 -0
- data/lib/sprockets/sass_processor.rb +313 -0
- data/lib/sprockets/sassc_compressor.rb +56 -0
- data/lib/sprockets/sassc_processor.rb +297 -0
- data/lib/sprockets/server.rb +295 -0
- data/lib/sprockets/source_map_processor.rb +66 -0
- data/lib/sprockets/source_map_utils.rb +483 -0
- data/lib/sprockets/transformers.rb +173 -0
- data/lib/sprockets/uglifier_compressor.rb +66 -0
- data/lib/sprockets/unloaded_asset.rb +139 -0
- data/lib/sprockets/uri_tar.rb +99 -0
- data/lib/sprockets/uri_utils.rb +191 -0
- data/lib/sprockets/utils.rb +202 -0
- data/lib/sprockets/utils/gzip.rb +99 -0
- data/lib/sprockets/version.rb +4 -0
- data/lib/sprockets/yui_compressor.rb +56 -0
- 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
|