Installing an Elm App - Ruby on Rails via esbuild

Webpacker is out, esbuild is in

Webpacker has been soft-deprecated by the Rails team (but Justin Gordon seems to be driving its continued development under a new gem: Shakapacker). The new paradigm is to get esbuild to bundle your assets, and dump them in the assets/build folder, where they are picked up by sprockets and minified/processed, according to the directives placed in your manifest.js file (I hope this is making sense). The jsbundling-rails gem largely takes care of the defaults/installation for you. Switching from webpacker to esbuild is not completely trivial, hence this tutorial.

Installation

Save yourself some installation headaches and install the jsbundling-rails gem with the es-build rake installation task:

1. Add jsbundling-rails to your Gemfile with `gem 'jsbundling-rails'`
2. Run `./bin/bundle install`
3. Run `./bin/rails javascript:install:esbuild`

You’ll need a esbuild compatible plugin loader so you can compile elm code into javascript. esbuild will not do that automatically for you. This is done by third party loaders. We’re going to use esbuild-plugin-elm by Akshay Nair to help us out.

  1. Ensure you have node installed. If you’re using Heroku, this may entail adding a node build pack. (I do note that Heroku may now have a mechanism where node will be installed if it is detected. Either way, you need node).
  2. yarn add elm
  3. yarn add -D esbuild-plugin-elm
  4. Set up your package.json file:
{
  "dependencies": {
    // .... i'm using turbo, and stimulus
  },
  "devDependencies": {
    "esbuild-plugin-elm": "0.0.8"
  },
  "engines": {
    "node": "16.13.2"
  },
  "scripts": {
    "build": "node ./esbuild.config.js", 
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}

You can use the command line to invoke esbuild. Or if you don’t want to do that, you can use its javascript API to programmatically toggle the configs you want. Please note that we are asking node to run the esbuild.config.js file placed in the root of our rails repository. You can name it differently etc.

// esbuild.config.js
const path = require('path')
const ElmPlugin = require('esbuild-plugin-elm')
const esbuild = require('esbuild')

// the absWorkingDirectory set below allows us to use paths relative to that location
esbuild.build({
  entryPoints: ['./application.js'], 
  bundle: true,
  outdir: path.join(process.cwd(), "app/assets/builds"),
  absWorkingDir: path.join(process.cwd(), "app/javascript"),  
  watch: process.argv.includes("--watch"),
  sourcemap: true,
  plugins: [
    ElmPlugin() // options are documented below
  ],
}).catch(e => (console.error(e), process.exit(1)))

Set up your elm code: app/javascript/Main.elm (note the singular ‘javascript’):

import Html exposing (text)

main =
  text "Hello!"

-- I'm using flags where I pass in some json,
-- you probably will have something different.

Now you’ll have to invoke it. I’m using Stimulus JS to invoke the elm code:

import {
  Elm
} from '../Main.elm'  // it's a relative path so make sure you set it accordingly. 
                      // remember this elm code is placed in: app/javascript/Main.elm 


import { Controller } from "stimulus"

export default class extends Controller {
  static values = { lineItems: Array}

  connect() {    
    let node = this.element;    
        
    Elm.Main.init( {node: node, flags: {lineItems: this.lineItemsValue}});    
  } 
}

And simply use stimulus to initialise that where relevant:

<div data-controller="elm-initializer" data-elm-initializer-line-items-value="<%= @line_items.as_json %>">
  <-- elm takes over -->
</div>

If this is confusing or wrong in any way, please post a comment so I can fix it up.

Written on February 8, 2022