sprockets 2.0.5 → 2.1.0.beta

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.

data/lib/sprockets.rb CHANGED
@@ -1,31 +1,63 @@
1
1
  require 'sprockets/version'
2
2
 
3
3
  module Sprockets
4
- autoload :ArgumentError, "sprockets/errors"
4
+ # Environment
5
+ autoload :Base, "sprockets/base"
6
+ autoload :Engines, "sprockets/engines"
7
+ autoload :Environment, "sprockets/environment"
8
+ autoload :Index, "sprockets/index"
9
+
10
+ # Assets
5
11
  autoload :Asset, "sprockets/asset"
6
- autoload :AssetAttributes, "sprockets/asset_attributes"
7
12
  autoload :BundledAsset, "sprockets/bundled_asset"
13
+ autoload :ProcessedAsset, "sprockets/processed_asset"
14
+ autoload :StaticAsset, "sprockets/static_asset"
15
+
16
+ # Processing
8
17
  autoload :CharsetNormalizer, "sprockets/charset_normalizer"
9
- autoload :CircularDependencyError, "sprockets/errors"
10
- autoload :ContentTypeMismatch, "sprockets/errors"
11
18
  autoload :Context, "sprockets/context"
12
19
  autoload :DirectiveProcessor, "sprockets/directive_processor"
13
20
  autoload :EcoTemplate, "sprockets/eco_template"
14
21
  autoload :EjsTemplate, "sprockets/ejs_template"
22
+ autoload :JstProcessor, "sprockets/jst_processor"
23
+ autoload :Processor, "sprockets/processor"
24
+ autoload :SafetyColons, "sprockets/safety_colons"
25
+
26
+ # Internal utilities
27
+ autoload :ArgumentError, "sprockets/errors"
28
+ autoload :AssetAttributes, "sprockets/asset_attributes"
29
+ autoload :CircularDependencyError, "sprockets/errors"
30
+ autoload :ContentTypeMismatch, "sprockets/errors"
15
31
  autoload :EngineError, "sprockets/errors"
16
- autoload :Engines, "sprockets/engines"
17
- autoload :Environment, "sprockets/environment"
18
32
  autoload :Error, "sprockets/errors"
19
33
  autoload :FileNotFound, "sprockets/errors"
20
- autoload :Index, "sprockets/index"
21
- autoload :JstProcessor, "sprockets/jst_processor"
22
- autoload :Processing, "sprockets/processing"
23
- autoload :Processor, "sprockets/processor"
24
- autoload :Server, "sprockets/server"
25
- autoload :StaticAsset, "sprockets/static_asset"
26
34
  autoload :Utils, "sprockets/utils"
27
35
 
28
36
  module Cache
29
37
  autoload :FileStore, "sprockets/cache/file_store"
30
38
  end
39
+
40
+ # Extend Sprockets module to provide global registry
41
+ extend Engines
42
+ @engines = {}
43
+
44
+ # Cherry pick the default Tilt engines that make sense for
45
+ # Sprockets. We don't need ones that only generate html like HAML.
46
+
47
+ # Mmm, CoffeeScript
48
+ register_engine '.coffee', Tilt::CoffeeScriptTemplate
49
+
50
+ # JST engines
51
+ register_engine '.jst', JstProcessor
52
+ register_engine '.eco', EcoTemplate
53
+ register_engine '.ejs', EjsTemplate
54
+
55
+ # CSS engines
56
+ register_engine '.less', Tilt::LessTemplate
57
+ register_engine '.sass', Tilt::SassTemplate
58
+ register_engine '.scss', Tilt::ScssTemplate
59
+
60
+ # Other
61
+ register_engine '.erb', Tilt::ERBTemplate
62
+ register_engine '.str', Tilt::StringTemplate
31
63
  end
@@ -1,90 +1,79 @@
1
1
  require 'time'
2
+ require 'set'
2
3
 
3
4
  module Sprockets
4
5
  # `Asset` is the base class for `BundledAsset` and `StaticAsset`.
5
6
  class Asset
6
7
  # Internal initializer to load `Asset` from serialized `Hash`.
7
8
  def self.from_hash(environment, hash)
