sprockets 2.2.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +68 -0
  3. data/README.md +482 -255
  4. data/bin/sprockets +20 -7
  5. data/lib/rake/sprocketstask.rb +28 -15
  6. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  7. data/lib/sprockets/asset.rb +142 -207
  8. data/lib/sprockets/autoload/babel.rb +8 -0
  9. data/lib/sprockets/autoload/closure.rb +8 -0
  10. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  11. data/lib/sprockets/autoload/eco.rb +8 -0
  12. data/lib/sprockets/autoload/ejs.rb +8 -0
  13. data/lib/sprockets/autoload/jsminc.rb +8 -0
  14. data/lib/sprockets/autoload/sass.rb +8 -0
  15. data/lib/sprockets/autoload/sassc.rb +8 -0
  16. data/lib/sprockets/autoload/uglifier.rb +8 -0
  17. data/lib/sprockets/autoload/yui.rb +8 -0
  18. data/lib/sprockets/autoload/zopfli.rb +7 -0
  19. data/lib/sprockets/autoload.rb +16 -0
  20. data/lib/sprockets/babel_processor.rb +66 -0
  21. data/lib/sprockets/base.rb +89 -249
  22. data/lib/sprockets/bower.rb +61 -0
  23. data/lib/sprockets/bundle.rb +105 -0
  24. data/lib/sprockets/cache/file_store.rb +190 -14
  25. data/lib/sprockets/cache/memory_store.rb +75 -0
  26. data/lib/sprockets/cache/null_store.rb +54 -0
  27. data/lib/sprockets/cache.rb +271 -0
  28. data/lib/sprockets/cached_environment.rb +64 -0
  29. data/lib/sprockets/closure_compressor.rb +48 -0
  30. data/lib/sprockets/coffee_script_processor.rb +39 -0
  31. data/lib/sprockets/compressing.rb +134 -0
  32. data/lib/sprockets/configuration.rb +79 -0
  33. data/lib/sprockets/context.rb +204 -135
  34. data/lib/sprockets/dependencies.rb +74 -0
  35. data/lib/sprockets/digest_utils.rb +200 -0
  36. data/lib/sprockets/directive_processor.rb +224 -216
  37. data/lib/sprockets/eco_processor.rb +33 -0
  38. data/lib/sprockets/ejs_processor.rb +32 -0
  39. data/lib/sprockets/encoding_utils.rb +262 -0
  40. data/lib/sprockets/environment.rb +23 -68
  41. data/lib/sprockets/erb_processor.rb +37 -0
  42. data/lib/sprockets/errors.rb +6 -13
  43. data/lib/sprockets/exporters/base.rb +72 -0
  44. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  45. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  46. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  47. data/lib/sprockets/exporting.rb +73 -0
  48. data/lib/sprockets/file_reader.rb +16 -0
  49. data/lib/sprockets/http_utils.rb +135 -0
  50. data/lib/sprockets/jsminc_compressor.rb +32 -0
  51. data/lib/sprockets/jst_processor.rb +36 -19
  52. data/lib/sprockets/loader.rb +343 -0
  53. data/lib/sprockets/manifest.rb +231 -96
  54. data/lib/sprockets/manifest_utils.rb +48 -0
  55. data/lib/sprockets/mime.rb +80 -32
  56. data/lib/sprockets/npm.rb +52 -0
  57. data/lib/sprockets/path_dependency_utils.rb +77 -0
  58. data/lib/sprockets/path_digest_utils.rb +48 -0
  59. data/lib/sprockets/path_utils.rb +367 -0
  60. data/lib/sprockets/paths.rb +82 -0
  61. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  62. data/lib/sprockets/processing.rb +140 -192
  63. data/lib/sprockets/processor_utils.rb +169 -0
  64. data/lib/sprockets/resolve.rb +295 -0
  65. data/lib/sprockets/sass_cache_store.rb +30 -0
  66. data/lib/sprockets/sass_compressor.rb +63 -0
  67. data/lib/sprockets/sass_functions.rb +3 -0
  68. data/lib/sprockets/sass_importer.rb +3 -0
  69. data/lib/sprockets/sass_processor.rb +313 -0
  70. data/lib/sprockets/sassc_compressor.rb +56 -0
  71. data/lib/sprockets/sassc_processor.rb +297 -0
  72. data/lib/sprockets/server.rb +138 -90
  73. data/lib/sprockets/source_map_processor.rb +66 -0
  74. data/lib/sprockets/source_map_utils.rb +483 -0
  75. data/lib/sprockets/transformers.rb +173 -0
  76. data/lib/sprockets/uglifier_compressor.rb +66 -0
  77. data/lib/sprockets/unloaded_asset.rb +139 -0
  78. data/lib/sprockets/uri_tar.rb +99 -0
  79. data/lib/sprockets/uri_utils.rb +191 -0
  80. data/lib/sprockets/utils/gzip.rb +99 -0
  81. data/lib/sprockets/utils.rb +186 -53
  82. data/lib/sprockets/version.rb +2 -1
  83. data/lib/sprockets/yui_compressor.rb +56 -0
  84. data/lib/sprockets.rb +217 -52
  85. metadata +250 -59
  86. data/LICENSE +0 -21
  87. data/lib/sprockets/asset_attributes.rb +0 -126
  88. data/lib/sprockets/bundled_asset.rb +0 -79
  89. data/lib/sprockets/caching.rb +0 -96
  90. data/lib/sprockets/charset_normalizer.rb +0 -41
  91. data/lib/sprockets/eco_template.rb +0 -38
  92. data/lib/sprockets/ejs_template.rb +0 -37
  93. data/lib/sprockets/engines.rb +0 -74
  94. data/lib/sprockets/index.rb +0 -99
  95. data/lib/sprockets/processed_asset.rb +0 -152
  96. data/lib/sprockets/processor.rb +0 -32
  97. data/lib/sprockets/safety_colons.rb +0 -28
  98. data/lib/sprockets/static_asset.rb +0 -57
  99. data/lib/sprockets/trail.rb +0 -90
