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
data/bin/sprockets CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ $VERBOSE = nil
2
3
 
3
4
  require 'sprockets'
4
5
  require 'optparse'
@@ -11,7 +12,7 @@ unless ARGV.delete("--noenv")
11
12
  end
12
13
  end
13
14
 
14
- filenames = []
15
+ paths = []
15
16
  environment = Sprockets::Environment.new(Dir.pwd)
16
17
  manifest = nil
17
18
 
@@ -51,6 +52,10 @@ OptionParser.new do |opts|
51
52
  opts.on("--noenv", "Disables .sprocketsrc file") do
52
53
  end
53
54
 
55
+ opts.on("--cache=DIRECTORY", "Enables the FileStore cache using the specified directory") do |directory|
56
+ environment.cache = Sprockets::Cache::FileStore.new(directory)
57
+ end
58
+
54
59
  opts.on_tail("-h", "--help", "Shows this help message") do
55
60
  opts.show_usage
56
61
  end
@@ -63,8 +68,8 @@ OptionParser.new do |opts|
63
68
  opts.show_usage if ARGV.empty?
64
69
 
65
70
  begin
66
- opts.order(ARGV) do |filename|
67
- filenames << File.expand_path(filename)
71
+ opts.order(ARGV) do |path|
72
+ paths << path
68
73
  end
69
74
  rescue OptionParser::ParseError => e
70
75
  opts.warn e.message
@@ -74,14 +79,14 @@ end
74
79
 
75
80
  if environment.paths.empty?
76
81
  warn "No load paths given"
77
- warn "Usage: sprockets -Ijavascripts/ filename"
82
+ warn "Usage: sprockets -Ijavascripts/ path"
78
83
  exit 1
79
84
  end
80
85
 
81
86
  if manifest
82
- manifest.compile(filenames)
83
- elsif filenames.length == 1
84
- puts environment.find_asset(filenames.first).to_s
87
+ manifest.compile(paths)
88
+ elsif paths.length == 1
89
+ puts environment.find_asset(paths.first).to_s
85
90
  else
86
91
  warn "Only one file can be compiled to stdout at a time"
87
92
  exit 1
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'rake'
2
3
  require 'rake/tasklib'
3
4
 
@@ -68,9 +69,12 @@ module Rake
68
69
  #
69
70
  attr_accessor :assets
70
71
 
71
- # Number of old assets to keep.
72
+ # Minimum number of old assets to keep. See Sprockets::Manifest#clean for more information.
72
73
  attr_accessor :keep
73
74
 
75
+ # Assets created within this age will be kept. See Sprockets::Manifest#clean for more information.
76
+ attr_accessor :age
77
+
74
78
  # Logger to use during rake tasks. Defaults to using stderr.
75
79
  #
76
80
  # t.logger = Logger.new($stdout)
@@ -102,6 +106,7 @@ module Rake
102
106
  @logger = Logger.new($stderr)
103
107
  @logger.level = Logger::INFO
104
108
  @keep = 2
109
+ @age = 3600
105
110
 
106
111
  yield self if block_given?
107
112
 
@@ -124,16 +129,16 @@ module Rake
124
129
  end
125
130
  end
126
131
 
127
- task :clobber => ["clobber_#{name}"]
132
+ task clobber: ["clobber_#{name}"]
128
133
 
129
134
  desc name == :assets ? "Clean old assets" : "Clean old #{name} assets"
130
135
  task "clean_#{name}" do
131
136
  with_logger do
132
- manifest.clean(keep)
137
+ manifest.clean(keep, age)
133
138
  end
134
139
  end
135
140
 
136
- task :clean => ["clean_#{name}"]
141
+ task clean: ["clean_#{name}"]
137
142
  end
138
143
 
