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