Here I have removed support of error pages (I'll do it in additional PR).
How it works and some considerations.
We can do 3 types of reload:
Full-page or hard reload (window.location.reload()). Why we would need it? We don't want it actually because it is slow -loads bunch of files, reevaluates all scripts and CSS, looses all data in forms, scroll positions etc. But we know, that when Turbo detects any changes in CSS or JS links in head - it will do all full-page navigation for us. So, what happens when we update some JS file and do Turbo.visit()? Turbo will first load our page, detect changes and do full-page visit for us, and we will load same page twice. Knowing that we can do full-page reload immediately, saving a bit of time on unneeded double request.
Important note about js bundling using external tools (esbuild for instance). You should not add source files to watched dirs. Otherwise what happens: first you modify some source file, hard reload command is sent (and performed), in parallel external tools is compiling your bundle, and when it completes bundle file is modified and new hard reload command is sent again. Depending of timing you can have various glitches: you can have 2 full reloads, or you can have just one reload, but with old bundle. Just don't watch on source files and you'll be safe
If you still use some sprockets-based js compilation (some legacy with coffeesecript may be) - you should watch on source files. When reload happened sprockets will hold your page request until js is compiled, and then render the rest. So just one reload happens.
If you use rails-importmaps, then again you should watch your sources and when any change happens you need a full reload.
Soft reload (or Turbo.visit(window.location.href). This works fine when rendered html is changed, but JS and CSS are intact. So any non-css and non-js file (views, controller, translations, locales, helpers, components, policies, commands, services etc). This is fast (as any other Turbo navigation), and allows to keep open websockets.
CSS reload. When CSS is changed we don't need to reload page at all. We can simply inject new CSS into the page and that's all. Extra fast, preserves websockets, any data entered in forms, any open modals/popovers states etc. The only tricky moment is naming. Because CSS files are included with digested filenames - we can't understand which tag must be updated. This can be fixed by adding data-stylesheet attribute:
When file with name "frontend.css" will be changed backend will send a message with "css" mode, "frontend.css" file, and digested filename in "path". Frontend then will use file attribute to find correct tag and path attribute to set "href" attribute. CSS will be updated. This is very useful, when you want to try some CSS changes, especially when you need to do some navigation after page reload (for instance open a modal and check some styling inside).
Important note: This mode will work only when using external bundling for css (like dart-sass). In this case you need to watch only resulting file.
If you still build css with sprockets, then it won't work. Add your css sources to general watch folder and do a FULL reload, as with JS files. Reason is the same: If you do a soft reload Turbo will notice that CSS file was changed and will do full reload. So, why wait, if we know already that we need a full reload
Additional note: in css reload mode a full reload will happen automatically on any navigation event after css update (because Turbo will detect that CSS was changed).
Regarding debounce: Originally I kept it only in soft mode, but after considerations I have removed it completely in this PR. It adds additional 300ms before reload happens, and helps only when you have improper configuration (like watching sources and compiled files at same time). Proper configuration will guarantee that everything works smoothly (and solution with debounce still depends on how fast your build is working).
A bit of details on implementation:
I have added additional configuration option css_listen_paths, where css folders with build files must be added (and if Cssbundling is defined /app/assets/builds/ is added automatically)
Next we watch all files, and depending of what files were changed we use one of methods:
soft by default
css if only files in css folders were changed
hard if any of changed files is in force_reload_paths
Also I added configuration settings js_bundling and css_bundling to force corresponding modes without including gem (for instance I use dart-sass, but I don't include CssBundling gem).
Regarding TurboStreams: I added support for :turbo_stream mode, but I don't see any benefits of using it. It works over ActionCable anyways, but web socket is reopened on each page navigation, which makes it a bit slower. I would drop support of it, reducing bundle size and the need of support.
This is a continuation of previous PR: https://github.com/kirillplatonov/hotwire-livereload/pull/34
Here I have removed support of error pages (I'll do it in additional PR).
How it works and some considerations.
We can do 3 types of reload:
window.location.reload()
). Why we would need it? We don't want it actually because it is slow -loads bunch of files, reevaluates all scripts and CSS, looses all data in forms, scroll positions etc. But we know, that when Turbo detects any changes in CSS or JS links in head - it will do all full-page navigation for us. So, what happens when we update some JS file and do Turbo.visit()? Turbo will first load our page, detect changes and do full-page visit for us, and we will load same page twice. Knowing that we can do full-page reload immediately, saving a bit of time on unneeded double request.Important note about js bundling using external tools (esbuild for instance). You should not add source files to watched dirs. Otherwise what happens: first you modify some source file, hard reload command is sent (and performed), in parallel external tools is compiling your bundle, and when it completes bundle file is modified and new hard reload command is sent again. Depending of timing you can have various glitches: you can have 2 full reloads, or you can have just one reload, but with old bundle. Just don't watch on source files and you'll be safe
If you still use some sprockets-based js compilation (some legacy with coffeesecript may be) - you should watch on source files. When reload happened sprockets will hold your page request until js is compiled, and then render the rest. So just one reload happens.
If you use rails-importmaps, then again you should watch your sources and when any change happens you need a full reload.
Soft reload (or Turbo.visit(window.location.href). This works fine when rendered html is changed, but JS and CSS are intact. So any non-css and non-js file (views, controller, translations, locales, helpers, components, policies, commands, services etc). This is fast (as any other Turbo navigation), and allows to keep open websockets.
CSS reload. When CSS is changed we don't need to reload page at all. We can simply inject new CSS into the page and that's all. Extra fast, preserves websockets, any data entered in forms, any open modals/popovers states etc. The only tricky moment is naming. Because CSS files are included with digested filenames - we can't understand which tag must be updated. This can be fixed by adding data-stylesheet attribute:
<%= stylesheet_link_tag "frontend", "data-turbo-track": "reload", "data-stylesheet": "frontend.css" %>
This generates something like
<link rel="stylesheet" href="/assets/frontend.debug-4cc794426ab9d6e346924d52db8bcd5f20944446e95d984f498dfac0e9b2160b.css" data-turbo-track="reload" data-stylesheet="frontend.css"/>
When file with name "frontend.css" will be changed backend will send a message with "css" mode, "frontend.css" file, and digested filename in "path". Frontend then will use file attribute to find correct tag and path attribute to set "href" attribute. CSS will be updated. This is very useful, when you want to try some CSS changes, especially when you need to do some navigation after page reload (for instance open a modal and check some styling inside).
Important note: This mode will work only when using external bundling for css (like dart-sass). In this case you need to watch only resulting file. If you still build css with sprockets, then it won't work. Add your css sources to general watch folder and do a FULL reload, as with JS files. Reason is the same: If you do a soft reload Turbo will notice that CSS file was changed and will do full reload. So, why wait, if we know already that we need a full reload
Additional note: in css reload mode a full reload will happen automatically on any navigation event after css update (because Turbo will detect that CSS was changed).
Regarding debounce: Originally I kept it only in soft mode, but after considerations I have removed it completely in this PR. It adds additional 300ms before reload happens, and helps only when you have improper configuration (like watching sources and compiled files at same time). Proper configuration will guarantee that everything works smoothly (and solution with debounce still depends on how fast your build is working).
A bit of details on implementation: I have added additional configuration option
css_listen_paths
, where css folders with build files must be added (and if Cssbundling is defined /app/assets/builds/ is added automatically)Next we watch all files, and depending of what files were changed we use one of methods:
Also I added configuration settings js_bundling and css_bundling to force corresponding modes without including gem (for instance I use dart-sass, but I don't include CssBundling gem).
Regarding TurboStreams: I added support for
:turbo_stream
mode, but I don't see any benefits of using it. It works over ActionCable anyways, but web socket is reopened on each page navigation, which makes it a bit slower. I would drop support of it, reducing bundle size and the need of support.PS: Sorry for being silent for so long.