diff --git a/lib/propshaft/assembly.rb b/lib/propshaft/assembly.rb index 3fbb11c..2deebfa 100644 --- a/lib/propshaft/assembly.rb +++ b/lib/propshaft/assembly.rb @@ -15,7 +15,7 @@ def initialize(config) end def load_path - @load_path ||= Propshaft::LoadPath.new(config.paths, version: config.version) + @load_path ||= Propshaft::LoadPath.new(config.paths, compilers: compilers, version: config.version) end def resolver diff --git a/lib/propshaft/asset.rb b/lib/propshaft/asset.rb index c9cc6bd..12c1f86 100644 --- a/lib/propshaft/asset.rb +++ b/lib/propshaft/asset.rb @@ -2,10 +2,10 @@ require "action_dispatch/http/mime_type" class Propshaft::Asset - attr_reader :path, :logical_path, :version + attr_reader :path, :logical_path, :load_path - def initialize(path, logical_path:, version: nil) - @path, @logical_path, @version = path, Pathname.new(logical_path), version + def initialize(path, logical_path:, load_path:) + @path, @logical_path, @load_path = path, Pathname.new(logical_path), load_path end def content @@ -21,7 +21,7 @@ def length end def digest - @digest ||= Digest::SHA1.hexdigest("#{content}#{version}").first(8) + @digest ||= Digest::SHA1.hexdigest("#{content_with_compile_dependencies}#{load_path.version}").first(8) end def digested_path @@ -44,4 +44,8 @@ def ==(other_asset) def already_digested? logical_path.to_s =~ /-([0-9a-zA-Z_-]{7,128})\.digested/ end + + def content_with_compile_dependencies + content + load_path.find_compiler_dependencies_to(self).collect(&:content).join + end end diff --git a/lib/propshaft/compiler.rb b/lib/propshaft/compiler.rb index b60dd65..ec355ab 100644 --- a/lib/propshaft/compiler.rb +++ b/lib/propshaft/compiler.rb @@ -9,10 +9,14 @@ def initialize(assembly) end # Override this in a specific compiler - def compile(logical_path, input) + def compile(asset) raise NotImplementedError end + def find_dependencies(asset) + Set.new + end + private def url_prefix @url_prefix ||= File.join(assembly.config.relative_url_root.to_s, assembly.config.prefix.to_s).chomp("/") diff --git a/lib/propshaft/compiler/css_asset_urls.rb b/lib/propshaft/compiler/css_asset_urls.rb index b263b7d..08f4ad8 100644 --- a/lib/propshaft/compiler/css_asset_urls.rb +++ b/lib/propshaft/compiler/css_asset_urls.rb @@ -5,8 +5,21 @@ class Propshaft::Compiler::CssAssetUrls < Propshaft::Compiler ASSET_URL_PATTERN = /url\(\s*["']?(?!(?:\#|%23|data|http|\/\/))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)/ - def compile(logical_path, input) - input.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(logical_path.dirname, $1), logical_path, $2, $1 } + def compile(asset) + asset.content.gsub(ASSET_URL_PATTERN) { asset_url resolve_path(asset.logical_path.dirname, $1), asset.logical_path, $2, $1 } + end + + def find_dependencies(asset) + Set.new.tap do |dependencies| + asset.content.scan(ASSET_URL_PATTERN).each do |dependent_asset_url, _| + dependent_asset = assembly.load_path.find(resolve_path(asset.logical_path.dirname, dependent_asset_url)) + + if dependencies.exclude?(dependent_asset) + dependencies << dependent_asset + dependencies.merge(find_dependencies(dependent_asset)) + end + end + end end private diff --git a/lib/propshaft/compiler/source_mapping_urls.rb b/lib/propshaft/compiler/source_mapping_urls.rb index bad3f0e..e09e685 100644 --- a/lib/propshaft/compiler/source_mapping_urls.rb +++ b/lib/propshaft/compiler/source_mapping_urls.rb @@ -5,8 +5,8 @@ class Propshaft::Compiler::SourceMappingUrls < Propshaft::Compiler SOURCE_MAPPING_PATTERN = %r{(//|/\*)# sourceMappingURL=(.+\.map)(\s*?\*\/)?\s*?\Z} - def compile(logical_path, input) - input.gsub(SOURCE_MAPPING_PATTERN) { source_mapping_url(logical_path, asset_path($2, logical_path), $1, $3) } + def compile(asset) + asset.content.gsub(SOURCE_MAPPING_PATTERN) { source_mapping_url(asset.logical_path, asset_path($2, asset.logical_path), $1, $3) } end private diff --git a/lib/propshaft/compilers.rb b/lib/propshaft/compilers.rb index c44d840..3ad63f7 100644 --- a/lib/propshaft/compilers.rb +++ b/lib/propshaft/compilers.rb @@ -23,11 +23,21 @@ def compile(asset) if relevant_registrations = registrations[asset.content_type.to_s] asset.content.dup.tap do |input| relevant_registrations.each do |compiler| - input.replace compiler.new(assembly).compile(asset.logical_path, input) + input.replace compiler.new(assembly).compile(asset) end end else asset.content end end + + def find_dependencies(asset) + Set.new.tap do |dependencies| + if relevant_registrations = registrations[asset.content_type.to_s] + relevant_registrations.each do |compiler| + dependencies.merge compiler.new(assembly).find_dependencies(asset) + end + end + end + end end diff --git a/lib/propshaft/load_path.rb b/lib/propshaft/load_path.rb index ab236b2..c497791 100644 --- a/lib/propshaft/load_path.rb +++ b/lib/propshaft/load_path.rb @@ -1,17 +1,20 @@ require "propshaft/asset" class Propshaft::LoadPath - attr_reader :paths, :version + attr_reader :paths, :compilers, :version - def initialize(paths = [], version: nil) - @paths = dedup(paths) - @version = version + def initialize(paths = [], compilers: nil, version: nil) + @paths, @compilers, @version = dedup(paths), compilers, version end def find(asset_name) assets_by_path[asset_name] end + def find_compiler_dependencies_to(asset) + compilers&.find_dependencies(asset) || Set.new + end + def assets(content_types: nil) if content_types assets_by_path.values.select { |asset| asset.content_type.in?(content_types) } @@ -48,7 +51,7 @@ def assets_by_path paths.each do |path| without_dotfiles(all_files_from_tree(path)).each do |file| logical_path = file.relative_path_from(path) - mapped[logical_path.to_s] ||= Propshaft::Asset.new(file, logical_path: logical_path, version: version) + mapped[logical_path.to_s] ||= Propshaft::Asset.new(file, logical_path: logical_path, load_path: self) end if path.exist? end end diff --git a/test/fixtures/assets/first_path/dependent/a.css b/test/fixtures/assets/first_path/dependent/a.css new file mode 100644 index 0000000..f0371d3 --- /dev/null +++ b/test/fixtures/assets/first_path/dependent/a.css @@ -0,0 +1 @@ +@import url('b.css') diff --git a/test/fixtures/assets/first_path/dependent/b.css b/test/fixtures/assets/first_path/dependent/b.css new file mode 100644 index 0000000..5a89d0f --- /dev/null +++ b/test/fixtures/assets/first_path/dependent/b.css @@ -0,0 +1 @@ +@import url('c.css') \ No newline at end of file diff --git a/test/fixtures/assets/first_path/dependent/c.css b/test/fixtures/assets/first_path/dependent/c.css new file mode 100644 index 0000000..e21e2c2 --- /dev/null +++ b/test/fixtures/assets/first_path/dependent/c.css @@ -0,0 +1,3 @@ +p { + color: red; +} \ No newline at end of file diff --git a/test/propshaft/asset_test.rb b/test/propshaft/asset_test.rb index 5c0749c..7d66412 100644 --- a/test/propshaft/asset_test.rb +++ b/test/propshaft/asset_test.rb @@ -54,10 +54,51 @@ class Propshaft::AssetTest < ActiveSupport::TestCase assert_equal asset.digest.object_id, asset.digest.object_id end + test "digest depends on first level of compiler dependency" do + open_asset_with_reset("dependent/b.css") do |asset_file| + digest_before_dependency_change = find_asset("dependent/a.css").digest + + asset_file.write "changes!" + asset_file.flush + + digest_after_dependency_change = find_asset("dependent/a.css").digest + + assert_not_equal digest_before_dependency_change, digest_after_dependency_change + end + end + + test "digest depends on second level of compiler dependency" do + open_asset_with_reset("dependent/c.css") do |asset_file| + digest_before_dependency_change = find_asset("dependent/a.css").digest + + asset_file.write "changes!" + asset_file.flush + + digest_after_dependency_change = find_asset("dependent/a.css").digest + + assert_not_equal digest_before_dependency_change, digest_after_dependency_change + end + end + private def find_asset(logical_path) root_path = Pathname.new("#{__dir__}/../fixtures/assets/first_path") path = root_path.join(logical_path) - Propshaft::Asset.new(path, logical_path: logical_path) + + assembly = Propshaft::Assembly.new(ActiveSupport::OrderedOptions.new.tap { |config| + config.paths = [ root_path ] + config.compilers = [[ "text/css", Propshaft::Compiler::CssAssetUrls ]] + }) + + Propshaft::Asset.new(path, logical_path: logical_path, load_path: assembly.load_path) + end + + def open_asset_with_reset(logical_path) + dependency_path = Pathname.new("#{__dir__}/../fixtures/assets/first_path/#{logical_path}") + existing_dependency_content = File.read(dependency_path) + + File.open(dependency_path, "a") { |f| yield f } + ensure + File.write(dependency_path, existing_dependency_content) end end diff --git a/test/propshaft/compiler/css_asset_urls_test.rb b/test/propshaft/compiler/css_asset_urls_test.rb index 4187682..3b672ad 100644 --- a/test/propshaft/compiler/css_asset_urls_test.rb +++ b/test/propshaft/compiler/css_asset_urls_test.rb @@ -130,10 +130,11 @@ def compile_asset_with_content(content) root_path = Pathname.new("#{__dir__}/../../fixtures/assets/vendor") logical_path = "foobar/source/test.css" - asset = Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path) + assembly = Propshaft::Assembly.new(@options) + assembly.compilers.register "text/css", Propshaft::Compiler::CssAssetUrls + + asset = Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path, load_path: assembly.load_path) asset.stub :content, content do - assembly = Propshaft::Assembly.new(@options) - assembly.compilers.register "text/css", Propshaft::Compiler::CssAssetUrls assembly.compilers.compile(asset) end end diff --git a/test/propshaft/compilers_test.rb b/test/propshaft/compilers_test.rb index c604c70..be5df7f 100644 --- a/test/propshaft/compilers_test.rb +++ b/test/propshaft/compilers_test.rb @@ -20,6 +20,6 @@ class Propshaft::CompilersTest < ActiveSupport::TestCase private def find_asset(logical_path) root_path = Pathname.new("#{__dir__}/../fixtures/assets/first_path") - Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path) + Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path, load_path: Propshaft::LoadPath.new([ root_path ])) end end diff --git a/test/propshaft/load_path_test.rb b/test/propshaft/load_path_test.rb index 88bb003..f654728 100644 --- a/test/propshaft/load_path_test.rb +++ b/test/propshaft/load_path_test.rb @@ -72,9 +72,7 @@ class Propshaft::LoadPathTest < ActiveSupport::TestCase private def find_asset(logical_path) - Propshaft::Asset.new( - Pathname.new("#{__dir__}/../fixtures/assets/first_path/#{logical_path}"), - logical_path: Pathname.new(logical_path) - ) + root_path = Pathname.new("#{__dir__}/../fixtures/assets/first_path") + Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path, load_path: Propshaft::LoadPath.new([ root_path ])) end end diff --git a/test/propshaft/output_path_test.rb b/test/propshaft/output_path_test.rb index b651ad3..ed47308 100644 --- a/test/propshaft/output_path_test.rb +++ b/test/propshaft/output_path_test.rb @@ -64,7 +64,7 @@ class Propshaft::OutputPathTest < ActiveSupport::TestCase private def output_asset(filename, content, created_at: Time.now) - asset = Propshaft::Asset.new(nil, logical_path: filename) + asset = Propshaft::Asset.new(nil, logical_path: filename, load_path: Propshaft::LoadPath.new) asset.stub :content, content do output_path = @output_path.path.join(asset.digested_path) `touch -mt #{created_at.strftime('%y%m%d%H%M')} #{output_path}` diff --git a/test/test_helper.rb b/test/test_helper.rb index 6d265a8..d9e5062 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -14,6 +14,6 @@ class ActiveSupport::TestCase def find_asset(logical_path, fixture_path:) root_path = Pathname.new("#{__dir__}/fixtures/assets/#{fixture_path}") path = root_path.join(logical_path) - Propshaft::Asset.new(path, logical_path: logical_path) + Propshaft::Asset.new(path, logical_path: logical_path, load_path: Propshaft::LoadPath.new([ root_path ])) end end