sprockets 3.0.3 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +101 -0
  3. data/{LICENSE → MIT-LICENSE} +2 -2
  4. data/README.md +531 -276
  5. data/bin/sprockets +12 -7
  6. data/lib/rake/sprocketstask.rb +9 -4
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +41 -28
  9. data/lib/sprockets/autoload/babel.rb +8 -0
  10. data/lib/sprockets/autoload/closure.rb +1 -0
  11. data/lib/sprockets/autoload/coffee_script.rb +1 -0
  12. data/lib/sprockets/autoload/eco.rb +1 -0
  13. data/lib/sprockets/autoload/ejs.rb +1 -0
  14. data/lib/sprockets/autoload/jsminc.rb +8 -0
  15. data/lib/sprockets/autoload/sass.rb +1 -0
  16. data/lib/sprockets/autoload/sassc.rb +8 -0
  17. data/lib/sprockets/autoload/uglifier.rb +1 -0
  18. data/lib/sprockets/autoload/yui.rb +1 -0
  19. data/lib/sprockets/autoload/zopfli.rb +7 -0
  20. data/lib/sprockets/autoload.rb +5 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +61 -13
  23. data/lib/sprockets/bower.rb +6 -3
  24. data/lib/sprockets/bundle.rb +41 -5
  25. data/lib/sprockets/cache/file_store.rb +32 -7
  26. data/lib/sprockets/cache/memory_store.rb +28 -10
  27. data/lib/sprockets/cache/null_store.rb +8 -0
  28. data/lib/sprockets/cache.rb +43 -6
  29. data/lib/sprockets/cached_environment.rb +15 -20
  30. data/lib/sprockets/closure_compressor.rb +6 -11
  31. data/lib/sprockets/coffee_script_processor.rb +20 -6
  32. data/lib/sprockets/compressing.rb +62 -2
  33. data/lib/sprockets/configuration.rb +5 -9
  34. data/lib/sprockets/context.rb +99 -25
  35. data/lib/sprockets/dependencies.rb +10 -9
  36. data/lib/sprockets/digest_utils.rb +103 -62
  37. data/lib/sprockets/directive_processor.rb +64 -36
  38. data/lib/sprockets/eco_processor.rb +4 -3
  39. data/lib/sprockets/ejs_processor.rb +4 -3
  40. data/lib/sprockets/encoding_utils.rb +1 -0
  41. data/lib/sprockets/environment.rb +9 -4
  42. data/lib/sprockets/erb_processor.rb +34 -21
  43. data/lib/sprockets/errors.rb +1 -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 +1 -0
  50. data/lib/sprockets/http_utils.rb +25 -7
  51. data/lib/sprockets/jsminc_compressor.rb +32 -0
  52. data/lib/sprockets/jst_processor.rb +11 -10
  53. data/lib/sprockets/loader.rb +244 -62
  54. data/lib/sprockets/manifest.rb +100 -46
  55. data/lib/sprockets/manifest_utils.rb +9 -6
  56. data/lib/sprockets/mime.rb +8 -42
  57. data/lib/sprockets/npm.rb +52 -0
  58. data/lib/sprockets/path_dependency_utils.rb +3 -11
  59. data/lib/sprockets/path_digest_utils.rb +2 -1
  60. data/lib/sprockets/path_utils.rb +107 -22
  61. data/lib/sprockets/paths.rb +1 -0
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +32 -52
  64. data/lib/sprockets/processor_utils.rb +38 -39
  65. data/lib/sprockets/resolve.rb +177 -97
  66. data/lib/sprockets/sass_cache_store.rb +1 -0
  67. data/lib/sprockets/sass_compressor.rb +21 -17
  68. data/lib/sprockets/sass_functions.rb +1 -0
  69. data/lib/sprockets/sass_importer.rb +1 -0
  70. data/lib/sprockets/sass_processor.rb +46 -18
  71. data/lib/sprockets/sassc_compressor.rb +56 -0
  72. data/lib/sprockets/sassc_processor.rb +297 -0
  73. data/lib/sprockets/server.rb +77 -44
  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 +63 -35
  77. data/lib/sprockets/uglifier_compressor.rb +23 -20
  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 +14 -14
  81. data/lib/sprockets/utils/gzip.rb +99 -0
  82. data/lib/sprockets/utils.rb +63 -71
  83. data/lib/sprockets/version.rb +2 -1
  84. data/lib/sprockets/yui_compressor.rb +5 -14
  85. data/lib/sprockets.rb +105 -33
  86. metadata +157 -27
  87. data/lib/sprockets/coffee_script_template.rb +0 -6
  88. data/lib/sprockets/eco_template.rb +0 -6
  89. data/lib/sprockets/ejs_template.rb +0 -6
  90. data/lib/sprockets/engines.rb +0 -81
  91. data/lib/sprockets/erb_template.rb +0 -6
  92. data/lib/sprockets/legacy.rb +0 -314
  93. data/lib/sprockets/legacy_proc_processor.rb +0 -35
  94. data/lib/sprockets/legacy_tilt_processor.rb +0 -29
  95. data/lib/sprockets/sass_template.rb +0 -7
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
  require 'sprockets/http_utils'
