From e28a76261982fc7cb8739ab99703aed8171e8ff4 Mon Sep 17 00:00:00 2001 From: Keith Thompson Date: Mon, 30 Sep 2024 16:57:32 -0400 Subject: [PATCH] Add Propshaft::Compiler::JsAssetUrls (#207) * Add Propshaft::Compiler::JsAssetUrls * Register JsAssetUrls compiler in railtie --- lib/propshaft/assembly.rb | 1 + lib/propshaft/compiler/js_asset_urls.rb | 45 +++++++++++++ lib/propshaft/railtie.rb | 3 +- test/propshaft/compiler/js_asset_urls_test.rb | 65 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 lib/propshaft/compiler/js_asset_urls.rb create mode 100644 test/propshaft/compiler/js_asset_urls_test.rb diff --git a/lib/propshaft/assembly.rb b/lib/propshaft/assembly.rb index f15a15e..29d6c48 100644 --- a/lib/propshaft/assembly.rb +++ b/lib/propshaft/assembly.rb @@ -5,6 +5,7 @@ require "propshaft/processor" require "propshaft/compilers" require "propshaft/compiler/css_asset_urls" +require "propshaft/compiler/js_asset_urls" require "propshaft/compiler/source_mapping_urls" class Propshaft::Assembly diff --git a/lib/propshaft/compiler/js_asset_urls.rb b/lib/propshaft/compiler/js_asset_urls.rb new file mode 100644 index 0000000..2299182 --- /dev/null +++ b/lib/propshaft/compiler/js_asset_urls.rb @@ -0,0 +1,45 @@ +# 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, $1), asset.logical_path, $2, $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/lib/propshaft/railtie.rb b/lib/propshaft/railtie.rb index 8f04777..395f59c 100644 --- a/lib/propshaft/railtie.rb +++ b/lib/propshaft/railtie.rb @@ -13,7 +13,8 @@ class Railtie < ::Rails::Railtie config.assets.compilers = [ [ "text/css", Propshaft::Compiler::CssAssetUrls ], [ "text/css", Propshaft::Compiler::SourceMappingUrls ], - [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ] + [ "text/javascript", Propshaft::Compiler::JsAssetUrls ], + [ "text/javascript", Propshaft::Compiler::SourceMappingUrls ], ] config.assets.sweep_cache = Rails.env.development? config.assets.server = Rails.env.development? || Rails.env.test? 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..3093b09 --- /dev/null +++ b/test/propshaft/compiler/js_asset_urls_test.rb @@ -0,0 +1,65 @@ +require "test_helper" +require "minitest/mock" +require "propshaft/asset" +require "propshaft/assembly" +require "propshaft/compilers" + +require "propshaft/compiler/js_asset_urls" + +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