sprockets 3.0.3 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 +1,9 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'time'
4
+
5
+ require 'concurrent'
6
+
3
7
  require 'sprockets/manifest_utils'
4
8
 
5
9
  module Sprockets
@@ -11,7 +15,7 @@ module Sprockets
11
15
  # The JSON is part of the public API and should be considered stable. This
12
16
  # should make it easy to read from other programming languages and processes
13
17
  # that don't have sprockets loaded. See `#assets` and `#files` for more
14
- # infomation about the structure.
18
+ # information about the structure.
15
19
  class Manifest
16
20
  include ManifestUtils
17
21
 
@@ -48,14 +52,8 @@ module Sprockets
48
52
  @directory ||= File.dirname(@filename) if @filename
49
53
 
50
54
  # If directory is given w/o filename, pick a random manifest location
51
- @rename_filename = nil
52
55
  if @directory && @filename.nil?
53
- @filename = find_directory_manifest(@directory)
54
-
55
- # If legacy manifest name autodetected, mark to rename on save
56
- if File.basename(@filename).start_with?("manifest")
57
- @rename_filename = File.join(@directory, generate_manifest_path)
58
- end
56
+ @filename = find_directory_manifest(@directory, logger)
59
57
  end
60
58
 
61
59
  unless @directory && @filename
@@ -114,38 +112,46 @@ module Sprockets
114
112
  # Public: Find all assets matching pattern set in environment.
115
113
  #
116
114
  # Returns Enumerator of Assets.
117
- def find(*args)
115
+ def find(*args, &block)
118
116
  unless environment
119
117
  raise Error, "manifest requires environment for compilation"
120
118
  end
121
119
 
122
120
  return to_enum(__method__, *args) unless block_given?
123
121
 
124
- paths, filters = args.flatten.partition { |arg| self.class.simple_logical_path?(arg) }
125
- filters = filters.map { |arg| self.class.compile_match_filter(arg) }
126
-
127
122
  environment = self.environment.cached
128
-
129
- paths.each do |path|
130
- environment.find_all_linked_assets(path) do |asset|
131
- yield asset
123
+ promises = args.flatten.map do |path|
124
+ Concurrent::Promise.execute(executor: executor) do
125
+ environment.find_all_linked_assets(path).to_a
132
126
  end
133
127
  end
134
128
 
135
- if filters.any?
136
- environment.logical_paths do |logical_path, filename|
137
- if filters.any? { |f| f.call(logical_path, filename) }
138
- environment.find_all_linked_assets(filename) do |asset|
139
- yield asset
140
- end
141
- end
142
- end
129
+ promises.each do |promise|
130
+ promise.value!.each(&block)
143
131
  end
144
132
 
145
133
  nil
146
134
  end
147
135
 
148
- # Compile and write asset to directory. The asset is written to a
136
+ # Public: Find the source of assets by paths.
137
+ #
138
+ # Returns Enumerator of assets file content.
139
+ def find_sources(*args)
140
+ return to_enum(__method__, *args) unless block_given?
141
+
142
+ if environment
143
+ find(*args).each do |asset|
144
+ yield asset.source
145
+ end
146
+ else
147
+ args.each do |path|
148
+ asset = assets[path]
149
+ yield File.binread(File.join(dir, asset)) if asset
150
+ end
151
+ end
152
+ end
153
+
154
+ # Compile asset to directory. The asset is written to a
149
155
  # fingerprinted filename like
150
156
  # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
151
157
  # also inserted into the manifest file.
@@ -157,29 +163,46 @@ module Sprockets
157
163
  raise Error, "manifest requires environment for compilation"
158
164
  end
159
165
 
160
- filenames = []
166
+ filenames = []
167
+ concurrent_exporters = []
161
168
 
169
+ assets_to_export = Concurrent::Array.new
162
170
  find(*args) do |asset|
