sprockets 2.12.5 → 3.0.0.beta.1

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 (62) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +2 -2
  3. data/README.md +61 -34
  4. data/lib/rake/sprocketstask.rb +5 -4
  5. data/lib/sprockets.rb +123 -85
  6. data/lib/sprockets/asset.rb +161 -200
  7. data/lib/sprockets/asset_uri.rb +64 -0
  8. data/lib/sprockets/base.rb +138 -373
  9. data/lib/sprockets/bower.rb +56 -0
  10. data/lib/sprockets/bundle.rb +32 -0
  11. data/lib/sprockets/cache.rb +220 -0
  12. data/lib/sprockets/cache/file_store.rb +145 -13
  13. data/lib/sprockets/cache/memory_store.rb +66 -0
  14. data/lib/sprockets/cache/null_store.rb +46 -0
  15. data/lib/sprockets/cached_environment.rb +103 -0
  16. data/lib/sprockets/closure_compressor.rb +30 -12
  17. data/lib/sprockets/coffee_script_template.rb +23 -0
  18. data/lib/sprockets/compressing.rb +20 -25
  19. data/lib/sprockets/configuration.rb +95 -0
  20. data/lib/sprockets/context.rb +68 -131
  21. data/lib/sprockets/directive_processor.rb +138 -179
  22. data/lib/sprockets/eco_template.rb +10 -19
  23. data/lib/sprockets/ejs_template.rb +10 -19
  24. data/lib/sprockets/encoding_utils.rb +246 -0
  25. data/lib/sprockets/engines.rb +40 -29
  26. data/lib/sprockets/environment.rb +10 -66
  27. data/lib/sprockets/erb_template.rb +23 -0
  28. data/lib/sprockets/errors.rb +5 -13
  29. data/lib/sprockets/http_utils.rb +97 -0
  30. data/lib/sprockets/jst_processor.rb +28 -15
  31. data/lib/sprockets/lazy_processor.rb +15 -0
  32. data/lib/sprockets/legacy.rb +23 -0
  33. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  34. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  35. data/lib/sprockets/manifest.rb +128 -99
  36. data/lib/sprockets/mime.rb +114 -33
  37. data/lib/sprockets/path_utils.rb +179 -0
  38. data/lib/sprockets/paths.rb +13 -26
  39. data/lib/sprockets/processing.rb +198 -107
  40. data/lib/sprockets/resolve.rb +289 -0
  41. data/lib/sprockets/sass_compressor.rb +36 -17
  42. data/lib/sprockets/sass_template.rb +269 -46
  43. data/lib/sprockets/server.rb +113 -83
  44. data/lib/sprockets/transformers.rb +69 -0
  45. data/lib/sprockets/uglifier_compressor.rb +36 -15
  46. data/lib/sprockets/utils.rb +161 -44
  47. data/lib/sprockets/version.rb +1 -1
  48. data/lib/sprockets/yui_compressor.rb +37 -12
  49. metadata +64 -106
  50. data/lib/sprockets/asset_attributes.rb +0 -137
  51. data/lib/sprockets/bundled_asset.rb +0 -78
  52. data/lib/sprockets/caching.rb +0 -96
  53. data/lib/sprockets/charset_normalizer.rb +0 -41
  54. data/lib/sprockets/index.rb +0 -100
  55. data/lib/sprockets/processed_asset.rb +0 -152
  56. data/lib/sprockets/processor.rb +0 -32
  57. data/lib/sprockets/safety_colons.rb +0 -28
  58. data/lib/sprockets/sass_cache_store.rb +0 -29
  59. data/lib/sprockets/sass_functions.rb +0 -70
  60. data/lib/sprockets/sass_importer.rb +0 -30
  61. data/lib/sprockets/scss_template.rb +0 -13
  62. data/lib/sprockets/static_asset.rb +0 -60
@@ -1,269 +1,230 @@
1
- require 'time'
2
- require 'set'
1
+ require 'fileutils'
2
+ require 'pathname'
3
3
 
4
4
  module Sprockets
5
- # `Asset` is the base class for `BundledAsset` and `StaticAsset`.
6
5
  class Asset