139
144
  private
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/uri_utils'
3
+ require 'sprockets/path_utils'
4
+
5
+ module Sprockets
6
+ # This is a processor designed to add a source map "comment"
7
+ # to the bottom of a css or JS file that is serving a source
8
+ # map. An example of a comment might look like this
9
+ #
10
+ # //# application.js-80af0efcc960fc2ac93eda2f7b12e3db40ab360bf6ea269ceed3bea3678326f9.map
11
+ #
12
+ # As an asset is built it gets source map information added
13
+ # to the `asset.to_hash[:metadata][:map]` key. This contains all the
14
+ # information that is needed to build a source map file.
15
+ #
16
+ # To add this comment we must have an asset we can link to.
17
+ # To do this we ensure that the original asset is loaded, then
18
+ # we use a use a special mime type. For example `application/js-sourcemap+json`
19
+ # for a JS source map.
20
+ #
21
+ # This will trigger a new asset to be loaded and generated by the
22
+ # `SourceMapProcessor` processor.
23
+ #
24
+ # Finally once we have that file, we can generate a link to it
25
+ # with it's full fingerprint. This is done and then
26
+ # added to the original asset as a comment at the bottom.
27
+ #
28
+ class AddSourceMapCommentToAssetProcessor
29
+ def self.call(input)
30
+
31
+ case input[:content_type]
32
+ when "application/javascript"
33
+ comment = "\n//# sourceMappingURL=%s"
34
+ map_type = "application/js-sourcemap+json"
35
+ when "text/css"
36
+ comment = "\n/*# sourceMappingURL=%s */"
37
+ map_type = "application/css-sourcemap+json"
38
+ else
39
+ fail input[:content_type]
40
+ end
41
+
42
+ env = input[:environment]
43
+
44
+ uri, _ = env.resolve!(input[:filename], accept: input[:content_type])
45
+ asset = env.load(uri)
46
+
47
+ uri, _ = env.resolve!(input[:filename], accept: map_type)
48
+ map = env.load(uri)
49
+
50
+ uri, params = URIUtils.parse_asset_uri(input[:uri])
51
+ uri = env.expand_from_root(params[:index_alias]) if params[:index_alias]
52
+ path = PathUtils.relative_path_from(PathUtils.split_subpath(input[:load_path], uri), map.digest_path)
53
+
54
+ asset.metadata.merge(
55
+ data: asset.source + (comment % path) + "\n",
56
+ links: asset.links + [asset.uri, map.uri]
57
+ )
58
+ end
59
+ end
60
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'fileutils'
2
3
  require 'sprockets/digest_utils'
3
4
 
@@ -5,7 +6,7 @@ module Sprockets
5
6
  class Asset
6
7
  attr_reader :logical_path
7
8
 
8
- # Private: Intialize Asset wrapper from attributes Hash.
9
+ # Private: Initialize Asset wrapper from attributes Hash.
9
10
  #
10
11
  # Asset wrappers should not be initialized directly, only
11
12
  # Environment#find_asset should vend them.
@@ -13,17 +14,14 @@ module Sprockets
13
14
  # attributes - Hash of ivars
14
15
  #
15
16
  # Returns Asset.
16
- def initialize(environment, attributes = {})
17
- @environment = environment
17
+ def initialize(attributes = {})
18
18
  @attributes = attributes
19
19
  @content_type = attributes[:content_type]
20
20
  @filename = attributes[:filename]
21
21
  @id = attributes[:id]
22
- @integrity = attributes[:integrity]
23
22
  @load_path = attributes[:load_path]
24
23
  @logical_path = attributes[:logical_path]
25
24
  @metadata = attributes[:metadata]
26
- @mtime = attributes[:mtime]
27
25
  @name = attributes[:name]
28
26
  @source = attributes[:source]
29
27
  @uri = attributes[:uri]
@@ -40,7 +38,7 @@ module Sprockets
40
38
  #
41
39
  # The API status of the keys is dependent on the pipeline processors
42
40
  # itself. So some values maybe considered public and others internal.
43
- # See the pipeline proccessor documentation itself.
41
+ # See the pipeline processor documentation itself.
44
42
  #
45
43
  # Returns Hash.
46
44
  attr_reader :metadata
@@ -55,7 +53,7 @@ module Sprockets
55
53
 
56
54
  # Public: Internal URI to lookup asset by.
57
55
  #
58
- # NOT a publically accessible URL.
56
+ # NOT a publicly accessible URL.
59
57
  #
60
58
  # Returns URI.
61
59
  attr_reader :uri
@@ -66,7 +64,18 @@ module Sprockets
66
64
  #
67
65
  # Returns String.
68
66
  def digest_path
69
- logical_path.sub(/\.(\w+)$/) { |ext| "-#{etag}#{ext}" }
67
+ if DigestUtils.already_digested?(@name)
68
+ logical_path
69
+ else
70
+ logical_path.sub(/\.(\w+)$/) { |ext| "-#{etag}#{ext}" }
71
+ end
72
+ end
73
+
74
+ # Public: Return load path + logical path with digest spliced in.
75
+ #
76
+ # Returns String.
77
+ def full_digest_path
78
+ File.join(@load_path, digest_path)
70
79
  end