3
4
  require 'sprockets/path_dependency_utils'
@@ -20,27 +21,21 @@ module Sprockets
20
21
  # # => "file:///path/to/app/javascripts/application.coffee?type=application/javascript"
21
22
  #
22
23
  # The String Asset URI is returned or nil if no results are found.
23
- def resolve(path, options = {})
24
- path = path.to_s
25
- paths = options[:load_paths] || config[:paths]
26
- accept = options[:accept]
24
+ def resolve(path, load_paths: config[:paths], accept: nil, pipeline: nil, base_path: nil)
25
+ paths = load_paths
27
26
 
28
27
  if valid_asset_uri?(path)
29
28
  uri, deps = resolve_asset_uri(path)
30
29
  elsif absolute_path?(path)
31
30
  filename, type, deps = resolve_absolute_path(paths, path, accept)
32
31
  elsif relative_path?(path)
33
- filename, type, pipeline, deps = resolve_relative_path(paths, path, options[:base_path], accept)
32
+ filename, type, path_pipeline, deps, index_alias = resolve_relative_path(paths, path, base_path, accept)
34
33
  else
35
- filename, type, pipeline, deps = resolve_logical_path(paths, path, accept)
34
+ filename, type, path_pipeline, deps, index_alias = resolve_logical_path(paths, path, accept)
36
35
  end
37
36
 
38
37
  if filename
39
- params = {}
40
- params[:type] = type if type
41
- params[:pipeline] = pipeline if pipeline
42
- params[:pipeline] = options[:pipeline] if options[:pipeline]
43
- uri = build_asset_uri(filename, params)
38
+ uri = build_asset_uri(filename, type: type, pipeline: pipeline || path_pipeline, index_alias: index_alias)
44
39
  end
45
40
 
46
41
  return uri, deps
@@ -48,18 +43,21 @@ module Sprockets
48
43
 
49
44
  # Public: Same as resolve() but raises a FileNotFound exception instead of
50
45
  # nil if no assets are found.
51
- def resolve!(path, options = {})
52
- uri, deps = resolve(path, options.merge(compat: false))
46
+ def resolve!(path, **kargs)
47
+ uri, deps = resolve(path, **kargs)
53
48
 
54
49
  unless uri
55
- message = "couldn't find file '#{path}'"
50
+ message = +"couldn't find file '#{path}'"
56
51
 
57
- if relative_path?(path) && options[:base_path]
58
- load_path, _ = paths_split(config[:paths], options[:base_path])
52
+ if relative_path?(path) && kargs[:base_path]
53
+ load_path, _ = paths_split(config[:paths], kargs[:base_path])
59
54
  message << " under '#{load_path}'"
60
55
  end
61
56
 
62
- message << " with type '#{options[:accept]}'" if options[:accept]
57
+ message << " with type '#{kargs[:accept]}'" if kargs[:accept]
58
+
59
+ load_paths = kargs[:load_paths] || config[:paths]
60
+ message << "\nChecked in these paths: \n #{ load_paths.join("\n ") }"
63
61
 
64
62
  raise FileNotFound, message
65
63
  end
@@ -68,148 +66,230 @@ module Sprockets
68
66
  end
69
67
 
70
68
  protected
69
+
70
+ # Internal: Finds an asset given a URI
71
+ #
72
+ # uri - String. Contains file:// scheme, absolute path to
73
+ # file.
74
+ # e.g. "file:///Users/schneems/sprockets/test/fixtures/default/gallery.js?type=application/javascript"
75
+ #
76
+ # Returns Array. Contains a String uri and Set of dependencies
71
77
  def resolve_asset_uri(uri)
