javascriptruby-on-railsesbuildjsbundling-rails

Image imported from Javascript file does not have fingerprint from asset precompile (Rails 6, esbuild, and jsbundling)


I am trying to import an image from JS in production. I have rails 6 with esbuild and jsbundling, and it works fine in development environment, but in production, images being imported from JS has no fingerprint. I've seen a Stack Overflow post like this one (Specific Image Not Loading After Rails 7 ESBuilt Update), but their solution was to move those image files to public/images folder. It's a solution, but this requires changing all JS files to load from a different location....

esbuild.config.js

const path = require('path')

require("esbuild").build({
  entryPoints: ['app/javascript/application.js'],
  bundle: true,
  tsconfig: path.join(process.cwd(), "tsconfig.json"),
  outdir: path.join(process.cwd(), "app/assets/builds"),
  absWorkingDir: path.join(process.cwd(), "app/javascript"),
  watch: process.argv.includes("--watch"),
  incremental: process.argv.includes("--watch"),
  assetNames: "[name]-[hash].digested",
  publicPath: "/assets",
  plugins: [],
  loader: {
    ".js": "jsx",
    ".locale.json": "file",
    ".json": "json",
    ".png": "file",
    ".jpeg": "file",
    ".jpg": "file",
    ".svg": "file",
  }
}).catch(() => process.exit(1))

Importing Image, where JavaScript file is under app/javascript/, and image directory is app/javascript/images.

import Logo from './images/logo.svg';

Logo file in app/assets/builds in development

āžœ  work git:(master) ls -la app/assets/builds/logo*
-rw-r--r--  1 yang  staff  4710 Jun 26 16:30 app/assets/builds/logo-M5AMKDMO.digested.svg

Logo file referenced in HTML after the page was rendered in DEVELOPMENT

<img class="logo" src="/assets/logo-M5AMKDMO.svg">

Logo file in public/assets in production

-rw------- 1 u18958 dyno 4710 Jun 24 02:48 public/assets/logo-M5AMKDMO.digested-abddddc51310100c173ac88db35b27e1492e730c91cb447b346f772532ac85ba.svg

Logo file referenced in HTML after the page was rendered in PRODUCTION

<img class="logo" src="/assets/logo-M5AMKDMO.svg">

The img tag in production is totally missing the fingerprint by asset precompile. The server is deployed in Heroku, and they do precompile during deployment.

One more thing: I did one test by setting config.assets.compile to true, and that actually worked, but you shouldn't do that per this post (config.assets.compile=true in Rails production, why not?), so I feel quite stuck.


Solution

  • I'm guessing you have an old version of sprockets. v4.1.0 is when .digested thing works properly:

    # Gemfile
    
    gem "sprockets", ">= 4.1.0"
    

    // esbuild.config.js
    
    require('esbuild').build({
      watch: process.argv.includes('--watch'),
      entryPoints: ['app/javascript/application.js'],
      outdir: 'app/assets/builds',
      bundle: true,
    
      publicPath: '/assets',
      assetNames: '[name]-[hash].digested',
    
      loader: {
        '.svg': 'file'
      }
    })
    
    // package.json
    
    "scripts": {
      "build": "node esbuild.config.js",
    ...
    

    I'm not sure how you're getting /assets/logo-M5AMKDMO.svg path, with this config it should be /assets/logo-M5AMKDMO.digested.svg.

    // app/javascript/application.js
    
    import logo from './images/logo.svg'
    
    console.log(logo) // => "/assets/logo-2RBQBF7Y.digested.svg"
    

    That's the url generated by esbuild, it doesn't change in different rails environments. .digested is a recent addition, it's meant to skip the extra sprockets digest:

    # gem "sprockets", "4.0.3"
    >> helper.asset_path("logo-2RBQBF7Y.digested.svg")
    => "/assets/logo-2RBQBF7Y.digested-0c3d0155537d0e4f72356e8d9c7b41e8aa779b007b82f88d5af2766ba6166f45.svg"
    
    # gem "sprockets", "4.1.0"
    >> helper.asset_path("logo-2RBQBF7Y.digested.svg")
    => "/assets/logo-2RBQBF7Y.digested.svg"
    
    $ RAILS_ENV=production bin/rails assets:precompile
    $ ls public/assets/logo* 
    public/assets/logo-2RBQBF7Y.digested.svg
    public/assets/logo-2RBQBF7Y.digested.svg.gz
    

    You can also just rename files after precompilation:

    >> URI.open("http://localhost:3000/assets/logo-2RBQBF7Y.digested.svg").read
    /home/alex/.rbenv/versions/3.1.2/lib/ruby/3.1.0/open-uri.rb:364:in 'open_http': 404 Not Found (OpenURI::HTTPError)
    
    >> Rails.root.join("public/assets").glob("*.digested*").each do |f|
         f.rename(f.to_s.remove(/\.digested\K-\w+/))
       end
    
    >> URI.open("http://localhost:3000/assets/logo-2RBQBF7Y.digested.svg").read
    => "<svg>i am image</svg>\n"