8
- asset = allocate
9
- asset.init_with(environment, hash)
10
- asset
11
- end
9
+ return unless hash.is_a?(Hash)
10
+
11
+ klass = case hash['class']
12
+ when 'BundledAsset'
13
+ BundledAsset
14
+ when 'ProcessedAsset'
15
+ ProcessedAsset
16
+ when 'StaticAsset'
17
+ StaticAsset
18
+ else
19
+ nil
20
+ end
12
21
 
13
- # Define base set of attributes to be serialized.
14
- def self.serialized_attributes
15
- %w( id logical_path pathname )
22
+ if klass
23
+ asset = klass.allocate
24
+ asset.init_with(environment, hash)
25
+ asset
26
+ end
27
+ rescue UnserializeError
28
+ nil
16
29
  end
17
30
 
18
- attr_reader :environment
19
- attr_reader :id, :logical_path, :pathname
31
+ attr_reader :logical_path, :pathname
32
+ attr_reader :content_type, :mtime, :length, :digest
20
33
 
21
34
  def initialize(environment, logical_path, pathname)
22
- @environment = environment
35
+ @root = environment.root
23
36
  @logical_path = logical_path.to_s
24
37
  @pathname = Pathname.new(pathname)
25
- @id = environment.digest.update(object_id.to_s).to_s
38
+ @content_type = environment.content_type_of(pathname)
39
+ @mtime = environment.stat(pathname).mtime
40
+ @length = environment.stat(pathname).size
41
+ @digest = environment.file_digest(pathname).hexdigest
26
42
  end
27
43
 
28
44
  # Initialize `Asset` from serialized `Hash`.
29
45
  def init_with(environment, coder)
30
- @environment = environment
31
- @pathname = @mtime = @length = nil
46
+ @root = environment.root
32
47
 
33
- self.class.serialized_attributes.each do |attr|
34
- instance_variable_set("@#{attr}", coder[attr].to_s) if coder[attr]
35
- end
48
+ @logical_path = coder['logical_path']
49
+ @content_type = coder['content_type']
50
+ @digest = coder['digest']
36
51
 
37
- if @pathname && @pathname.is_a?(String)
52
+ if pathname = coder['pathname']
38
53
  # Expand `$root` placeholder and wrapper string in a `Pathname`
39
- @pathname = Pathname.new(expand_root_path(@pathname))
54
+ @pathname = Pathname.new(expand_root_path(pathname))
40
55
  end
41
56
 
42
- if @mtime && @mtime.is_a?(String)
57
+ if mtime = coder['mtime']
43
58
  # Parse time string
44
- @mtime = Time.parse(@mtime)
59
+ @mtime = Time.parse(mtime)
45
60
  end
46
61
 
47
- if @length && @length.is_a?(String)
62
+ if length = coder['length']
48
63
  # Convert length to an `Integer`
49
- @length = Integer(@length)
64
+ @length = Integer(length)
50
65
  end
51
66
  end
52
67
 
53
68
  # Copy serialized attributes to the coder object
54
69
  def encode_with(coder)
55
- coder['class'] = self.class.name.sub(/Sprockets::/, '')
56
-
57
- self.class.serialized_attributes.each do |attr|
58
- value = send(attr)
59
- coder[attr] = case value
60
- when Time
61
- value.iso8601
62
- else
63
- value.to_s
64
- end
65
- end
66
-
67
- coder['pathname'] = relativize_root_path(coder['pathname'])
68
- end
69
-
70
- # Returns `Content-Type` from pathname.
71
- def content_type
72
- @content_type ||= environment.content_type_of(pathname)
73
- end
74
-
75
- # Get mtime at the time the `Asset` is built.
76
- def mtime
77
- @mtime ||= environment.stat(pathname).mtime
78
- end
79
-
80
- # Get length at the time the `Asset` is built.
81
- def length
82
- @length ||= environment.stat(pathname).size
83
- end
84
-
85
- # Get content digest at the time the `Asset` is built.
86
- def digest
87
- @digest ||= environment.file_digest(pathname).hexdigest
70
+ coder['class'] = self.class.name.sub(/Sprockets::/, '')
71
+ coder['logical_path'] = logical_path
72
+ coder['pathname'] = relativize_root_path(pathname).to_s
73
+ coder['content_type'] = content_type
74
+ coder['mtime'] = mtime.iso8601
75
+ coder['length'] = length
76
+ coder['digest'] = digest
88
77
  end
89
78
 
90
79
  # Return logical path with digest spliced in.
@@ -92,7 +81,7 @@ module Sprockets
92
81
  # "foo/bar-37b51d194a7513e45b56f6524f2d51f2.js"