72
- filename, _ = parse_asset_uri(uri)
73
- return uri, Set.new([build_file_digest_uri(filename)])
78
+ filename, _ = URIUtils.parse_asset_uri(uri)
79
+ return uri, Set.new( [URIUtils.build_file_digest_uri(filename)] )
74
80
  end
75
81
 
82
+ # Internal: Finds a file in a set of given paths
83
+ #
84
+ # paths - Array of Strings.
85
+ # filename - String containing absolute path to a file including extension.
86
+ # e.g. "/Users/schneems/sprockets/test/fixtures/asset/application.js"
87
+ # accept - String. A Quality value incoded set of
88
+ # mime types that we are looking for. Can be nil.
89
+ # e.g. "application/javascript" or "text/css, */*"
90
+ #
91
+ # Returns Array. Filename, type, path_pipeline, deps, index_alias
76
92
  def resolve_absolute_path(paths, filename, accept)
77
93
  deps = Set.new
78
94
  filename = File.expand_path(filename)
79
95
 
80
96
  # Ensure path is under load paths
81
- return nil, nil, deps unless paths_split(paths, filename)
97
+ return nil, nil, deps unless PathUtils.paths_split(paths, filename)
82
98
 
83
- _, mime_type, _, _ = parse_path_extnames(filename)
99
+ _, mime_type = PathUtils.match_path_extname(filename, config[:mime_exts])
84
100
  type = resolve_transform_type(mime_type, accept)
85
101
  return nil, nil, deps if accept && !type
86
102
 
87
103
  return nil, nil, deps unless file?(filename)
88
104
 
89
- deps << build_file_digest_uri(filename)
105
+ deps << URIUtils.build_file_digest_uri(filename)
90
106
  return filename, type, deps
91
107
  end
92
108
 
109
+ # Internal: Finds a relative file in a set of given paths
110
+ #
111
+ # paths - Array of Strings.
112
+ # path - String. A relative filename with or without extension
113
+ # e.g. "./jquery" or "../foo.js"
114
+ # dirname - String. Base path where we start looking for the given file.
115
+ # accept - String. A Quality value incoded set of
116
+ # mime types that we are looking for. Can be nil.
117
+ # e.g. "application/javascript" or "text/css, */*"
118
+ #
119
+ # Returns Array. Filename, type, path_pipeline, deps, index_alias
93
120
  def resolve_relative_path(paths, path, dirname, accept)
94
121
  filename = File.expand_path(path, dirname)
95
- load_path, _ = paths_split(paths, dirname)
96
- if load_path && logical_path = split_subpath(load_path, filename)
122
+ load_path, _ = PathUtils.paths_split(paths, dirname)
123
+ if load_path && logical_path = PathUtils.split_subpath(load_path, filename)
97
124
  resolve_logical_path([load_path], logical_path, accept)
98
125
  else
99
- return nil, nil, Set.new
126
+ return nil, nil, nil, Set.new
100
127
  end
101
128
  end
102
129
 
130
+ # Internal: Finds a file in a set of given paths
131
+ #
132
+ # paths - Array of Strings.
133
+ # logical_path - String. A filename with extension
134
+ # e.g. "coffee/foo.js" or "foo.js"
135
+ # accept - String. A Quality value incoded set of
136
+ # mime types that we are looking for. Can be nil.
137
+ # e.g. "application/javascript" or "text/css, */*"
138
+ #
139
+ # Finds a file on the given paths.
140
+ #
141
+ # Returns Array. Filename, type, path_pipeline, deps, index_alias
103
142
  def resolve_logical_path(paths, logical_path, accept)
104
- logical_name, mime_type, _, pipeline = parse_path_extnames(logical_path)
143
+ extname, mime_type = PathUtils.match_path_extname(logical_path, config[:mime_exts])
144
+ logical_name = logical_path.chomp(extname)
145
+
146
+ extname, pipeline = PathUtils.match_path_extname(logical_name, config[:pipeline_exts])
147
+ logical_name = logical_name.chomp(extname)
148
+
105
149
  parsed_accept = parse_accept_options(mime_type, accept)
106
150
  transformed_accepts = expand_transform_accepts(parsed_accept)
