ruby-on-railsasset-pipelinestimulusjs

Rails 7/StimulusJS relative import: working on dev, but not production


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:

Any thoughts appreciated


Solution

  • 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"
    // which doesn't match the import-map
    import "/assets/plugin";
    //      ^
    // browser sends request in development and production
    
    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.

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


    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
                            // 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