diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb new file mode 100644 index 0000000..a091fb8 --- /dev/null +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "propshaft/compiler" + +class Propshaft::Compiler::JsAssetUrls < Propshaft::Compiler + ASSET_URL_PATTERN = %r{rails_asset_url\(\s*["']?(?!(?:\#|%23|data|http|//))([^"'\s?#)]+)([#?][^"')]+)?\s*["']?\)} + + def compile(asset, input) + input.gsub(ASSET_URL_PATTERN) { asset_url(resolve_path(asset.logical_path.dirname, ::Regexp.last_match(1)), asset.logical_path, ::Regexp.last_match(2), ::Regexp.last_match(1)) } + end + + def referenced_by(asset, references: Set.new) + asset.content.scan(ASSET_URL_PATTERN).each do |referenced_asset_url, _| + referenced_asset = load_path.find(resolve_path(asset.logical_path.dirname, referenced_asset_url)) + + if referenced_asset && references.exclude?(referenced_asset) + references << referenced_asset + references.merge referenced_by(referenced_asset, references: references) + end + end + + references + end + + private + + def resolve_path(directory, filename) + if filename.start_with?("../") + Pathname.new(directory + filename).relative_path_from("").to_s + elsif filename.start_with?("/") + filename.delete_prefix("/").to_s + else + (directory + filename.delete_prefix("./")).to_s + end + end + + def asset_url(resolved_path, logical_path, fingerprint, pattern) + asset = load_path.find(resolved_path) + if asset + %["#{url_prefix}/#{asset.digested_path}#{fingerprint}"] + else + Propshaft.logger.warn("Unable to resolve '#{pattern}' for missing asset '#{resolved_path}' in #{logical_path}") + %["#{pattern}"] + end + end +end diff --git a/test/propshaft/compiler/js_asset_urls_test.rb b/test/propshaft/compiler/js_asset_urls_test.rb new file mode 100644 index 0000000..c6c5074 --- /dev/null +++ b/test/propshaft/compiler/js_asset_urls_test.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "test_helper" +require "minitest/mock" +require "propshaft/asset" +require "propshaft/assembly" +require "propshaft/compilers" + +module Propshaft + class Compiler + class JsAssetUrlsTest < ActiveSupport::TestCase + setup do + @options = ActiveSupport::OrderedOptions.new.tap do |config| + config.paths = [Pathname.new("#{__dir__}/../../fixtures/assets/vendor")] + config.output_path = Pathname.new("#{__dir__}/../../fixtures/output") + config.prefix = "/assets" + end + end + + test "the asset exists" do + js_content = <<~JS + export default class extends Controller { + init() { + this.img = rails_asset_url("/foobar/source/file.svg"); + } + } + JS + + compiled = compile_asset_with_content(js_content) + + assert_match(%r{this\.img = "/assets/foobar/source/file-[a-z0-9]{8}.svg"\;}, compiled) + end + + test "the asset does not exist" do + js_content = <<~JS + export default class extends Controller { + init() { + this.img = rails_asset_url("missing.svg"); + } + } + JS + + compiled = compile_asset_with_content(js_content) + + assert_match(/this\.img = "missing.svg"\;/, compiled) + end + + private + + def compile_asset_with_content(content) + # This has one more set of .. than it would in the propshaft repo + root_path = Pathname.new("#{__dir__}/../../fixtures/assets/vendor") + logical_path = "foobar/source/test.js" + + assembly = Propshaft::Assembly.new(@options) + assembly.compilers.register("text/javascript", Propshaft::Compiler::JsAssetUrls) + + asset = Propshaft::Asset.new(root_path.join(logical_path), logical_path: logical_path, load_path: assembly.load_path) + asset.stub(:content, content) do + assembly.compilers.compile(asset) + end + end + end + end +end