@@ -1,32 +1,208 @@
1
- require 'digest/md5'
1
+ # frozen_string_literal: true
2
2
  require 'fileutils'
3
- require 'pathname'
3
+ require 'logger'
4
+ require 'sprockets/encoding_utils'
5
+ require 'sprockets/path_utils'
6
+ require 'zlib'
4
7
 
5
8
  module Sprockets
6
- module Cache
7
- # A simple file system cache store.
9
+ class Cache
10
+ # Public: A file system cache store that automatically cleans up old keys.
11
+ #
12
+ # Assign the instance to the Environment#cache.
8
13
  #
9
14
  # environment.cache = Sprockets::Cache::FileStore.new("/tmp")
10
15
  #
16
+ # See Also
17
+ #
18
+ # ActiveSupport::Cache::FileStore
19
+ #
11
20
  class FileStore
12
- def initialize(root)
13
- @root = Pathname.new(root)
21
+ # Internal: Default key limit for store.
22
+ DEFAULT_MAX_SIZE = 25 * 1024 * 1024
23
+ EXCLUDED_DIRS = ['.', '..'].freeze
24
+ GITKEEP_FILES = ['.gitkeep', '.keep'].freeze
25
+
26
+ # Internal: Default standard error fatal logger.
27
+ #
28
+ # Returns a Logger.
29
+ def self.default_logger
30
+ logger = Logger.new($stderr)
31
+ logger.level = Logger::FATAL
32
+ logger
14
33
  end
15
34
 