71
80
 
72
81
  # Public: Returns String MIME type of asset. Returns nil if type is unknown.
@@ -81,14 +90,6 @@ module Sprockets
81
90
  metadata[:links] || Set.new
82
91
  end
83
92
 
84
- # Public: Get all internally required assets that were concated into this
85
- # asset.
86
- #
87
- # Returns Array of String asset URIs.
88
- def included
89
- metadata[:included]
90
- end
91
-
92
93
  # Public: Return `String` of concatenated source.
93
94
  #
94
95
  # Returns String.
@@ -121,26 +122,41 @@ module Sprockets
121
122
  end
122
123
  alias_method :bytesize, :length
123
124
 
125
+ # Public: Returns String byte digest of source.
126
+ def digest
127
+ metadata[:digest]
128
+ end
129
+
130
+ # Private: Return the version of the environment where the asset was generated.
131
+ def environment_version
132
+ metadata[:environment_version]
133
+ end
134
+
124
135
  # Public: Returns String hexdigest of source.
125
136
  def hexdigest
126
- DigestUtils.pack_hexdigest(metadata[:digest])
137
+ DigestUtils.pack_hexdigest(digest)
127
138
  end
128
139
 
129
- # Deprecated: Returns String hexdigest of source.
130
- #
131
- # In 4.x this will be changed to return a raw Digest byte String.
132
- alias_method :digest, :hexdigest
133
-
134
140
  # Pubic: ETag String of Asset.
135
- alias_method :etag, :hexdigest
141
+ def etag
142
+ version = environment_version
143
+
144
+ if version && version != ""
145
+ DigestUtils.hexdigest(version + digest)
146
+ else
147
+ DigestUtils.pack_hexdigest(digest)
148
+ end
149
+ end
136
150
 
137
151
  # Public: Returns String base64 digest of source.
138
152
  def base64digest
139
- DigestUtils.pack_base64digest(metadata[:digest])
153
+ DigestUtils.pack_base64digest(digest)
140
154
  end
141
155
 
142
156
  # Public: A "named information" URL for subresource integrity.
143
- attr_reader :integrity
157
+ def integrity
158
+ DigestUtils.integrity_uri(digest)
159
+ end
144
160
 
145
161
  # Public: Add enumerator to allow `Asset` instances to be used as Rack
146
162
  # compatible body objects.
@@ -165,9 +181,6 @@ module Sprockets
165
181
  f.write source
166
182
  end
167
183
 
168
- # Set mtime correctly
169
- File.utime(mtime, mtime, filename)
170
-
171
184
  nil
172
185
  end
173
186
 
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'babel/transpiler'
3
+
4
+ module Sprockets
5
+ module Autoload
6
+ Babel = ::Babel
7
+ end
8
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'closure-compiler'
2
3
 
3
4
  module Sprockets
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'coffee_script'
2
3
 
3
4
  module Sprockets
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'eco'
2
3
 
3
4
  module Sprockets
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'ejs'
2
3
 
3
4
  module Sprockets
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'jsminc'
3
+
4
+ module Sprockets
5
+ module Autoload
6
+ JSMinC = ::JSMinC
7
+ end
8
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sass'
2
3
 
3
4
  module Sprockets
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ require 'sassc'
3
+
4
+ module Sprockets
5
+ module Autoload
6
+ SassC = ::SassC
7
+ end
8
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'uglifier'
2
3
 
3
4
  module Sprockets
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'yui/compressor'
2
3
 
3
4
  module Sprockets
@@ -0,0 +1,7 @@
1
+ require 'zopfli'
2
+
3
+ module Sprockets
4
+ module Autoload
5
+ Zopfli = ::Zopfli
6
+ end
7
+ end
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
1
2
  module Sprockets
2
3
  module Autoload
4
+ autoload :Babel, 'sprockets/autoload/babel'
3
5
  autoload :Closure, 'sprockets/autoload/closure'
4
6
  autoload :CoffeeScript, 'sprockets/autoload/coffee_script'
5
7
  autoload :Eco, 'sprockets/autoload/eco'
6
8
  autoload :EJS, 'sprockets/autoload/ejs'
9
+ autoload :JSMinC, 'sprockets/autoload/jsminc'
7
10
  autoload :Sass, 'sprockets/autoload/sass'
