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.

1

There are 1 best solutions below

1
On BEST ANSWER

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"