16
- # Lookup value in cache
17
- def [](key)
18
- pathname = @root.join(key)
19
- pathname.exist? ? pathname.open('rb') { |f| Marshal.load(f) } : nil
35
+ # Public: Initialize the cache store.
36
+ #
37
+ # root - A String path to a directory to persist cached values to.
38
+ # max_size - A Integer of the maximum size the store will hold (in bytes).
39
+ # (default: 25MB).
40
+ # logger - The logger to which some info will be printed.
41
+ # (default logger level is FATAL and won't output anything).
42
+ def initialize(root, max_size = DEFAULT_MAX_SIZE, logger = self.class.default_logger)
43
+ @root = root
44
+ @max_size = max_size
45
+ @gc_size = max_size * 0.75
46
+ @logger = logger
47
+ end
48
+
49
+ # Public: Retrieve value from cache.
50
+ #
51
+ # This API should not be used directly, but via the Cache wrapper API.
52
+ #
53
+ # key - String cache key.
54
+ #
55
+ # Returns Object or nil or the value is not set.
56
+ def get(key)
57
+ path = File.join(@root, "#{key}.cache")
58
+
59
+ value = safe_open(path) do |f|
60
+ begin
61
+ EncodingUtils.unmarshaled_deflated(f.read, Zlib::MAX_WBITS)
62
+ rescue Exception => e
63
+ @logger.error do
64
+ "#{self.class}[#{path}] could not be unmarshaled: " +
65
+ "#{e.class}: #{e.message}"
66
+ end
67
+ nil
68
+ end
69
+ end
70
+
71
+ if value
72
+ FileUtils.touch(path)
73
+ value
74
+ end
20
75
  end
21
76
 
22
- # Save value to cache
23
- def []=(key, value)
77
+ # Public: Set a key and value in the cache.
78
+ #
79
+ # This API should not be used directly, but via the Cache wrapper API.
80
+ #
81
+ # key - String cache key.
82
+ # value - Object value.
83
+ #
84
+ # Returns Object value.
85
+ def set(key, value)
86
+ path = File.join(@root, "#{key}.cache")
87
+
24
88
  # Ensure directory exists
25
- FileUtils.mkdir_p @root.join(key).dirname
89
+ FileUtils.mkdir_p File.dirname(path)
90
+
91
+ # Check if cache exists before writing
92
+ exists = File.exist?(path)
93
+
94
+ # Serialize value
95
+ marshaled = Marshal.dump(value)
96
+
97
+ # Compress if larger than 4KB
98
+ if marshaled.bytesize > 4 * 1024
99
+ deflater = Zlib::Deflate.new(
100
+ Zlib::BEST_COMPRESSION,
101
+ Zlib::MAX_WBITS,
102
+ Zlib::MAX_MEM_LEVEL,
103
+ Zlib::DEFAULT_STRATEGY
104
+ )
105
+ deflater << marshaled
106
+ raw = deflater.finish
107
+ else
108
+ raw = marshaled
109
+ end
110
+
111
+ # Write data
112
+ PathUtils.atomic_write(path) do |f|
113
+ f.write(raw)
114
+ @size = size + f.size unless exists
115
+ end
116
+
117
+ # GC if necessary
118
+ gc! if size > @max_size
26
119
 
27
- @root.join(key).open('w') { |f| Marshal.dump(value, f)}
28
120
  value
29
121
  end