11
+ autoload :SassC, 'sprockets/autoload/sassc'
8
12
  autoload :Uglifier, 'sprockets/autoload/uglifier'
9
13
  autoload :YUI, 'sprockets/autoload/yui'
14
+ autoload :Zopfli, 'sprockets/autoload/zopfli'
10
15
  end
11
16
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/autoload'
3
+ require 'sprockets/path_utils'
4
+ require 'sprockets/source_map_utils'
5
+ require 'json'
6
+
7
+ module Sprockets
8
+ class BabelProcessor
9
+ VERSION = '1'
10
+
11
+ def self.instance
12
+ @instance ||= new
13
+ end
14
+
15
+ def self.call(input)
16
+ instance.call(input)
17
+ end
18
+
19
+ def self.cache_key
20
+ instance.cache_key
21
+ end
22
+
23
+ attr_reader :cache_key
24
+
25
+ def initialize(options = {})
26
+ @options = options.merge({
27
+ 'blacklist' => (options['blacklist'] || []) + ['useStrict'],
28
+ 'sourceMap' => true
29
+ }).freeze
30
+
31
+ @cache_key = [
32
+ self.class.name,
33
+ Autoload::Babel::Transpiler::VERSION,
34
+ Autoload::Babel::Source::VERSION,
35
+ VERSION,
36
+ @options
37
+ ].freeze
38
+ end
39
+
40
+ def call(input)
41
+ data = input[:data]
42
+
43
+ result = input[:cache].fetch(@cache_key + [input[:filename]] + [data]) do
44
+ opts = {
45
+ 'moduleRoot' => nil,
46
+ 'filename' => input[:filename],
47
+ 'filenameRelative' => PathUtils.split_subpath(input[:load_path], input[:filename]),
48
+ 'sourceFileName' => File.basename(input[:filename]),
49
+ 'sourceMapTarget' => input[:filename]
50
+ }.merge(@options)
51
+
52
+ if opts['moduleIds'] && opts['moduleRoot']
53
+ opts['moduleId'] ||= File.join(opts['moduleRoot'], input[:name])
54
+ elsif opts['moduleIds']
55
+ opts['moduleId'] ||= input[:name]
56
+ end
57
+ Autoload::Babel::Transpiler.transform(data, opts)
58
+ end
59
+
60
+ map = SourceMapUtils.format_source_map(result["map"], input)
61
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
62
+
63
+ { data: result['code'], map: map }
64
+ end
65
+ end
66
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/asset'
2
3
  require 'sprockets/bower'
3
4
  require 'sprockets/cache'
@@ -5,20 +6,36 @@ require 'sprockets/configuration'
5
6
  require 'sprockets/digest_utils'
6
7
  require 'sprockets/errors'
7
8
  require 'sprockets/loader'
8
- require 'sprockets/path_digest_utils'
9
+ require 'sprockets/npm'
9
10
  require 'sprockets/path_dependency_utils'
11
+ require 'sprockets/path_digest_utils'
10
12
  require 'sprockets/path_utils'
11
13
  require 'sprockets/resolve'
12
14
  require 'sprockets/server'
15
+ require 'sprockets/source_map_utils'
16
+ require 'sprockets/uri_tar'
13
17
 
14
18
  module Sprockets
