sprockets 2.1.0 → 2.2.3
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of sprockets might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/README.md +18 -1
- data/bin/sprockets +80 -0
- data/lib/rake/sprocketstask.rb +140 -0
- data/lib/sprockets.rb +1 -0
- data/lib/sprockets/asset.rb +2 -0
- data/lib/sprockets/base.rb +46 -6
- data/lib/sprockets/cache/file_store.rb +4 -4
- data/lib/sprockets/context.rb +20 -4
- data/lib/sprockets/directive_processor.rb +14 -2
- data/lib/sprockets/environment.rb +4 -0
- data/lib/sprockets/index.rb +4 -0
- data/lib/sprockets/manifest.rb +203 -0
- data/lib/sprockets/mime.rb +10 -0
- data/lib/sprockets/processed_asset.rb +15 -11
- data/lib/sprockets/server.rb +10 -17
- data/lib/sprockets/static_asset.rb +2 -0
- data/lib/sprockets/utils.rb +14 -12
- data/lib/sprockets/version.rb +1 -1
- metadata +176 -175
@@ -126,7 +126,7 @@ module Sprockets
|
|
126
126
|
@directives ||= header.lines.each_with_index.map { |line, index|
|
127
127
|
if directive = line[DIRECTIVE_PATTERN, 1]
|
128
128
|
name, *args = Shellwords.shellwords(directive)
|
129
|
-
if respond_to?("process_#{name}_directive")
|
129
|
+
if respond_to?("process_#{name}_directive", true)
|
130
130
|
[index + 1, name, *args]
|
131
131
|
end
|
132
132
|
end
|
@@ -260,7 +260,7 @@ module Sprockets
|
|
260
260
|
root = pathname.dirname.join(path).expand_path
|
261
261
|
|
262
262
|
unless (stats = stat(root)) && stats.directory?
|
263
|
-
raise ArgumentError, "
|
263
|
+
raise ArgumentError, "require_directory argument must be a directory"
|
264
264
|
end
|
265
265
|
|
266
266
|
context.depend_on(root)
|
@@ -340,6 +340,18 @@ module Sprockets
|
|
340
340
|
context.depend_on_asset(path)
|
341
341
|
end
|
342
342
|
|
343
|
+
# Allows dependency to be excluded from the asset bundle.
|
344
|
+
#
|
345
|
+
# The `path` must be a valid asset and may or may not already
|
346
|
+
# be part of the bundle. Once stubbed, it is blacklisted and
|
347
|
+
# can't be brought back by any other `require`.
|
348
|
+
#
|
349
|
+
# //= stub "jquery"
|
350
|
+
#
|
351
|
+
def process_stub_directive(path)
|
352
|
+
context.stub_asset(path)
|
353
|
+
end
|
354
|
+
|
343
355
|
# Enable Sprockets 1.x compat mode.
|
344
356
|
#
|
345
357
|
# Makes it possible to use the same JavaScript source
|
@@ -23,6 +23,10 @@ module Sprockets
|
|
23
23
|
self.logger = Logger.new($stderr)
|
24
24
|
self.logger.level = Logger::FATAL
|
25
25
|
|
26
|
+
if respond_to?(:default_external_encoding)
|
27
|
+
self.default_external_encoding = Encoding::UTF_8
|
28
|
+
end
|
29
|
+
|
26
30
|
# Create a safe `Context` subclass to mutate
|
27
31
|
@context_class = Class.new(Context)
|
28
32
|
|
data/lib/sprockets/index.rb
CHANGED
@@ -14,6 +14,10 @@ module Sprockets
|
|
14
14
|
def initialize(environment)
|
15
15
|
@environment = environment
|
16
16
|
|
17
|
+
if environment.respond_to?(:default_external_encoding)
|
18
|
+
@default_external_encoding = environment.default_external_encoding
|
19
|
+
end
|
20
|
+
|
17
21
|
# Copy environment attributes
|
18
22
|
@logger = environment.logger
|
19
23
|
@context_class = environment.context_class
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Sprockets
|
5
|
+
# The Manifest logs the contents of assets compiled to a single
|
6
|
+
# directory. It records basic attributes about the asset for fast
|
7
|
+
# lookup without having to compile. A pointer from each logical path
|
8
|
+
# indicates with fingerprinted asset is the current one.
|
9
|
+
#
|
10
|
+
# The JSON is part of the public API and should be considered
|
11
|
+
# stable. This should make it easy to read from other programming
|
12
|
+
# languages and processes that don't have sprockets loaded. See
|
13
|
+
# `#assets` and `#files` for more infomation about the structure.
|
14
|
+
class Manifest
|
15
|
+
attr_reader :environment, :path, :dir
|
16
|
+
|
17
|
+
# Create new Manifest associated with an `environment`. `path` is
|
18
|
+
# a full path to the manifest json file. The file may or may not
|
19
|
+
# already exist. The dirname of the `path` will be used to write
|
20
|
+
# compiled assets to. Otherwise, if the path is a directory, the
|
21
|
+
# filename will default to "manifest.json" in that directory.
|
22
|
+
#
|
23
|
+
# Manifest.new(environment, "./public/assets/manifest.json")
|
24
|
+
#
|
25
|
+
def initialize(environment, path)
|
26
|
+
@environment = environment
|
27
|
+
|
28
|
+
if File.extname(path) == ""
|
29
|
+
@dir = File.expand_path(path)
|
30
|
+
@path = File.join(@dir, 'manifest.json')
|
31
|
+
else
|
32
|
+
@path = File.expand_path(path)
|
33
|
+
@dir = File.dirname(path)
|
34
|
+
end
|
35
|
+
|
36
|
+
data = nil
|
37
|
+
|
38
|
+
begin
|
39
|
+
if File.exist?(@path)
|
40
|
+
data = MultiJson.decode(File.read(@path))
|
41
|
+
end
|
42
|
+
rescue MultiJson::DecodeError => e
|
43
|
+
logger.error "#{@path} is invalid: #{e.class} #{e.message}"
|
44
|
+
end
|
45
|
+
|
46
|
+
@data = data.is_a?(Hash) ? data : {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns internal assets mapping. Keys are logical paths which
|
50
|
+
# map to the latest fingerprinted filename.
|
51
|
+
#
|
52
|
+
# Logical path (String): Fingerprint path (String)
|
53
|
+
#
|
54
|
+
# { "application.js" => "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js",
|
55
|
+
# "jquery.js" => "jquery-ae0908555a245f8266f77df5a8edca2e.js" }
|
56
|
+
#
|
57
|
+
def assets
|
58
|
+
@data['assets'] ||= {}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns internal file directory listing. Keys are filenames
|
62
|
+
# which map to an attributes array.
|
63
|
+
#
|
64
|
+
# Fingerprint path (String):
|
65
|
+
# logical_path: Logical path (String)
|
66
|
+
# mtime: ISO8601 mtime (String)
|
67
|
+
# digest: Base64 hex digest (String)
|
68
|
+
#
|
69
|
+
# { "application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js" =>
|
70
|
+
# { 'logical_path' => "application.js",
|
71
|
+
# 'mtime' => "2011-12-13T21:47:08-06:00",
|
72
|
+
# 'digest' => "2e8e9a7c6b0aafa0c9bdeec90ea30213" } }
|
73
|
+
#
|
74
|
+
def files
|
75
|
+
@data['files'] ||= {}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Compile and write asset to directory. The asset is written to a
|
79
|
+
# fingerprinted filename like
|
80
|
+
# `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
|
81
|
+
# also inserted into the manifest file.
|
82
|
+
#
|
83
|
+
# compile("application.js")
|
84
|
+
#
|
85
|
+
def compile(*args)
|
86
|
+
paths = environment.each_logical_path(*args).to_a +
|
87
|
+
args.flatten.select { |fn| Pathname.new(fn).absolute? }
|
88
|
+
|
89
|
+
paths.each do |path|
|
90
|
+
if asset = find_asset(path)
|
91
|
+
files[asset.digest_path] = {
|
92
|
+
'logical_path' => asset.logical_path,
|
93
|
+
'mtime' => asset.mtime.iso8601,
|
94
|
+
'digest' => asset.digest
|
95
|
+
}
|
96
|
+
assets[asset.logical_path] = asset.digest_path
|
97
|
+
|
98
|
+
target = File.join(dir, asset.digest_path)
|
99
|
+
|
100
|
+
if File.exist?(target)
|
101
|
+
logger.debug "Skipping #{target}, already exists"
|
102
|
+
else
|
103
|
+
logger.info "Writing #{target}"
|
104
|
+
asset.write_to target
|
105
|
+
end
|
106
|
+
|
107
|
+
save
|
108
|
+
asset
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Removes file from directory and from manifest. `filename` must
|
114
|
+
# be the name with any directory path.
|
115
|
+
#
|
116
|
+
# manifest.remove("application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js")
|
117
|
+
#
|
118
|
+
def remove(filename)
|
119
|
+
path = File.join(dir, filename)
|
120
|
+
logical_path = files[filename]['logical_path']
|
121
|
+
|
122
|
+
if assets[logical_path] == filename
|
123
|
+
assets.delete(logical_path)
|
124
|
+
end
|
125
|
+
|
126
|
+
files.delete(filename)
|
127
|
+
FileUtils.rm(path) if File.exist?(path)
|
128
|
+
|
129
|
+
save
|
130
|
+
|
131
|
+
logger.warn "Removed #{filename}"
|
132
|
+
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Cleanup old assets in the compile directory. By default it will
|
137
|
+
# keep the latest version plus 2 backups.
|
138
|
+
def clean(keep = 2)
|
139
|
+
self.assets.keys.each do |logical_path|
|
140
|
+
# Get assets sorted by ctime, newest first
|
141
|
+
assets = backups_for(logical_path)
|
142
|
+
|
143
|
+
# Keep the last N backups
|
144
|
+
assets = assets[keep..-1] || []
|
145
|
+
|
146
|
+
# Remove old assets
|
147
|
+
assets.each { |path, _| remove(path) }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Wipe directive
|
152
|
+
def clobber
|
153
|
+
FileUtils.rm_r(@dir) if File.exist?(@dir)
|
154
|
+
logger.warn "Removed #{@dir}"
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
# Finds all the backup assets for a logical path. The latest
|
160
|
+
# version is always excluded. The return array is sorted by the
|
161
|
+
# assets mtime in descending order (Newest to oldest).
|
162
|
+
def backups_for(logical_path)
|
163
|
+
files.select { |filename, attrs|
|
164
|
+
# Matching logical paths
|
165
|
+
attrs['logical_path'] == logical_path &&
|
166
|
+
# Excluding whatever asset is the current
|
167
|
+
assets[logical_path] != filename
|
168
|
+
}.sort_by { |filename, attrs|
|
169
|
+
# Sort by timestamp
|
170
|
+
Time.parse(attrs['mtime'])
|
171
|
+
}.reverse
|
172
|
+
end
|
173
|
+
|
174
|
+
# Basic wrapper around Environment#find_asset. Logs compile time.
|
175
|
+
def find_asset(logical_path)
|
176
|
+
asset = nil
|
177
|
+
ms = benchmark do
|
178
|
+
asset = environment.find_asset(logical_path)
|
179
|
+
end
|
180
|
+
logger.warn "Compiled #{logical_path} (#{ms}ms)"
|
181
|
+
asset
|
182
|
+
end
|
183
|
+
|
184
|
+
# Persist manfiest back to FS
|
185
|
+
def save
|
186
|
+
FileUtils.mkdir_p dir
|
187
|
+
File.open(path, 'w') do |f|
|
188
|
+
f.write MultiJson.encode(@data)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
def logger
|
194
|
+
environment.logger
|
195
|
+
end
|
196
|
+
|
197
|
+
def benchmark
|
198
|
+
start_time = Time.now.to_f
|
199
|
+
yield
|
200
|
+
((Time.now.to_f - start_time) * 1000).to_i
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/sprockets/mime.rb
CHANGED
@@ -30,6 +30,16 @@ module Sprockets
|
|
30
30
|
ext = Sprockets::Utils.normalize_extension(ext)
|
31
31
|
@mime_types[ext] = mime_type
|
32
32
|
end
|
33
|
+
|
34
|
+
if defined? Encoding
|
35
|
+
# Returns the correct encoding for a given mime type, while falling
|
36
|
+
# back on the default external encoding, if it exists.
|
37
|
+
def encoding_for_mime_type(type)
|
38
|
+
encoding = Encoding::BINARY if type =~ %r{^(image|audio|video)/}
|
39
|
+
encoding ||= default_external_encoding if respond_to?(:default_external_encoding)
|
40
|
+
encoding
|
41
|
+
end
|
42
|
+
end
|
33
43
|
end
|
34
44
|
|
35
45
|
# Extend Sprockets module to provide global registry
|
@@ -94,27 +94,31 @@ module Sprockets
|
|
94
94
|
|
95
95
|
private
|
96
96
|
def build_required_assets(environment, context)
|
97
|
-
@required_assets = []
|
98
|
-
|
97
|
+
@required_assets = resolve_dependencies(environment, context._required_paths + [pathname.to_s]) -
|
98
|
+
resolve_dependencies(environment, context._stubbed_assets.to_a)
|
99
|
+
end
|
100
|
+
|
101
|
+
def resolve_dependencies(environment, paths)
|
102
|
+
assets = []
|
103
|
+
cache = {}
|
99
104
|
|
100
|
-
|
105
|
+
paths.each do |path|
|
101
106
|
if path == self.pathname.to_s
|
102
|
-
unless
|
103
|
-
|
104
|
-
|
107
|
+
unless cache[self]
|
108
|
+
cache[self] = true
|
109
|
+
assets << self
|
105
110
|
end
|
106
111
|
elsif asset = environment.find_asset(path, :bundle => false)
|
107
112
|
asset.required_assets.each do |asset_dependency|
|
108
|
-
unless
|
109
|
-
|
110
|
-
|
113
|
+
unless cache[asset_dependency]
|
114
|
+
cache[asset_dependency] = true
|
115
|
+
assets << asset_dependency
|
111
116
|
end
|
112
117
|
end
|
113
118
|
end
|
114
119
|
end
|
115
120
|
|
116
|
-
|
117
|
-
required_assets_cache = nil
|
121
|
+
assets
|
118
122
|
end
|
119
123
|
|
120
124
|
def build_dependency_paths(environment, context)
|
data/lib/sprockets/server.rb
CHANGED
@@ -25,11 +25,6 @@ module Sprockets
|
|
25
25
|
|
26
26
|
msg = "Served asset #{env['PATH_INFO']} -"
|
27
27
|
|
28
|
-
# URLs containing a `".."` are rejected for security reasons.
|
29
|
-
if forbidden_request?(env)
|
30
|
-
return forbidden_response
|
31
|
-
end
|
32
|
-
|
33
28
|
# Mark session as "skipped" so no `Set-Cookie` header is set
|
34
29
|
env['rack.session.options'] ||= {}
|
35
30
|
env['rack.session.options'][:defer] = true
|
@@ -43,6 +38,11 @@ module Sprockets
|
|
43
38
|
path = path.sub("-#{fingerprint}", '')
|
44
39
|
end
|
45
40
|
|
41
|
+
# URLs containing a `".."` are rejected for security reasons.
|
42
|
+
if forbidden_request?(path)
|
43
|
+
return forbidden_response
|
44
|
+
end
|
45
|
+
|
46
46
|
# Look up the asset.
|
47
47
|
asset = find_asset(path, :bundle => !body_only?(env))
|
48
48
|
|
@@ -53,9 +53,8 @@ module Sprockets
|
|
53
53
|
# Return a 404 Not Found
|
54
54
|
not_found_response
|
55
55
|
|
56
|
-
# Check request headers `
|
57
|
-
|
58
|
-
elsif not_modified?(asset, env) || etag_match?(asset, env)
|
56
|
+
# Check request headers `HTTP_IF_NONE_MATCH` against the asset digest
|
57
|
+
elsif etag_match?(asset, env)
|
59
58
|
logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
|
60
59
|
|
61
60
|
# Return a 304 Not Modified
|
@@ -86,12 +85,12 @@ module Sprockets
|
|
86
85
|
end
|
87
86
|
|
88
87
|
private
|
89
|
-
def forbidden_request?(
|
88
|
+
def forbidden_request?(path)
|
90
89
|
# Prevent access to files elsewhere on the file system
|
91
90
|
#
|
92
91
|
# http://example.org/assets/../../../etc/passwd
|
93
92
|
#
|
94
|
-
|
93
|
+
path.include?("..") || Pathname.new(path).absolute?
|
95
94
|
end
|
96
95
|
|
97
96
|
# Returns a 403 Forbidden response tuple
|
@@ -174,12 +173,6 @@ module Sprockets
|
|
174
173
|
gsub('/', '\\\\002f ')
|
175
174
|
end
|
176
175
|
|
177
|
-
# Compare the requests `HTTP_IF_MODIFIED_SINCE` against the
|
178
|
-
# assets mtime
|
179
|
-
def not_modified?(asset, env)
|
180
|
-
env["HTTP_IF_MODIFIED_SINCE"] == asset.mtime.httpdate
|
181
|
-
end
|
182
|
-
|
183
176
|
# Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
|
184
177
|
def etag_match?(asset, env)
|
185
178
|
env["HTTP_IF_NONE_MATCH"] == etag(asset)
|
@@ -229,7 +222,7 @@ module Sprockets
|
|
229
222
|
# # => "0aa2105d29558f3eb790d411d7d8fb66"
|
230
223
|
#
|
231
224
|
def path_fingerprint(path)
|
232
|
-
path[/-([0-9a-f]{7,40})\.[^.]
|
225
|
+
path[/-([0-9a-f]{7,40})\.[^.]+\z/, 1]
|
233
226
|
end
|
234
227
|
|
235
228
|
# URI.unescape is deprecated on 1.9. We need to use URI::Parser
|
@@ -23,6 +23,8 @@ module Sprockets
|
|
23
23
|
# Gzip contents if filename has '.gz'
|
24
24
|
options[:compress] ||= File.extname(filename) == '.gz'
|
25
25
|
|
26
|
+
FileUtils.mkdir_p File.dirname(filename)
|
27
|
+
|
26
28
|
if options[:compress]
|
27
29
|
# Open file and run it through `Zlib`
|
28
30
|
pathname.open('rb') do |rd|
|
data/lib/sprockets/utils.rb
CHANGED
@@ -8,19 +8,21 @@ module Sprockets
|
|
8
8
|
# encoding and we want to avoid syntax errors in other interpreters.
|
9
9
|
UTF8_BOM_PATTERN = Regexp.new("\\A\uFEFF".encode('utf-8'))
|
10
10
|
|
11
|
-
def self.read_unicode(pathname)
|
12
|
-
pathname.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
"#{
|
11
|
+
def self.read_unicode(pathname, external_encoding = Encoding.default_external)
|
12
|
+
pathname.open("r:#{external_encoding}") do |f|
|
13
|
+
f.read.tap do |data|
|
14
|
+
# Eager validate the file's encoding. In most cases we
|
15
|
+
# expect it to be UTF-8 unless `default_external` is set to
|
16
|
+
# something else. An error is usually raised if the file is
|
17
|
+
# saved as UTF-16 when we expected UTF-8.
|
18
|
+
if !data.valid_encoding?
|
19
|
+
raise EncodingError, "#{pathname} has a invalid " +
|
20
|
+
"#{data.encoding} byte sequence"
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
|
22
|
+
# If the file is UTF-8 and theres a BOM, strip it for safe concatenation.
|
23
|
+
elsif data.encoding.name == "UTF-8" && data =~ UTF8_BOM_PATTERN
|
24
|
+
data.sub!(UTF8_BOM_PATTERN, "")
|
25
|
+
end
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
data/lib/sprockets/version.rb
CHANGED