171
+ assets_to_export << asset
172
+ end
173
+
174
+ assets_to_export.each do |asset|
175
+ mtime = Time.now.iso8601
163
176
  files[asset.digest_path] = {
164
177
  'logical_path' => asset.logical_path,
165
- 'mtime' => asset.mtime.iso8601,
178
+ 'mtime' => mtime,
166
179
  'size' => asset.bytesize,
167
180
  'digest' => asset.hexdigest,
168
- 'integrity' => asset.integrity
181
+
182
+ # Deprecated: Remove beta integrity attribute in next release.
183
+ # Callers should DigestUtils.hexdigest_integrity_uri to compute the
184
+ # digest themselves.
185
+ 'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest)
169
186
  }
170
187
  assets[asset.logical_path] = asset.digest_path
171
188
 
172
- target = File.join(dir, asset.digest_path)
189
+ filenames << asset.filename
173
190
 
174
- if File.exist?(target)
175
- logger.debug "Skipping #{target}, already exists"
176
- else
177
- logger.info "Writing #{target}"
178
- asset.write_to target
179
- end
191
+ promise = nil
192
+ exporters_for_asset(asset) do |exporter|
193
+ next if exporter.skip?(logger)
180
194
 
181
- filenames << asset.filename
195
+ if promise.nil?
196
+ promise = Concurrent::Promise.new(executor: executor) { exporter.call }
197
+ concurrent_exporters << promise.execute
198
+ else
199
+ concurrent_exporters << promise.then { exporter.call }
200
+ end
201
+ end
182
202
  end
203
+
204
+ # make sure all exporters have finished before returning the main thread
205
+ concurrent_exporters.each(&:wait!)
183
206
  save
184
207
 
185
208
  filenames
@@ -192,6 +215,7 @@ module Sprockets
192
215
  #
193
216
  def remove(filename)
194
217
  path = File.join(dir, filename)
218
+ gzip = "#{path}.gz"
195
219
  logical_path = files[filename]['logical_path']
196
220
 
197
221
  if assets[logical_path] == filename
@@ -200,6 +224,7 @@ module Sprockets
200
224
 
201
225
  files.delete(filename)
202
226
  FileUtils.rm(path) if File.exist?(path)
227
+ FileUtils.rm(gzip) if File.exist?(gzip)
203
228
 
204
229
  save
205
230
 
@@ -230,9 +255,9 @@ module Sprockets
230
255
  # Sort by timestamp
231
256
  Time.parse(attrs['mtime'])
