base.rb 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. require 'sprockets/asset_attributes'
  2. require 'sprockets/bundled_asset'
  3. require 'sprockets/caching'
  4. require 'sprockets/processed_asset'
  5. require 'sprockets/processing'
  6. require 'sprockets/server'
  7. require 'sprockets/static_asset'
  8. require 'sprockets/trail'
  9. require 'pathname'
  10. module Sprockets
  11. # `Base` class for `Environment` and `Index`.
  12. class Base
  13. include Caching, Processing, Server, Trail
  14. # Returns a `Digest` implementation class.
  15. #
  16. # Defaults to `Digest::MD5`.
  17. attr_reader :digest_class
  18. # Assign a `Digest` implementation class. This maybe any Ruby
  19. # `Digest::` implementation such as `Digest::MD5` or
  20. # `Digest::SHA1`.
  21. #
  22. # environment.digest_class = Digest::SHA1
  23. #
  24. def digest_class=(klass)
  25. expire_index!
  26. @digest_class = klass
  27. end
  28. # The `Environment#version` is a custom value used for manually
  29. # expiring all asset caches.
  30. #
  31. # Sprockets is able to track most file and directory changes and
  32. # will take care of expiring the cache for you. However, its
  33. # impossible to know when any custom helpers change that you mix
  34. # into the `Context`.
  35. #
  36. # It would be wise to increment this value anytime you make a
  37. # configuration change to the `Environment` object.
  38. attr_reader :version
  39. # Assign an environment version.
  40. #
  41. # environment.version = '2.0'
  42. #
  43. def version=(version)
  44. expire_index!
  45. @version = version
  46. end
  47. # Returns a `Digest` instance for the `Environment`.
  48. #
  49. # This value serves two purposes. If two `Environment`s have the
  50. # same digest value they can be treated as equal. This is more
  51. # useful for comparing environment states between processes rather
  52. # than in the same. Two equal `Environment`s can share the same
  53. # cached assets.
  54. #
  55. # The value also provides a seed digest for all `Asset`
  56. # digests. Any change in the environment digest will affect all of
  57. # its assets.
  58. def digest
  59. # Compute the initial digest using the implementation class. The
  60. # Sprockets release version and custom environment version are
  61. # mixed in. So any new releases will affect all your assets.
  62. @digest ||= digest_class.new.update(VERSION).update(version.to_s)
  63. # Returned a dupped copy so the caller can safely mutate it with `.update`
  64. @digest.dup
  65. end
  66. # Get and set `Logger` instance.
  67. attr_accessor :logger
  68. # Get `Context` class.
  69. #
  70. # This class maybe mutated and mixed in with custom helpers.
  71. #
  72. # environment.context_class.instance_eval do
  73. # include MyHelpers
  74. # def asset_url; end
  75. # end
  76. #
  77. attr_reader :context_class
  78. # Get persistent cache store
  79. attr_reader :cache
  80. # Set persistent cache store
  81. #
  82. # The cache store must implement a pair of getters and
  83. # setters. Either `get(key)`/`set(key, value)`,
  84. # `[key]`/`[key]=value`, `read(key)`/`write(key, value)`.
  85. def cache=(cache)
  86. expire_index!
  87. @cache = cache
  88. end
  89. # Return an `Index`. Must be implemented by the subclass.
  90. def index
  91. raise NotImplementedError
  92. end
  93. # Works like `Dir.entries`.
  94. #
  95. # Subclasses may cache this method.
  96. def entries(pathname)
  97. trail.entries(pathname)
  98. end
  99. # Works like `File.stat`.
  100. #
  101. # Subclasses may cache this method.
  102. def stat(path)
  103. trail.stat(path)
  104. end
  105. # Read and compute digest of filename.
  106. #
  107. # Subclasses may cache this method.
  108. def file_digest(path)
  109. if stat = self.stat(path)
  110. # If its a file, digest the contents
  111. if stat.file?
  112. digest.file(path.to_s)
  113. # If its a directive, digest the list of filenames
  114. elsif stat.directory?
  115. contents = self.entries(path).join(',')
  116. digest.update(contents)
  117. end
  118. end
  119. end
  120. # Internal. Return a `AssetAttributes` for `path`.
  121. def attributes_for(path)
  122. AssetAttributes.new(self, path)
  123. end
  124. # Internal. Return content type of `path`.
  125. def content_type_of(path)
  126. attributes_for(path).content_type
  127. end
  128. # Find asset by logical path or expanded path.
  129. def find_asset(path, options = {})
  130. logical_path = path
  131. pathname = Pathname.new(path)
  132. if pathname.absolute?
  133. return unless stat(pathname)
  134. logical_path = attributes_for(pathname).logical_path
  135. else
  136. begin
  137. pathname = resolve(logical_path)
  138. rescue FileNotFound
  139. return nil
  140. end
  141. end
  142. build_asset(logical_path, pathname, options)
  143. end
  144. # Preferred `find_asset` shorthand.
  145. #
  146. # environment['application.js']
  147. #
  148. def [](*args)
  149. find_asset(*args)
  150. end
  151. def each_entry(root, &block)
  152. return to_enum(__method__, root) unless block_given?
  153. root = Pathname.new(root) unless root.is_a?(Pathname)
  154. paths = []
  155. entries(root).sort.each do |filename|
  156. path = root.join(filename)
  157. paths << path
  158. if stat(path).directory?
  159. each_entry(path) do |subpath|
  160. paths << subpath
  161. end
  162. end
  163. end
  164. paths.sort_by(&:to_s).each(&block)
  165. nil
  166. end
  167. def each_file
  168. return to_enum(__method__) unless block_given?
  169. paths.each do |root|
  170. each_entry(root) do |path|
  171. if !stat(path).directory?
  172. yield path
  173. end
  174. end
  175. end
  176. nil
  177. end
  178. def each_logical_path
  179. return to_enum(__method__) unless block_given?
  180. files = {}
  181. each_file do |filename|
  182. logical_path = attributes_for(filename).logical_path
  183. yield logical_path unless files[logical_path]
  184. files[logical_path] = true
  185. end
  186. nil
  187. end
  188. # Pretty inspect
  189. def inspect
  190. "#<#{self.class}:0x#{object_id.to_s(16)} " +
  191. "root=#{root.to_s.inspect}, " +
  192. "paths=#{paths.inspect}, " +
  193. "digest=#{digest.to_s.inspect}" +
  194. ">"
  195. end
  196. protected
  197. # Clear index after mutating state. Must be implemented by the subclass.
  198. def expire_index!
  199. raise NotImplementedError
  200. end
  201. def build_asset(logical_path, pathname, options)
  202. pathname = Pathname.new(pathname)
  203. # If there are any processors to run on the pathname, use
  204. # `BundledAsset`. Otherwise use `StaticAsset` and treat is as binary.
  205. if attributes_for(pathname).processors.any?
  206. if options[:bundle] == false
  207. circular_call_protection(pathname.to_s) do
  208. ProcessedAsset.new(index, logical_path, pathname)
  209. end
  210. else
  211. BundledAsset.new(index, logical_path, pathname)
  212. end
  213. else
  214. StaticAsset.new(index, logical_path, pathname)
  215. end
  216. end
  217. def cache_key_for(path, options)
  218. "#{path}:#{options[:bundle] ? '1' : '0'}"
  219. end
  220. def circular_call_protection(path)
  221. reset = Thread.current[:sprockets_circular_calls].nil?
  222. calls = Thread.current[:sprockets_circular_calls] ||= Set.new
  223. if calls.include?(path)
  224. raise CircularDependencyError, "#{path} has already been required"
  225. end
  226. calls << path
  227. yield
  228. ensure
  229. Thread.current[:sprockets_circular_calls] = nil if reset
  230. end
  231. end
  232. end