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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +68 -0
- data/README.md +482 -255
- data/bin/sprockets +20 -7
- data/lib/rake/sprocketstask.rb +28 -15
- data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
- data/lib/sprockets/asset.rb +142 -207
- 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/autoload.rb +16 -0
- data/lib/sprockets/babel_processor.rb +66 -0
- data/lib/sprockets/base.rb +89 -249
- data/lib/sprockets/bower.rb +61 -0
- data/lib/sprockets/bundle.rb +105 -0
- data/lib/sprockets/cache/file_store.rb +190 -14
- data/lib/sprockets/cache/memory_store.rb +75 -0
- data/lib/sprockets/cache/null_store.rb +54 -0
- data/lib/sprockets/cache.rb +271 -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 +204 -135
- data/lib/sprockets/dependencies.rb +74 -0
- data/lib/sprockets/digest_utils.rb +200 -0
- data/lib/sprockets/directive_processor.rb +224 -216
- 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 +23 -68
- data/lib/sprockets/erb_processor.rb +37 -0
- data/lib/sprockets/errors.rb +6 -13
- data/lib/sprockets/exporters/base.rb +72 -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 +36 -19
- data/lib/sprockets/loader.rb +343 -0
- data/lib/sprockets/manifest.rb +231 -96
- data/lib/sprockets/manifest_utils.rb +48 -0
- data/lib/sprockets/mime.rb +80 -32
- 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 +140 -192
- 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 +138 -90
- 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/gzip.rb +99 -0
- data/lib/sprockets/utils.rb +186 -53
- data/lib/sprockets/version.rb +2 -1
- data/lib/sprockets/yui_compressor.rb +56 -0
- data/lib/sprockets.rb +217 -52
- metadata +250 -59
- data/LICENSE +0 -21
- data/lib/sprockets/asset_attributes.rb +0 -126
- data/lib/sprockets/bundled_asset.rb +0 -79
- data/lib/sprockets/caching.rb +0 -96
- data/lib/sprockets/charset_normalizer.rb +0 -41
- data/lib/sprockets/eco_template.rb +0 -38
- data/lib/sprockets/ejs_template.rb +0 -37
- data/lib/sprockets/engines.rb +0 -74
- data/lib/sprockets/index.rb +0 -99
- data/lib/sprockets/processed_asset.rb +0 -152
- data/lib/sprockets/processor.rb +0 -32
- data/lib/sprockets/safety_colons.rb +0 -28
- data/lib/sprockets/static_asset.rb +0 -57
- data/lib/sprockets/trail.rb +0 -90
data/lib/sprockets/server.rb
CHANGED
@@ -1,11 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'set'
|
1
3
|
require 'time'
|
2
|
-
require '
|
4
|
+
require 'rack/utils'
|
3
5
|
|
4
6
|
module Sprockets
|
5
7
|
# `Server` is a concern mixed into `Environment` and
|
6
|
-
# `
|
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
|
-
|
31
|
+
unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
|
32
|
+
return method_not_allowed_response
|
33
|
+
end
|
27
34
|
|
28
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
60
|
+
if env['HTTP_IF_NONE_MATCH']
|
61
|
+
if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
|
62
|
+
end
|
59
63
|
|
60
|
-
|
61
|
-
|
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
|
-
|
76
|
+
status = :ok
|
77
|
+
end
|
65
78
|
|
66
|
-
|
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
|
74
|
-
when "
|
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 "
|
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?("..") ||
|
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
|
-
|
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
|
-
|
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" =>
|
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" =>
|
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
|
-
|
177
|
-
|
178
|
-
env["HTTP_IF_NONE_MATCH"] == etag(asset)
|
179
|
-
end
|
247
|
+
def cache_headers(env, etag)
|
248
|
+
headers = {}
|
180
249
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
end
|
250
|
+
# Set caching headers
|
251
|
+
headers["Cache-Control"] = +"public"
|
252
|
+
headers["ETag"] = %("#{etag}")
|
185
253
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
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,
|
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
|