jamesmartin / inline_svg

Embed SVG documents in your Rails views and style them with CSS
MIT License
716 stars 73 forks source link

Webpacker #91

Closed richjdsmith closed 5 years ago

richjdsmith commented 6 years ago

Looks like this gem struggles to handle files served from Rails Webpacker?

I have this inline <%= inline_svg("#{asset_pack_path('images/grapes.svg')}") %>

But it results in a no asset found error.

When I actually visit the path, the image is there. I'll take a look and see if I can do a PR on this.

moxie commented 5 years ago

If someone else lands here with a similar problem. Here's how I've approached the issue. The following solution works with Webpacker, whether the dev server is running or the assets have been compiled to my public/ directory.

InlineSvg allows for creation of custom asset finders so that's what this does.

I'm fairly certain there may be a cleaner way to do this, but this works.

# In config/initializers/inline_svg.rb
class WebpackerAssetFinder
  class FoundAsset
    attr_reader :path

    def initialize(path)
      @path = path
    end

    def pathname
      if Webpacker.dev_server.running?
        # The dev server is running. Load the SVG into a Tempfile.
        asset = open("#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}#{path}")

        tempfile = Tempfile.new(path)
        tempfile.binmode
        tempfile.write(asset.read)
        tempfile.rewind

        tempfile

      else
        # The dev server isn't running. The asset will be compiled to public.
        Rails.application.root.join 'public', path.gsub(/\A\//, '')

      end
    end
  end

  def find_asset(filename)
    if webpack_asset_path = Webpacker.manifest.lookup(filename)
      return FoundAsset.new(webpack_asset_path)
    end
  end
end

# Override the Sprockets asset finder.
InlineSvg.configuration.asset_finder = WebpackerAssetFinder.new
jamesmartin commented 5 years ago

@moxie thanks for posting that solution, it seems like a perfect use of the custom asset finder. ✨

It seems the problem is related to the fact that Webpacker assets are served from a separate server in development mode. I guess another solution would be to only use your custom asset finder in Rails' development mode.

I'm going to close this out for now.

subdigital commented 5 years ago

I also ran into this. To get around it I had to make some changes to the solution @moxie posted above.

First, ensure you don't use asset_pack_path in your inline_svg call, otherwise the asset will not be found in the webpack manifest.

The next issue I had (likely because I have some nested paths), is that the creation of the Tempfile failed (and was caught by the gem, so the issue was hard to track down). I opted to leave off the path and just let it generate a unique filename for me.

Lastly, since I depend on both webpacker and the asset pipeline, I ended up with a fallback approach.

Here's what I ended up with:

class WebpackerAssetFinder
  class FoundAsset
    attr_reader :path

    def initialize(path)
      @path = path
    end

    def pathname
      if Webpacker.dev_server.running?
        asset = open(webpacker_dev_server_url)

        begin
          tempfile = Tempfile.new(path)
          tempfile.binmode
          tempfile.write(asset.read)
          tempfile.rewind
          tempfile
        rescue StandardError => e
          Rails.logger.error "Error creating tempfile: #{e}"
          raise
        end
      else
        Rails.application.root.join("public", path.gsub(/\A\//, ""))
      end
    end

    private

    def webpacker_dev_server_url
      "#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}#{path}"
    end
  end

  def find_asset(filename)
    if webpack_asset_path = Webpacker.manifest.lookup(filename)
      FoundAsset.new(webpack_asset_path)
    end
  end
end

Then in the config/initializers/inline_svg.rb initializer:

class FallbackAssetFinder
  attr_reader :finders

  def initialize(finders)
    @finders = finders
  end

  def find_asset(filename)
    # try each finder, in order, return the first path found
    finders.lazy.map { |f| f.find_asset(filename) }.find { |x| x != nil }
  end
end

# Some of our SVG assets will be in the asset pipeline, some from webpacker,
# This allows us to find the SVGs for inlining in both cases.
InlineSvg.configuration.asset_finder = FallbackAssetFinder.new([
  WebpackerAssetFinder.new,
  InlineSvg::StaticAssetFinder,
])

Hope this helps someone else trying to do the same thing. Thanks to @moxie for providing the basis for this workaround.

richjdsmith commented 5 years ago

Since upgrading to Rails 6.0 RC1, neither @moxie or @subdigital solutions work. Not entirely sure why either.

jamesmartin commented 5 years ago

Added support for Webpacker in v1.5.0.

jamesmartin commented 5 years ago

Webpacker is "opt-in" as of v1.5.2 because it was breaking users that were using both Sprockets with precompiled assets and Webpacker: https://github.com/jamesmartin/inline_svg/issues/98#issuecomment-503774840

carolineartz commented 5 years ago

In case anyone comes across this, I've been fighting this config today and was able to get it working finally for Rail v6.0.0. The general idea of the previous exmples works, but I had to adapt it in two ways:

  1. Finding the asset to support identifying the asset with or without the media/images path segments, I had to modify the lookup. You could do this in various more straightforward ways... but given I had the hashie dependency already, I went with the following quick update to maintain consistency with the path formats accepted by the asset_path_tag helper

    require 'hashie'
    
    def find_asset(filename)
     assets = Hashie::Rash.new(Webpacker.manifest.instance_variable_get('@data'))
     webpack_asset_path = assets[%r{(media\/images\/)?#{filename}}]
    
     return unless webpack_asset_path.present?
    
     WebpackerAssetFinder::FoundAsset.new(webpack_asset_path)
    end
  2. Reading the svg data I was seeing this error when attempting acceses the resource on the dev server via open as in the above examples despite it being the correct location (I can visit it directly and see the content).

    Errno::ENOENT: No such file or directory @ rb_sysopen http://localhost:3035/packs/media/i.....

    So, I make a request and get the data from the reponse...

    def pathname
     # The dev server is running. Load the SVG into a Tempfile.
     if Webpacker.dev_server.running?
       http = Net::HTTP.new(Webpacker.config.dev_server[:host], Webpacker.config.dev_server[:port])
       resp = http.get(path)
    
       return unless resp.code == '200'
    
       tempfile = Tempfile.new(path)
       tempfile.binmode
       tempfile.write(resp.body)
       tempfile.rewind
    
       tempfile
     # The dev server isn't running. The asset will be compiled to public.
     else
       Rails.application.root.join 'public', path.gsub(%r{\A\/}, '')
     end
    end

    convoluted, yes...works, yes.

    I personally don't want to support finding sprockets compiled assets, but extending this with @subdigital nice fallback approach would be straightforward.

growpathjadamson commented 3 years ago

Seeing as a lot of other people have looked into this, I figured I might as well tack on my own solution. It seems like the inline_svg implementation of the webpack svg resolver is similar to many of the examples here, so this is all it took for me to get it working properly in my own setup:

class AppWebpackAssetFinder < InlineSvg::WebpackAssetFinder
  def initialize(filename)
    super(filename)
    @asset_path = Webpacker.manifest.lookup("media/svg/#{@filename}")
  end
end

class FallbackAssetFinder
  attr_reader :finders

  def initialize(finders)
    @finders = finders
  end

  def find_asset(filename)
    # try each finder, in order, return the first path found
    finders.lazy.map { |f| f.find_asset(filename) }.find { |x| x != nil }
  end
end

# Some of our SVG assets will be in the asset pipeline, some from webpacker,
# This allows us to find the SVGs for inlining in both cases.
InlineSvg.configuration.asset_finder = FallbackAssetFinder.new([
  AppWebpackAssetFinder,
  InlineSvg::StaticAssetFinder,
])