7
- # Internal initializer to load `Asset` from serialized `Hash`.
8
- def self.from_hash(environment, hash)
9
- return unless hash.is_a?(Hash)
6
+ attr_reader :logical_path
10
7
 
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
21
-
22
- if klass
23
- asset = klass.allocate
24
- asset.init_with(environment, hash)
25
- asset
8
+ # Private: Intialize Asset wrapper from attributes Hash.
9
+ #
10
+ # Asset wrappers should not be initialized directly, only
11
+ # Environment#find_asset should vend them.
12
+ #
13
+ # attributes - Hash of ivars
14
+ #
15
+ # Returns Asset.
16
+ def initialize(environment, attributes = {})
17
+ @environment = environment
18
+ @attributes = attributes
19
+ attributes.each do |name, value|
20
+ instance_variable_set("@#{name}", value)
26
21
  end
27
- rescue UnserializeError
28
- nil
29
22
  end
30
23
 
31
- attr_reader :logical_path, :pathname
32
- attr_reader :content_type, :mtime, :length, :digest
33
- alias_method :bytesize, :length
34
-
35
- def initialize(environment, logical_path, pathname)
36
- raise ArgumentError, "Asset logical path has no extension: #{logical_path}" if File.extname(logical_path) == ""
37
-
38
- @root = environment.root
39
- @logical_path = logical_path.to_s
40
- @pathname = Pathname.new(pathname)
41
- @content_type = environment.content_type_of(pathname)
42
- # drop precision to 1 second, same pattern followed elsewhere
43
- @mtime = Time.at(environment.stat(pathname).mtime.to_i)
44
- @length = environment.stat(pathname).size
45
- @digest = environment.file_digest(pathname).hexdigest
24
+ # Internal: Return all internal instance variables as a hash.
25
+ #
26
+ # Returns a Hash.
27
+ def to_hash
28
+ @attributes
46
29
  end
47
30
 
48
- # Initialize `Asset` from serialized `Hash`.
49
- def init_with(environment, coder)
50
- @root = environment.root
51
-
52
- @logical_path = coder['logical_path']
53
- @content_type = coder['content_type']
54
- @digest = coder['digest']
55
-
56
- if pathname = coder['pathname']
57
- # Expand `$root` placeholder and wrapper string in a `Pathname`
58
- @pathname = Pathname.new(expand_root_path(pathname))
59
- end
31
+ # Public: Metadata accumulated from pipeline process.
32
+ #
33
+ # The API status of the keys is dependent on the pipeline processors
34
+ # itself. So some values maybe considered public and others internal.
35
+ # See the pipeline proccessor documentation itself.
36
+ #
37
+ # Returns Hash.
38
+ attr_reader :metadata
60
39
 
61
- if mtime = coder['mtime']
62
- @mtime = Time.at(mtime)
63
- end
40
+ # Public: Returns String path of asset.
41
+ attr_reader :filename
64
42
 
65
- if length = coder['length']
66
- # Convert length to an `Integer`
67
- @length = Integer(length)
68
- end
43
+ # Deprecated: Use #filename instead.
44
+ #
45
+ # Returns Pathname.
46
+ def pathname
47
+ @pathname ||= Pathname.new(filename)
69
48
  end
70
49
 
71
- # Copy serialized attributes to the coder object
72
- def encode_with(coder)
73
- coder['class'] = self.class.name.sub(/Sprockets::/, '')
74
- coder['logical_path'] = logical_path
75
- coder['pathname'] = relativize_root_path(pathname).to_s
76
- coder['content_type'] = content_type
77
- coder['mtime'] = mtime.to_i
78
- coder['length'] = length
79
- coder['digest'] = digest
80
- end
50
+ # Internal: Unique asset object ID.
51
+ #
52
+ # Returns String SHA1 String.
53
+ attr_reader :id
54
+
55
+ # Public: Internal URI to lookup asset by.
56
+ #
57
+ # NOT a publically accessible URL.
58
+ #
59
+ # Returns URI.
60
+ attr_reader :uri
81
61
 
82
- # Return logical path with digest spliced in.
62
+ # Public: Return logical path with digest spliced in.
83
63
  #
