DEV Community

Stuart Harrison
Stuart Harrison

Posted on

Managing Javascript the easy way in Rails 7

Rails 7 is here, and with it, comes a whole new way of managing Javscript with "UJS with import maps plus Turbo and Stimulus from Hotwire as the defaults".

I'm a pretty experienced dev, but I must admit, I struggled to wrestle with these concepts, and I had a very short greenfield project that I wanted to get shipped and out the door as quickly as possible.

I wanted an approach that mirrored what I was used to with Rails 6 and Webpacker, but while still using something that felt new, and wouldn't succumb to bitrot as soon as I shipped it.

After some background research and deliberation with colleagues, I eventually settled on JSbundling. This allows a similar approach to asset building to the rest of the Javascript world, but with the added magic from the asset pipeline we know and love.

First, add jsbundling-rails to the Gemfile:

gem "jsbundling-rails", "~> 1.0"
Enter fullscreen mode Exit fullscreen mode

And run bundle install

Then run the generator (I'm using rollup, as it's marginally easier to grasp than Webpack, and is focussed wholly on building JS):

bin/rails javascript:install:rollup
Enter fullscreen mode Exit fullscreen mode

This adds the following to your Rails app:

  • An app/assets/builds folder - This will contain your built JS, which will then be served via the asset pipeline
  • Add app/javascript/application.js file - This is the entrypoint for all your application's frontend Javascript
  • A bin/dev file - This should be how you run your application in development. It runs (and optionally installs if you don't have it) Foreman, so you can run your server and build Javsacript on the fly
  • A Procfile.dev file - This tells Foreman what processes to run when you run the bin/dev script
  • A rollup.config.js file - This tells Rollup how to build the Javascript in your application.js

In addition, it also adds some extra bits and bobs to your .gitignore (so you don't commit the built files to git), and adds the builds folder to the assets/manifest.js file. It also adds the necessary dependencies to your package.json, as well as an NPM command to build your JS.

When you start the server with bin/dev, your application will spin up as usual, whilst also watching your application.js adn rollup.config.js for any changes and building the result.

Some gotchas

This mainly worked for me off the bat, however, I had some teething problems.

Firstly, I wanted to use Babel to make sure my JS was compatible with older browsers (we still have to support IE11 as we work with Government clients). Adding this to me rollup.config.js seemed to work:

import resolve from "@rollup/plugin-node-resolve"
import { babel } from '@rollup/plugin-babel';

export default {
  input: "app/javascript/application.js",
  output: {
    file: "app/assets/builds/application.js",
    format: "es",
    inlineDynamicImports: true,
    sourcemap: true
  },
  plugins: [
    babel({
      babelHelpers: 'runtime',
      exclude: 'node_modules/**',
      presets: [
        [
          '@babel/preset-env', {
            'useBuiltIns': 'usage',
            'corejs': '3'
          }
        ]
      ],
      plugins: ['@babel/plugin-transform-runtime']
    }),
    resolve()
  ]
}
Enter fullscreen mode Exit fullscreen mode

(I also had to install @babel/plugin-transform-runtime and @babel/runtime for this to work).

Secondly, I like to keep my JS nice and modular, with seperate files for each piece of functionality, and importing each file. For this to work, I had to set the rollup output format to iife(Immediately Invoked Function Expression), which wraps a function around all your code, so it runs as soon as the JS is loaded. Now, my rollup config looks something like this:

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs';

import { babel } from '@rollup/plugin-babel';

export default {
  input: 'app/javascript/application.js',
  output: {
    file: 'app/assets/builds/application.js',
    format: 'iife',
    inlineDynamicImports: true,
    sourcemap: true,
  },
  plugins: [
    commonjs(),
    babel({
      babelHelpers: 'runtime',
      exclude: 'node_modules/**',
      presets: [
        [
          '@babel/preset-env', {
            'useBuiltIns': 'usage',
            'corejs': '3'
          }
        ]
      ],
      plugins: ['@babel/plugin-transform-runtime']
    }),
    resolve(),
  ]
}
Enter fullscreen mode Exit fullscreen mode

And that's it! Hopefully this will save you hours of painful googling, and prevent you from getting migraine from staring at the same config file for hours on end!

Top comments (0)