Read more

esbuild: Make your Rails application show build errors

Arne Hartherz
January 13, 2022Software engineer at makandra GmbH

Building application assets with esbuild is the new way to do it, and it's great, especially in combination with Sprockets (or Propshaft on Rails 7).
You might be missing some convenience features, though.

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Here we cover one specific issue:
Once you have started your development Rails server and esbuild with the --watch option (if you used jsbundling-rails to set up, you probably use bin/dev), esbuild will recompile your assets upon change, but build errors will only be printed to the terminal. Your application won't complain about them -- instead it just delivers the files that were previously built and you might not notice something went wrong.

Cry no more, here is how to solve that.

  1. Have esbuild write errors to a file,
  2. make the Rails app render such errors,
  3. automatically reload to show errors immediately (optional).

Make esbuild write errors to a file

In your esbuild.config.js, you probably have something like this:

const watch = process.argv.includes('--watch')

require('esbuild').build({
  ...
  watch: watch,
  ...
})

Now tell it to write errors to a file esbuild_error_development.txt in your project root:

const watch = process.argv.includes('--watch')
const railsEnv = process.env.RAILS_ENV || 'development'
const errorFilePath = `esbuild_error_${railsEnv}.txt`

const path = require('path')
const fs = require('fs')

function handleError(error) {
  if (error) fs.writeFileSync(errorFilePath, error.toString())
  else if (fs.existsSync(errorFilePath)) fs.truncate(errorFilePath, 0, () => {})
}

require('esbuild').build({
  ...
  watch: watch && { onRebuild: handleError },
  ...
})

Tell your Rails application about esbuild errors

We now tell our Rails app to render contents from that file, if there are any.
That should only happen in development, since tests or production environments will fail when trying to compile their assets up front.

class ApplicationController < ActionController::Base

  include EsbuildErrorRendering if Rails.env.development?

end
module ApplicationController::EsbuildErrorRendering

  ESBUILD_ERROR = Rails.root.join("esbuild_error_#{Rails.env}.txt") # see esbuild.config.js

  def self.included(base)
    base.before_action :render_esbuild_error, if: :render_esbuild_error?
  end

  private

  def render_esbuild_error
    heading, errors = ESBUILD_ERROR.read.split("\n", 2)

    # Render error as HTML so rack-livereload can inject its code into <head>
    # and refresh the error page when assets are modified.
    render html: <<~HTML.html_safe, layout: false
      <html>
        <head></head>
        <body>
          <h1>#{ERB::Util.html_escape(heading)}</h1>
          <pre>#{ERB::Util.html_escape(errors)}</pre>
        </body>
      </html>
    HTML
  end

  def render_esbuild_error?
    ESBUILD_ERROR.exist? && ESBUILD_ERROR.size > 0
  end

end

Note how we are rendering HTML. We don't do that because it's pretty, but to allow tools like Rack::LiveReload to inject themselves into the HTML response, and trigger a reload when the error page is shown (and show the application when the error has been fixed).

Optional: Configure Guard / Rack::LiveReload

If you use Guard with Rack::LiveReload to reload your application when assets change, tell it about that new file. Add to your Guardfile:

watch(%r(^esbuild_error_development\.txt$))

Note that the file must exist when Guard starts, but we'll take care of that in the next step.

Configure git

As mentioned above, the esbuild_error_development.txt file must exist for Guard to watch it. [1]

Sadly, Git does not offer a way to add a file and ignore further changes, at least not in a way where this information is shared with the upstream repo Show archive.org snapshot . With bin/setup, Rails offers a convention to prepare your application for use (e.g. to make it work for a new developer), so we simply leverage that. If your project uses a different approach, adjust accordingly.

  1. Add to your project's .gitignore:
    /esbuild_error_*
    
  2. Add to your project's bin/setup:
    FileUtils.touch('esbuild_error_development.txt')
    

Try it out

That's it! Restart your application and esbuild, and break stuff. You should see error messages be delivered by your application.

Demo


[1] Side note: Our implementation of the controller module also expects the file to exist. If you don't use Guard and consider that requirement annoying, you could change render_esbuild_error? to just check for ESBUILD_ERROR.exist? and not add an empty error file to your repo.

Arne Hartherz
January 13, 2022Software engineer at makandra GmbH
Posted by Arne Hartherz to makandra dev (2022-01-13 14:31)