84
64
  # "foo/bar-37b51d194a7513e45b56f6524f2d51f2.js"
85
65
  #
66
+ # Returns String.
86
67
  def digest_path
87
68
  logical_path.sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
88
69
  end
89
70
 
90
- # Return an `Array` of `Asset` files that are declared dependencies.
91
- def dependencies
92
- []
71
+ # Public: Returns String MIME type of asset. Returns nil if type is unknown.
72
+ attr_reader :content_type
73
+
74
+ # Public: Get all externally linked asset filenames from asset.
75
+ #
76
+ # All linked assets should be compiled anytime this asset is.
77
+ #
78
+ # Returns Set of String asset URIs.
79
+ def links
80
+ metadata[:links] || Set.new
93
81
  end
94
82
 
95
- # Expand asset into an `Array` of parts.
83
+ # Public: Get all internally required assets that were concated into this
84
+ # asset.
85
+ #
86
+ # Returns Array of String asset URIs.
87
+ def included
88
+ metadata[:included]
89
+ end
90
+
91
+ # Deprecated: Expand asset into an `Array` of parts.
96
92
  #
97
93
  # Appending all of an assets body parts together should give you
98
94
  # the asset's contents as a whole.
99
95
  #
100
96
  # This allows you to link to individual files for debugging
101
97
  # purposes.
98
+ #
99
+ # Use Asset#included instead. Keeping a full copy of the bundle's processed
100
+ # assets in memory (and in cache) is expensive and redundant. The common use
101
+ # case is to relink to the assets anyway.
102
+ #
103
+ # Returns Array of Assets.
102
104
  def to_a
103
- [self]
105
+ if metadata[:included]
106
+ metadata[:included].map { |uri| @environment.find_asset_by_uri(uri) }
107
+ else
108
+ [self]
109
+ end
104
110
  end
105
111
 
106
- # `body` is aliased to source by default if it can't have any dependencies.
107
- def body
108
- source
112
+ # Deprecated: Get all required Assets.
113
+ #
114
+ # See Asset#to_a
115
+ #
116
+ # Returns Array of Assets.
117
+ def dependencies
118
+ to_a.reject { |a| a.filename.eql?(self.filename) }
119
+ end
120
+
121
+ # Public: Return `String` of concatenated source.
122
+ #
123
+ # Returns String.
124
+ def source
125
+ if defined? @source
126
+ @source
127
+ else
128
+ # File is read everytime to avoid memory bloat of large binary files
129
+ File.open(filename, 'rb') { |f| f.read }
130
+ end
109
131
  end
110
132
 
111
- # Return `String` of concatenated source.
133
+ # Public: Alias for #source.
134
+ #
135
+ # Returns String.
112
136
  def to_s
113
137
  source
114
138
  end
115
139
 
116
- # Add enumerator to allow `Asset` instances to be used as Rack
117
- # compatible body objects.
118
- def each
119
- yield to_s
140
+ # Public: HTTP encoding for Asset, "deflate", "gzip", etc.
141
+ #
142
+ # Note: This is not the Ruby Encoding of the source. See Asset#charset.
143
+ #
144
+ # Returns a String or nil if encoding is "identity".
145
+ def encoding
146
+ metadata[:encoding]
120
147
  end
121
148
 
122
- # Checks if Asset is fresh by comparing the actual mtime and
123
- # digest to the inmemory model.
149
+ # Public: Get charset of source.
124
150
  #
125
- # Used to test if cached models need to be rebuilt.
126
- def fresh?(environment)
127
- # Check current mtime and digest
128
- dependency_fresh?(environment, self)
129
- end
151
+ # Returns a String charset name or nil if binary.
152
+ attr_reader :charset
130
153
 
131
- # Checks if Asset is stale by comparing the actual mtime and
132
- # digest to the inmemory model.
154
+ # Public: Returns Integer length of source.
155
+ attr_reader :length
156
+ alias_method :bytesize, :length
157
+
158
+ # Deprecated: Returns Time of the last time the source was modified.
159
+ #
160
+ # Time resolution is normalized to the nearest second.
133
161
  #
