3

I'm using Rails again after a few years away (last used Rails 4). I have multiple stimulus controllers that reference a file called metric_defaults.js. That file just contains a flat set of definitions such as:

Chart.defaults.elements.line.tension = 0.25;
Chart.defaults.elements.line.borderWidth = 5;

In rails 7 development env, this import works fine with import '../metric_defaults.js' from each stimulus controller, but in production I get:

Failed to load resource: the server responded with a status of 404 (Not Found) (metric_defaults.js)

I've spent a day trying to track this down, but all efforts have failed. A few tidbits:

  • I'm using a standard rails 7 configuration (importmaps, sprockets)
  • I've confirmed the production server is precompiling assets correctly. This includes metric_defaults.js, however it has a fingerprint in its name (metric_defaults-9032be9e....js), whereas the 404 seems to be trying to access the unfingerprinted file?
  • The production server is a Heroku instance
  • The path to the metric_defaults.js file is app/javascript/metric_defaults.js
  • The stimulus controllers are in app/javascript/controllers/
  • As mentioned, works fine in development
  • I'm at a loss

Any thoughts appreciated

PlankTon
  • 12,443
  • 16
  • 84
  • 153

1 Answers1

3

Setup:

# config/importmap.rb
pin "application"
pin "plugin"
// app/javascript/application.js
import "./plugin";

See generated importmap:

$ bin/importmap json
{
  "imports": {
    "application": "/assets/application-6aad68dfc16d361773f71cfe7fe74ae0ace4eea0b74067bc717475bbbbf4e580.js",
    "plugin":      "/assets/plugin-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
  }#  ^              ^
}  # imports        urls
   # for you        for browser

It's pretty simple:

import "plugin";
// will match "plugin" from import-maps
"plugin": "/assets/plugin-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js"
// and turn it into
import "/assets/plugin-04024382391bb910584145d8113cf35ef376b55d125bb4516cebeb14ce788597.js";
// browser sends request to this url ^

But:

import "./plugin";
// is relative to `application.js`, because that's where the import is.
// application.js is imported correctly with `import "application"` in the layout
"application": "/assets/application-6aad68dfc16d361773f71cfe7fe74ae0ace4eea0b74067bc717475bbbbf4e580.js"
//                      ^^^^^^^^^^^
// so "./plugin" is relative to this, which resolves to "/assets/plugin"

import "/assets/plugin"; // doesn't match
               "plugin"  // the import-map

import "/assets/plugin";
//      ^
// browser sends request in development and production

Pretty sure this is the first time I turned off config.assets.quiet:

Started GET "/assets/application-6aad68dfc16d361773f71cfe7fe74ae0ace4eea0b74067bc717475bbbbf4e580.js" for 127.0.0.1 at 2023-04-27 00:28:21 -0400
Started GET "/assets/es-module-shims.js-32db422c5db541b7129a2ce936aed905edc2cd481748f8d67ffe84e28313158a.map" for 127.0.0.1 at 2023-04-27 00:28:21 -0400
Started GET "/assets/plugin" for 127.0.0.1 at 2023-04-27 00:28:21 -0400
#                    ^
# NOTE: see how this one didn't get mapped to anything, it is just a plain url.

In development /assets is routed to sprockets that can handle digested and undigested assets and it works fine:

>> Rails.application.routes.routes.detect {|i| i.ast.to_s =~ /assets/ }.app.app.class
=> Sprockets::Environment
#      ^ same thing  v                     undigested path  v
>> Rails.application.assets.call(Rack::MockRequest.env_for("plugin")).last
=> #<Sprockets::Asset:fe718 "file:///home/alex/code/stackoverflow/app/javascript/plugin.js?type=application/javascript&id=187d193631f6880345ca4c2a2ac5d3a7c06ec09a64d4fbbd2cc1eed3a614997e">

In production, web server does the work instead and it only has precompiled assets, /assets/plugin gets a 404.

>> Rails.application.routes.routes.detect {|i| i.ast.to_s =~ /assets/ }
=> nil

Fix #1

Stop using relative imports.

Fix #2

Make an import-map that would match the relative import:

# config/importmap.rb

pin "application"
pin "/assets/plugin", to: "plugin"
#    ^ look familiar?
import "./plugin";      // this will be
import "/assets/plugin" // resolved to this
       "/assets/plugin" // and will match the import-map
Started GET "/assets/application-6aad68dfc16d361773f71cfe7fe74ae0ace4eea0b74067bc717475bbbbf4e580.js" for 127.0.0.1 at 2023-04-27 03:52:47 -0400
Started GET "/assets/plugin-c8122d51d5713808bd0206fb036b098e74b576f45c42480e977eb11b9040f1f4.js" for 127.0.0.1 at 2023-04-27 03:52:47 -0400
Started GET "/assets/es-module-shims.js-32db422c5db541b7129a2ce936aed905edc2cd481748f8d67ffe84e28313158a.map" for 127.0.0.1 at 2023-04-27 03:52:47 -0400

If you want to go this route, I'll borrow @cesoid's helper method, it'll get you started:

# config/importmap.rb

def pin_all_relative(dir_name)
  pin_all_from "app/javascript/#{dir_name}",
    under: "#{Rails.application.config.assets.prefix}/#{dir_name}",
    to: dir_name
end

pin_all_relative "controllers"
# etc
Alex
  • 16,409
  • 6
  • 40
  • 56