93
82
  #
94
83
  def digest_path
95
- environment.attributes_for(logical_path).path_with_fingerprint(digest)
84
+ logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
96
85
  end
97
86
 
98
87
  # Return an `Array` of `Asset` files that are declared dependencies.
@@ -111,6 +100,16 @@ module Sprockets
111
100
  [self]
112
101
  end
113
102
 
103
+ # `body` is aliased to source by default if it can't have any dependencies.
104
+ def body
105
+ source
106
+ end
107
+
108
+ # Return `String` of concatenated source.
109
+ def to_s
110
+ source
111
+ end
112
+
114
113
  # Add enumerator to allow `Asset` instances to be used as Rack
115
114
  # compatible body objects.
116
115
  def each
@@ -121,18 +120,47 @@ module Sprockets
121
120
  # digest to the inmemory model.
122
121
  #
123
122
  # Used to test if cached models need to be rebuilt.
124
- #
125
- # Subclass must override `fresh?` or `stale?`.
126
- def fresh?
127
- !stale?
123
+ def fresh?(environment)
124
+ # Check current mtime and digest
125
+ dependency_fresh?(environment, self)
128
126
  end
129
127
 
130
128
  # Checks if Asset is stale by comparing the actual mtime and
131
129
  # digest to the inmemory model.
132
130
  #
133
131
  # Subclass must override `fresh?` or `stale?`.
134
- def stale?
135
- !fresh?
132
+ def stale?(environment)
133
+ !fresh?(environment)
134
+ end
135
+
136
+ # Save asset to disk.
137
+ def write_to(filename, options = {})
138
+ # Gzip contents if filename has '.gz'
139
+ options[:compress] ||= File.extname(filename) == '.gz'
140
+
141
+ File.open("#{filename}+", 'wb') do |f|
142
+ if options[:compress]
143
+ # Run contents through `Zlib`
144
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
145
+ gz.write to_s
146
+ gz.close
147
+ else
148
+ # Write out as is
149
+ f.write to_s
150
+ f.close
151
+ end
152
+ end
153
+
154
+ # Atomic write
155
+ FileUtils.mv("#{filename}+", filename)
156
+
157
+ # Set mtime correctly
158
+ File.utime(mtime, mtime, filename)
159
+
160
+ nil
161
+ ensure
162
+ # Ensure tmp file gets cleaned up
163
+ FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
136
164
  end
137
165
 
138
166
  # Pretty inspect
@@ -144,29 +172,47 @@ module Sprockets
144
172
  ">"
145
173
  end
146
174
 
175
+ def hash
176
+ digest.hash
177
+ end
178
+
147
179
  # Assets are equal if they share the same path, mtime and digest.
148
180
  def eql?(other)
149
181
  other.class == self.class &&
150
- other.relative_pathname == self.relative_pathname &&
182
+ other.logical_path == self.logical_path &&
151
183
  other.mtime.to_i == self.mtime.to_i &&
152
184
  other.digest == self.digest
153
185
  end
154
186
  alias_method :==, :eql?
155
187
 
156
188
  protected
189
+ # Internal: String paths that are marked as dependencies after processing.
190
+ #
191
+ # Default to an empty `Array`.
192
+ def dependency_paths
193
+ @dependency_paths ||= []
194
+ end
195
+
196
+ # Internal: `ProccessedAsset`s that are required after processing.
197
+ #
198
+ # Default to an empty `Array`.
199
+ def required_assets
200
+ @required_assets ||= []
201
+ end
202
+
157
203
  # Get pathname with its root stripped.
158
204
  def relative_pathname
159
- Pathname.new(relativize_root_path(pathname))
205
+ @relative_pathname ||= Pathname.new(relativize_root_path(pathname))
160
206
  end
161
207
 
162
208
  # Replace `$root` placeholder with actual environment root.
163
209
  def expand_root_path(path)
164
- environment.attributes_for(path).expand_root
210
+ path.to_s.sub(/^\$root/, @root)
165
211
  end
166
212
 
167
213
  # Replace actual environment root with `$root` placeholder.
168
214
  def relativize_root_path(path)