134
- # Subclass must override `fresh?` or `stale?`.
135
- def stale?(environment)
136
- !fresh?(environment)
162
+ # Returns Time.
163
+ def mtime
164
+ Time.at(@mtime)
137
165
  end
138
166
 
139
- # Save asset to disk.
140
- def write_to(filename, options = {})
141
- # Gzip contents if filename has '.gz'
142
- unless options.key?(:compress)
143
- options[:compress] = File.extname(filename) == '.gz' && File.extname(logical_path) != '.gz'
144
- end
167
+ # Public: Returns String hexdigest of source.
168
+ attr_reader :digest
145
169
 
170
+ # Pubic: ETag String of Asset.
171
+ alias_method :etag, :digest
172
+
173
+ # Public: Add enumerator to allow `Asset` instances to be used as Rack
174
+ # compatible body objects.
175
+ #
176
+ # block
177
+ # part - String body chunk
178
+ #
179
+ # Returns nothing.
180
+ def each
181
+ yield to_s
182
+ end
183
+
184
+ # Public: Save asset to disk.
185
+ #
186
+ # filename - String target
187
+ #
188
+ # Returns nothing.
189
+ def write_to(filename)
146
190
  FileUtils.mkdir_p File.dirname(filename)
147
191
 
148
- File.open("#{filename}+", 'wb') do |f|
149
- if options[:compress]
150
- # Run contents through `Zlib`
151
- gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
152
- gz.mtime = mtime.to_i
153
- gz.write to_s
154
- gz.close
155
- else
156
- # Write out as is
157
- f.write to_s
158
- end
192
+ PathUtils.atomic_write(filename) do |f|
193
+ f.write source
159
194
  end
160
195
 
161
- # Atomic write
162
- FileUtils.mv("#{filename}+", filename)
163
-
164
196
  # Set mtime correctly
165
197
  File.utime(mtime, mtime, filename)
166
198
 
167
199
  nil
168
- ensure
169
- # Ensure tmp file gets cleaned up
170
- FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
171
200
  end
172
201
 
173
- # Pretty inspect
202
+ # Public: Pretty inspect
203
+ #
204
+ # Returns String.
174
205
  def inspect
175
- "#<#{self.class}:0x#{object_id.to_s(16)} " +
176
- "pathname=#{pathname.to_s.inspect}, " +
177
- "mtime=#{mtime.inspect}, " +
206
+ "#<#{self.class}:#{id} " +
207
+ "filename=#{filename.inspect}, " +
178
208
  "digest=#{digest.inspect}" +
179
209
  ">"
180
210
  end
181
211
 
212
+ # Public: Implements Object#hash so Assets can be used as a Hash key or
213
+ # in a Set.
214
+ #
215
+ # Returns Integer hash of the id.
182
216
  def hash
183
- digest.hash
217
+ id.hash
184
218
  end
185
219
 
186
- # Assets are equal if they share the same path, mtime and digest.
220
+ # Public: Compare assets.
221
+ #
222
+ # Assets are equal if they share the same path and digest.
223
+ #
224
+ # Returns true or false.
187
225
  def eql?(other)
188
- other.class == self.class &&
189
- other.logical_path == self.logical_path &&
190
- other.mtime.to_i == self.mtime.to_i &&
191
- other.digest == self.digest
226
+ self.class == other.class && self.id == other.id
192
227
  end
193
228
  alias_method :==, :eql?