107
- filename, mime_type, deps = resolve_under_paths(paths, logical_name, transformed_accepts)
151
+
152
+ filename, mime_type, deps, index_alias = resolve_under_paths(paths, logical_name, transformed_accepts)
108
153
 
109
154
  if filename
110
155
  deps << build_file_digest_uri(filename)
111
156
  type = resolve_transform_type(mime_type, parsed_accept)
112
- return filename, type, pipeline, deps
157
+ return filename, type, pipeline, deps, index_alias
113
158
  else
114
159
  return nil, nil, nil, deps
115
160
  end
116
161
  end
117
162
 
163
+ # Internal: Finds a file in a set of given paths
164
+ #
165
+ # paths - Array of Strings.
166
+ # logical_name - String. A filename without extension
167
+ # e.g. "application" or "coffee/foo"
168
+ # accepts - Array of array containing mime/version pairs
169
+ # e.g. [["application/javascript", 1.0]]
170
+ #
171
+ # Finds a file with the same name as `logical_name` or "index" inside
172
+ # of the `logical_name` directory that matches a valid mime-type/version from
173
+ # `accepts`.
174
+ #
175
+ # Returns Array. Filename, type, dependencies, and index_alias
118
176
  def resolve_under_paths(paths, logical_name, accepts)
119
- all_deps = Set.new
120
- return nil, nil, all_deps if accepts.empty?
177
+ deps = Set.new
178
+ return nil, nil, deps if accepts.empty?
179
+
180
+ # TODO: Allow new path resolves to be registered
181
+ @resolvers ||= [
182
+ method(:resolve_main_under_path),
183
+ method(:resolve_alts_under_path),
184
+ method(:resolve_index_under_path)
185
+ ]
186
+ mime_exts = config[:mime_exts]
121
187
 
122
- logical_basename = File.basename(logical_name)
123
188
  paths.each do |load_path|
124
- candidates, deps = path_matches(load_path, logical_name, logical_basename)
125
- all_deps.merge(deps)
126
- candidate = find_best_q_match(accepts, candidates) do |c, matcher|
127
- match_mime_type?(c[1] || "application/octet-stream", matcher)
189
+ candidates = []
190
+ @resolvers.each do |fn|
191
+ result = fn.call(load_path, logical_name, mime_exts)
192
+ candidates.concat(result[0])
193
+ deps.merge(result[1])
128
194
  end
129
- return candidate + [all_deps] if candidate
130
- end
131
195
 
132
- return nil, nil, all_deps
133
- end
134
-
135
- def parse_accept_options(mime_type, types)
136
- accepts = []
137
- accepts += parse_q_values(types) if types
138
-
139
- if mime_type
140
- if accepts.empty? || accepts.any? { |accept, _| match_mime_type?(mime_type, accept) }
141
- accepts = [[mime_type, 1.0]]
142
- else
143
- return []
196
+ candidate = HTTPUtils.find_best_q_match(accepts, candidates) do |c, matcher|
197
+ match_mime_type?(c[:type] || "application/octet-stream", matcher)
144
198
  end
199
+ return candidate[:filename], candidate[:type], deps, candidate[:index_alias] if candidate
145
200
  end
146
201
 
147
- if accepts.empty?
148
- accepts << ['*/*', 1.0]
149
- end
150
-
151
- accepts
202
+ return nil, nil, deps
152
203
  end
153
204
 
154
- def normalize_logical_path(path)
155
- dirname, basename = File.split(path)
156
- path = dirname if basename == 'index'
157
- path
158
- end
159
-
160
- def path_matches(load_path, logical_name, logical_basename)
205
+ # Internal: Finds candidate files on a given path
206
+ #
207
+ # load_path - String. An absolute path to a directory
208
+ # logical_name - String. A filename without extension
209
+ # e.g. "application" or "coffee/foo"
210
+ # mime_exts - Hash of file extensions and their mime types
211
+ # e.g. {".xml.builder"=>"application/xml+builder"}
212
+ #
213
+ # Finds files that match a given `logical_name` with an acceptable
214
+ # mime type that is included in `mime_exts` on the `load_path`.
215
+ #
216
+ # Returns Array. First element is an Array of hashes or empty, second is a String
217
+ def resolve_main_under_path(load_path, logical_name, mime_exts)
161
218
  dirname = File.dirname(File.join(load_path, logical_name))
