sprockets 1.0.2 → 2.0.0

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.

Files changed (64) hide show
  1. data/LICENSE +21 -0
  2. data/README.md +356 -0
  3. data/lib/sprockets.rb +26 -37
  4. data/lib/sprockets/asset.rb +212 -0
  5. data/lib/sprockets/asset_attributes.rb +158 -0
  6. data/lib/sprockets/base.rb +163 -0
  7. data/lib/sprockets/bundled_asset.rb +258 -0
  8. data/lib/sprockets/cache/file_store.rb +41 -0
  9. data/lib/sprockets/caching.rb +123 -0
  10. data/lib/sprockets/charset_normalizer.rb +41 -0
  11. data/lib/sprockets/context.rb +217 -0
  12. data/lib/sprockets/digest.rb +67 -0
  13. data/lib/sprockets/directive_processor.rb +380 -0
  14. data/lib/sprockets/eco_template.rb +38 -0
  15. data/lib/sprockets/ejs_template.rb +37 -0
  16. data/lib/sprockets/engines.rb +98 -0
  17. data/lib/sprockets/environment.rb +81 -40
  18. data/lib/sprockets/errors.rb +17 -0
  19. data/lib/sprockets/index.rb +79 -0
  20. data/lib/sprockets/jst_processor.rb +26 -0
  21. data/lib/sprockets/mime.rb +38 -0
  22. data/lib/sprockets/processing.rb +280 -0
  23. data/lib/sprockets/processor.rb +32 -0
  24. data/lib/sprockets/safety_colons.rb +28 -0
  25. data/lib/sprockets/server.rb +272 -0
  26. data/lib/sprockets/static_asset.rb +86 -0
  27. data/lib/sprockets/trail.rb +114 -0
  28. data/lib/sprockets/utils.rb +67 -0
  29. data/lib/sprockets/version.rb +1 -7
  30. metadata +212 -64
  31. data/Rakefile +0 -19
  32. data/bin/sprocketize +0 -54
  33. data/ext/nph-sprockets.cgi +0 -127
  34. data/lib/sprockets/concatenation.rb +0 -36
  35. data/lib/sprockets/error.rb +0 -5
  36. data/lib/sprockets/pathname.rb +0 -37
  37. data/lib/sprockets/preprocessor.rb +0 -91
  38. data/lib/sprockets/secretary.rb +0 -106
  39. data/lib/sprockets/source_file.rb +0 -54
  40. data/lib/sprockets/source_line.rb +0 -82
  41. data/test/fixtures/assets/images/script_with_assets/one.png +0 -1
  42. data/test/fixtures/assets/images/script_with_assets/two.png +0 -1
  43. data/test/fixtures/assets/stylesheets/script_with_assets.css +0 -1
  44. data/test/fixtures/constants.yml +0 -1
  45. data/test/fixtures/double_slash_comments_that_are_not_requires_should_be_ignored_when_strip_comments_is_false.js +0 -8
  46. data/test/fixtures/double_slash_comments_that_are_not_requires_should_be_removed_by_default.js +0 -2
  47. data/test/fixtures/multiline_comments_should_be_removed_by_default.js +0 -4
  48. data/test/fixtures/requiring_a_file_after_it_has_already_been_required_should_do_nothing.js +0 -5
  49. data/test/fixtures/requiring_a_file_that_does_not_exist_should_raise_an_error.js +0 -1
  50. data/test/fixtures/requiring_a_single_file_should_replace_the_require_comment_with_the_file_contents.js +0 -3
  51. data/test/fixtures/requiring_the_current_file_should_do_nothing.js +0 -1
  52. data/test/fixtures/src/constants.yml +0 -3
  53. data/test/fixtures/src/foo.js +0 -1
  54. data/test/fixtures/src/foo/bar.js +0 -4
  55. data/test/fixtures/src/foo/foo.js +0 -1
  56. data/test/fixtures/src/script_with_assets.js +0 -3
  57. data/test/test_concatenation.rb +0 -28
  58. data/test/test_environment.rb +0 -64
  59. data/test/test_helper.rb +0 -55
  60. data/test/test_pathname.rb +0 -43
  61. data/test/test_preprocessor.rb +0 -107
  62. data/test/test_secretary.rb +0 -83
  63. data/test/test_source_file.rb +0 -34
  64. data/test/test_source_line.rb +0 -89
