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,41 @@
1
+ require 'digest/md5'
2
+ require 'fileutils'
3
+ require 'pathname'
4
+
5
+ module Sprockets
6
+ module Cache
7
+ # A simple file system cache store.
8
+ #
9
+ # environment.cache = Sprockets::Cache::FileStore.new("tmp/sprockets")
10
+ #
11
+ class FileStore
12
+ def initialize(root)
13
+ @root = Pathname.new(root)
14
+
15
+ # Ensure directory exists
16
+ FileUtils.mkdir_p @root
17
+ end
18
+
19
+ # Lookup value in cache
20
+ def [](key)
21
+ pathname = path_for(key)
22
+ pathname.exist? ? pathname.open('rb') { |f| Marshal.load(f) } : nil
23
+ end
24
+
25
+ # Save value to cache
26
+ def []=(key, value)
27
+ path_for(key).open('w') { |f| Marshal.dump(value, f)}
28
+ value
29
+ end
30
+
31
+ private
32
+ # Returns path for cache key.
33
+ #
34
+ # The key may include some funky characters so hash it into
35
+ # safe hex.
36
+ def path_for(key)
37
+ @root.join(::Digest::MD5.hexdigest(key))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,123 @@
1
+ require 'sprockets/bundled_asset'
2
+ require 'sprockets/static_asset'
3
+
4
+ module Sprockets
5
+ # `Caching` is an internal mixin whose public methods are exposed on
6
+ # the `Environment` and `Index` classes.
7
+ module Caching
8
+ # Return `Asset` instance for serialized `Hash`.
9
+ def asset_from_hash(hash)
10
+ return unless hash.is_a?(Hash)
11
+ case hash['class']
12
+ when 'BundledAsset'
13
+ BundledAsset.from_hash(self, hash)
14
+ when 'StaticAsset'
15
+ StaticAsset.from_hash(self, hash)
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def cache_hash(key, version)
22
+ if cache.nil?
23
+ yield
24
+ elsif hash = cache_get_hash(key, version)
25
+ hash
26
+ elsif hash = yield
27
+ cache_set_hash(key, version, hash)
28
+ hash
29
+ end
30
+ end
31
+
32
+ protected
33
+ # Cache helper method. Takes a `path` argument which maybe a
34
+ # logical path or fully expanded path. The `&block` is passed
35
+ # for finding and building the asset if its not in cache.
36
+ def cache_asset(path)
37
+ # If `cache` is not set, return fast
38
+ if cache.nil?
39
+ yield
40
+
41
+ # Check cache for `path`
42
+ elsif (asset = asset_from_hash(cache_get_hash(path.to_s, digest.hexdigest))) && asset.fresh?
43
+ asset
44
+
45
+ # Otherwise yield block that slowly finds and builds the asset
46
+ elsif asset = yield
47
+ hash = {}
48
+ asset.encode_with(hash)
49
+
50
+ # Save the asset to at its path
51
+ cache_set_hash(path.to_s, digest.hexdigest, hash)
52
+
53
+ # Since path maybe a logical or full pathname, save the
54
+ # asset its its full path too
55
+ if path.to_s != asset.pathname.to_s
56
+ cache_set_hash(asset.pathname.to_s, digest.hexdigest, hash)
57
+ end
58
+
59
+ asset
60
+ end
61
+ end
62
+
63
+ private
64
+ # Strips `Environment#root` from key to make the key work
65
+ # consisently across different servers. The key is also hashed
66
+ # so it does not exceed 250 characters.
67
+ def cache_key_for(key)
68
+ File.join('sprockets', digest.hexdigest(key.sub(root, '')))
69
+ end
70
+
71
+ def cache_get_hash(key, version)
72
+ hash = cache_get(cache_key_for(key))
73
+ if hash.is_a?(Hash) && version == hash['_version']
74
+ hash
75
+ end
76
+ end
77
+
78
+ def cache_set_hash(key, version, hash)
79
+ hash['_version'] = version
80
+ cache_set(cache_key_for(key), hash)
81
+ hash
82
+ end
83
+
84
+ # Low level cache getter for `key`. Checks a number of supported
85
+ # cache interfaces.
86
+ def cache_get(key)
87
+ # `Cache#get(key)` for Memcache
88
+ if cache.respond_to?(:get)
89
+ cache.get(key)
90
+
91
+ # `Cache#[key]` so `Hash` can be used
92
+ elsif cache.respond_to?(:[])
93
+ cache[key]
94
+
95
+ # `Cache#read(key)` for `ActiveSupport::Cache` support
96
+ elsif cache.respond_to?(:read)
97
+ cache.read(key)
98
+
99
+ else
100
+ nil
101
+ end
102
+ end
103
+
104
+ # Low level cache setter for `key`. Checks a number of supported
105
+ # cache interfaces.
106
+ def cache_set(key, value)
107
+ # `Cache#set(key, value)` for Memcache
108
+ if cache.respond_to?(:set)
109
+ cache.set(key, value)
110
+
111
+ # `Cache#[key]=value` so `Hash` can be used
112
+ elsif cache.respond_to?(:[]=)
113
+ cache[key] = value
114
+
115
+ # `Cache#write(key, value)` for `ActiveSupport::Cache` support
116
+ elsif cache.respond_to?(:write)
117
+ cache.write(key, value)
118
+ end
119
+
120
+ value
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,41 @@
1
+ require 'tilt'
2
+
3
+ module Sprockets
4
+ # Some browsers have issues with stylesheets that contain multiple
5
+ # `@charset` definitions. The issue surfaces while using Sass since
6
+ # it inserts a `@charset` at the top of each file. Then Sprockets
7
+ # concatenates them together.
8
+ #
9
+ # The `CharsetNormalizer` processor strips out multiple `@charset`
10
+ # definitions.
11
+ #
12
+ # The current implementation is naive. It picks the first `@charset`
13
+ # it sees and strips the others. This works for most people because
14
+ # the other definitions are usually `UTF-8`. A more sophisticated
15
+ # approach would be to re-encode stylesheets with mixed encodings.
16
+ #
17
+ # This behavior can be disabled with:
18
+ #
19
+ # environment.unregister_bundle_processor 'text/css', Sprockets::CharsetNormalizer
20
+ #
21
+ class CharsetNormalizer < Tilt::Template
22
+ def prepare
23
+ end
24
+
25
+ def evaluate(context, locals, &block)
26
+ charset = nil
27
+
28
+ # Find and strip out any `@charset` definitions
29
+ filtered_data = data.gsub(/^@charset "([^"]+)";$/) {
30
+ charset ||= $1; ""
31
+ }
32
+
33
+ if charset
34
+ # If there was a charset, move it to the top
35
+ "@charset \"#{charset}\";#{filtered_data}"
36
+ else
37
+ data
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,217 @@
1
+ require 'base64'
2
+ require 'rack/utils'
3
+ require 'sprockets/errors'
4
+ require 'sprockets/utils'
5
+ require 'pathname'
6
+ require 'set'
7
+
8
+ module Sprockets
9
+ # `Context` provides helper methods to all `Tilt` processors. They
10
+ # are typically accessed by ERB templates. You can mix in custom
11
+ # helpers by injecting them into `Environment#context_class`. Do not
12
+ # mix them into `Context` directly.
13
+ #
14
+ # environment.instance_eval do
15
+ # include MyHelper
16
+ # def asset_url; end
17
+ # end
18
+ #
19
+ # <%= asset_url "foo.png" %>
20
+ #
21
+ # The `Context` also collects dependencies declared by
22
+ # assets. See `DirectiveProcessor` for an example of this.
23
+ class Context
24
+ attr_reader :environment, :pathname
25
+ attr_reader :_required_paths, :_dependency_paths, :_dependency_assets
26
+ attr_writer :__LINE__
27
+
28
+ def initialize(environment, logical_path, pathname)
29
+ @environment = environment
30
+ @logical_path = logical_path
31
+ @pathname = pathname
32
+ @__LINE__ = nil
33
+
34
+ @_required_paths = []
35
+ @_dependency_paths = Set.new([pathname.to_s])
36
+ @_dependency_assets = Set.new
37
+ end
38
+
39
+ # Returns the environment path that contains the file.
40
+ #
41
+ # If `app/javascripts` and `app/stylesheets` are in your path, and
42
+ # current file is `app/javascripts/foo/bar.js`, `root_path` would
43
+ # return `app/javascripts`.
44
+ def root_path
45
+ environment.paths.detect { |path| pathname.to_s[path] }
46
+ end
47
+
48
+ # Returns logical path without any file extensions.
49
+ #
50
+ # 'app/javascripts/application.js'
51
+ # # => 'application'
52
+ #
53
+ def logical_path
54
+ @logical_path[/^([^.]+)/, 0]
55
+ end
56
+
57
+ # Returns content type of file
58
+ #
59
+ # 'application/javascript'
60
+ # 'text/css'
61
+ #
62
+ def content_type
63
+ environment.content_type_of(pathname)
64
+ end
65
+
66
+ # Given a logical path, `resolve` will find and return the fully
67
+ # expanded path. Relative paths will also be resolved. An optional
68
+ # `:content_type` restriction can be supplied to restrict the
69
+ # search.
70
+ #
71
+ # resolve("foo.js")
72
+ # # => "/path/to/app/javascripts/foo.js"
73
+ #
74
+ # resolve("./bar.js")
75
+ # # => "/path/to/app/javascripts/bar.js"
76
+ #
77
+ def resolve(path, options = {}, &block)
78
+ pathname = Pathname.new(path)
79
+ attributes = environment.attributes_for(pathname)
80
+
81
+ if pathname.absolute?
82
+ pathname
83
+
84
+ elsif content_type = options[:content_type]
85
+ content_type = self.content_type if content_type == :self
86
+
87
+ if attributes.format_extension
88
+ if content_type != attributes.content_type
89
+ raise ContentTypeMismatch, "#{path} is " +
90
+ "'#{attributes.content_type}', not '#{content_type}'"
91
+ end
92
+ end
93
+
94
+ resolve(path) do |candidate|
95
+ if self.content_type == environment.content_type_of(candidate)
96
+ return candidate
97
+ end
98
+ end
99
+
100
+ raise FileNotFound, "couldn't find file '#{path}'"
101
+ else
102
+ environment.resolve(path, :base_path => self.pathname.dirname, &block)
103
+ end
104
+ end
105
+
106
+ # `depend_on` allows you to state a dependency on a file without
107
+ # including it.
108
+ #
109
+ # This is used for caching purposes. Any changes made to
110
+ # the dependency file with invalidate the cache of the
111
+ # source file.
112
+ def depend_on(path)
113
+ @_dependency_paths << resolve(path).to_s
114
+ nil
115
+ end
116
+
117
+ # `depend_on_asset` allows you to state an asset dependency
118
+ # without including it.
119
+ #
120
+ # This is used for caching purposes. Any changes that would
121
+ # invalidate the dependency asset will invalidate the source
122
+ # file. Unlike `depend_on`, this will include recursively include
123
+ # the target asset's dependencies.
124
+ def depend_on_asset(path)
125
+ filename = resolve(path).to_s
126
+ @_dependency_assets << filename
127
+ nil
128
+ end
129
+
130
+ # `require_asset` declares `path` as a dependency of the file. The
131
+ # dependency will be inserted before the file and will only be
132
+ # included once.
133
+ #
134
+ # If ERB processing is enabled, you can use it to dynamically
135
+ # require assets.
136
+ #
137
+ # <%= require_asset "#{framework}.js" %>
138
+ #
139
+ def require_asset(path)
140
+ pathname = resolve(path, :content_type => :self)
141
+ depend_on_asset(pathname)
142
+ @_required_paths << pathname.to_s
143
+ nil
144
+ end
145
+
146
+ # Tests if target path is able to be safely required into the
147
+ # current concatenation.
148
+ def asset_requirable?(path)
149
+ pathname = resolve(path)
150
+ content_type = environment.content_type_of(pathname)
151
+ pathname.file? && (self.content_type.nil? || self.content_type == content_type)
152
+ end
153
+
154
+ # Reads `path` and runs processors on the file.
155
+ #
156
+ # This allows you to capture the result of an asset and include it
157
+ # directly in another.
158
+ #
159
+ # <%= evaluate "bar.js" %>
160
+ #
161
+ def evaluate(path, options = {})
162
+ pathname = resolve(path)
163
+ attributes = environment.attributes_for(pathname)
164
+ processors = options[:processors] || attributes.processors
165
+
166
+ if options[:data]
167
+ result = options[:data]
168
+ else
169
+ result = Sprockets::Utils.read_unicode(pathname)
170
+ end
171
+
172
+ processors.each do |processor|
173
+ begin
174
+ template = processor.new(pathname.to_s) { result }
175
+ result = template.render(self, {})
176
+ rescue Exception => e
177
+ annotate_exception! e
178
+ raise
179
+ end
180
+ end
181
+
182
+ result
183
+ end
184
+
185
+ # Returns a Base64-encoded `data:` URI with the contents of the
186
+ # asset at the specified path, and marks that path as a dependency
187
+ # of the current file.
188
+ #
189
+ # Use `asset_data_uri` from ERB with CSS or JavaScript assets:
190
+ #
191
+ # #logo { background: url(<%= asset_data_uri 'logo.png' %>) }
192
+ #
193
+ # $('<img>').attr('src', '<%= asset_data_uri 'avatar.jpg' %>')
194
+ #
195
+ def asset_data_uri(path)
196
+ depend_on(path)
197
+ asset = environment.find_asset(path)
198
+ base64 = Base64.encode64(asset.to_s).gsub(/\s+/, "")
199
+ "data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}"
200
+ end
201
+
202
+ private
203
+ # Annotates exception backtrace with the original template that
204
+ # the exception was raised in.
205
+ def annotate_exception!(exception)
206
+ location = pathname.to_s
207
+ location << ":#{@__LINE__}" if @__LINE__
208
+
209
+ exception.extend(Sprockets::EngineError)
210
+ exception.sprockets_annotation = " (in #{location})"
211
+ end
212
+
213
+ def logger
214
+ environment.logger
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,67 @@
1
+ module Sprockets
2
+ # `Digest` is an internal mixin whose public methods are exposed on
3
+ # the `Environment` and `Index` classes.
4
+ module Digest
5
+ # Returns a `Digest` implementation class.
6
+ #
7
+ # Defaults to `Digest::MD5`.
8
+ def digest_class
9
+ @digest_class
10
+ end
11
+
12
+ # Assign a `Digest` implementation class. This maybe any Ruby
13
+ # `Digest::` implementation such as `Digest::MD5` or
14
+ # `Digest::SHA1`.
15
+ #
16
+ # environment.digest_class = Digest::SHA1
17
+ #
18
+ def digest_class=(klass)
19
+ expire_index!
20
+ @digest_class = klass
21
+ end
22
+
23
+ # The `Environment#version` is a custom value used for manually
24
+ # expiring all asset caches.
25
+ #
26
+ # Sprockets is able to track most file and directory changes and
27
+ # will take care of expiring the cache for you. However, its
28
+ # impossible to know when any custom helpers change that you mix
29
+ # into the `Context`.
30
+ #
31
+ # It would be wise to increment this value anytime you make a
32
+ # configuration change to the `Environment` object.
33
+ def version
34
+ @version
35
+ end
36
+
37
+ # Assign an environment version.
38
+ #
39
+ # environment.version = '2.0'
40
+ #
41
+ def version=(version)
42
+ expire_index!
43
+ @version = version
44
+ end
45
+
46
+ # Returns a `Digest` instance for the `Environment`.
47
+ #
48
+ # This value serves two purposes. If two `Environment`s have the
49
+ # same digest value they can be treated as equal. This is more
50
+ # useful for comparing environment states between processes rather
51
+ # than in the same. Two equal `Environment`s can share the same
52
+ # cached assets.
53
+ #
54
+ # The value also provides a seed digest for all `Asset`
55
+ # digests. Any change in the environment digest will affect all of
56
+ # its assets.
57
+ def digest
58
+ # Compute the initial digest using the implementation class. The
59
+ # Sprockets release version and custom environment version are
60
+ # mixed in. So any new releases will affect all your assets.
61
+ @digest ||= digest_class.new.update(VERSION).update(version.to_s)
62
+
63
+ # Returned a dupped copy so the caller can safely mutate it with `.update`
64
+ @digest.dup
65
+ end
66
+ end
67
+ end