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,74 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/digest_utils'
3
+ require 'sprockets/path_digest_utils'
4
+ require 'sprockets/uri_utils'
5
+
6
+ module Sprockets
7
+ # `Dependencies` is an internal mixin whose public methods are exposed on the
8
+ # `Environment` and `CachedEnvironment` classes.
9
+ module Dependencies
10
+ include DigestUtils, PathDigestUtils, URIUtils
11
+
12
+ # Public: Mapping dependency schemes to resolver functions.
13
+ #
14
+ # key - String scheme
15
+ # value - Proc.call(Environment, String)
16
+ #
17
+ # Returns Hash.
18
+ def dependency_resolvers
19
+ config[:dependency_resolvers]
20
+ end
21
+
22
+ # Public: Default set of dependency URIs for assets.
23
+ #
24
+ # Returns Set of String URIs.
25
+ def dependencies
26
+ config[:dependencies]
27
+ end
28
+
29
+ # Public: Register new dependency URI resolver.
30
+ #
31
+ # scheme - String scheme
32
+ # block -
33
+ # environment - Environment
34
+ # uri - String dependency URI
35
+ #
36
+ # Returns nothing.
37
+ def register_dependency_resolver(scheme, &block)
38
+ self.config = hash_reassoc(config, :dependency_resolvers) do |hash|
39
+ hash.merge(scheme => block)
40
+ end
41
+ end
42
+
43
+ # Public: Add environmental dependency inheirted by all assets.
44
+ #
45
+ # uri - String dependency URI
46
+ #
47
+ # Returns nothing.
48
+ def add_dependency(uri)
49
+ self.config = hash_reassoc(config, :dependencies) do |set|
50
+ set + Set.new([uri])
51
+ end
52
+ end
53
+ alias_method :depend_on, :add_dependency
54
+
55
+ # Internal: Resolve dependency URIs.
56
+ #
57
+ # Returns resolved Object.
58
+ def resolve_dependency(str)
59
+ # Optimize for the most common scheme to
60
+ # save 22k allocations on an average Spree app.
61
+ scheme = if str.start_with?('file-digest:'.freeze)
62
+ 'file-digest'.freeze
63
+ else
64
+ str[/([^:]+)/, 1]
65
+ end
66
+
67
+ if resolver = config[:dependency_resolvers][scheme]
68
+ resolver.call(self, str)
69
+ else
70
+ nil
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+ require 'digest/md5'
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+ require 'set'
6
+
7
+ module Sprockets
8
+ # Internal: Hash functions and digest related utilities. Mixed into
9
+ # Environment.
10
+ module DigestUtils
11
+ extend self
12
+
13
+ # Internal: Default digest class.
14
+ #
15
+ # Returns a Digest::Base subclass.
16
+ def digest_class
17
+ Digest::SHA256
18
+ end
19
+
20
+ # Internal: Maps digest bytesize to the digest class.
21
+ DIGEST_SIZES = {
22
+ 16 => Digest::MD5,
23
+ 20 => Digest::SHA1,
24
+ 32 => Digest::SHA256,
25
+ 48 => Digest::SHA384,
26
+ 64 => Digest::SHA512
27
+ }
28
+
29
+ # Internal: Detect digest class hash algorithm for digest bytes.
30
+ #
31
+ # While not elegant, all the supported digests have a unique bytesize.
32
+ #
33
+ # Returns Digest::Base or nil.
34
+ def detect_digest_class(bytes)
35
+ DIGEST_SIZES[bytes.bytesize]
36
+ end
37
+
38
+ ADD_VALUE_TO_DIGEST = {
39
+ String => ->(val, digest) { digest << val },
40
+ FalseClass => ->(val, digest) { digest << 'FalseClass'.freeze },
41
+ TrueClass => ->(val, digest) { digest << 'TrueClass'.freeze },
42
+ NilClass => ->(val, digest) { digest << 'NilClass'.freeze },
43
+
44
+ Symbol => ->(val, digest) {
45
+ digest << 'Symbol'.freeze
46
+ digest << val.to_s
47
+ },
48
+ Integer => ->(val, digest) {
49
+ digest << 'Integer'.freeze
50
+ digest << val.to_s
51
+ },
52
+ Array => ->(val, digest) {
53
+ digest << 'Array'.freeze
54
+ val.each do |element|
55
+ ADD_VALUE_TO_DIGEST[element.class].call(element, digest)
56
+ end
57
+ },
58
+ Hash => ->(val, digest) {
59
+ digest << 'Hash'.freeze
60
+ val.sort.each do |array|
61
+ ADD_VALUE_TO_DIGEST[Array].call(array, digest)
62
+ end
63
+ },
64
+ Set => ->(val, digest) {
65
+ digest << 'Set'.freeze
66
+ ADD_VALUE_TO_DIGEST[Array].call(val, digest)
67
+ },
68
+ Encoding => ->(val, digest) {
69
+ digest << 'Encoding'.freeze
70
+ digest << val.name
71
+ },
72
+ }
73
+ if 0.class != Integer # Ruby < 2.4
74
+ ADD_VALUE_TO_DIGEST[Fixnum] = ->(val, digest) {
75
+ digest << 'Integer'.freeze
76
+ digest << val.to_s
77
+ }
78
+ ADD_VALUE_TO_DIGEST[Bignum] = ->(val, digest) {
79
+ digest << 'Integer'.freeze
80
+ digest << val.to_s
81
+ }
82
+ end
83
+
84
+ ADD_VALUE_TO_DIGEST.compare_by_identity.rehash
85
+
86
+ ADD_VALUE_TO_DIGEST.default_proc = ->(_, val) {
87
+ raise TypeError, "couldn't digest #{ val }"
88
+ }
89
+ private_constant :ADD_VALUE_TO_DIGEST
90
+
91
+ # Internal: Generate a hexdigest for a nested JSON serializable object.
92
+ #
93
+ # This is used for generating cache keys, so its pretty important its
94
+ # wicked fast. Microbenchmarks away!
95
+ #
96
+ # obj - A JSON serializable object.
97
+ #
98
+ # Returns a String digest of the object.
99
+ def digest(obj)
100
+ build_digest(obj).digest
101
+ end
102
+
103
+ # Internal: Generate a hexdigest for a nested JSON serializable object.
104
+ #
105
+ # The same as `pack_hexdigest(digest(obj))`.
106
+ #
107
+ # obj - A JSON serializable object.
108
+ #
109
+ # Returns a String digest of the object.
110
+ def hexdigest(obj)
111
+ build_digest(obj).hexdigest!
112
+ end
113
+
114
+ # Internal: Pack a binary digest to a hex encoded string.
115
+ #
116
+ # bin - String bytes
117
+ #
118
+ # Returns hex String.
119
+ def pack_hexdigest(bin)
120
+ bin.unpack('H*'.freeze).first
121
+ end
122
+
123
+ # Internal: Unpack a hex encoded digest string into binary bytes.
124
+ #
125
+ # hex - String hex
126
+ #
127
+ # Returns binary String.
128
+ def unpack_hexdigest(hex)
129
+ [hex].pack('H*')
130
+ end
131
+
132
+ # Internal: Pack a binary digest to a base64 encoded string.
133
+ #
134
+ # bin - String bytes
135
+ #
136
+ # Returns base64 String.
137
+ def pack_base64digest(bin)
138
+ [bin].pack('m0')
139
+ end
140
+
141
+ # Internal: Pack a binary digest to a urlsafe base64 encoded string.
142
+ #
143
+ # bin - String bytes
144
+ #
145
+ # Returns urlsafe base64 String.
146
+ def pack_urlsafe_base64digest(bin)
147
+ str = pack_base64digest(bin)
148
+ str.tr!('+/'.freeze, '-_'.freeze)
149
+ str.tr!('='.freeze, ''.freeze)
150
+ str
151
+ end
152
+
153
+ # Internal: Maps digest class to the CSP hash algorithm name.
154
+ HASH_ALGORITHMS = {
155
+ Digest::SHA256 => 'sha256'.freeze,
156
+ Digest::SHA384 => 'sha384'.freeze,
157
+ Digest::SHA512 => 'sha512'.freeze
158
+ }
159
+
160
+ # Public: Generate hash for use in the `integrity` attribute of an asset tag
161
+ # as per the subresource integrity specification.
162
+ #
163
+ # digest - The String byte digest of the asset content.
164
+ #
165
+ # Returns a String or nil if hash algorithm is incompatible.
166
+ def integrity_uri(digest)
167
+ case digest
168
+ when Digest::Base
169
+ digest_class = digest.class
170
+ digest = digest.digest
171
+ when String
172
+ digest_class = DIGEST_SIZES[digest.bytesize]
173
+ else
174
+ raise TypeError, "unknown digest: #{digest.inspect}"
175
+ end
176
+
177
+ if hash_name = HASH_ALGORITHMS[digest_class]
178
+ "#{hash_name}-#{pack_base64digest(digest)}"
179
+ end
180
+ end
181
+
182
+ # Public: Generate hash for use in the `integrity` attribute of an asset tag
183
+ # as per the subresource integrity specification.
184
+ #
185
+ # digest - The String hexbyte digest of the asset content.
186
+ #
187
+ # Returns a String or nil if hash algorithm is incompatible.
188
+ def hexdigest_integrity_uri(hexdigest)
189
+ integrity_uri(unpack_hexdigest(hexdigest))
190
+ end
191
+
192
+ private
193
+ def build_digest(obj)
194
+ digest = digest_class.new
195
+
196
+ ADD_VALUE_TO_DIGEST[obj.class].call(obj, digest)
197
+ digest
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+ require 'shellwords'
4
+
5
+ module Sprockets
6
+ # The `DirectiveProcessor` is responsible for parsing and evaluating
7
+ # directive comments in a source file.
8
+ #
9
+ # A directive comment starts with a comment prefix, followed by an "=",
10
+ # then the directive name, then any arguments.
11
+ #
12
+ # // JavaScript
13
+ # //= require "foo"
14
+ #
15
+ # # CoffeeScript
16
+ # #= require "bar"
17
+ #
18
+ # /* CSS
19
+ # *= require "baz"
20
+ # */
21
+ #
22
+ # This makes it possible to disable or modify the processor to do whatever
23
+ # you'd like. You could add your own custom directives or invent your own
24
+ # directive syntax.
25
+ #
26
+ # `Environment#processors` includes `DirectiveProcessor` by default.
27
+ #
28
+ # To remove the processor entirely:
29
+ #
30
+ # env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
31
+ # env.unregister_processor('application/javascript', Sprockets::DirectiveProcessor)
32
+ #
33
+ # Then inject your own preprocessor:
34
+ #
35
+ # env.register_processor('text/css', MyProcessor)
36
+ #
37
+ class DirectiveProcessor
38
+ # Directives are denoted by a `=` followed by the name, then
39
+ # argument list.
40
+ #
41
+ # A few different styles are allowed:
42
+ #
43
+ # // =require foo
44
+ # //= require foo
45
+ # //= require "foo"
46
+ #
47
+ DIRECTIVE_PATTERN = /
48
+ ^ \W* = \s* (\w+.*?) (\*\/)? $
49
+ /x
50
+
51
+ def self.instance
52
+ # Default to C comment styles
53
+ @instance ||= new(comments: ["//", ["/*", "*/"]])
54
+ end
55
+
56
+ def self.call(input)
57
+ instance.call(input)
58
+ end
59
+
60
+ def initialize(comments: [])
61
+ @header_pattern = compile_header_pattern(Array(comments))
62
+ end
63
+
64
+ def call(input)
65
+ dup._call(input)
66
+ end
67
+
68
+ def _call(input)
69
+ @environment = input[:environment]
70
+ @uri = input[:uri]
71
+ @filename = input[:filename]
72
+ @dirname = File.dirname(@filename)
73
+ # If loading a source map file like `application.js.map` resolve
74
+ # dependencies using `.js` instead of `.js.map`
75
+ @content_type = SourceMapProcessor.original_content_type(input[:content_type], error_when_not_found: false)
76
+ @required = Set.new(input[:metadata][:required])
77
+ @stubbed = Set.new(input[:metadata][:stubbed])
78
+ @links = Set.new(input[:metadata][:links])
79
+ @dependencies = Set.new(input[:metadata][:dependencies])
80
+ @to_link = Set.new
81
+ @to_load = Set.new
82
+
83
+ data, directives = process_source(input[:data])
84
+ process_directives(directives)
85
+
86
+ {
87
+ data: data,
88
+ required: @required,
89
+ stubbed: @stubbed,
90
+ links: @links,
91
+ to_load: @to_load,
92
+ to_link: @to_link,
93
+ dependencies: @dependencies
94
+ }
95
+ end
96
+
97
+ protected
98
+ # Directives will only be picked up if they are in the header
99
+ # of the source file. C style (/* */), JavaScript (//), and
100
+ # Ruby (#) comments are supported.
101
+ #
102
+ # Directives in comments after the first non-whitespace line
103
+ # of code will not be processed.
104
+ def compile_header_pattern(comments)
105
+ re = comments.map { |c|
106
+ case c
107
+ when String
108
+ "(?:#{Regexp.escape(c)}.*\\n?)+"
109
+ when Array
110
+ "(?:#{Regexp.escape(c[0])}(?m:.*?)#{Regexp.escape(c[1])})"
111
+ else
112
+ raise TypeError, "unknown comment type: #{c.class}"
113
+ end
114
+ }.join("|")
115
+ Regexp.compile("\\A(?:(?m:\\s*)(?:#{re}))+")
116
+ end
117
+
118
+ def process_source(source)
119
+ header = source[@header_pattern, 0] || ""
120
+ body = $' || source
121
+
122
+ header, directives = extract_directives(header)
123
+
124
+ data = +""
125
+ data.force_encoding(body.encoding)
126
+ data << header unless header.empty?
127
+ data << body
128
+ # Ensure body ends in a new line
129
+ data << "\n" if data.length > 0 && data[-1] != "\n"
130
+
131
+ return data, directives
132
+ end
133
+
134
+ # Returns an Array of directive structures. Each structure
135
+ # is an Array with the line number as the first element, the
136
+ # directive name as the second element, followed by any
137
+ # arguments.
138
+ #
139
+ # [[1, "require", "foo"], [2, "require", "bar"]]
140
+ #
141
+ def extract_directives(header)
142
+ processed_header = +""
143
+ directives = []
144
+
145
+ header.lines.each_with_index do |line, index|
146
+ if directive = line[DIRECTIVE_PATTERN, 1]
147
+ name, *args = Shellwords.shellwords(directive)
148
+ if respond_to?("process_#{name}_directive", true)
149
+ directives << [index + 1, name, *args]
150
+ # Replace directive line with a clean break
151
+ line = "\n"
152
+ end
153
+ end
154
+ processed_header << line
155
+ end
156
+
157
+ processed_header.chomp!
158
+ # Ensure header ends in a new line like before it was processed
159
+ processed_header << "\n" if processed_header.length > 0 && header[-1] == "\n"
160
+
161
+ return processed_header, directives
162
+ end
163
+
164
+ # Gathers comment directives in the source and processes them.
165
+ # Any directive method matching `process_*_directive` will
166
+ # automatically be available. This makes it easy to extend the
167
+ # processor.
168
+ #
169
+ # To implement a custom directive called `require_glob`, subclass
170
+ # `Sprockets::DirectiveProcessor`, then add a method called
171
+ # `process_require_glob_directive`.
172
+ #
173
+ # class DirectiveProcessor < Sprockets::DirectiveProcessor
174
+ # def process_require_glob_directive(glob)
175
+ # Dir["#{dirname}/#{glob}"].sort.each do |filename|
176
+ # require(filename)
177
+ # end
178
+ # end
179
+ # end
180
+ #
181
+ # Replace the current processor on the environment with your own:
182
+ #
183
+ # env.unregister_processor('text/css', Sprockets::DirectiveProcessor)
184
+ # env.register_processor('text/css', DirectiveProcessor)
185
+ #
186
+ def process_directives(directives)
187
+ directives.each do |line_number, name, *args|
188
+ begin
189
+ send("process_#{name}_directive", *args)
190
+ rescue Exception => e
191
+ e.set_backtrace(["#{@filename}:#{line_number}"] + e.backtrace)
192
+ raise e
193
+ end
194
+ end
195
+ end
196
+
197
+ # The `require` directive functions similar to Ruby's own `require`.
198
+ # It provides a way to declare a dependency on a file in your path
199
+ # and ensures it's only loaded once before the source file.
200
+ #
201
+ # `require` works with files in the environment path:
202
+ #
203
+ # //= require "foo.js"
204
+ #
205
+ # Extensions are optional. If your source file is ".js", it
206
+ # assumes you are requiring another ".js".
207
+ #
208
+ # //= require "foo"
209
+ #
210
+ # Relative paths work too. Use a leading `./` to denote a relative
211
+ # path:
212
+ #
213
+ # //= require "./bar"
214
+ #
215
+ def process_require_directive(path)
216
+ @required << resolve(path, accept: @content_type, pipeline: :self)
217
+ end
218
+
219
+ # `require_self` causes the body of the current file to be inserted
220
+ # before any subsequent `require` directives. Useful in CSS files, where
221
+ # it's common for the index file to contain global styles that need to
222
+ # be defined before other dependencies are loaded.
223
+ #
224
+ # /*= require "reset"
225
+ # *= require_self
226
+ # *= require_tree .
227
+ # */
228
+ #
229
+ def process_require_self_directive
230
+ if @required.include?(@uri)
231
+ raise ArgumentError, "require_self can only be called once per source file"
232
+ end
233
+ @required << @uri
234
+ end
235
+
236
+ # `require_directory` requires all the files inside a single
237
+ # directory. It's similar to `path/*` since it does not follow
238
+ # nested directories.
239
+ #
240
+ # //= require_directory "./javascripts"
241
+ #
242
+ def process_require_directory_directive(path = ".")
243
+ path = expand_relative_dirname(:require_directory, path)
244
+ require_paths(*@environment.stat_directory_with_dependencies(path))
245
+ end
246
+
247
+ # `require_tree` requires all the nested files in a directory.
248
+ # Its glob equivalent is `path/**/*`.
249
+ #
250
+ # //= require_tree "./public"
251
+ #
252
+ def process_require_tree_directive(path = ".")
253
+ path = expand_relative_dirname(:require_tree, path)
254
+ require_paths(*@environment.stat_sorted_tree_with_dependencies(path))
255
+ end
256
+
257
+ # Allows you to state a dependency on a file without
258
+ # including it.
259
+ #
260
+ # This is used for caching purposes. Any changes made to
261
+ # the dependency file will invalidate the cache of the
262
+ # source file.
263
+ #
264
+ # This is useful if you are using ERB and File.read to pull
265
+ # in contents from another file.
266
+ #
267
+ # //= depend_on "foo.png"
268
+ #
269
+ def process_depend_on_directive(path)
270
+ resolve(path)
271
+ end
272
+
273
+ # Allows you to state a dependency on an asset without including
274
+ # it.
275
+ #
276
+ # This is used for caching purposes. Any changes that would
277
+ # invalidate the asset dependency will invalidate the cache of
278
+ # the source file.
279
+ #
280
+ # Unlike `depend_on`, the path must be a requirable asset.
281
+ #
282
+ # //= depend_on_asset "bar.js"
283
+ #
284
+ def process_depend_on_asset_directive(path)
285
+ to_load(resolve(path))
286
+ end
287
+
288
+ # Allows dependency to be excluded from the asset bundle.
289
+ #
290
+ # The `path` must be a valid asset and may or may not already
291
+ # be part of the bundle. Once stubbed, it is blacklisted and
292
+ # can't be brought back by any other `require`.
293
+ #
294
+ # //= stub "jquery"
295
+ #
296
+ def process_stub_directive(path)
297
+ @stubbed << resolve(path, accept: @content_type, pipeline: :self)
298
+ end
299
+
300
+ # Declares a linked dependency on the target asset.
301
+ #
302
+ # The `path` must be a valid asset and should not already be part of the
303
+ # bundle. Any linked assets will automatically be compiled along with the
304
+ # current.
305
+ #
306
+ # /*= link "logo.png" */
307
+ #
308
+ def process_link_directive(path)
309
+ uri = to_load(resolve(path))
310
+ @to_link << uri
311
+ end
312
+
313
+ # `link_directory` links all the files inside a single
314
+ # directory. It's similar to `path/*` since it does not follow
315
+ # nested directories.
316
+ #
317
+ # //= link_directory "./fonts"
318
+ #
319
+ # Use caution when linking against JS or CSS assets. Include an explicit
320
+ # extension or content type in these cases.
321
+ #
322
+ # //= link_directory "./scripts" .js
323
+ #
324
+ def process_link_directory_directive(path = ".", accept = nil)
325
+ path = expand_relative_dirname(:link_directory, path)
326
+ accept = expand_accept_shorthand(accept)
327
+ link_paths(*@environment.stat_directory_with_dependencies(path), accept)
328
+ end
329
+
330
+ # `link_tree` links all the nested files in a directory.
331
+ # Its glob equivalent is `path/**/*`.
332
+ #
333
+ # //= link_tree "./images"
334
+ #
335
+ # Use caution when linking against JS or CSS assets. Include an explicit
336
+ # extension or content type in these cases.
337
+ #
338
+ # //= link_tree "./styles" .css
339
+ #
340
+ def process_link_tree_directive(path = ".", accept = nil)
341
+ path = expand_relative_dirname(:link_tree, path)
342
+ accept = expand_accept_shorthand(accept)
343
+ link_paths(*@environment.stat_sorted_tree_with_dependencies(path), accept)
344
+ end
345
+
346
+ private
347
+ def expand_accept_shorthand(accept)
348
+ if accept.nil?
349
+ nil
350
+ elsif accept.include?("/")
351
+ accept
352
+ elsif accept.start_with?(".")
353
+ @environment.mime_exts[accept]
354
+ else
355
+ @environment.mime_exts[".#{accept}"]
356
+ end
357
+ end
358
+
359
+ def require_paths(paths, deps)
360
+ resolve_paths(paths, deps, accept: @content_type, pipeline: :self) do |uri|
361
+ @required << uri
362
+ end
363
+ end
364
+
365
+ def link_paths(paths, deps, accept)
366
+ resolve_paths(paths, deps, accept: accept) do |uri|
367
+ @to_link << to_load(uri)
368
+ end
369
+ end
370
+
371
+ def resolve_paths(paths, deps, **kargs)
372
+ @dependencies.merge(deps)
373
+ paths.each do |subpath, stat|
374
+ next if subpath == @filename || stat.directory?
375
+ uri, deps = @environment.resolve(subpath, **kargs)
376
+ @dependencies.merge(deps)
377
+ yield uri if uri
378
+ end
379
+ end
380
+
381
+ def expand_relative_dirname(directive, path)
382
+ if @environment.relative_path?(path)
383
+ path = File.expand_path(path, @dirname)
384
+ stat = @environment.stat(path)
385
+
386
+ if stat && stat.directory?
387
+ path
388
+ else
389
+ raise ArgumentError, "#{directive} argument must be a directory"
390
+ end
391
+ else
392
+ # The path must be relative and start with a `./`.
393
+ raise ArgumentError, "#{directive} argument must be a relative path"
394
+ end
395
+ end
396
+
397
+ def to_load(uri)
398
+ @to_load << uri
399
+ uri
400
+ end
401
+
402
+ def resolve(path, **kargs)
403
+ # Prevent absolute paths in directives
404
+ if @environment.absolute_path?(path)
405
+ raise FileOutsidePaths, "can't require absolute file: #{path}"
406
+ end
407
+
408
+ kargs[:base_path] = @dirname
409
+ uri, deps = @environment.resolve!(path, **kargs)
410
+ @dependencies.merge(deps)
411
+ uri
412
+ end
413
+ end
414
+ end