162
- candidates = dirname_matches(dirname, logical_basename)
163
- deps = file_digest_dependency_set(dirname)
164
-
165
- result = resolve_alternates(load_path, logical_name)
166
- result[0].each do |fn|
167
- candidates << [fn, parse_path_extnames(fn)[1]]
219
+ candidates = self.find_matching_path_for_extensions(dirname, File.basename(logical_name), mime_exts)
220
+ candidates.map! do |c|
221
+ { filename: c[0], type: c[1] }
168
222
  end
169
- deps.merge(result[1])
223
+ return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
224
+ end
225
+
170
226
 
227
+ # Internal: Finds candidate index files in a given path
228
+ #
229
+ # load_path - String. An absolute path to a directory
230
+ # logical_name - String. A filename without extension
231
+ # e.g. "application" or "coffee/foo"
232
+ # mime_exts - Hash of file extensions and their mime types
233
+ # e.g. {".xml.builder"=>"application/xml+builder"}
234
+ #
235
+ # Looking in the given `load_path` this method will find all files under the `logical_name` directory
236
+ # that are named `index` and have a matching mime type in `mime_exts`.
237
+ #
238
+ # Returns Array. First element is an Array of hashes or empty, second is a String
239
+ def resolve_index_under_path(load_path, logical_name, mime_exts)
171
240
  dirname = File.join(load_path, logical_name)
172
- if directory? dirname
173
- result = dirname_matches(dirname, "index")
174
- candidates.concat(result)
241
+
242
+ if self.directory?(dirname)
243
+ candidates = self.find_matching_path_for_extensions(dirname, "index".freeze, mime_exts)
244
+ else
245
+ candidates = []
175
246
  end
176
247
 
177
- deps.merge(file_digest_dependency_set(dirname))
248
+ candidates.map! do |c|
249
+ { filename: c[0],
250
+ type: c[1],
251
+ index_alias: compress_from_root(c[0].sub(/\/index(\.[^\/]+)$/, '\1')) }
252
+ end
178
253
 
179
- return candidates.select { |fn, _| file?(fn) }, deps
254
+ return candidates, [ URIUtils.build_file_digest_uri(dirname) ]
180
255
  end
181
256
 
182
- def dirname_matches(dirname, basename)
183
- candidates = []
184
- entries = self.entries(dirname)
185
- entries.each do |entry|
186
- name, type, _, _ = parse_path_extnames(entry)
187
- if basename == name
188
- candidates << [File.join(dirname, entry), type]
189
- end
257
+ def resolve_alts_under_path(load_path, logical_name, mime_exts)
258
+ filenames, deps = self.resolve_alternates(load_path, logical_name)
259
+ filenames.map! do |fn|
260
+ _, mime_type = PathUtils.match_path_extname(fn, mime_exts)
261
+ { filename: fn, type: mime_type }
190
262
  end
191
- candidates
263
+ return filenames, deps
192
264
  end
193
265
 
194
- def resolve_alternates(load_path, logical_name)
195
- return [], Set.new
196
- end
197
-
198
- # Internal: Returns the name, mime type and `Array` of engine extensions.
266
+ # Internal: Converts mimetype into accept Array
199
267
  #
200
- # "foo.js.coffee.erb"
201
- # # => ["foo", "application/javascript", [".coffee", ".erb"]]
268
+ # - mime_type - String, optional. e.g. "text/html"
269
+ # - explicit_type - String, optional. e.g. "application/javascript"
202
270
  #
203
- def parse_path_extnames(path)
204
- engines = []
205
- extname, value = match_path_extname(path, extname_map)
206
-
207
- if extname
208
- path = path.chomp(extname)
209
- type, engines, pipeline = value.values_at(:type, :engines, :pipeline)
271
+ # When called with an explicit_type and a mime_type, only a mime_type
272
+ # that matches the given explicit_type will be accepted.
273
+ #
274
+ # Returns Array of Array
275
+ #
276
+ # [["application/javascript", 1.0]]
277
+ # [["*/*", 1.0]]
278
+ # []
279
+ def parse_accept_options(mime_type, explicit_type)
280
+ if mime_type
281
+ return [[mime_type, 1.0]] if explicit_type.nil?
282
+ return [[mime_type, 1.0]] if HTTPUtils.parse_q_values(explicit_type).any? { |accept, _| HTTPUtils.match_mime_type?(mime_type, accept) }
283
+ return []
210
284
  end
