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,158 @@
1
+ require 'pathname'
2
+
3
+ module Sprockets
4
+ # `AssetAttributes` is a wrapper similar to `Pathname` that provides
5
+ # some helper accessors.
6
+ #
7
+ # These methods should be considered internalish.
8
+ class AssetAttributes
9
+ attr_reader :environment, :pathname
10
+
11
+ def initialize(environment, path)
12
+ @environment = environment
13
+ @pathname = path.is_a?(Pathname) ? path : Pathname.new(path.to_s)
14
+ end
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
+ # Returns paths search the load path for.
27
+ def search_paths
28
+ paths = [pathname.to_s]
29
+
30
+ if pathname.basename(extensions.join).to_s != 'index'
31
+ path_without_extensions = extensions.inject(pathname) { |p, ext| p.sub(ext, '') }
32
+ index_path = path_without_extensions.join("index#{extensions.join}").to_s
33
+ paths << index_path
34
+ end
35
+
36
+ paths
37
+ end
38
+
39
+ # Reverse guess logical path for fully expanded path.
40
+ #
41
+ # This has some known issues. For an example if a file is
42
+ # shaddowed in the path, but is required relatively, its logical
43
+ # path will be incorrect.
44
+ def logical_path
45
+ raise ArgumentError unless pathname.absolute?
46
+
47
+ if root_path = environment.paths.detect { |path| pathname.to_s[path] }
48
+ path = pathname.relative_path_from(Pathname.new(root_path)).to_s
49
+ path = engine_extensions.inject(path) { |p, ext| p.sub(ext, '') }
50
+ path = "#{path}#{engine_format_extension}" unless format_extension
51
+ path
52
+ end
53
+ end
54
+
55
+ # Returns `Array` of extension `String`s.
56
+ #
57
+ # "foo.js.coffee"
58
+ # # => [".js", ".coffee"]
59
+ #
60
+ def extensions
61
+ @extensions ||= @pathname.basename.to_s.scan(/\.[^.]+/)
62
+ end
63
+
64
+ # Returns the format extension.
65
+ #
66
+ # "foo.js.coffee"
67
+ # # => ".js"
68
+ #
69
+ def format_extension
70
+ extensions.detect { |ext|
71
+ @environment.mime_types(ext) && !@environment.engines(ext)
72
+ }
73
+ end
74
+
75
+ # Returns an `Array` of engine extensions.
76
+ #
77
+ # "foo.js.coffee.erb"
78
+ # # => [".coffee", ".erb"]
79
+ #
80
+ def engine_extensions
81
+ exts = extensions
82
+
83
+ if offset = extensions.index(format_extension)
84
+ exts = extensions[offset+1..-1]
85
+ end
86
+
87
+ exts.select { |ext| @environment.engines(ext) }
88
+ end
89
+
90
+ # Returns engine classes.
91
+ def engines
92
+ engine_extensions.map { |ext| @environment.engines(ext) }
93
+ end
94
+
95
+ # Returns all processors to run on the path.
96
+ def processors
97
+ environment.preprocessors(content_type) +
98
+ engines.reverse +
99
+ environment.postprocessors(content_type)
100
+ end
101
+
102
+ # Returns the content type for the pathname. Falls back to `application/octet-stream`.
103
+ def content_type
104
+ @content_type ||= begin
105
+ if format_extension.nil?
106
+ engine_content_type || 'application/octet-stream'
107
+ else
108
+ @environment.mime_types(format_extension) ||
109
+ engine_content_type ||
110
+ 'application/octet-stream'
111
+ end
112
+ end
113
+ end
114
+
115
+ # Gets digest fingerprint.
116
+ #
117
+ # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
118
+ # # => "0aa2105d29558f3eb790d411d7d8fb66"
119
+ #
120
+ def path_fingerprint
121
+ pathname.basename(extensions.join).to_s =~ /-([0-9a-f]{7,40})$/ ? $1 : nil
122
+ end
123
+
124
+ # Injects digest fingerprint into path.
125
+ #
126
+ # "foo.js"
127
+ # # => "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
128
+ #
129
+ def path_with_fingerprint(digest)
130
+ if old_digest = path_fingerprint
131
+ pathname.sub(old_digest, digest).to_s
132
+ else
133
+ basename = "#{pathname.basename(extensions.join)}-#{digest}#{extensions.join}"
134
+ pathname.dirname.to_s == '.' ? basename : pathname.dirname.join(basename).to_s
135
+ end
136
+ end
137
+
138
+ private
139
+ # Returns implicit engine content type.
140
+ #
141
+ # `.coffee` files carry an implicit `application/javascript`
142
+ # content type.
143
+ def engine_content_type
144
+ engines.reverse.each do |engine|
145
+ if engine.respond_to?(:default_mime_type) && engine.default_mime_type
146
+ return engine.default_mime_type
147
+ end
148
+ end
149
+ nil
150
+ end
151
+
152
+ def engine_format_extension
153
+ if content_type = engine_content_type
154
+ environment.extension_for_mime_type(content_type)
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,163 @@
1
+ require 'sprockets/asset_attributes'
2
+ require 'sprockets/bundled_asset'
3
+ require 'sprockets/caching'
4
+ require 'sprockets/digest'
5
+ require 'sprockets/processing'
6
+ require 'sprockets/server'
7
+ require 'sprockets/static_asset'
8
+ require 'sprockets/trail'
9
+ require 'pathname'
10
+
11
+ module Sprockets
12
+ # `Base` class for `Environment` and `Index`.
13
+ class Base
14
+ include Digest
15
+ include Caching, Processing, Server, Trail
16
+
17
+ # Get and set `Logger` instance.
18
+ attr_accessor :logger
19
+
20
+ # Get `Context` class.
21
+ #
22
+ # This class maybe mutated and mixed in with custom helpers.
23
+ #
24
+ # environment.context_class.instance_eval do
25
+ # include MyHelpers
26
+ # def asset_url; end
27
+ # end
28
+ #
29
+ attr_reader :context_class
30
+
31
+ # Get persistent cache store
32
+ attr_reader :cache
33
+
34
+ # Set persistent cache store
35
+ #
36
+ # The cache store must implement a pair of getters and
37
+ # setters. Either `get(key)`/`set(key, value)`,
38
+ # `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
39
+ def cache=(cache)
40
+ expire_index!
41
+ @cache = cache
42
+ end
43
+
44
+ # Return an `Index`. Must be implemented by the subclass.
45
+ def index
46
+ raise NotImplementedError
47
+ end
48
+
49
+ # Works like `Dir.entries`.
50
+ #
51
+ # Subclasses may cache this method.
52
+ def entries(pathname)
53
+ trail.entries(pathname)
54
+ end
55
+
56
+ # Works like `File.stat`.
57
+ #
58
+ # Subclasses may cache this method.
59
+ def stat(path)
60
+ trail.stat(path)
61
+ end
62
+
63
+ # Read and compute digest of filename.
64
+ #
65
+ # Subclasses may cache this method.
66
+ def file_digest(path, data = nil)
67
+ if stat = self.stat(path)
68
+ # `data` maybe provided
69
+ if data
70
+ digest.update(data)
71
+
72
+ # If its a file, digest the contents
73
+ elsif stat.file?
74
+ digest.file(path.to_s)
75
+
76
+ # If its a directive, digest the list of filenames
77
+ elsif stat.directory?
78
+ contents = self.entries(path).join(',')
79
+ digest.update(contents)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Internal. Return a `AssetAttributes` for `path`.
85
+ def attributes_for(path)
86
+ AssetAttributes.new(self, path)
87
+ end
88
+
89
+ # Internal. Return content type of `path`.
90
+ def content_type_of(path)
91
+ attributes_for(path).content_type
92
+ end
93
+
94
+ # Find asset by logical path or expanded path.
95
+ def find_asset(path, options = {})
96
+ pathname = Pathname.new(path)
97
+
98
+ if pathname.absolute?
99
+ build_asset(attributes_for(pathname).logical_path, pathname, options)
100
+ else
101
+ find_asset_in_path(pathname, options)
102
+ end
103
+ end
104
+
105
+ # Preferred `find_asset` shorthand.
106
+ #
107
+ # environment['application.js']
108
+ #
109
+ def [](*args)
110
+ find_asset(*args)
111
+ end
112
+
113
+ def each_file
114
+ return to_enum(__method__) unless block_given?
115
+ paths.each do |base_path|
116
+ Dir["#{base_path}/**/*"].each do |filename|
117
+ yield filename unless File.directory?(filename)
118
+ end
119
+ end
120
+ nil
121
+ end
122
+
123
+ def each_logical_path
124
+ return to_enum(__method__) unless block_given?
125
+ files = {}
126
+ each_file do |filename|
127
+ logical_path = attributes_for(filename).logical_path
128
+ yield logical_path unless files[logical_path]
129
+ files[logical_path] = true
130
+ end
131
+ nil
132
+ end
133
+
134
+ # Pretty inspect
135
+ def inspect
136
+ "#<#{self.class}:0x#{object_id.to_s(16)} " +
137
+ "root=#{root.to_s.inspect}, " +
138
+ "paths=#{paths.inspect}, " +
139
+ "digest=#{digest.to_s.inspect}" +
140
+ ">"
141
+ end
142
+
143
+ protected
144
+ # Clear index after mutating state. Must be implemented by the subclass.
145
+ def expire_index!
146
+ raise NotImplementedError
147
+ end
148
+
149
+ def build_asset(logical_path, pathname, options)
150
+ pathname = Pathname.new(pathname)
151
+
152
+ return unless stat(pathname)
153
+
154
+ # If there are any processors to run on the pathname, use
155
+ # `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
156
+ if attributes_for(pathname).processors.any?
157
+ BundledAsset.new(self, logical_path, pathname, options)
158
+ else
159
+ StaticAsset.new(self, logical_path, pathname)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,258 @@
1
+ require 'sprockets/asset'
2
+ require 'sprockets/errors'
3
+ require 'fileutils'
4
+ require 'set'
5
+ require 'zlib'
6
+
7
+ module Sprockets
8
+ # `BundledAsset`s are used for files that need to be processed and
9
+ # concatenated with other assets. Use for `.js` and `.css` files.
10
+ class BundledAsset < Asset
11
+ # Define extra attributes to be serialized.
12
+ def self.serialized_attributes
13
+ super + %w( content_type mtime )
14
+ end
15
+
16
+ def initialize(environment, logical_path, pathname, options)
17
+ super(environment, logical_path, pathname)
18
+ @options = options || {}
19
+ end
20
+
21
+ # Initialize `BundledAsset` from serialized `Hash`.
22
+ def init_with(environment, coder)
23
+ @options = {}
24
+
25
+ super
26
+
27
+ @body = coder['body']
28
+ @assets = coder['asset_paths'].map { |p|
29
+ p = expand_root_path(p)
30
+ p == pathname.to_s ? self : environment[p, @options]
31
+ }
32
+
33
+ @dependency_paths = coder['dependency_paths'].map { |h|
34
+ h.merge('path' => expand_root_path(h['path']))
35
+ }
36
+ @dependency_paths.each do |dep|
37
+ dep['mtime'] = Time.parse(dep['mtime']) if dep['mtime'].is_a?(String)
38
+ end
39
+ end
40
+
41
+ # Serialize custom attributes in `BundledAsset`.
42
+ def encode_with(coder)
43
+ super
44
+
45
+ coder['body'] = body
46
+ coder['asset_paths'] = to_a.map { |a| relativize_root_path(a.pathname) }
47
+ coder['dependency_paths'] = dependency_paths.map { |h|
48
+ h.merge('path' => relativize_root_path(h['path']))
49
+ }
50
+ end
51
+
52
+ # Get asset's own processed contents. Excludes any of its required
53
+ # dependencies but does run any processors or engines on the
54
+ # original file.
55
+ def body
56
+ @body ||= build_dependency_context_and_body[1]
57
+ end
58
+
59
+ # Get latest mtime of all its dependencies.
60
+ def mtime
61
+ @mtime ||= dependency_paths.map { |h| h['mtime'] }.max
62
+ end
63
+
64
+ # Get size of concatenated source.
65
+ def length
66
+ @length ||= build_source['length']
67
+ end
68
+
69
+ # Compute digest of concatenated source.
70
+ def digest
71
+ @digest ||= build_source['digest']
72
+ end
73
+
74
+ # Return an `Array` of `Asset` files that are declared dependencies.
75
+ def dependencies
76
+ to_a - [self]
77
+ end
78
+
79
+ # Expand asset into an `Array` of parts.
80
+ def to_a
81
+ @assets ||= build_dependencies_paths_and_assets[1]
82
+ end
83
+
84
+ # Checks if Asset is stale by comparing the actual mtime and
85
+ # digest to the inmemory model.
86
+ def fresh?
87
+ # Check freshness of all declared dependencies
88
+ dependency_paths.all? { |h| dependency_fresh?(h) }
89
+ end
90
+
91
+ # Return `String` of concatenated source.
92
+ def to_s
93
+ @source ||= build_source['source']
94
+ end
95
+
96
+ # Save asset to disk.
97
+ def write_to(filename, options = {})
98
+ # Gzip contents if filename has '.gz'
99
+ options[:compress] ||= File.extname(filename) == '.gz'
100
+
101
+ File.open("#{filename}+", 'wb') do |f|
102
+ if options[:compress]
103
+ # Run contents through `Zlib`
104
+ gz = Zlib::GzipWriter.new(f, Zlib::BEST_COMPRESSION)
105
+ gz.write to_s
106
+ gz.close
107
+ else
108
+ # Write out as is
109
+ f.write to_s
110
+ f.close
111
+ end
112
+ end
113
+
114
+ # Atomic write
115
+ FileUtils.mv("#{filename}+", filename)
116
+
117
+ # Set mtime correctly
118
+ File.utime(mtime, mtime, filename)
119
+
120
+ nil
121
+ ensure
122
+ # Ensure tmp file gets cleaned up
123
+ FileUtils.rm("#{filename}+") if File.exist?("#{filename}+")
124
+ end
125
+
126
+ protected
127
+ # Return new blank `Context` to evaluate processors in.
128
+ def blank_context
129
+ environment.context_class.new(environment, logical_path.to_s, pathname)
130
+ end
131
+
132
+ # Get `Context` after processors have been ran on it. This
133
+ # trackes any dependencies that processors have added to it.
134
+ def dependency_context
135
+ @dependency_context ||= build_dependency_context_and_body[0]
136
+ end
137
+
138
+ # All paths that this asset depends on. This list may include
139
+ # non-assets like directories.
140
+ def dependency_paths
141
+ @dependency_paths ||= build_dependencies_paths_and_assets[0]
142
+ end
143
+
144
+ private
145
+ def logger
146
+ environment.logger
147
+ end
148
+
149
+ # Check if self has already been required and raise a fast
150
+ # error. Otherwise you end up with a StackOverflow error.
151
+ def check_circular_dependency!
152
+ requires = @options[:_requires] ||= []
153
+ if requires.include?(pathname.to_s)
154
+ raise CircularDependencyError, "#{pathname} has already been required"
155
+ end
156
+ requires << pathname.to_s
157
+ end
158
+
159
+ def build_dependency_context_and_body
160
+ start_time = Time.now.to_f
161
+
162
+ context = blank_context
163
+
164
+ # Read original data once and pass it along to `Context`
165
+ data = Sprockets::Utils.read_unicode(pathname)
166
+
167
+ # Prime digest cache with data, since we happen to have it
168
+ environment.file_digest(pathname, data)
169
+
170
+ # Runs all processors on `Context`
171
+ body = context.evaluate(pathname, :data => data)
172
+
173
+ @dependency_context, @body = context, body
174
+
175
+ elapsed_time = ((Time.now.to_f - start_time) * 1000).to_i
176
+ logger.info "Compiled #{logical_path} (#{elapsed_time}ms) (pid #{Process.pid})"
177
+
178
+ return context, body
179
+ end
180
+
181
+ def build_dependencies_paths_and_assets
182
+ check_circular_dependency!
183
+
184
+ paths, assets = {}, []
185
+
186
+ # Define an `add_dependency` helper
187
+ add_dependency = lambda do |asset|
188
+ unless assets.any? { |a| a.pathname == asset.pathname }
189
+ assets << asset
190
+ end
191
+ end
192
+
193
+ # Iterate over all the declared require paths from the `Context`
194
+ dependency_context._required_paths.each do |required_path|
195
+ # Catch `require_self`
196
+ if required_path == pathname.to_s
197
+ add_dependency.call(self)
198
+ else
199
+ # Recursively lookup required asset
200
+ environment[required_path, @options].to_a.each do |asset|
201
+ add_dependency.call(asset)
202
+ end
203
+ end
204
+ end
205
+
206
+ # Ensure self is added to the dependency list
207
+ add_dependency.call(self)
208
+
209
+ dependency_context._dependency_paths.each do |path|
210
+ paths[path] ||= {
211
+ 'path' => path,
212
+ 'mtime' => environment.stat(path).mtime,
213
+ 'hexdigest' => environment.file_digest(path).hexdigest
214
+ }
215
+ end
216
+
217
+ dependency_context._dependency_assets.each do |path|
218
+ # Skip if depending on self
219
+ next if path == pathname.to_s
220
+
221
+ # Recursively lookup required asset
222
+ environment[path, @options].to_a.each do |asset|
223
+ asset.dependency_paths.each do |dep|
224
+ paths[dep['path']] ||= dep
225
+ end
226
+ end
227
+ end
228
+
229
+ @dependency_paths, @assets = paths.values, assets
230
+
231
+ return @dependency_paths, @assets
232
+ end
233
+
234
+ def build_source
235
+ hash = environment.cache_hash("#{pathname}:source", id) do
236
+ data = ""
237
+
238
+ # Explode Asset into parts and gather the dependency bodies
239
+ to_a.each { |dependency| data << dependency.body }
240
+
241
+ # Run bundle processors on concatenated source
242
+ data = blank_context.evaluate(pathname, :data => data,
243
+ :processors => environment.bundle_processors(content_type))
244
+
245
+ { 'length' => Rack::Utils.bytesize(data),
246
+ 'digest' => environment.digest.update(data).hexdigest,
247
+ 'source' => data }
248
+ end
249
+ hash['length'] = Integer(hash['length']) if hash['length'].is_a?(String)
250
+
251
+ @length = hash['length']
252
+ @digest = hash['digest']
253
+ @source = hash['source']
254
+
255
+ hash
256
+ end
257
+ end
258
+ end