169
- environment.attributes_for(path).relativize_root
215
+ path.to_s.sub(/^#{Regexp.escape(@root)}/, '$root')
170
216
  end
171
217
 
172
218
  # Check if dependency is fresh.
@@ -175,8 +221,8 @@ module Sprockets
175
221
  #
176
222
  # A `Hash` is used rather than other `Asset` object because we
177
223
  # want to test non-asset files and directories.
178
- def dependency_fresh?(dep = {})
179
- path, mtime, hexdigest = dep.values_at('path', 'mtime', 'hexdigest')
224
+ def dependency_fresh?(environment, dep)
225
+ path, mtime, hexdigest = dep.pathname.to_s, dep.mtime, dep.digest
180
226
 
181
227
  stat = environment.stat(path)
182
228
 
@@ -13,16 +13,6 @@ module Sprockets
13
13
  @pathname = path.is_a?(Pathname) ? path : Pathname.new(path.to_s)
14
14
  end
15
15
 
16
- # Replaces `$root` placeholder with actual environment root.
17
- def expand_root
18
- pathname.to_s.sub(/^\$root/, environment.root)
19
- end
20
-
21
- # Replaces environment root with `$root` placeholder.
22
- def relativize_root
23
- pathname.to_s.sub(/^#{Regexp.escape(environment.root)}/, '$root')
24
- end
25
-
26
16
  # Returns paths search the load path for.
27
17
  def search_paths
28
18
  paths = [pathname.to_s]
@@ -42,9 +32,8 @@ module Sprockets
42
32
  # shaddowed in the path, but is required relatively, its logical
43
33
  # path will be incorrect.
44
34
  def logical_path
45
- raise ArgumentError unless pathname.absolute?
46
-
47
35
  if root_path = environment.paths.detect { |path| pathname.to_s[path] }
36
+ path = pathname.to_s.sub("#{root_path}/", '')
48
37
  path = pathname.relative_path_from(Pathname.new(root_path)).to_s
49
38
  path = engine_extensions.inject(path) { |p, ext| p.sub(ext, '') }
50
39
  path = "#{path}#{engine_format_extension}" unless format_extension
@@ -114,28 +103,6 @@ module Sprockets
114
103
  end
115
104
  end
116
105
 
117
- # Gets digest fingerprint.
118
- #
119
- # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
120
- # # => "0aa2105d29558f3eb790d411d7d8fb66"
121
- #
122
- def path_fingerprint
123
- pathname.basename(extensions.join).to_s =~ /-([0-9a-f]{7,40})$/ ? $1 : nil
124
- end
125
-
126
- # Injects digest fingerprint into path.
127
- #
128
- # "foo.js"
129
- # # => "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
130
- #
131
- def path_with_fingerprint(digest)
132
- if old_digest = path_fingerprint
133
- pathname.sub(old_digest, digest).to_s
134
- else
135
- pathname.to_s.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
136
- end
137
- end
138
-
139
106
  private
140
107
  # Returns implicit engine content type.
141
108
  #
@@ -1,7 +1,7 @@
1
1
  require 'sprockets/asset_attributes'
2
2
  require 'sprockets/bundled_asset'
3
3
  require 'sprockets/caching'
4
- require 'sprockets/digest'
4
+ require 'sprockets/processed_asset'
5
5
  require 'sprockets/processing'
6
6
  require 'sprockets/server'
7
7
  require 'sprockets/static_asset'
@@ -11,9 +11,66 @@ require 'pathname'
11
11
  module Sprockets
12
12
  # `Base` class for `Environment` and `Index`.
13
13
  class Base
14
- include Digest
15
14
  include Caching, Processing, Server, Trail
16
15
 
16
+ # Returns a `Digest` implementation class.
17
+ #
18
+ # Defaults to `Digest::MD5`.
19
+ attr_reader :digest_class
20
+
21
+ # Assign a `Digest` implementation class. This maybe any Ruby
22
+ # `Digest::` implementation such as `Digest::MD5` or
23
+ # `Digest::SHA1`.
24
+ #
25
+ # environment.digest_class = Digest::SHA1
26
+ #
27
+ def digest_class=(klass)
28
+ expire_index!
29
+ @digest_class = klass
30
+ end
31
+
32
+ # The `Environment#version` is a custom value used for manually
33
+ # expiring all asset caches.
34
+ #
35
+ # Sprockets is able to track most file and directory changes and
36
+ # will take care of expiring the cache for you. However, its
37
+ # impossible to know when any custom helpers change that you mix
38
+ # into the `Context`.
39
+ #
40
+ # It would be wise to increment this value anytime you make a
41
+ # configuration change to the `Environment` object.
42
+ attr_reader :version
43
+
44
+ # Assign an environment version.
45
+ #
46
+ # environment.version = '2.0'
47
+ #
48
+ def version=(version)
49
+ expire_index!
50
+ @version = version
51
+ end
52
+
53
+ # Returns a `Digest` instance for the `Environment`.
54
+ #
55
+ # This value serves two purposes. If two `Environment`s have the
56
+ # same digest value they can be treated as equal. This is more
57
+ # useful for comparing environment states between processes rather
58
+ # than in the same. Two equal `Environment`s can share the same
59
+ # cached assets.
60
+ #
61
+ # The value also provides a seed digest for all `Asset`
62
+ # digests. Any change in the environment digest will affect all of
63
+ # its assets.
64
+ def digest
65
+ # Compute the initial digest using the implementation class. The
66
+ # Sprockets release version and custom environment version are
67
+ # mixed in. So any new releases will affect all your assets.
68
+ @digest ||= digest_class.new.update(VERSION).update(version.to_s)
69
+
70
+ # Returned a dupped copy so the caller can safely mutate it with `.update`
71
+ @digest.dup
72
+ end
73
+
17
74
  # Get and set `Logger` instance.
18
75
  attr_accessor :logger
19
76
 
@@ -63,14 +120,10 @@ module Sprockets
63
120
  # Read and compute digest of filename.
64
121
  #
65
122
  # Subclasses may cache this method.
66
- def file_digest(path, data = nil)
123
+ def file_digest(path)
67
124
  if stat = self.stat(path)
68
- # `data` maybe provided
69
- if data
70
- digest.update(data)
71
-
72
125
  # If its a file, digest the contents
73
- elsif stat.file?
126
+ if stat.file?
74
127
  digest.file(path.to_s)
75
128
 
76
129
  # If its a directive, digest the list of filenames
@@ -93,13 +146,21 @@ module Sprockets
93
146
 
94
147
  # Find asset by logical path or expanded path.
95
148
  def find_asset(path, options = {})
96
- pathname = Pathname.new(path)
149
+ logical_path = path
150
+ pathname = Pathname.new(path)
97
151
 
98
- if pathname.absolute?
99
- build_asset(attributes_for(pathname).logical_path, pathname, options)
152
+ if pathname.to_s =~ /^\//
153
+ return unless stat(pathname)
154
+ logical_path = attributes_for(pathname).logical_path
100
155
  else
101
- find_asset_in_path(pathname, options)
156
+ begin
157
+ pathname = resolve(logical_path)
158
+ rescue FileNotFound
159
+ return nil
160
+ end
102
161
  end
162
+
163
+ build_asset(logical_path, pathname, options)
103
164
  end
104
165
 
105
166
  # Preferred `find_asset` shorthand.
@@ -172,15 +233,35 @@ module Sprockets
172
233
  def build_asset(logical_path, pathname, options)
173
234
  pathname = Pathname.new(pathname)
174
235
 
175
- return unless stat(pathname)
176
-
177
236
  # If there are any processors to run on the pathname, use
178
237
  # `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
179
238
  if attributes_for(pathname).processors.any?
180
- BundledAsset.new(self, logical_path, pathname, options)
239
+ if options[:bundle] == false
240
+ circular_call_protection(pathname.to_s) do
241
+ ProcessedAsset.new(index, logical_path, pathname)
242
+ end
243
+ else
244
+ BundledAsset.new(index, logical_path, pathname)
245
+ end
181
246
  else
182
- StaticAsset.new(self, logical_path, pathname)
247
+ StaticAsset.new(index, logical_path, pathname)
248
+ end
249
+ end
250
+
251
+ def cache_key_for(path, options)
252
+ "#{path}:#{options[:bundle] ? '1' : '0'}"
253
+ end
254
+
255
+ def circular_call_protection(path)
256
+ reset = Thread.current[:sprockets_circular_calls].nil?
257
+ calls = Thread.current[:sprockets_circular_calls] ||= Set.new
258
+ if calls.include?(path)
259
+ raise CircularDependencyError, "#{path} has already been required"
183
260
  end
261
+ calls << path
262
+ yield
263
+ ensure
264
+ Thread.current[:sprockets_circular_calls] = nil if reset
184
265
  end
185
266
  end
186
267
  end