sprockets 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +665 -0
  4. data/bin/sprockets +93 -0
  5. data/lib/rake/sprocketstask.rb +153 -0
  6. data/lib/sprockets.rb +229 -0
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +202 -0
  9. data/lib/sprockets/autoload.rb +16 -0
  10. data/lib/sprockets/autoload/babel.rb +8 -0
  11. data/lib/sprockets/autoload/closure.rb +8 -0
  12. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  13. data/lib/sprockets/autoload/eco.rb +8 -0
  14. data/lib/sprockets/autoload/ejs.rb +8 -0
  15. data/lib/sprockets/autoload/jsminc.rb +8 -0
  16. data/lib/sprockets/autoload/sass.rb +8 -0
  17. data/lib/sprockets/autoload/sassc.rb +8 -0
  18. data/lib/sprockets/autoload/uglifier.rb +8 -0
  19. data/lib/sprockets/autoload/yui.rb +8 -0
  20. data/lib/sprockets/autoload/zopfli.rb +7 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +147 -0
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache.rb +271 -0
  26. data/lib/sprockets/cache/file_store.rb +208 -0
  27. data/lib/sprockets/cache/memory_store.rb +75 -0
  28. data/lib/sprockets/cache/null_store.rb +54 -0
  29. data/lib/sprockets/cached_environment.rb +64 -0
  30. data/lib/sprockets/closure_compressor.rb +48 -0
  31. data/lib/sprockets/coffee_script_processor.rb +39 -0
  32. data/lib/sprockets/compressing.rb +134 -0
  33. data/lib/sprockets/configuration.rb +79 -0
  34. data/lib/sprockets/context.rb +304 -0
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +200 -0
  37. data/lib/sprockets/directive_processor.rb +414 -0
  38. data/lib/sprockets/eco_processor.rb +33 -0
  39. data/lib/sprockets/ejs_processor.rb +32 -0
  40. data/lib/sprockets/encoding_utils.rb +262 -0
  41. data/lib/sprockets/environment.rb +46 -0
  42. data/lib/sprockets/erb_processor.rb +37 -0
  43. data/lib/sprockets/errors.rb +12 -0
  44. data/lib/sprockets/exporters/base.rb +71 -0
  45. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  46. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  47. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  48. data/lib/sprockets/exporting.rb +73 -0
  49. data/lib/sprockets/file_reader.rb +16 -0
  50. data/lib/sprockets/http_utils.rb +135 -0
  51. data/lib/sprockets/jsminc_compressor.rb +32 -0
  52. data/lib/sprockets/jst_processor.rb +50 -0
  53. data/lib/sprockets/loader.rb +345 -0
  54. data/lib/sprockets/manifest.rb +338 -0
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +96 -0
  57. data/lib/sprockets/npm.rb +52 -0
  58. data/lib/sprockets/path_dependency_utils.rb +77 -0
  59. data/lib/sprockets/path_digest_utils.rb +48 -0
  60. data/lib/sprockets/path_utils.rb +367 -0
  61. data/lib/sprockets/paths.rb +82 -0
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +228 -0
  64. data/lib/sprockets/processor_utils.rb +169 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +30 -0
  67. data/lib/sprockets/sass_compressor.rb +63 -0
  68. data/lib/sprockets/sass_functions.rb +3 -0
  69. data/lib/sprockets/sass_importer.rb +3 -0
  70. data/lib/sprockets/sass_processor.rb +313 -0
  71. data/lib/sprockets/sassc_compressor.rb +56 -0
  72. data/lib/sprockets/sassc_processor.rb +297 -0
  73. data/lib/sprockets/server.rb +295 -0
  74. data/lib/sprockets/source_map_processor.rb +66 -0
  75. data/lib/sprockets/source_map_utils.rb +483 -0
  76. data/lib/sprockets/transformers.rb +173 -0
  77. data/lib/sprockets/uglifier_compressor.rb +66 -0
  78. data/lib/sprockets/unloaded_asset.rb +139 -0
  79. data/lib/sprockets/uri_tar.rb +99 -0
  80. data/lib/sprockets/uri_utils.rb +191 -0
  81. data/lib/sprockets/utils.rb +202 -0
  82. data/lib/sprockets/utils/gzip.rb +99 -0
  83. data/lib/sprockets/version.rb +4 -0
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. metadata +444 -0
@@ -0,0 +1,24 @@
1
+ require 'sprockets/exporters/base'
2
+
3
+ module Sprockets
4
+ module Exporters
5
+ # Writes a an asset file to disk
6
+ class FileExporter < Exporters::Base
7
+ def skip?(logger)
8
+ if ::File.exist?(target)
9
+ logger.debug "Skipping #{ target }, already exists"
10
+ true
11
+ else
12
+ logger.info "Writing #{ target }"
13
+ false
14
+ end
15
+ end
16
+
17
+ def call
18
+ write(target) do |file|
19
+ file.write(asset.source)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+ require 'sprockets/exporters/base'
2
+ require 'sprockets/utils/gzip'
3
+
4
+ module Sprockets
5
+ module Exporters
6
+ # Generates a `.gz` file using the zlib algorithm built into
7
+ # Ruby's standard library.
8
+ class ZlibExporter < Exporters::Base
9
+ def setup
10
+ @gzip_target = "#{ target }.gz"
11
+ @gzip = Sprockets::Utils::Gzip.new(asset, archiver: Utils::Gzip::ZlibArchiver)
12
+ end
13
+
14
+ def skip?(logger)
15
+ return true if environment.skip_gzip?
16
+ return true if @gzip.cannot_compress?
17
+ if ::File.exist?(@gzip_target)
18
+ logger.debug "Skipping #{ @gzip_target }, already exists"
19
+ true
20
+ else
21
+ logger.info "Writing #{ @gzip_target }"
22
+ false
23
+ end
24
+ end
25
+
26
+ def call
27
+ write(@gzip_target) do |file|
28
+ @gzip.compress(file, target)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ require 'sprockets/exporters/zlib_exporter'
2
+
3
+ module Sprockets
4
+ module Exporters
5
+ # Generates a `.gz` file using the zopfli algorithm from the
6
+ # Zopfli gem.
7
+ class ZopfliExporter < ZlibExporter
8
+ def setup
9
+ @gzip_target = "#{ target }.gz"
10
+ @gzip = Sprockets::Utils::Gzip.new(asset, archiver: Utils::Gzip::ZopfliArchiver)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,73 @@
1
+ module Sprockets
2
+ # `Exporting` is an internal mixin whose public methods are exposed on
3
+ # the `Environment` and `CachedEnvironment` classes.
4
+ module Exporting
5
+ # Exporters are ran on the assets:precompile task
6
+ def exporters
7
+ config[:exporters]
8
+ end
9
+
10
+ # Public: Registers a new Exporter `klass` for `mime_type`.
11
+ #
12
+ # If your exporter depends on one or more other exporters you can
13
+ # specify this via the `depend_on` keyword.
14
+ #
15
+ # register_exporter '*/*', Sprockets::Exporters::ZlibExporter
16
+ #
17
+ # This ensures that `Sprockets::Exporters::File` will always execute before
18
+ # `Sprockets::Exporters::Zlib`
19
+ def register_exporter(mime_types, klass = nil)
20
+ mime_types = Array(mime_types)
21
+
22
+ mime_types.each do |mime_type|
23
+ self.config = hash_reassoc(config, :exporters, mime_type) do |_exporters|
24
+ _exporters << klass
25
+ end
26
+ end
27
+ end
28
+
29
+ # Public: Remove Exporting processor `klass` for `mime_type`.
30
+ #
31
+ # environment.unregister_exporter '*/*', Sprockets::Exporters::Zlib
32
+ #
33
+ # Can be called without a mime type
34
+ #
35
+ # environment.unregister_exporter Sprockets::Exporters::Zlib
36
+ #
37
+ # Does not remove any exporters that depend on `klass`.
38
+ def unregister_exporter(mime_types, exporter = nil)
39
+ unless mime_types.is_a? Array
40
+ if mime_types.is_a? String
41
+ mime_types = [mime_types]
42
+ else # called with no mime type
43
+ exporter = mime_types
44
+ mime_types = nil
45
+ end
46
+ end
47
+
48
+ self.config = hash_reassoc(config, :exporters) do |_exporters|
49
+ _exporters.each do |mime_type, exporters_array|
50
+ next if mime_types && !mime_types.include?(mime_type)
51
+ if exporters_array.include? exporter
52
+ _exporters[mime_type] = exporters_array.dup.delete exporter
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Public: Checks if concurrent exporting is allowed
59
+ def export_concurrent
60
+ config[:export_concurrent]
61
+ end
62
+
63
+ # Public: Enable or disable the concurrently exporting files
64
+ #
65
+ # Defaults to true.
66
+ #
67
+ # environment.export_concurrent = false
68
+ #
69
+ def export_concurrent=(export_concurrent)
70
+ self.config = config.merge(export_concurrent: export_concurrent).freeze
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+
4
+ module Sprockets
5
+ # Internal: The first processor in the pipeline that reads the file into
6
+ # memory and passes it along as `input[:data]`.
7
+ class FileReader
8
+ def self.call(input)
9
+ env = input[:environment]
10
+ data = env.read_file(input[:filename], input[:content_type])
11
+ dependencies = Set.new(input[:metadata][:dependencies])
12
+ dependencies += [env.build_file_digest_uri(input[:filename])]
13
+ { data: data, dependencies: dependencies }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+ module Sprockets
3
+ # Internal: HTTP URI utilities. Many adapted from Rack::Utils. Mixed into
4
+ # Environment.
5
+ module HTTPUtils
6
+ extend self
7
+
8
+ # Public: Test mime type against mime range.
9
+ #
10
+ # match_mime_type?('text/html', 'text/*') => true
11
+ # match_mime_type?('text/plain', '*') => true
12
+ # match_mime_type?('text/html', 'application/json') => false
13
+ #
14
+ # Returns true if the given value is a mime match for the given mime match
15
+ # specification, false otherwise.
16
+ def match_mime_type?(value, matcher)
17
+ v1, v2 = value.split('/'.freeze, 2)
18
+ m1, m2 = matcher.split('/'.freeze, 2)
19
+ (m1 == '*'.freeze || v1 == m1) && (m2.nil? || m2 == '*'.freeze || m2 == v2)
20
+ end
21
+
22
+ # Public: Return values from Hash where the key matches the mime type.
23
+ #
24
+ # hash - Hash of String matcher keys to Object values
25
+ # mime_type - String mime type
26
+ #
27
+ # Returns Array of Object values.
28
+ def match_mime_type_keys(hash, mime_type)
29
+ type, subtype = mime_type.split('/', 2)
30
+ [
31
+ hash["*"],
32
+ hash["*/*"],
33
+ hash["#{type}/*"],
34
+ hash["#{type}/#{subtype}"]
35
+ ].compact
36
+ end
37
+
38
+ # Internal: Parse Accept header quality values.
39
+ #
40
+ # values - String e.g. "application/javascript"
41
+ #
42
+ # Adapted from Rack::Utils#q_values. Quality values are
43
+ # described in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
44
+ #
45
+ # parse_q_values("application/javascript")
46
+ # # => [["application/javascript", 1.0]]
47
+ #
48
+ # parse_q_values("*/*")
49
+ # # => [["*/*", 1.0]]
50
+ #
51
+ # parse_q_values("text/plain; q=0.5, image/*")
52
+ # # => [["text/plain", 0.5], ["image/*", 1.0]]
53
+ #
54
+ # parse_q_values("application/javascript, text/css")
55
+ # # => [["application/javascript", 1.0], ["text/css", 1.0]]
56
+ #
57
+ # Returns an Array of [String, Float].
58
+ def parse_q_values(values)
59
+ values.to_s.split(/\s*,\s*/).map do |part|
60
+ value, parameters = part.split(/\s*;\s*/, 2)
61
+ quality = 1.0
62
+ if md = /\Aq=([\d.]+)/.match(parameters)
63
+ quality = md[1].to_f
64
+ end
65
+ [value, quality]
66
+ end
67
+ end
68
+
69
+ # Internal: Find all qvalue matches from an Array of available options.
70
+ #
71
+ # Adapted from Rack::Utils#q_values.
72
+ #
73
+ # Returns Array of matched Strings from available Array or [].
74
+ def find_q_matches(q_values, available, &matcher)
75
+ matcher ||= lambda { |a, b| a == b }
76
+
77
+ matches = []
78
+
79
+ case q_values
80
+ when Array
81
+ when String
82
+ q_values = parse_q_values(q_values)
83
+ when NilClass
84
+ q_values = []
85
+ else
86
+ raise TypeError, "unknown q_values type: #{q_values.class}"
87
+ end
88
+
89
+ i = 0
90
+ q_values.each do |accepted, quality|
91
+ if match = available.find { |option| matcher.call(option, accepted) }
92
+ i += 1
93
+ matches << [-quality, i, match]
94
+ end
95
+ end
96
+
97
+ matches.sort!
98
+ matches.map! { |_, _, match| match }
99
+ matches
100
+ end
101
+
102
+ # Internal: Find the best qvalue match from an Array of available options.
103
+ #
104
+ # Adapted from Rack::Utils#q_values.
105
+ #
106
+ # Returns the matched String from available Array or nil.
107
+ def find_best_q_match(q_values, available, &matcher)
108
+ find_q_matches(q_values, available, &matcher).first
109
+ end
110
+
111
+ # Internal: Find the all qvalue match from an Array of available mime type
112
+ # options.
113
+ #
114
+ # Adapted from Rack::Utils#q_values.
115
+ #
116
+ # Returns Array of matched mime type Strings from available Array or [].
117
+ def find_mime_type_matches(q_value_header, available)
118
+ find_q_matches(q_value_header, available) do |a, b|
119
+ match_mime_type?(a, b)
120
+ end
121
+ end
122
+
123
+ # Internal: Find the best qvalue match from an Array of available mime type
124
+ # options.
125
+ #
126
+ # Adapted from Rack::Utils#q_values.
127
+ #
128
+ # Returns the matched mime type String from available Array or nil.
129
+ def find_best_mime_type_match(q_value_header, available)
130
+ find_best_q_match(q_value_header, available) do |a, b|
131
+ match_mime_type?(a, b)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/autoload'
3
+ require 'sprockets/digest_utils'
4
+
5
+ module Sprockets
6
+ class JSMincCompressor
7
+ VERSION = '1'
8
+
9
+ def self.instance
10
+ @instance ||= new
11
+ end
12
+
13
+ def self.call(input)
14
+ instance.call(input)
15
+ end
16
+
17
+ def self.cache_key
18
+ instance.cache_key
19
+ end
20
+
21
+ attr_reader :cache_key
22
+
23
+ def initialize(options = {})
24
+ @compressor_class = Autoload::JSMinC
25
+ @cache_key = "#{self.class.name}:#{Autoload::JSMinC::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
26
+ end
27
+
28
+ def call(input)
29
+ @compressor_class.minify(input[:data])
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ module Sprockets
3
+ # Public: JST transformer.
4
+ #
5
+ # Exports server side compiled templates to an object.
6
+ #
7
+ # Name your template "users/show.ejs", "users/new.eco", etc.
8
+ #
9
+ # To accept the default options
10
+ #
11
+ # environment.register_transformer
12
+ # 'application/javascript+function',
13
+ # 'application/javascript', JstProcessor
14
+ #
15
+ # Change the default namespace.
16
+ #
17
+ # environment.register_transformer
18
+ # 'application/javascript+function',
19
+ # 'application/javascript', JstProcessor.new(namespace: 'App.templates')
20
+ #
21
+ class JstProcessor
22
+ def self.default_namespace
23
+ 'this.JST'
24
+ end
25
+
26
+ # Public: Return singleton instance with default options.
27
+ #
28
+ # Returns JstProcessor object.
29
+ def self.instance
30
+ @instance ||= new
31
+ end
32
+
33
+ def self.call(input)
34
+ instance.call(input)
35
+ end
36
+
37
+ def initialize(namespace: self.class.default_namespace)
38
+ @namespace = namespace
39
+ end
40
+
41
+ def call(input)
42
+ data = input[:data].gsub(/$(.)/m, "\\1 ").strip
43
+ key = input[:name]
44
+ <<-JST
45
+ (function() { #{@namespace} || (#{@namespace} = {}); #{@namespace}[#{key.inspect}] = #{data};
46
+ }).call(this);
47
+ JST
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/asset'
3
+ require 'sprockets/digest_utils'
4
+ require 'sprockets/errors'
5
+ require 'sprockets/file_reader'
6
+ require 'sprockets/mime'
7
+ require 'sprockets/path_utils'
8
+ require 'sprockets/processing'
9
+ require 'sprockets/processor_utils'
10
+ require 'sprockets/resolve'
11
+ require 'sprockets/transformers'
12
+ require 'sprockets/uri_utils'
13
+ require 'sprockets/unloaded_asset'
14
+
15
+ module Sprockets
16
+
17
+ # The loader phase takes a asset URI location and returns a constructed Asset
18
+ # object.
19
+ module Loader
20
+ include DigestUtils, PathUtils, ProcessorUtils, URIUtils
21
+ include Mime, Processing, Resolve, Transformers
22
+
23
+
24
+ # Public: Load Asset by Asset URI.
25
+ #
26
+ # uri - A String containing complete URI to a file including schema
27
+ # and full path such as:
28
+ # "file:///Path/app/assets/js/app.js?type=application/javascript"
29
+ #
30
+ # Returns Asset.
31
+ def load(uri)
32
+ unloaded = UnloadedAsset.new(uri, self)
33
+ if unloaded.params.key?(:id)
34
+ unless asset = asset_from_cache(unloaded.asset_key)
35
+ id = unloaded.params.delete(:id)
36
+ uri_without_id = build_asset_uri(unloaded.filename, unloaded.params)
37
+ asset = load_from_unloaded(UnloadedAsset.new(uri_without_id, self))
38
+ if asset[:id] != id
39
+ @logger.warn "Sprockets load error: Tried to find #{uri}, but latest was id #{asset[:id]}"
40
+ end
41
+ end
42
+ else
43
+ asset = fetch_asset_from_dependency_cache(unloaded) do |paths|
44
+ # When asset is previously generated, its "dependencies" are stored in the cache.
45
+ # The presence of `paths` indicates dependencies were stored.
46
+ # We can check to see if the dependencies have not changed by "resolving" them and
47
+ # generating a digest key from the resolved entries. If this digest key has not
48
+ # changed, the asset will be pulled from cache.
49
+ #
50
+ # If this `paths` is present but the cache returns nothing then `fetch_asset_from_dependency_cache`
51
+ # will confusingly be called again with `paths` set to nil where the asset will be
52
+ # loaded from disk.
53
+ if paths
54
+ digest = DigestUtils.digest(resolve_dependencies(paths))
55
+ if uri_from_cache = cache.get(unloaded.digest_key(digest), true)
56
+ asset_from_cache(UnloadedAsset.new(uri_from_cache, self).asset_key)
57
+ end
58
+ else
59
+ load_from_unloaded(unloaded)
60
+ end
61
+ end
62
+ end
63
+ Asset.new(asset)
64
+ end
65
+
66
+ private
67
+ def compress_key_from_hash(hash, key)
68
+ return unless hash.key?(key)
69
+ value = hash[key].dup
70
+ return if !value
71
+
72
+ if block_given?
73
+ value.map! do |x|
74
+ if yield x
75
+ compress_from_root(x)
76
+ else
77
+ x
78
+ end
79
+ end
80
+ else
81
+ value.map! { |x| compress_from_root(x) }
82
+ end
83
+ hash[key] = value
84
+ end
85
+
86
+
87
+ def expand_key_from_hash(hash, key)
88
+ return unless hash.key?(key)
89
+ value = hash[key].dup
90
+ return if !value
91
+ if block_given?
92
+ value.map! do |x|
93
+ if yield x
94
+ expand_from_root(x)
95
+ else
96
+ x
97
+ end
98
+ end
99
+ else
100
+ value.map! { |x| expand_from_root(x) }
101
+ end
102
+ hash[key] = value
103
+ end
104
+
105
+ # Internal: Load asset hash from cache
106
+ #
107
+ # key - A String containing lookup information for an asset
108
+ #
109
+ # This method converts all "compressed" paths to absolute paths.
110
+ # Returns a hash of values representing an asset
111
+ def asset_from_cache(key)
112
+ asset = cache.get(key, true)
113
+ if asset
114
+ asset[:uri] = expand_from_root(asset[:uri])
115
+ asset[:load_path] = expand_from_root(asset[:load_path])
116
+ asset[:filename] = expand_from_root(asset[:filename])
117
+ expand_key_from_hash(asset[:metadata], :included)
118
+ expand_key_from_hash(asset[:metadata], :links)
119
+ expand_key_from_hash(asset[:metadata], :stubbed)
120
+ expand_key_from_hash(asset[:metadata], :required)
121
+ expand_key_from_hash(asset[:metadata], :to_load)
122
+ expand_key_from_hash(asset[:metadata], :to_link)
123
+ expand_key_from_hash(asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
124
+
125
+ asset[:metadata].each_key do |k|
126
+ next unless k.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
127
+ expand_key_from_hash(asset[:metadata], k)
128
+ end
129
+ end
130
+ asset
131
+ end
132
+
133
+ # Internal: Loads an asset and saves it to cache
134
+ #
135
+ # unloaded - An UnloadedAsset
136
+ #
137
+ # This method is only called when the given unloaded asset could not be
138
+ # successfully pulled from cache.
139
+ def load_from_unloaded(unloaded)
140
+ unless file?(unloaded.filename)
141
+ raise FileNotFound, "could not find file: #{unloaded.filename}"
142
+ end
143
+
144
+ path_to_split =
145
+ if index_alias = unloaded.params[:index_alias]
146
+ expand_from_root index_alias
147
+ else
148
+ unloaded.filename
149
+ end
150
+
151
+ load_path, logical_path = paths_split(config[:paths], path_to_split)
152
+
153
+ unless load_path
154
+ target = path_to_split
155
+ target += " (index alias of #{unloaded.filename})" if unloaded.params[:index_alias]
156
+ raise FileOutsidePaths, "#{target} is no longer under a load path: #{self.paths.join(', ')}"
157
+ end
158
+
159
+ extname, file_type = match_path_extname(logical_path, mime_exts)
160
+ logical_path = logical_path.chomp(extname)
161
+ name = logical_path
162
+
163
+ if pipeline = unloaded.params[:pipeline]
164
+ logical_path += ".#{pipeline}"
165
+ end
166
+
167
+ if type = unloaded.params[:type]
168
+ logical_path += config[:mime_types][type][:extensions].first
169
+ end
170
+
171
+ if type != file_type && !config[:transformers][file_type][type]
172
+ raise ConversionError, "could not convert #{file_type.inspect} to #{type.inspect}"
173
+ end
174
+
175
+ processors = processors_for(type, file_type, pipeline)
176
+
177
+ processors_dep_uri = build_processors_uri(type, file_type, pipeline)
178
+ dependencies = config[:dependencies] + [processors_dep_uri]
179
+
180
+ # Read into memory and process if theres a processor pipeline
181
+ if processors.any?
182
+ result = call_processors(processors, {
183
+ environment: self,
184
+ cache: self.cache,
185
+ uri: unloaded.uri,
186
+ filename: unloaded.filename,
187
+ load_path: load_path,
188
+ name: name,
189
+ content_type: type,
190
+ metadata: {
191
+ dependencies: dependencies
192
+ }
193
+ })
194
+ validate_processor_result!(result)
195
+ source = result.delete(:data)
196
+ metadata = result
197
+ metadata[:charset] = source.encoding.name.downcase unless metadata.key?(:charset)
198
+ metadata[:digest] = digest(source)
199
+ metadata[:length] = source.bytesize
200
+ metadata[:environment_version] = version
201
+ else
202
+ dependencies << build_file_digest_uri(unloaded.filename)
203
+ metadata = {
204
+ digest: file_digest(unloaded.filename),
205
+ length: self.stat(unloaded.filename).size,
206
+ dependencies: dependencies,
207
+ environment_version: version,
208
+ }
209
+ end
210
+
211
+ asset = {
212
+ uri: unloaded.uri,
213
+ load_path: load_path,
214
+ filename: unloaded.filename,
215
+ name: name,
216
+ logical_path: logical_path,
217
+ content_type: type,
218
+ source: source,
219
+ metadata: metadata,
220
+ dependencies_digest: DigestUtils.digest(resolve_dependencies(metadata[:dependencies]))
221
+ }
222
+
223
+ asset[:id] = hexdigest(asset)
224
+ asset[:uri] = build_asset_uri(unloaded.filename, unloaded.params.merge(id: asset[:id]))
225
+
226
+ store_asset(asset, unloaded)
227
+ asset
228
+ end
229
+
230
+ # Internal: Save a given asset to the cache
231
+ #
232
+ # asset - A hash containing values of loaded asset
233
+ # unloaded - The UnloadedAsset used to lookup the `asset`
234
+ #
235
+ # This method converts all absolute paths to "compressed" paths
236
+ # which are relative if they're in the root.
237
+ def store_asset(asset, unloaded)
238
+ # Save the asset in the cache under the new URI
239
+ cached_asset = asset.dup
240
+ cached_asset[:uri] = compress_from_root(asset[:uri])
241
+ cached_asset[:filename] = compress_from_root(asset[:filename])
242
+ cached_asset[:load_path] = compress_from_root(asset[:load_path])
243
+
244
+ if cached_asset[:metadata]
245
+ # Deep dup to avoid modifying `asset`
246
+ cached_asset[:metadata] = cached_asset[:metadata].dup
247
+ compress_key_from_hash(cached_asset[:metadata], :included)
248
+ compress_key_from_hash(cached_asset[:metadata], :links)
249
+ compress_key_from_hash(cached_asset[:metadata], :stubbed)
250
+ compress_key_from_hash(cached_asset[:metadata], :required)
251
+ compress_key_from_hash(cached_asset[:metadata], :to_load)
252
+ compress_key_from_hash(cached_asset[:metadata], :to_link)
253
+ compress_key_from_hash(cached_asset[:metadata], :dependencies) { |uri| uri.start_with?("file-digest://") }
254
+
255
+ cached_asset[:metadata].each do |key, value|
256
+ next unless key.match?(/_dependencies\z/) # rubocop:disable Performance/EndWith
257
+ compress_key_from_hash(cached_asset[:metadata], key)
258
+ end
259
+ end
260
+
261
+ # Unloaded asset and stored_asset now have a different URI
262
+ stored_asset = UnloadedAsset.new(asset[:uri], self)
263
+ cache.set(stored_asset.asset_key, cached_asset, true)
264
+
265
+ # Save the new relative path for the digest key of the unloaded asset
266
+ cache.set(unloaded.digest_key(asset[:dependencies_digest]), stored_asset.compressed_path, true)
267
+ end
268
+
269
+
270
+ # Internal: Resolve set of dependency URIs.
271
+ #
272
+ # uris - An Array of "dependencies" for example:
273
+ # ["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
274
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
275
+ # "processors:type=text/css&file_type=text/css&pipeline=self",
276
+ # "file-digest:///Full/path/app/assets/stylesheets"]
277
+ #
278
+ # Returns back array of things that the given uri depends on
279
+ # For example the environment version, if you're using a different version of sprockets
280
+ # then the dependencies should be different, this is used only for generating cache key
281
+ # for example the "environment-version" may be resolved to "environment-1.0-3.2.0" for
282
+ # version "3.2.0" of sprockets.
283
+ #
284
+ # Any paths that are returned are converted to relative paths
285
+ #
286
+ # Returns array of resolved dependencies
287
+ def resolve_dependencies(uris)
288
+ uris.map { |uri| resolve_dependency(uri) }
289
+ end
290
+
291
+ # Internal: Retrieves an asset based on its digest
292
+ #
293
+ # unloaded - An UnloadedAsset
294
+ # limit - A Fixnum which sets the maximum number of versions of "histories"
295
+ # stored in the cache
296
+ #
297
+ # This method attempts to retrieve the last `limit` number of histories of an asset
298
+ # from the cache a "history" which is an array of unresolved "dependencies" that the asset needs
299
+ # to compile. In this case a dependency can refer to either an asset e.g. index.js
300
+ # may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
301
+ # compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
302
+ # are used.
303
+ #
304
+ # For example a history array may look something like this
305
+ #
306
+ # [["environment-version", "environment-paths", "processors:type=text/css&file_type=text/css",
307
+ # "file-digest:///Full/path/app/assets/stylesheets/application.css",
308
+ # "processors:type=text/css&file_digesttype=text/css&pipeline=self",
309
+ # "file-digest:///Full/path/app/assets/stylesheets"]]
310
+ #
311
+ # Where the first entry is a Set of dependencies for last generated version of that asset.
312
+ # Multiple versions are stored since Sprockets keeps the last `limit` number of assets
313
+ # generated present in the system.
314
+ #
315
+ # If a "history" of dependencies is present in the cache, each version of "history" will be
316
+ # yielded to the passed block which is responsible for loading the asset. If found, the existing
317
+ # history will be saved with the dependency that found a valid asset moved to the front.
318
+ #
319
+ # If no history is present, or if none of the histories could be resolved to a valid asset then,
320
+ # the block is yielded to and expected to return a valid asset.
321
+ # When this happens the dependencies for the returned asset are added to the "history", and older
322
+ # entries are removed if the "history" is above `limit`.
323
+ def fetch_asset_from_dependency_cache(unloaded, limit = 3)
324
+ key = unloaded.dependency_history_key
325
+
326
+ history = cache.get(key) || []
327
+ history.each_with_index do |deps, index|
328
+ expanded_deps = deps.map do |path|
329
+ path.start_with?("file-digest://") ? expand_from_root(path) : path
330
+ end
331
+ if asset = yield(expanded_deps)
332
+ cache.set(key, history.rotate!(index)) if index > 0
333
+ return asset
334
+ end
335
+ end
336
+
337
+ asset = yield
338
+ deps = asset[:metadata][:dependencies].dup.map! do |uri|
339
+ uri.start_with?("file-digest://") ? compress_from_root(uri) : uri
340
+ end
341
+ cache.set(key, history.unshift(deps).take(limit))
342
+ asset
343
+ end
344
+ end
345
+ end