232
257
  }.reverse.each_with_index.drop_while { |(_, attrs), index|
233
- age = [0, Time.now - Time.parse(attrs['mtime'])].max
258
+ _age = [0, Time.now - Time.parse(attrs['mtime'])].max
234
259
  # Keep if under age or within the count limit
235
- age < age || index < count
260
+ _age < age || index < count
236
261
  }.each { |(path, _), _|
237
262
  # Remove old assets
238
263
  remove(path)
@@ -244,18 +269,13 @@ module Sprockets
244
269
  def clobber
245
270
  FileUtils.rm_r(directory) if File.exist?(directory)
246
271
  logger.info "Removed #{directory}"
272
+ # if we have an environment clear the cache too
273
+ environment.cache.clear if environment
247
274
  nil
248
275
  end
249
276
 
250
- # Persist manfiest back to FS
277
+ # Persist manifest back to FS
251
278
  def save
252
- if @rename_filename
253
- logger.info "Renaming #{@filename} to #{@rename_filename}"
254
- FileUtils.mv(@filename, @rename_filename)
255
- @filename = @rename_filename
256
- @rename_filename = nil
257
- end
258
-
259
279
  data = json_encode(@data)
260
280
  FileUtils.mkdir_p File.dirname(@filename)
261
281
  PathUtils.atomic_write(@filename) do |f|
@@ -264,6 +284,36 @@ module Sprockets
264
284
  end
265
285
 
266
286
  private
287
+
288
+ # Given an asset, finds all exporters that
289
+ # match its mime-type.
290
+ #
291
+ # Will yield each expoter to the passed in block.
292
+ #
293
+ # array = []
294
+ # puts asset.content_type # => "application/javascript"
295
+ # exporters_for_asset(asset) do |exporter|
296
+ # array << exporter
297
+ # end
298
+ # # puts array => [Exporters::FileExporter, Exporters::ZlibExporter]
299
+ def exporters_for_asset(asset)
300
+ exporters = [Exporters::FileExporter]
301
+
302
+ environment.exporters.each do |mime_type, exporter_list|
303
+ next unless asset.content_type
304
+ next unless environment.match_mime_type? asset.content_type, mime_type
305
+ exporter_list.each do |exporter|
306
+ exporters << exporter
307
+ end
308
+ end
309
+
310
+ exporters.uniq!
311
+
312
+ exporters.each do |exporter|
313
+ yield exporter.new(asset: asset, environment: environment, directory: dir)
314
+ end
315
+ end
316
+
267
317
  def json_decode(obj)
268
318
  JSON.parse(obj, create_additions: false)
269
319
  end
@@ -281,5 +331,9 @@ module Sprockets
281
331
  logger
282
332
  end
283
333
  end
334
+
335
+ def executor
336
+ @executor ||= environment.export_concurrent ? :fast : :immediate
337
+ end
284
338
  end
285
339
  end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'securerandom'
3
+ require 'logger'
2
4
 
3
5
  module Sprockets
4
6
  # Public: Manifest utilities.
@@ -6,7 +8,6 @@ module Sprockets
6
8
  extend self
7
9
 
8
10
  MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/
9
- LEGACY_MANIFEST_RE = /^manifest(-[0-9a-f]{32})?.json$/
10
11
 
11
12
  # Public: Generate a new random manifest path.
12
13
  #
@@ -33,12 +34,14 @@ module Sprockets
33
34
  # # => "/app/public/assets/.sprockets-manifest-abc123.json"
34
35
  #
35
36
  # Returns String filename.
36
- def find_directory_manifest(dirname)
37
+ def find_directory_manifest(dirname, logger = Logger.new($stderr))
37
38
  entries = File.directory?(dirname) ? Dir.entries(dirname) : []
38
- entry = entries.find { |e| e =~ MANIFEST_RE } ||
39
- # Deprecated: Will be removed in 4.x
40
- entries.find { |e| e =~ LEGACY_MANIFEST_RE } ||
41
- generate_manifest_path
39
+ manifest_entries = entries.select { |e| e =~ MANIFEST_RE }
40
+ if manifest_entries.length > 1
41
+ manifest_entries.sort!
42
+ logger.warn("Found multiple manifests: #{manifest_entries}. Choosing the first alphabetically: #{manifest_entries.first}")
43
+ end
44
+ entry = manifest_entries.first || generate_manifest_path
42
45
  File.join(dirname, entry)
43
46
  end
44
47
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/encoding_utils'
2
3
  require 'sprockets/http_utils'
3
4
  require 'sprockets/utils'
@@ -36,29 +37,18 @@ module Sprockets
36
37
 
37
38
  # Public: Register a new mime type.
38
39
  #
39
- # mime_type - String MIME Type
40
- # options - Hash
41
- # extensions: Array of String extnames
42
- # charset: Proc/Method that detects the charset of a file.
43
- # See EncodingUtils.
40
+ # mime_type - String MIME Type
41
+ # extensions - Array of String extnames
42
+ # charset - Proc/Method that detects the charset of a file.
43
+ # See EncodingUtils.
44
44
  #
45
45
  # Returns nothing.
46
- def register_mime_type(mime_type, options = {})
47
- # Legacy extension argument, will be removed from 4.x
48
- if options.is_a?(String)
49
- options = { extensions: [options] }
50
- end
51
-
52
- extnames = Array(options[:extensions]).map { |extname|
53
- Sprockets::Utils.normalize_extension(extname)
54
- }
46
+ def register_mime_type(mime_type, extensions: [], charset: nil)
47
+ extnames = Array(extensions)
55
48
 
56
- charset = options[:charset]
57
49
  charset ||= :default if mime_type.start_with?('text/')
58
50
  charset = EncodingUtils::CHARSET_DETECT[charset] if charset.is_a?(Symbol)
59
51
 
60
- self.computed_config = {}
61
-
62
52
  self.config = hash_reassoc(config, :mime_exts) do |mime_exts|
63
53
  extnames.each do |extname|
64
54
  mime_exts[extname] = mime_type
@@ -97,34 +87,10 @@ module Sprockets
97
87
  data = File.binread(filename)
98
88
 
99
89
  if detect = mime_type_charset_detecter(content_type)
100
- detect.call(data).encode(Encoding::UTF_8, :universal_newline => true)
90
+ detect.call(data).encode(Encoding::UTF_8, universal_newline: true)
101
91
  else
102
92
  data
103
93
  end
104
94
  end
105
-
106
- private
107
- def extname_map
108
- self.computed_config[:_extnames] ||= compute_extname_map
109
- end
110
-
111
- def compute_extname_map
112
- graph = {}
113
-
114
- ([nil] + pipelines.keys.map(&:to_s)).each do |pipeline|
115
- pipeline_extname = ".#{pipeline}" if pipeline
116
- ([[nil, nil]] + config[:mime_exts].to_a).each do |format_extname, format_type|
117
- 4.times do |n|
118
- config[:engines].keys.permutation(n).each do |engine_extnames|
119
- key = "#{pipeline_extname}#{format_extname}#{engine_extnames.join}"
120
- type = format_type || config[:engine_mime_types][engine_extnames.first]
121
- graph[key] = {type: type, engines: engine_extnames, pipeline: pipeline}
122
- end
123
- end
124
- end
125
- end
126
-
127
- graph
128
- end
129
95
  end
130
96
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module Sprockets
5
+ module Npm
6
+ # Internal: Override resolve_alternates to install package.json behavior.
7
+ #
8
+ # load_path - String environment path
9
+ # logical_path - String path relative to base
10
+ #
11
+ # Returns candidate filenames.
12
+ def resolve_alternates(load_path, logical_path)
13
+ candidates, deps = super
14
+
15
+ dirname = File.join(load_path, logical_path)
16
+
17
+ if directory?(dirname)
18
+ filename = File.join(dirname, 'package.json')
19
+
20
+ if self.file?(filename)
21
+ deps << build_file_digest_uri(filename)
22
+ read_package_directives(dirname, filename) do |path|
23
+ if file?(path)
24
+ candidates << path
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ return candidates, deps
31
+ end
32
+
33
+ # Internal: Read package.json's main and style directives.
34
+ #
35
+ # dirname - String path to component directory.
36
+ # filename - String path to package.json.
37
+ #
38
+ # Returns nothing.
39
+ def read_package_directives(dirname, filename)
40
+ package = JSON.parse(File.read(filename), create_additions: false)
41
+
42
+ case package['main']
43
+ when String
44
+ yield File.expand_path(package['main'], dirname)
45
+ when nil
46
+ yield File.expand_path('index.js', dirname)
47
+ end
48
+
49
+ yield File.expand_path(package['style'], dirname) if package['style']
50
+ end
51
+ end
52
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'set'
2
3
  require 'sprockets/path_utils'
3
4
  require 'sprockets/uri_utils'
@@ -41,7 +42,7 @@ module Sprockets
41
42
  #
42
43
  # Returns an Array of entry names and a Set of dependency URIs.
43
44
  def entries_with_dependencies(path)
44
- return entries(path), file_digest_dependency_set(path)
45
+ return entries(path), Set.new([build_file_digest_uri(path)])
45
46
  end
46
47
 
47
48
  # Internal: List directory filenames and associated Stats under a
@@ -53,16 +54,7 @@ module Sprockets
53
54
  #
54
55
  # Returns an Array of filenames and a Set of dependency URIs.
55
56
  def stat_directory_with_dependencies(dir)
56
- return stat_directory(dir).to_a, file_digest_dependency_set(dir)
57
- end
58
-
59
- # Internal: Returns a set of dependencies for a particular path.
60
- #
61
- # path - String directory path
62
- #
63
- # Returns a Set of dependency URIs.
64
- def file_digest_dependency_set(path)
65
- Set.new([build_file_digest_uri(path)])
57
+ return stat_directory(dir).to_a, Set.new([build_file_digest_uri(dir)])
66
58
  end
67
59
 
68
60
  # Internal: List directory filenames and associated Stats under an entire
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/digest_utils'
2
3
  require 'sprockets/path_utils'
3
4
 
@@ -15,7 +16,7 @@ module Sprockets
15
16
  def stat_digest(path, stat)
16
17
  if stat.directory?
17
18
  # If its a directive, digest the list of filenames
18
- digest_class.digest(self.entries(path).join(','))
19
+ digest_class.digest(self.entries(path).join(','.freeze))
19
20
  elsif stat.file?
20
21
  # If its a file, digest the contents
21
22
  digest_class.file(path.to_s).digest
@@ -1,5 +1,4 @@
1
- require 'fileutils'
2
-
1
+ # frozen_string_literal: true
3
2
  module Sprockets
4
3
  # Internal: File and path related utilities. Mixed into Environment.
5
4
  #
@@ -7,6 +6,7 @@ module Sprockets
7
6
  # when code actually wants to reference ::FileUtils.
8
7
  module PathUtils
9
8
  extend self
9
+ require 'pathname'
10
10
 
11
11
  # Public: Like `File.stat`.
12
12
  #
@@ -55,9 +55,14 @@ module Sprockets
55
55
  # Returns an empty `Array` if the directory does not exist.
56
56
  def entries(path)
57
57
  if File.directory?(path)
58
- Dir.entries(path, :encoding => Encoding.default_internal).reject! { |entry|
59
- entry =~ /^\.|~$|^\#.*\#$/
60
- }.sort!
58
+ entries = Dir.entries(path, encoding: Encoding.default_internal)
59
+ entries.reject! { |entry|
60
+ entry.start_with?(".".freeze) ||
61
+ (entry.start_with?("#".freeze) && entry.end_with?("#".freeze)) ||
62
+ entry.end_with?("~".freeze)
63
+ }
64
+ entries.sort!
65
+ entries
61
66
  else
62
67
  []
63
68
  end
@@ -69,8 +74,6 @@ module Sprockets
69
74
  #
70
75
  # Returns true if path is absolute, otherwise false.
71
76
  if File::ALT_SEPARATOR
72
- require 'pathname'
73
-
74
77
  # On Windows, ALT_SEPARATOR is \
75
78
  # Delegate to Pathname since the logic gets complex.
76
79
  def absolute_path?(path)
@@ -78,7 +81,7 @@ module Sprockets
78
81
  end
79
82
  else
80
83
  def absolute_path?(path)
81
- path[0] == File::SEPARATOR
84
+ path.start_with?(File::SEPARATOR)
82
85
  end
83
86
  end
84
87
 
@@ -95,7 +98,58 @@ module Sprockets
95
98
  #
96
99
  # Returns true if path is relative, otherwise false.
97
100
  def relative_path?(path)
98
- path =~ /^\.\.?($|#{SEPARATOR_PATTERN})/ ? true : false
101
+ path.match?(/^\.\.?($|#{SEPARATOR_PATTERN})/) ? true : false
102
+ end
103
+
104
+ # Public: Get relative path from `start` to `dest`.
105
+ #
106
+ # start - String start path (file or dir)
107
+ # dest - String destination path
108
+ #
109
+ # Returns relative String path from `start` to `dest`
110
+ def relative_path_from(start, dest)
111
+ start, dest = Pathname.new(start), Pathname.new(dest)
112
+ start = start.dirname unless start.directory?
113
+ dest.relative_path_from(start).to_s
114
+ end
115
+
116
+ # Public: Joins path to base path.
117
+ #
118
+ # base - Root path
119
+ # path - Extending path
120
+ #
121
+ # Example
122
+ #
123
+ # join('base/path/', '../file.js')
124
+ # # => 'base/file.js'
125
+ #
126
+ # Returns string path starting from base and ending at path
127
+ def join(base, path)
128
+ (Pathname.new(base) + path).to_s
129
+ end
130
+
131
+ # Public: Sets pipeline for path
132
+ #
133
+ # path - String path
134
+ # extensions - List of file extensions
135
+ # pipeline - Pipeline
136
+ #
137
+ # Examples
138
+ #
139
+ # set_pipeline('path/file.js.erb', config[:mime_exts], config[:pipeline_exts], :source)
140
+ # # => 'path/file.source.js.erb'
141
+ #
142
+ # set_pipeline('path/some.file.source.js.erb', config[:mime_exts], config[:pipeline_exts], :debug)
143
+ # # => 'path/some.file.debug.js.erb'
144
+ #
145
+ # Returns string path with pipeline parsed in
146
+ def set_pipeline(path, mime_exts, pipeline_exts, pipeline)
147
+ extension, _ = match_path_extname(path, mime_exts)
148
+ path.chomp!(extension)
149
+ pipeline_old, _ = match_path_extname(path, pipeline_exts)
150
+ path.chomp!(pipeline_old)
151
+
152
+ "#{path}.#{pipeline}#{extension}"
99
153
  end
100
154
 
101
155
  # Internal: Get relative path for root path and subpath.
@@ -107,8 +161,8 @@ module Sprockets
107
161
  # subpath is outside of path.
108
162
  def split_subpath(path, subpath)
109
163
  return "" if path == subpath
110
- path = File.join(path, '')
111
- if subpath.start_with?(path)
164
+ path = File.join(path, ''.freeze)
165
+ if subpath&.start_with?(path)
112
166
  subpath[path.length..-1]
113
167
  else
114
168
  nil
@@ -146,16 +200,47 @@ module Sprockets
146
200
  #
147
201
  # Returns [String extname, Object value] or nil nothing matched.
148
202
  def match_path_extname(path, extensions)
149
- match, key = nil, ""
150
- path_extnames(path).reverse_each do |extname|
151
- key.prepend(extname)
152
- if value = extensions[key]
153
- match = [key.dup, value]
154
- elsif match
155
- break
203
+ basename = File.basename(path)
204
+
205
+ i = basename.index('.'.freeze)
206
+ while i && i < basename.length - 1
207
+ extname = basename[i..-1]
208
+ if value = extensions[extname]
209
+ return extname, value
210
+ end
211
+
212
+ i = basename.index('.'.freeze, i+1)
213
+ end
214
+
215
+ nil
216
+ end
217
+
218
+ # Internal: Match paths in a directory against available extensions.
219
+ #
220
+ # path - String directory
221
+ # basename - String basename of target file
222
+ # extensions - Hash of String extnames to values
223
+ #
224
+ # Examples
225
+ #
226
+ # exts = { ".js" => "application/javascript" }
227
+ # find_matching_path_for_extensions("app/assets", "application", exts)
228
+ # # => ["app/assets/application.js", "application/javascript"]
229
+ #
230
+ # Returns an Array of [String path, Object value] matches.
231
+ def find_matching_path_for_extensions(path, basename, extensions)
232
+ matches = []
233
+ entries(path).each do |entry|
234
+ next unless File.basename(entry).start_with?(basename)
235
+ extname, value = match_path_extname(entry, extensions)
236
+ if basename == entry.chomp(extname)
237
+ filename = File.join(path, entry)
238
+ if file?(filename)
239
+ matches << [filename, value]
240
+ end
156
241
  end
157
242
  end
158
- match
243
+ matches
159
244
  end
160
245
 
161
246
  # Internal: Returns all parents for path
@@ -267,16 +352,16 @@ module Sprockets
267
352
  Thread.current.object_id,
268
353
  Process.pid,
269
354
  rand(1000000)
270
- ].join('.')
355
+ ].join('.'.freeze)
271
356
  tmpname = File.join(dirname, basename)
272
357
 
273
358
  File.open(tmpname, 'wb+') do |f|
274
359
  yield f
275
360
  end
276
361
 
277
- FileUtils.mv(tmpname, filename)
362
+ File.rename(tmpname, filename)
278
363
  ensure
279
- FileUtils.rm(tmpname) if File.exist?(tmpname)
364
+ File.delete(tmpname) if File.exist?(tmpname)
280
365
  end
281
366
  end
282
367
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/path_utils'
2
3
  require 'sprockets/utils'
3
4