194
-
195
- protected
196
- # Internal: String paths that are marked as dependencies after processing.
197
- #
198
- # Default to an empty `Array`.
199
- def dependency_paths
200
- @dependency_paths ||= []
201
- end
202
-
203
- # Internal: `ProccessedAsset`s that are required after processing.
204
- #
205
- # Default to an empty `Array`.
206
- def required_assets
207
- @required_assets ||= []
208
- end
209
-
210
- # Get pathname with its root stripped.
211
- def relative_pathname
212
- @relative_pathname ||= Pathname.new(relativize_root_path(pathname))
213
- end
214
-
215
- # Replace `$root` placeholder with actual environment root.
216
- def expand_root_path(path)
217
- path.to_s.sub(/^\$root/, @root)
218
- end
219
-
220
- # Replace actual environment root with `$root` placeholder.
221
- def relativize_root_path(path)
222
- path.to_s.sub(/^#{Regexp.escape(@root)}/, '$root')
223
- end
224
-
225
- # Check if dependency is fresh.
226
- #
227
- # `dep` is a `Hash` with `path`, `mtime` and `hexdigest` keys.
228
- #
229
- # A `Hash` is used rather than other `Asset` object because we
230
- # want to test non-asset files and directories.
231
- def dependency_fresh?(environment, dep)
232
- path, mtime, hexdigest = dep.pathname.to_s, dep.mtime, dep.digest
233
-
234
- stat = environment.stat(path)
235
-
236
- # If path no longer exists, its definitely stale.
237
- if stat.nil?
238
- return false
239
- end
240
-
241
- # Compare dependency mtime to the actual mtime. If the
242
- # dependency mtime is newer than the actual mtime, the file
243
- # hasn't changed since we created this `Asset` instance.
244
- #
245
- # However, if the mtime is newer it doesn't mean the asset is
246
- # stale. Many deployment environments may recopy or recheckout
247
- # assets on each deploy. In this case the mtime would be the
248
- # time of deploy rather than modified time.
249
- #
250
- # Note: to_i is used in eql? and write_to we assume fidelity of 1 second
251
- # if people save files more frequently than 1 second sprockets may
252
- # not pick it up, by design
253
- if mtime.to_i >= stat.mtime.to_i
254
- return true
255
- end
256
-
257
- digest = environment.file_digest(path)
258
-
259
- # If the mtime is newer, do a full digest comparsion. Return
260
- # fresh if the digests match.
261
- if hexdigest == digest.hexdigest
262
- return true
263
- end
264
-
265
- # Otherwise, its stale.
266
- false
267
- end
268
229
  end
269
230
  end
@@ -0,0 +1,64 @@
1
+ require 'sprockets/errors'
2
+ require 'uri'
3
+
4
+ module Sprockets
5
+ module AssetURI
6
+ # Internal: Parse Asset URI.
7
+ #
8
+ # Examples
9
+ #
10
+ # parse("file:///tmp/js/application.coffee?type=application/javascript")
11
+ # # => "/tmp/js/application.coffee", {type: "application/javascript"}
12
+ #
13
+ # str - String asset URI
14
+ #
15
+ # Returns String path and Hash of symbolized parameters.
16
+ def self.parse(str)
17
+ uri = URI(str)
18
+
19
+ unless uri.scheme == 'file'
20
+ raise URI::InvalidURIError, "expected file:// scheme: #{str}"
21
+ end
22
+
23
+ path = URI::Generic::DEFAULT_PARSER.unescape(uri.path)
24
+ path.force_encoding(Encoding::UTF_8)
25
+
26
+ params = uri.query.to_s.split('&').reduce({}) do |h, p|
27
+ k, v = p.split('=', 2)
28
+ h.merge(k.to_sym => v || true)
29
+ end
30
+
31
+ return path, params
32
+ end
33
+
34
+ # Internal: Build Asset URI.
35
+ #
36
+ # Examples
37
+ #
38
+ # build("/tmp/js/application.coffee", type: "application/javascript")
39
+ # # => "file:///tmp/js/application.coffee?type=application/javascript"
40
+ #
41
+ # path - String file path
42
+ # params - Hash of optional parameters
43
+ #
44
+ # Returns String URI.
45
+ def self.build(path, params = {})
46
+ query = []
47
+ params.each do |key, value|
48
+ case value
49
+ when String
50
+ query << "#{key}=#{value}"
51
+ when TrueClass
52
+ query << "#{key}"
53
+ when FalseClass, NilClass
54
+ else
55
+ raise TypeError, "unexpected type: #{value.class}"
56
+ end
57
+ end
58
+
59
+ uri = "file://#{URI::Generic::DEFAULT_PARSER.escape(path)}"
60
+ uri << "?#{query.join('&')}" if query.any?
61
+ uri
62
+ end
63
+ end
64
+ end