211
285
 
212
- return path, type, engines, pipeline
286
+ accepts = HTTPUtils.parse_q_values(explicit_type)
287
+ accepts << ['*/*'.freeze, 1.0] if accepts.empty?
288
+ return accepts
289
+ end
290
+
291
+ def resolve_alternates(load_path, logical_name)
292
+ return [], Set.new
213
293
  end
214
294
  end
215
295
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sass'
2
3
 
3
4
  module Sprockets
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/autoload'
3
+ require 'sprockets/digest_utils'
4
+ require 'sprockets/source_map_utils'
2
5
 
3
6
  module Sprockets
4
7
  # Public: Sass CSS minifier.
@@ -34,26 +37,27 @@ module Sprockets
34
37
  attr_reader :cache_key
35
38
 
36
39
  def initialize(options = {})
37
- @options = options
38
- @cache_key = [
39
- self.class.name,
40
- Autoload::Sass::VERSION,
41
- VERSION,
42
- options
43
- ].freeze
40
+ @options = {
41
+ syntax: :scss,
42
+ cache: false,
43
+ read_cache: false,
44
+ style: :compressed
45
+ }.merge(options).freeze
46
+ @cache_key = "#{self.class.name}:#{Autoload::Sass::VERSION}:#{VERSION}:#{DigestUtils.digest(options)}".freeze
44
47
  end
45
48
 
46
49
  def call(input)
47
- data = input[:data]
48
- input[:cache].fetch(@cache_key + [data]) do
49
- options = {
50
- syntax: :scss,
51
- cache: false,
52
- read_cache: false,
53
- style: :compressed
54
- }.merge(@options)
55
- Autoload::Sass::Engine.new(data, options).render
56
- end
50
+ css, map = Autoload::Sass::Engine.new(
51
+ input[:data],
52
+ @options.merge(filename: input[:filename])
53
+ ).render_with_sourcemap('')
54
+
55
+ css = css.sub("/*# sourceMappingURL= */\n", '')
56
+
57
+ map = SourceMapUtils.format_source_map(JSON.parse(map.to_json(css_uri: '')), input)
58
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
59
+
60
+ { data: css, map: map }
57
61
  end
58
62
  end
59
63
  end
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
  # Deprecated: Require sprockets/sass_processor instead
2
3
  require 'sprockets/sass_processor'
@@ -1,2 +1,3 @@
1
+ # frozen_string_literal: true
1
2
  # Deprecated: Require sprockets/sass_processor instead
2
3
  require 'sprockets/sass_processor'
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
1
2
  require 'rack/utils'
2
3
  require 'sprockets/autoload'
4
+ require 'sprockets/source_map_utils'
3
5
  require 'uri'
4
6
 
5
7
  module Sprockets
6
8
  # Processor engine class for the SASS/SCSS compiler. Depends on the `sass` gem.
7
9
  #
8
- # For more infomation see:
10
+ # For more information see:
9
11
  #
10
12
  # https://github.com/sass/sass
11
13
  # https://github.com/rails/sass-rails
@@ -39,18 +41,14 @@ module Sprockets
39
41
  # Public: Initialize template with custom options.
40
42
  #
41
43
  # options - Hash
42
- # cache_version - String custom cache version. Used to force a cache
43
- # change after code changes are made to Sass Functions.
44
+ # cache_version - String custom cache version. Used to force a cache
45
+ # change after code changes are made to Sass Functions.
44
46
  #
45
47
  def initialize(options = {}, &block)
46
48
  @cache_version = options[:cache_version]
47
- @cache_key = [
48
- self.class.name,
49
- VERSION,
50
- Autoload::Sass::VERSION,
51
- @cache_version
52
- ].freeze
53
-
49
+ @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::Sass::VERSION}:#{@cache_version}".freeze
50
+ @importer_class = options[:importer] || Sass::Importers::Filesystem
51
+ @sass_config = options[:sass_config] || {}
54
52
  @functions = Module.new do
55
53
  include Functions
56
54
  include options[:functions] if options[:functions]
@@ -61,24 +59,30 @@ module Sprockets
61
59
  def call(input)
