Read more

Livereload + esbuild

Tobias Kraze
December 06, 2023Software engineer at makandra GmbH

Getting CSS (and JS) live reloading to work in a esbuild / Rails project is a bit of a hassle, but the following seems to work decently well.

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

We assume that you already use a standard "esbuild in Rails" setup, and have an esbuild watcher running that picks up your source code in app/assets and compiles to public/assets; if not change the paths below accordingly.

Basic idea

We will

  • use the guard-livereload gem as the livereload server (which send updates to the browser),
  • use the livereload-js npm package in the browser to connect to the livereload server,
  • not use rack-livereload any more as this is unnecessarily complex and does not work with many CSP policies.

We will configure guard-livereload to watch the generated files in public/assets. Inconveniently, esbuild always rewrites all files in public/assets on any change, triggering full page loads on any change, even CSS changes. To fix this, we employ a little hack, see the Guardfile below.

Livereload server

Add the guard-livereload gem to the :development section of your Gemfile. Create a Guardfile like this

# Speed up Guard by allowlisting specific directories
directories %w[app config public spec]

allowed_js_reload_window = 10 # seconds
last_js_change = Time.at(0)

guard 'livereload', apply_css_live: true, host: '127.0.0.1' do
  watch(%r(^public/assets/esbuild_error_development\.txt$))

  # Livereload + ESbuild is not the best combo
  # We want
  # - to reload CSS files without doing a full page reload
  # - do a full page reload for JS changes
  #
  # Unfortunately, every time anything changes in app/assets, ESbuild will rewrite all files
  # in public/assets. On the other hand, we have to watch these files, otherwise we do not
  # know when ESbuild is done.
  #
  # As a compromise we do the following:
  # - when a JS file in **app/asset** changes, we remember the timestamp.
  # - when a CSS file in **public/asset** changes, we instruct live-reload to reload it.
  # - when a JS file in **public/asset** changes, we instruct live-reload to reload the page *only if*
  #   a JS change in **app/assets** occured within th `allowed_js_reload_window`.

  watch(%r(^public(/assets/.*\.css)$)) do |match|
    # Any generated CSS changed, send its name to livereload-js
    match[1]
  end

  watch(%r(^app/assets/.*\.js$)) do |_match|
    # Any source Javascript changed. Assume the next changes in public/assets refer to JS.
    last_js_change = Time.now
    nil
  end

  watch(%r(^public/assets/.*\.js$)) do
    # Any generated JS changed. This might be because of a JS or CSS change!
    if Time.now < last_js_change + allowed_js_reload_window
      'all-js' # anything that is not a .css file will trigger a full page load
    end
  end

  # All of these will trigger full page loads.
  watch(%r(^app/views/.+\.(html|erb|haml)$))
  watch(%r(^app/(?:controllers|helpers|inputs)/.+\.rb))
  watch(%r(^config/locales/.+\.yml))
end

Running guard or guard -P liveload will start your livereload server. It will log when it triggers a reload, and when a browser connects.

Livereload client

Add the livereload-js npm package. Then, in your esbuild config, add an entrypoint for dev.js and add the following files:

// app/assets/dev.js
import './dev/livereload'
// app/assets/dev/livereload/index.js
import './settings'
import 'livereload-js/dist/livereload'
// app/assets/dev/livereload/settings.js
window.LiveReloadOptions = {
  host: 'localhost',
  port: 35729,
}

Finally, require this new entrypoint in your layout with

- if Rails.env.development?
  = javascript_include_tag 'dev', nonce: true

(skip the nonce, if you do not have a CSP configured)

Debugging the integration

You can add #LR-verbose to your URL to get debugging output from the livereload-js script.

Tobias Kraze
December 06, 2023Software engineer at makandra GmbH
Posted by Tobias Kraze to makandra dev (2023-12-06 13:33)