122
+
123
+ # Public: Pretty inspect
124
+ #
125
+ # Returns String.
126
+ def inspect
127
+ "#<#{self.class} size=#{size}/#{@max_size}>"
128
+ end
129
+
130
+ # Public: Clear the cache
131
+ #
132
+ # adapted from ActiveSupport::Cache::FileStore#clear
133
+ #
134
+ # Deletes all items from the cache. In this case it deletes all the entries in the specified
135
+ # file store directory except for .keep or .gitkeep. Be careful which directory is specified
136
+ # as @root because everything in that directory will be deleted.
137
+ #
138
+ # Returns true
139
+ def clear(options=nil)
140
+ if File.exist?(@root)
141
+ root_dirs = Dir.entries(@root).reject { |f| (EXCLUDED_DIRS + GITKEEP_FILES).include?(f) }
142
+ FileUtils.rm_r(root_dirs.collect{ |f| File.join(@root, f) })
143
+ end
144
+ true
145
+ end
146
+
147
+ private
148
+ # Internal: Get all cache files along with stats.
149
+ #
150
+ # Returns an Array of [String filename, File::Stat] pairs sorted by
151
+ # mtime.
152
+ def find_caches
153
+ Dir.glob(File.join(@root, '**/*.cache')).reduce([]) { |stats, filename|
154
+ stat = safe_stat(filename)
155
+ # stat maybe nil if file was removed between the time we called
156
+ # dir.glob and the next stat
157
+ stats << [filename, stat] if stat
158
+ stats
159
+ }.sort_by { |_, stat| stat.mtime.to_i }
160
+ end
161
+
162
+ def size
163
+ @size ||= compute_size(find_caches)
164
+ end
165
+
166
+ def compute_size(caches)
167
+ caches.inject(0) { |sum, (_, stat)| sum + stat.size }
168
+ end
169
+
170
+ def safe_stat(fn)
171
+ File.stat(fn)
172
+ rescue Errno::ENOENT
173
+ nil
174
+ end
175
+
176
+ def safe_open(path, &block)
177
+ if File.exist?(path)
178
+ File.open(path, 'rb', &block)
179
+ end
180
+ rescue Errno::ENOENT
181
+ end
182
+
183
+ def gc!
184
+ start_time = Time.now
185
+
186
+ caches = find_caches
187
+ size = compute_size(caches)
188
+
189
+ delete_caches, keep_caches = caches.partition { |filename, stat|
190
+ deleted = size > @gc_size
191
+ size -= stat.size
192
+ deleted
193
+ }
194
+
195
+ return if delete_caches.empty?
196
+
197
+ FileUtils.remove(delete_caches.map(&:first), force: true)
198
+ @size = compute_size(keep_caches)
199
+
200
+ @logger.warn do
201
+ secs = Time.now.to_f - start_time.to_f
202
+ "#{self.class}[#{@root}] garbage collected " +
203
+ "#{delete_caches.size} files (#{(secs * 1000).to_i}ms)"
204
+ end
205
+ end
30
206
  end
31
207
  end