@@ -0,0 +1,32 @@
1
+ require 'tilt'
2
+
3
+ module Sprockets
4
+ # `Processor` creates an anonymous processor class from a block.
5
+ #
6
+ # register_preprocessor :my_processor do |context, data|
7
+ # # ...
8
+ # end
9
+ #
10
+ class Processor < Tilt::Template
11
+ # `processor` is a lambda or block
12
+ def self.processor
13
+ @processor
14
+ end
15
+
16
+ def self.name
17
+ "Sprockets::Processor (#{@name})"
18
+ end
19
+
20
+ def self.to_s
21
+ name
22
+ end
23
+
24
+ def prepare
25
+ end
26
+
27
+ # Call processor block with `context` and `data`.
28
+ def evaluate(context, locals)
29
+ self.class.processor.call(context, data)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ require 'tilt'
2
+
3
+ module Sprockets
4
+ # For JS developers who are colonfobic, concatenating JS files using
5
+ # the module pattern usually leads to syntax errors.
6
+ #
7
+ # The `SafetyColons` processor will insert missing semicolons to the
8
+ # end of the file.
9
+ #
10
+ # This behavior can be disabled with:
11
+ #
12
+ # environment.unregister_postprocessor 'application/javascript', Sprockets::SafetyColons
13
+ #
14
+ class SafetyColons < Tilt::Template
15
+ def prepare
16
+ end
17
+
18
+ def evaluate(context, locals, &block)
19
+ # If the file is blank or ends in a semicolon, leave it as is
20
+ if data =~ /\A\s*\Z/m || data =~ /;\s*\Z/m
21
+ data
22
+ else
23
+ # Otherwise, append a semicolon and newline
24
+ "#{data};\n"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,272 @@
1
+ require 'rack/request'
2
+ require 'time'
3
+
4
+ module Sprockets
5
+ # `Server` is a concern mixed into `Environment` and
6
+ # `Index` that provides a Rack compatible `call`
7
+ # interface and url generation helpers.
8
+ module Server
9
+ # `call` implements the Rack 1.x specification which accepts an
10
+ # `env` Hash and returns a three item tuple with the status code,
11
+ # headers, and body.
12
+ #
13
+ # Mapping your environment at a url prefix will serve all assets
14
+ # in the path.
15
+ #
16
+ # map "/assets" do
17
+ # run Sprockets::Environment.new
18
+ # end
19
+ #
20
+ # A request for `"/assets/foo/bar.js"` will search your
21
+ # environment for `"foo/bar.js"`.
22
+ def call(env)
23
+ start_time = Time.now.to_f
24
+ time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
+
26
+ msg = "Served asset #{env['PATH_INFO']} -"
27
+
28
+ # URLs containing a `".."` are rejected for security reasons.
29
+ if forbidden_request?(env)
30
+ return forbidden_response
31
+ end
32
+
33
+ # Mark session as "skipped" so no `Set-Cookie` header is set
34
+ env['rack.session.options'] ||= {}
35
+ env['rack.session.options'][:defer] = true
36
+ env['rack.session.options'][:skip] = true
37
+
38
+ # Extract the path from everything after the leading slash
39
+ path = env['PATH_INFO'].to_s.sub(/^\//, '')
40
+
41
+ # Look up the asset.
42
+ asset = find_asset(path)
43
+ asset.to_a if asset
44
+
45
+ # `find_asset` returns nil if the asset doesn't exist
46
+ if asset.nil?
47
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
48
+
49
+ # Return a 404 Not Found
50
+ not_found_response
51
+
52
+ # Check request headers `HTTP_IF_MODIFIED_SINCE` and
53
+ # `HTTP_IF_NONE_MATCH` against the assets mtime and digest
54
+ elsif not_modified?(asset, env) || etag_match?(asset, env)
55
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
56
+
57
+ # Return a 304 Not Modified
58
+ not_modified_response(asset, env)
59
+
60
+ else
61
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
62
+
63
+ # Return a 200 with the asset contents
64
+ ok_response(asset, env)
65
+ end
66
+ rescue Exception => e
67
+ logger.error "Error compiling asset #{path}:"
68
+ logger.error "#{e.class.name}: #{e.message}"
69
+
70
+ case content_type_of(path)
71
+ when "application/javascript"
72
+ # Re-throw JavaScript asset exceptions to the browser
73
+ logger.info "#{msg} 500 Internal Server Error\n\n"
74
+ return javascript_exception_response(e)
75
+ when "text/css"
76
+ # Display CSS asset exceptions in the browser
77
+ logger.info "#{msg} 500 Internal Server Error\n\n"
78
+ return css_exception_response(e)
79
+ else
80
+ raise
81
+ end
82
+ end
83
+
84
+ # Deprecated: `path` is a url helper that looks up an asset given a
85
+ # `logical_path` and returns a path String. By default, the
86
+ # asset's digest fingerprint is spliced into the filename.
87
+ #
88
+ # /assets/application-3676d55f84497cbeadfc614c1b1b62fc.js
89
+ #
90
+ # A third `prefix` argument can be pass along to be prepended to
91
+ # the string.
92
+ def path(logical_path, fingerprint = true, prefix = nil)
93
+ logger.warn "Sprockets::Environment#path is deprecated\n#{caller[0..2].join("\n")}"
94
+ if fingerprint && asset = find_asset(logical_path.to_s.sub(/^\//, ''))
95
+ url = asset.digest_path
96
+ else
97
+ url = logical_path
98
+ end
99
+
100
+ url = File.join(prefix, url) if prefix
101
+ url = "/#{url}" unless url =~ /^\//
102
+
103
+ url
104
+ end
105
+
106
+ # Deprecated: Similar to `path`, `url` returns a full url given a Rack `env`
107
+ # Hash and a `logical_path`.
108
+ def url(env, logical_path, fingerprint = true, prefix = nil)
109
+ logger.warn "Sprockets::Environment#url is deprecated\n#{caller[0..2].join("\n")}"
110
+ req = Rack::Request.new(env)
111
+
112
+ url = req.scheme + "://"
113
+ url << req.host
114
+
115
+ if req.scheme == "https" && req.port != 443 ||
116
+ req.scheme == "http" && req.port != 80
117
+ url << ":#{req.port}"
118
+ end
119
+
120
+ url << path(logical_path, fingerprint, prefix)
121
+
122
+ url
123
+ end
124
+
125
+ private
126
+ def forbidden_request?(env)
127
+ # Prevent access to files elsewhere on the file system
128
+ #
129
+ # http://example.org/assets/../../../etc/passwd
130
+ #
131
+ env["PATH_INFO"].include?("..")
132
+ end
133
+
134
+ # Returns a 403 Forbidden response tuple
135
+ def forbidden_response
136
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
137
+ end
138
+
139
+ # Returns a 404 Not Found response tuple
140
+ def not_found_response
141
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
142
+ end
143
+
144
+ # Returns a JavaScript response that re-throws a Ruby exception
145
+ # in the browser
146
+ def javascript_exception_response(exception)
147
+ err = "#{exception.class.name}: #{exception.message}"
148
+ body = "throw Error(#{err.inspect})"
149
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
150
+ end
151
+
152
+ # Returns a CSS response that hides all elements on the page and
153
+ # displays the exception
154
+ def css_exception_response(exception)
155
+ message = "\n#{exception.class.name}: #{exception.message}"
156
+ backtrace = "\n #{exception.backtrace.first}"
157
+
158
+ body = <<-CSS
159
+ html {
160
+ padding: 18px 36px;
161
+ }
162
+
163
+ head {
164
+ display: block;
165
+ }
166
+
167
+ body {
168
+ margin: 0;
169
+ padding: 0;
170
+ }
171
+
172
+ body > * {
173
+ display: none !important;
174
+ }
175
+
176
+ head:after, body:before, body:after {
177
+ display: block !important;
178
+ }
179
+
180
+ head:after {
181
+ font-family: sans-serif;
182
+ font-size: large;
183
+ font-weight: bold;
184
+ content: "Error compiling CSS asset";
185
+ }
186
+
187
+ body:before, body:after {
188
+ font-family: monospace;
189
+ white-space: pre-wrap;
190
+ }
191
+
192
+ body:before {
193
+ font-weight: bold;
194
+ content: "#{escape_css_content(message)}";
195
+ }
196
+
197
+ body:after {
198
+ content: "#{escape_css_content(backtrace)}";
199
+ }
200
+ CSS
201
+
202
+ [ 200, { "Content-Type" => "text/css;charset=utf-8", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
203
+ end
204
+
205
+ # Escape special characters for use inside a CSS content("...") string
206
+ def escape_css_content(content)
207
+ content.
208
+ gsub('\\', '\\\\005c ').
209
+ gsub("\n", '\\\\000a ').
210
+ gsub('"', '\\\\0022 ').
211
+ gsub('/', '\\\\002f ')
212
+ end
213
+
214
+ # Compare the requests `HTTP_IF_MODIFIED_SINCE` against the
215
+ # assets mtime
216
+ def not_modified?(asset, env)
217
+ env["HTTP_IF_MODIFIED_SINCE"] == asset.mtime.httpdate
218
+ end
219
+
220
+ # Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
221
+ def etag_match?(asset, env)
222
+ env["HTTP_IF_NONE_MATCH"] == etag(asset)
223
+ end
224
+
225
+ # Test if `?body=1` or `body=true` query param is set
226
+ def body_only?(env)
227
+ env["QUERY_STRING"].to_s =~ /body=(1|t)/
228
+ end
229
+
230
+ # Returns a 304 Not Modified response tuple
231
+ def not_modified_response(asset, env)
232
+ [ 304, {}, [] ]
233
+ end
234
+
235
+ # Returns a 200 OK response tuple
236
+ def ok_response(asset, env)
237
+ if body_only?(env)
238
+ [ 200, headers(env, asset, Rack::Utils.bytesize(asset.body)), [asset.body] ]
239
+ else
240
+ [ 200, headers(env, asset, asset.length), asset ]
241
+ end
242
+ end
243
+
244
+ def headers(env, asset, length)
245
+ Hash.new.tap do |headers|
246
+ # Set content type and length headers
247
+ headers["Content-Type"] = asset.content_type
248
+ headers["Content-Length"] = length.to_s
249
+
250
+ # Set caching headers
251
+ headers["Cache-Control"] = "public"
252
+ headers["Last-Modified"] = asset.mtime.httpdate
253
+ headers["ETag"] = etag(asset)
254
+
255
+ # If the request url contains a fingerprint, set a long
256
+ # expires on the response
257
+ if attributes_for(env["PATH_INFO"]).path_fingerprint
258
+ headers["Cache-Control"] << ", max-age=31536000"
259
+
260
+ # Otherwise set `must-revalidate` since the asset could be modified.
261
+ else
262
+ headers["Cache-Control"] << ", must-revalidate"
263
+ end
264
+ end
265
+ end
266
+
267
+ # Helper to quote the assets digest for use as an ETag.
268
+ def etag(asset)
269
+ %("#{asset.digest}")
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,86 @@
1
+ require 'sprockets/asset'
2
+ require 'fileutils'
3
+ require 'zlib'
4
+
5
+ module Sprockets
6
+ # `StaticAsset`s are used for files that are served verbatim without
7
+ # any processing or concatenation. These are typical images and
8
+ # other binary files.
9
+ class StaticAsset < Asset
10
+ # Define extra attributes to be serialized.
11
+ def self.serialized_attributes
12
+ super + %w( content_type mtime length digest )
13
+ end
14
+
15
+ def initialize(environment, logical_path, pathname, digest = nil)
16
+ super(environment, logical_path, pathname)
17
+ @digest = digest
18
+ load!
19
+ end
20
+
21
+ # Returns file contents as its `body`.
22
+ def body
23
+ # File is read everytime to avoid memory bloat of large binary files
24
+ pathname.open('rb') { |f| f.read }
25
+ end
26
+
27
+ # Checks if Asset is fresh by comparing the actual mtime and
28
+ # digest to the inmemory model.
29
+ def fresh?
30
+ # Check current mtime and digest
31
+ dependency_fresh?('path' => pathname, 'mtime' => mtime, 'hexdigest' => digest)
32
+ end
33
+
34
+ # Implemented for Rack SendFile support.
35
+ def to_path
36
+ pathname.to_s
37
+ end
38
+
39
+ # `to_s` is aliased to body since static assets can't have any dependencies.
40
+ def to_s
41
+ body
42
+ end
43
+
44
+ # Save asset to disk.
45
+ def write_to(filename, options = {})
46
+ # Gzip contents if filename has '.gz'
47
+ options[:compress] ||= File.extname(filename) == '.gz'
48
+
49
+ if options[:compress]
50
+ # Open file and run it through `Zlib`
51
+ pathname.open('rb') do |rd|
52
+ File.open("#{filename}+", 'wb') do |wr|
53
+ gz = Zlib::GzipWriter.new(wr, Zlib::BEST_COMPRESSION)
54
+ buf = ""
55
+ while rd.read(16384, buf)
56
+ gz.write(buf)
57
+ end
58
+ gz.close
59
+ end
60
+ end
61
+ else
62
+ # If no compression needs to be done, we can just copy it into place.
63
+ FileUtils.cp(pathname, "#{filename}+")
64
+ end
65
+
66
+ # Atomic write
67
+ FileUtils.mv("#{filename}+", filename)
68
+
69
+ # Set mtime correctly
70
+ File.utime(mtime, mtime, filename)
71
+
72
+ nil
73
+ ensure
74
+ # Ensure tmp file gets cleaned up
75
+ FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
76
+ end
77
+
78
+ private
79
+ def load!
80
+ content_type
81
+ mtime
82
+ length
83
+ digest
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ require 'sprockets/errors'
2
+ require 'pathname'
3
+
4
+ module Sprockets
5
+ # `Trail` is an internal mixin whose public methods are exposed on
6
+ # the `Environment` and `Index` classes.
7
+ module Trail
8
+ # Returns `Environment` root.
9
+ #
10
+ # All relative paths are expanded with root as its base. To be
11
+ # useful set this to your applications root directory. (`Rails.root`)
12
+ def root
13
+ trail.root.dup
14
+ end
15
+
16
+ # Returns an `Array` of path `String`s.
17
+ #
18
+ # These paths will be used for asset logical path lookups.
19
+ #
20
+ # Note that a copy of the `Array` is returned so mutating will
21
+ # have no affect on the environment. See `append_path`,
22
+ # `prepend_path`, and `clear_paths`.
23
+ def paths
24
+ trail.paths.dup
25
+ end
26
+
27
+ # Prepend a `path` to the `paths` list.
28
+ #
29
+ # Paths at the end of the `Array` have the least priority.
30
+ def prepend_path(path)
31
+ expire_index!
32
+ @trail.prepend_path(path)
33
+ end
34
+
35
+ # Append a `path` to the `paths` list.
36
+ #
37
+ # Paths at the beginning of the `Array` have a higher priority.
38
+ def append_path(path)
39
+ expire_index!
40
+ @trail.append_path(path)
41
+ end
42
+
43
+ # Clear all paths and start fresh.
44
+ #
45
+ # There is no mechanism for reordering paths, so its best to
46
+ # completely wipe the paths list and reappend them in the order
47
+ # you want.
48
+ def clear_paths
49
+ expire_index!
50
+ @trail.paths.dup.each { |path| @trail.remove_path(path) }
51
+ end
52
+
53
+ # Returns an `Array` of extensions.
54
+ #
55
+ # These extensions maybe omitted from logical path searches.
56
+ #
57
+ # # => [".js", ".css", ".coffee", ".sass", ...]
58
+ #
59
+ def extensions
60
+ trail.extensions.dup
61
+ end
62
+
63
+ # Finds the expanded real path for a given logical path by
64
+ # searching the environment's paths.
65
+ #
66
+ # resolve("application.js")
67
+ # # => "/path/to/app/javascripts/application.js.coffee"
68
+ #
69
+ # A `FileNotFound` exception is raised if the file does not exist.
70
+ def resolve(logical_path, options = {})
71
+ # If a block is given, preform an iterable search
72
+ if block_given?
73
+ args = attributes_for(logical_path).search_paths + [options]
74
+ trail.find(*args) do |path|
75
+ yield Pathname.new(path)
76
+ end
77
+ else
78
+ resolve(logical_path, options) do |pathname|
79
+ return pathname
80
+ end
81
+ raise FileNotFound, "couldn't find file '#{logical_path}'"
82
+ end
83
+ end
84
+
85
+ protected
86
+ def trail
87
+ @trail
88
+ end
89
+
90
+ def find_asset_in_path(logical_path, options = {})
91
+ # Strip fingerprint on logical path if there is one.
92
+ # Not sure how valuable this feature is...
93
+ if fingerprint = attributes_for(logical_path).path_fingerprint
94
+ pathname = resolve(logical_path.to_s.sub("-#{fingerprint}", ''))
95
+ else
96
+ pathname = resolve(logical_path)
97
+ end
98
+ rescue FileNotFound
99
+ nil
100
+ else
101
+ # Build the asset for the actual pathname
102
+ asset = build_asset(logical_path, pathname, options)
103
+
104
+ # Double check request fingerprint against actual digest
105
+ # Again, not sure if this code path is even reachable
106
+ if fingerprint && fingerprint != asset.digest
107
+ logger.error "Nonexistent asset #{logical_path} @ #{fingerprint}"
108
+ asset = nil
109
+ end
110
+
111
+ asset
112
+ end
113
+ end
114
+ end