62
60
  context = input[:environment].context_class.new(input)
63
61
 
64
- options = {
62
+ engine_options = merge_options({
65
63
  filename: input[:filename],
66
64
  syntax: self.class.syntax,
67
- cache_store: CacheStore.new(input[:cache], @cache_version),
68
- load_paths: input[:environment].paths,
65
+ cache_store: build_cache_store(input, @cache_version),
66
+ load_paths: context.environment.paths.map { |p| @importer_class.new(p.to_s) },
67
+ importer: @importer_class.new(Pathname.new(context.filename).to_s),
69
68
  sprockets: {
70
69
  context: context,
71
70
  environment: input[:environment],
72
71
  dependencies: context.metadata[:dependencies]
73
72
  }
74
- }
73
+ })
75
74
 
76
- engine = Autoload::Sass::Engine.new(input[:data], options)
75
+ engine = Autoload::Sass::Engine.new(input[:data], engine_options)
77
76
 
78
- css = Utils.module_include(Autoload::Sass::Script::Functions, @functions) do
79
- engine.render
77
+ css, map = Utils.module_include(Autoload::Sass::Script::Functions, @functions) do
78
+ engine.render_with_sourcemap('')
80
79
  end
81
80
 
81
+ css = css.sub("\n/*# sourceMappingURL= */\n", '')
82
+
83
+ map = SourceMapUtils.format_source_map(JSON.parse(map.to_json(css_uri: '')), input)
84
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
85
+
82
86
  # Track all imported files
83
87
  sass_dependencies = Set.new([input[:filename]])
84
88
  engine.dependencies.map do |dependency|
@@ -86,7 +90,31 @@ module Sprockets
86
90
  context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.options[:filename])
87
91
  end
88
92
 
89
- context.metadata.merge(data: css, sass_dependencies: sass_dependencies)
93
+ context.metadata.merge(data: css, sass_dependencies: sass_dependencies, map: map)
94
+ end
95
+
96
+ private
97
+
98
+ # Public: Build the cache store to be used by the Sass engine.
99
+ #
100
+ # input - the input hash.
101
+ # version - the cache version.
102
+ #
103
+ # Override this method if you need to use a different cache than the
104
+ # Sprockets cache.
105
+ def build_cache_store(input, version)
106
+ CacheStore.new(input[:cache], version)
107
+ end
108
+
109
+ def merge_options(options)
110
+ defaults = @sass_config.dup
111
+
112
+ if load_paths = defaults.delete(:load_paths)
113
+ options[:load_paths] += load_paths
114
+ end
115
+
116
+ options.merge!(defaults)
117
+ options
90
118
  end
91
119
 
92
120
  # Public: Functions injected into Sass context during Sprockets evaluation.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/autoload'
3
+ require 'sprockets/source_map_utils'
4
+
5
+ module Sprockets
6
+ # Public: Sass CSS minifier.
7
+ #
8
+ # To accept the default options
9
+ #
10
+ # environment.register_bundle_processor 'text/css',
11
+ # Sprockets::SasscCompressor
12
+ #
13
+ # Or to pass options to the Sass::Engine class.
14
+ #
15
+ # environment.register_bundle_processor 'text/css',
16
+ # Sprockets::SasscCompressor.new({ ... })
17
+ #
18
+ class SasscCompressor
19
+ # Public: Return singleton instance with default options.
20
+ #
21
+ # Returns SasscCompressor object.
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ def self.call(input)
27
+ instance.call(input)
28
+ end
29
+
30
+ def initialize(options = {})
31
+ @options = {
32
+ syntax: :scss,
33
+ style: :compressed,
34
+ source_map_contents: false,
35
+ omit_source_map_url: true,
36
+ }.merge(options).freeze
37
+ end
38
+
39
+ def call(input)
40
+ # SassC requires the template to be modifiable
41
+ input_data = input[:data].frozen? ? input[:data].dup : input[:data]
42
+ engine = Autoload::SassC::Engine.new(input_data, @options.merge(filename: input[:filename], source_map_file: "#{input[:filename]}.map"))
43
+
44
+ css = engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
45
+
46
+ begin
47
+ map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
48
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
49
+ rescue SassC::NotRenderedError
50
+ map = input[:metadata][:map]
51
+ end
52
+
53
+ { data: css, map: map }
54
+ end
55
+ end
56
+ end