32
208
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ module Sprockets
3
+ class Cache
4
+ # Public: Basic in memory LRU cache.
5
+ #
6
+ # Assign the instance to the Environment#cache.
7
+ #
8
+ # environment.cache = Sprockets::Cache::MemoryStore.new(1000)
9
+ #
10
+ # See Also
11
+ #
12
+ # ActiveSupport::Cache::MemoryStore
13
+ #
14
+ class MemoryStore
15
+ # Internal: Default key limit for store.
16
+ DEFAULT_MAX_SIZE = 1000
17
+
18
+ # Public: Initialize the cache store.
19
+ #
20
+ # max_size - A Integer of the maximum number of keys the store will hold.
21
+ # (default: 1000).
22
+ def initialize(max_size = DEFAULT_MAX_SIZE)
23
+ @max_size = max_size
24
+ @cache = {}
25
+ end
26
+
27
+ # Public: Retrieve value from cache.
28
+ #
29
+ # This API should not be used directly, but via the Cache wrapper API.
30
+ #
31
+ # key - String cache key.
32
+ #
33
+ # Returns Object or nil or the value is not set.
34
+ def get(key)
35
+ exists = true
36
+ value = @cache.delete(key) { exists = false }
37
+ if exists
38
+ @cache[key] = value
39
+ else
40
+ nil
41
+ end
42
+ end
43
+
44
+ # Public: Set a key and value in the cache.
45
+ #
46
+ # This API should not be used directly, but via the Cache wrapper API.
47
+ #
48
+ # key - String cache key.
49
+ # value - Object value.
50
+ #
51
+ # Returns Object value.
52
+ def set(key, value)
53
+ @cache.delete(key)
54
+ @cache[key] = value
55
+ @cache.shift if @cache.size > @max_size
56
+ value
57
+ end
58
+
59
+ # Public: Pretty inspect
60
+ #
61
+ # Returns String.
62
+ def inspect
63
+ "#<#{self.class} size=#{@cache.size}/#{@max_size}>"
64
+ end
65
+
66
+ # Public: Clear the cache
67
+ #
68
+ # Returns true
69
+ def clear(options=nil)
70
+ @cache.clear
71
+ true
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ module Sprockets
3
+ class Cache
4
+ # Public: A compatible cache store that doesn't store anything. Used by
5
+ # default when no Environment#cache is configured.
6
+ #
7
+ # Assign the instance to the Environment#cache.
8
+ #
9
+ # environment.cache = Sprockets::Cache::NullStore.new
10
+ #
11
+ # See Also
12
+ #
13
+ # ActiveSupport::Cache::NullStore
14
+ #
15
+ class NullStore
16
+ # Public: Simulate a cache miss.
17
+ #
18
+ # This API should not be used directly, but via the Cache wrapper API.
19
+ #
20
+ # key - String cache key.
21
+ #
22
+ # Returns nil.
23
+ def get(key)
24
+ nil
25
+ end
26
+
27
+ # Public: Simulate setting a value in the cache.
28
+ #
29
+ # This API should not be used directly, but via the Cache wrapper API.
30
+ #
31
+ # key - String cache key.
32
+ # value - Object value.
33
+ #
34
+ # Returns Object value.
35
+ def set(key, value)
36
+ value
37
+ end
38
+
39
+ # Public: Pretty inspect
40
+ #
41
+ # Returns String.
42
+ def inspect
43
+ "#<#{self.class}>"
44
+ end
45
+
46
+ # Public: Simulate clearing the cache
47
+ #
48
+ # Returns true
49
+ def clear(options=nil)
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+ require 'logger'
3
+ require 'sprockets/digest_utils'
4
+
5
+ module Sprockets
6
+ # Public: Wrapper interface to backend cache stores. Ensures a consistent API
7
+ # even when the backend uses get/set or read/write.
8
+ #
9
+ # Public cache interface
10
+ #
11
+ # Always assign the backend store instance to Environment#cache=.
12
+ #
13
+ # environment.cache = Sprockets::Cache::MemoryStore.new(1000)
14
+ #
15
+ # Environment#cache will always return a wrapped Cache interface. See the
16
+ # methods marked public on this class.
17
+ #
18
+ #
19
+ # Backend cache interface
20
+ #
21
+ # The Backend cache store must implement two methods.
22
+ #
23
+ # get(key)
24
+ #
25
+ # key - An opaque String with a length less than 250 characters.
26
+ #
27
+ # Returns an JSON serializable object.
28
+ #
29
+ # set(key, value)
30
+ #
31
+ # Will only be called once per key. Setting a key "foo" with value "bar",
32
+ # then later key "foo" with value "baz" is an undefined behavior.
33
+ #
34
+ # key - An opaque String with a length less than 250 characters.
35
+ # value - A JSON serializable object.
36
+ #
37
+ # Returns argument value.
38
+ #
39
+ # clear(options)
40
+ #
41
+ # Clear the entire cache. Be careful with this method since it could
42
+ # affect other processes if shared cache is being used.
43
+ #
44
+ # The options hash is passed to the underlying cache implementation.
45
+ class Cache
46
+ # Builtin cache stores.
47
+ autoload :FileStore, 'sprockets/cache/file_store'
48
+ autoload :MemoryStore, 'sprockets/cache/memory_store'
49
+ autoload :NullStore, 'sprockets/cache/null_store'
50
+
51
+ # Internal: Cache key version for this class. Rarely should have to change
52
+ # unless the cache format radically changes. Will be bump on major version
53
+ # releases though.
54
+ VERSION = '4.0.0'
55
+
56
+ def self.default_logger
57
+ logger = Logger.new($stderr)
58
+ logger.level = Logger::FATAL
59
+ logger
60
+ end
61
+
62
+ # Internal: Wrap a backend cache store.
63
+ #
64
+ # Always assign a backend cache store instance to Environment#cache= and
65
+ # use Environment#cache to retreive a wrapped interface.
66
+ #
67
+ # cache - A compatible backend cache store instance.
68
+ def initialize(cache = nil, logger = self.class.default_logger)
69
+ @cache_wrapper = get_cache_wrapper(cache)
70
+ @fetch_cache = Cache::MemoryStore.new(1024)
71
+ @logger = logger
72
+ end
73
+
74
+ # Public: Prefer API to retrieve and set values in the cache store.
75
+ #
76
+ # key - JSON serializable key
77
+ # block -
78
+ # Must return a consistent JSON serializable object for the given key.
79
+ #
80
+ # Examples
81
+ #
82
+ # cache.fetch("foo") { "bar" }
83
+ #
84
+ # Returns a JSON serializable object.
85
+ def fetch(key)
86
+ start = Time.now.to_f
87
+ expanded_key = expand_key(key)
88
+ value = @fetch_cache.get(expanded_key)
89
+ if value.nil?
90
+ value = @cache_wrapper.get(expanded_key)
91
+ if value.nil?
92
+ value = yield
93
+ @cache_wrapper.set(expanded_key, value)
94
+ @logger.debug do
95
+ ms = "(#{((Time.now.to_f - start) * 1000).to_i}ms)"
96
+ "Sprockets Cache miss #{peek_key(key)} #{ms}"
97
+ end
98
+ end
99
+ @fetch_cache.set(expanded_key, value)
100
+ end
101
+ value
102
+ end
103
+
104
+ # Public: Low level API to retrieve item directly from the backend cache
105
+ # store.
106
+ #
107
+ # This API may be used publicly, but may have undefined behavior
108
+ # depending on the backend store being used. Prefer the
109
+ # Cache#fetch API over using this.
110
+ #
111
+ # key - JSON serializable key
112
+ # local - Check local cache first (default: false)
113
+ #
114
+ # Returns a JSON serializable object or nil if there was a cache miss.
115
+ def get(key, local = false)
116
+ expanded_key = expand_key(key)
117
+
118
+ if local && value = @fetch_cache.get(expanded_key)
119
+ return value
120
+ end
121
+
122
+ value = @cache_wrapper.get(expanded_key)
123
+ @fetch_cache.set(expanded_key, value) if local
124
+
125
+ value
126
+ end
127
+
128
+ # Public: Low level API to set item directly to the backend cache store.
129
+ #
130
+ # This API may be used publicly, but may have undefined behavior
131
+ # depending on the backend store being used. Prefer the
132
+ # Cache#fetch API over using this.
133
+ #
134
+ # key - JSON serializable key
135
+ # value - A consistent JSON serializable object for the given key. Setting
136
+ # a different value for the given key has undefined behavior.
137
+ # local - Set on local cache (default: false)
138
+ #
139
+ # Returns the value argument.
140
+ def set(key, value, local = false)
141
+ expanded_key = expand_key(key)
142
+ @fetch_cache.set(expanded_key, value) if local
143
+ @cache_wrapper.set(expanded_key, value)
144
+ end
145
+
146
+ # Public: Pretty inspect
147
+ #
148
+ # Returns String.
149
+ def inspect
150
+ "#<#{self.class} local=#{@fetch_cache.inspect} store=#{@cache_wrapper.cache.inspect}>"
151
+ end
152
+
153
+ # Public: Clear cache
154
+ #
155
+ # Returns truthy on success, potentially raises exception on failure
156
+ def clear(options=nil)
157
+ @cache_wrapper.clear
158
+ @fetch_cache.clear
159
+ end
160
+
161
+ private
162
+ # Internal: Expand object cache key into a short String key.
163
+ #
164
+ # The String should be under 250 characters so its compatible with
165
+ # Memcache.
166
+ #
167
+ # key - JSON serializable key
168
+ #
169
+ # Returns a String with a length less than 250 characters.
170
+ def expand_key(key)
171
+ digest_key = DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key))
172
+ namespace = digest_key[0, 2]
173
+ "sprockets/v#{VERSION}/#{namespace}/#{digest_key}"
174
+ end
175
+
176
+ PEEK_SIZE = 100
177
+
178
+ # Internal: Show first 100 characters of cache key for logging purposes.
179
+ #
180
+ # Returns a String with a length less than 100 characters.
181
+ def peek_key(key)
182
+ case key
183
+ when Integer
184
+ key.to_s
185
+ when String
186
+ key[0, PEEK_SIZE].inspect
187
+ when Array
188
+ str = []
189
+ key.each { |k| str << peek_key(k) }
190
+ str.join(':')[0, PEEK_SIZE]
191
+ else
192
+ peek_key(DigestUtils.pack_urlsafe_base64digest(DigestUtils.digest(key)))
193
+ end
194
+ end
195
+
196
+ def get_cache_wrapper(cache)
197
+ if cache.is_a?(Cache)
198
+ cache
199
+
200
+ # `Cache#get(key)` for Memcache
201
+ elsif cache.respond_to?(:get)
202
+ GetWrapper.new(cache)
203
+
204
+ # `Cache#[key]` so `Hash` can be used
205
+ elsif cache.respond_to?(:[])
206
+ HashWrapper.new(cache)
207
+
208
+ # `Cache#read(key)` for `ActiveSupport::Cache` support
209
+ elsif cache.respond_to?(:read)
210
+ ReadWriteWrapper.new(cache)
211
+
212
+ else
213
+ cache = Sprockets::Cache::NullStore.new
214
+ GetWrapper.new(cache)
215
+ end
216
+ end
217
+
218
+ class Wrapper < Struct.new(:cache)
219
+ end
220
+
221
+ class GetWrapper < Wrapper
222
+ def get(key)
223
+ cache.get(key)
224
+ end
225
+
226
+ def set(key, value)
227
+ cache.set(key, value)
228
+ end
229
+
230
+ def clear(options=nil)
231
+ # dalli has a #flush method so try it
232
+ if cache.respond_to?(:flush)
233
+ cache.flush(options)
234
+ else
235
+ cache.clear(options)
236
+ end
237
+ true
238
+ end
239
+ end
240
+
241
+ class HashWrapper < Wrapper
242
+ def get(key)
243
+ cache[key]
244
+ end
245
+
246
+ def set(key, value)
247
+ cache[key] = value
248
+ end
249
+
250
+ def clear(options=nil)
251
+ cache.clear
252
+ true
253
+ end
254
+ end
255
+
256
+ class ReadWriteWrapper < Wrapper
257
+ def get(key)
258
+ cache.read(key)
259
+ end
260
+
261
+ def set(key, value)
262
+ cache.write(key, value)
263
+ end
264
+
265
+ def clear(options=nil)
266
+ cache.clear(options)
267
+ true
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/base'
3
+
4
+ module Sprockets
5
+ # `CachedEnvironment` is a special cached version of `Environment`.
6
+ #
7
+ # The expection is that all of its file system methods are cached
8
+ # for the instances lifetime. This makes `CachedEnvironment` much faster. This
9
+ # behavior is ideal in production environments where the file system
10
+ # is immutable.
11
+ #
12
+ # `CachedEnvironment` should not be initialized directly. Instead use
13
+ # `Environment#cached`.
14
+ class CachedEnvironment < Base
15
+ def initialize(environment)
16
+ initialize_configuration(environment)
17
+
18
+ @cache = environment.cache
19
+ @stats = {}
20
+ @entries = {}
21
+ @uris = {}
22
+ @processor_cache_keys = {}
23
+ @resolved_dependencies = {}
24
+ end
25
+
26
+ # No-op return self as cached environment.
27
+ def cached
28
+ self
29
+ end
30
+ alias_method :index, :cached
31
+
32
+ # Internal: Cache Environment#entries
33
+ def entries(path)
34
+ @entries[path] ||= super(path)
35
+ end
36
+
37
+ # Internal: Cache Environment#stat
38
+ def stat(path)
39
+ @stats[path] ||= super(path)
40
+ end
41
+
42
+ # Internal: Cache Environment#load
43
+ def load(uri)
44
+ @uris[uri] ||= super(uri)
45
+ end
46
+
47
+ # Internal: Cache Environment#processor_cache_key
48
+ def processor_cache_key(str)
49
+ @processor_cache_keys[str] ||= super(str)
50
+ end
51
+
52
+ # Internal: Cache Environment#resolve_dependency
53
+ def resolve_dependency(str)
54
+ @resolved_dependencies[str] ||= super(str)
55
+ end
56
+
57
+ private
58
+ # Cache is immutable, any methods that try to change the runtime config
59
+ # should bomb.
60
+ def config=(config)
61
+ raise RuntimeError, "can't modify immutable cached environment"
62
+ end
63
+ end
64
+ end