15
- # `Base` class for `Environment` and `Cached`.
19
+
20
+ class DoubleLinkError < Sprockets::Error
21
+ def initialize(parent_filename:, logical_path:, last_filename:, filename:)
22
+ super <<~MSG
23
+ Multiple files with the same output path cannot be linked (#{logical_path.inspect})
24
+ In #{parent_filename.inspect} these files were linked:
25
+ - #{last_filename}
26
+ - #{filename}
27
+ MSG
28
+ end
29
+ end
30
+
31
+ # `Base` class for `Environment` and `CachedEnvironment`.
16
32
  class Base
17
- include PathUtils, PathDependencyUtils, PathDigestUtils, DigestUtils
33
+ include PathUtils, PathDependencyUtils, PathDigestUtils, DigestUtils, SourceMapUtils
18
34
  include Configuration
19
35
  include Server
20
36
  include Resolve, Loader
21
37
  include Bower
38
+ include Npm
22
39
 
23
40
  # Get persistent cache store
24
41
  attr_reader :cache
@@ -32,7 +49,7 @@ module Sprockets
32
49
  @cache = Cache.new(cache, logger)
33
50
  end
34
51
 
35
- # Return an `Cached`. Must be implemented by the subclass.
52
+ # Return an `CachedEnvironment`. Must be implemented by the subclass.
36
53
  def cached
37
54
  raise NotImplementedError
38
55
  end
@@ -48,33 +65,46 @@ module Sprockets
48
65
  # Caveat: Digests are cached by the path's current mtime. Its possible
49
66
  # for a files contents to have changed and its mtime to have been
50
67
  # negligently reset thus appearing as if the file hasn't changed on
51
- # disk. Also, the mtime is only read to the nearest second. Its
68
+ # disk. Also, the mtime is only read to the nearest second. It's
52
69
  # also possible the file was updated more than once in a given second.
53
- cache.fetch("file_digest:#{path}:#{stat.mtime.to_i}") do
70
+ key = UnloadedAsset.new(path, self).file_digest_key(stat.mtime.to_i)
71
+ cache.fetch(key) do
54
72
  self.stat_digest(path, stat)
55
73
  end
56
74
  end
57
75
  end
58
76
 
59
77
  # Find asset by logical path or expanded path.
60
- def find_asset(path, options = {})
61
- uri, _ = resolve(path, options.merge(compat: false))
78
+ def find_asset(*args, **options)
79
+ uri, _ = resolve(*args, **options)
62
80
  if uri
63
81
  load(uri)
64
82
  end
65
83
  end
66
84
 
67
- def find_all_linked_assets(path, options = {})
68
- return to_enum(__method__, path, options) unless block_given?
85
+ def find_all_linked_assets(*args)
86
+ return to_enum(__method__, *args) unless block_given?
69
87
 
70
- asset = find_asset(path, options)
88
+ parent_asset = asset = find_asset(*args)
71
89
  return unless asset
72
90
 
73
91
  yield asset
74
92
  stack = asset.links.to_a
93
+ linked_paths = {}
75
94
 
76
95
  while uri = stack.shift
77
96
  yield asset = load(uri)
97
+
98
+ last_filename = linked_paths[asset.logical_path]
99
+ if last_filename && last_filename != asset.filename
100
+ raise DoubleLinkError.new(
101
+ parent_filename: parent_asset.filename,
102
+ last_filename: last_filename,
103
+ logical_path: asset.logical_path,
104
+ filename: asset.filename
105
+ )
106
+ end
107
+ linked_paths[asset.logical_path] = asset.filename
78
108
  stack = asset.links.to_a + stack
79
109
  end
80
110
 
@@ -85,8 +115,18 @@ module Sprockets
85
115
  #
86
116
  # environment['application.js']
87
117
  #
88
- def [](*args)
89
- find_asset(*args)
118
+ def [](*args, **options)
119
+ find_asset(*args, **options)
120
+ end
121
+
122
+ # Find asset by logical path or expanded path.
123
+ #
124
+ # If the asset is not found an error will be raised.
125
+ def find_asset!(*args)
126
+ uri, _ = resolve!(*args)
127
+ if uri
128
+ load(uri)
129
+ end
90
130
  end
91
131
 
92
132
  # Pretty inspect
@@ -95,5 +135,13 @@ module Sprockets
95
135
  "root=#{root.to_s.inspect}, " +
96
136
  "paths=#{paths.inspect}>"
97
137
  end
138
+
139
+ def compress_from_root(uri)
140
+ URITar.new(uri, self).compress
141
+ end
142
+
143
+ def expand_from_root(uri)
144
+ URITar.new(uri, self).expand
145
+ end
98
146
  end
99
147
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
 
3
4
  module Sprockets
@@ -12,12 +13,12 @@ module Sprockets
12
13
  # load_path - String environment path
13
14
  # logical_path - String path relative to base
14
15
  #
15
- # Returns candiate filenames.
16
+ # Returns candidate filenames.
16
17
  def resolve_alternates(load_path, logical_path)
17
18
  candidates, deps = super
18
19
 
19
20
  # bower.json can only be nested one level deep
20
- if !logical_path.index('/')
21
+ if !logical_path.index('/'.freeze)
21
22
  dirname = File.join(load_path, logical_path)
22
23
 
23
24
  if directory?(dirname)
@@ -27,7 +28,9 @@ module Sprockets
27
28
  if filename
28
29
  deps << build_file_digest_uri(filename)
29
30
  read_bower_main(dirname, filename) do |path|
30
- candidates << path
31
+ if file?(path)
32
+ candidates << path
33
+ end
31
34
  end
32
35
  end
33
36
  end