feat: add --live-reload flag for auto-refresh on file changes#111
Conversation
Injects a small script into HTML responses that connects to the WebSocket notifications endpoint. When files change, browser reloads. - Auto-enables notifications WebSocket when --live-reload is used - Script injected before </body> in HTML responses - Reconnects automatically if connection drops Usage: jss start --live-reload Closes #110
There was a problem hiding this comment.
Pull request overview
Adds a --live-reload CLI flag to automatically refresh HTML pages when server-side resources change by injecting a client-side WebSocket script and ensuring the notifications WebSocket is available.
Changes:
- Add
liveReloadconfiguration/CLI flag and pass it into server creation. - Register the notifications WebSocket plugin automatically when live reload is enabled.
- Inject a live-reload client script into HTML GET responses.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/server.js | Wires liveReload into request context and registers notifications plugin when needed. |
| src/handlers/resource.js | Adds HTML response injection helper and injects live-reload script for HTML GET responses. |
| src/config.js | Adds liveReload default and JSS_LIVE_RELOAD env mapping. |
| bin/jss.js | Adds --live-reload CLI flag and passes through to server options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Inject live reload script into HTML | ||
| if (actualContentType === 'text/html' && request.liveReloadEnabled) { | ||
| return reply.send(injectLiveReload(content, true)); | ||
| } |
There was a problem hiding this comment.
There are existing tests for HTML GET responses and notifications behavior, but no tests cover the new --live-reload behavior (script injection and auto-enabling notifications). Add tests that start the server with { liveReload: true } and assert that HTML responses contain the injected snippet and that a change (PUT/DELETE) results in a WebSocket "pub" notification (or equivalent) without requiring notifications=true.
| function injectLiveReload(content, enabled) { | ||
| if (!enabled) return content; | ||
| const html = content.toString(); | ||
| // Inject before </body> or at end | ||
| if (html.includes('</body>')) { | ||
| return Buffer.from(html.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>')); | ||
| } | ||
| return Buffer.from(html + LIVE_RELOAD_SCRIPT); |
There was a problem hiding this comment.
injectLiveReload takes an "enabled" parameter but is always called with true after already checking request.liveReloadEnabled. This makes the signature misleading and invites incorrect future use. Consider removing the parameter and relying on the outer condition, or pass request.liveReloadEnabled directly.
| /** | ||
| * Live reload script - injected into HTML when --live-reload is enabled | ||
| */ | ||
| const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//' +location.host+'/notifications/');ws.onmessage=function(){location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},1000)}})();</script>`; |
There was a problem hiding this comment.
LIVE_RELOAD_SCRIPT is connecting to "/notifications/", but the WebSocket notifications route registered by notificationsPlugin is "/.notifications" (src/notifications/index.js). Also, the server immediately sends a "protocol solid-0.1" greeting on connect, so reloading on any onmessage will cause an infinite reload loop. Update the script to connect to "/.notifications", send a "sub " message on open, and only reload on "pub" messages (ignore protocol/ack/err frames).
| const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//' +location.host+'/notifications/');ws.onmessage=function(){location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},1000)}})();</script>`; | |
| const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//' +location.host+'/.notifications');ws.onopen=function(){try{ws.send('sub '+location.href)}catch(e){}};ws.onmessage=function(ev){var data=ev.data;if(typeof data==='string'&&data.startsWith('pub ')){location.reload()}};ws.onclose=function(){setTimeout(function(){location.reload()},1000)}})();</script>`; |
| // Inject live reload script into HTML | ||
| if (actualContentType === 'text/html' && request.liveReloadEnabled) { | ||
| return reply.send(injectLiveReload(content, true)); | ||
| } |
There was a problem hiding this comment.
When live reload is enabled, GET responses for HTML are modified by injecting a script, but the response headers still include the original file ETag from stats.etag. This makes caches/conditional requests inconsistent (304 may be returned for a different body) and can cause clients to cache the injected variant incorrectly. Consider disabling caching when liveReloadEnabled (e.g., add Cache-Control: no-store and omit ETag) or generate an ETag that incorporates the injected variant.
| // Inject live reload script into HTML | ||
| if (actualContentType === 'text/html' && request.liveReloadEnabled) { | ||
| return reply.send(injectLiveReload(content, true)); | ||
| } |
There was a problem hiding this comment.
Live reload injection is only applied in the final "Serve content as-is" path. HTML served earlier via the container index.html branch returns before reaching this logic, so directory roots that serve index.html won’t get the live-reload script injected. Apply injection in the index.html response path as well (and any other HTML-returning early exits) so --live-reload works for common cases like "/" -> index.html.
- Fix WebSocket path: /notifications/ -> /.notifications - Only reload on 'pub' messages (not protocol greeting) - Subscribe to current page URL on connect - Add Cache-Control: no-store when injecting script - Simplify injectLiveReload function (remove unused param) - Inject script for index.html container path too
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
src/server.js:145
request.notificationsEnabledis still derived only fromoptions.notifications. When--live-reloadis enabled without--notifications, the WebSocket route gets registered but change events are never emitted (allemitChange(...)calls are gated byrequest.notificationsEnabled), so live reload will not actually trigger. Consider settingrequest.notificationsEnabled = notificationsEnabled || liveReloadEnabled(and/or derivingnotificationsEnabledthe same way) so live reload truly auto-enables notifications behavior.
fastify.addHook('onRequest', async (request) => {
request.connegEnabled = connegEnabled;
request.notificationsEnabled = notificationsEnabled;
request.idpEnabled = idpEnabled;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Inject live reload script into HTML (disable caching since content is modified) | ||
| if (actualContentType === 'text/html' && request.liveReloadEnabled) { | ||
| reply.header('Cache-Control', 'no-store'); | ||
| return reply.send(injectLiveReload(content)); | ||
| } |
There was a problem hiding this comment.
Live-reload injection is only applied to HTML read from storage (and index.html), but other HTML responses generated by the server (e.g., Mashlib/SolidOS UI wrappers returned earlier in handleGet) bypass injectLiveReload. If the intent is “inject into HTML responses”, consider applying the same injection to those generated HTML responses as well when request.liveReloadEnabled is true.
| /** | ||
| * Live reload script - injected into HTML when --live-reload is enabled | ||
| */ | ||
| const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//' +location.host+'/.notifications');ws.onopen=function(){ws.send('sub '+location.href)};ws.onmessage=function(e){if(e.data.startsWith('pub '))location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},1000)}})();</script>`; |
There was a problem hiding this comment.
The PR description/issue text references a /notifications/ WebSocket endpoint, but the server’s notifications plugin exposes /.notifications (and the injected script also connects to /.notifications). Please align the docs/description (and any external consumers) with the actual endpoint used by this codebase to avoid confusion.
| // Register WebSocket notifications plugin if enabled (or live reload needs it) | ||
| if (notificationsEnabled || liveReloadEnabled) { | ||
| fastify.register(notificationsPlugin); |
There was a problem hiding this comment.
This introduces new user-facing behavior (live reload flag, HTML mutation, and implicit notifications enablement) but there are no tests covering: (1) that HTML responses include the injected script when liveReload is enabled, and (2) that a write triggers a pub ... leading to a reload scenario when notifications was otherwise disabled. Given the existing test/notifications.test.js suite, please add coverage for liveReload mode.
| // Inject live reload script into HTML (disable caching since content is modified) | ||
| if (actualContentType === 'text/html' && request.liveReloadEnabled) { | ||
| reply.header('Cache-Control', 'no-store'); | ||
| return reply.send(injectLiveReload(content)); |
There was a problem hiding this comment.
When request.liveReloadEnabled is on, the response body is modified but the headers (notably ETag) are still computed from the underlying file’s stats. This makes the ETag no longer represent the actual response entity and can lead to incorrect conditional GET behavior (including 304s for a different payload). Consider omitting ETag (and skipping the early If-None-Match short-circuit) for injected responses, or generating a distinct ETag for the injected variant.
Summary
Adds
--live-reloadflag that auto-refreshes the browser when files change, leveraging existing WebSocket notifications.Implementation
~35 lines total:
Usage
Testing
Closes #110