Skip to content

feat: add --live-reload flag for auto-refresh on file changes#111

Merged
melvincarvalho merged 4 commits into
gh-pagesfrom
feature/live-reload
Jan 28, 2026
Merged

feat: add --live-reload flag for auto-refresh on file changes#111
melvincarvalho merged 4 commits into
gh-pagesfrom
feature/live-reload

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

Adds --live-reload flag that auto-refreshes the browser when files change, leveraging existing WebSocket notifications.

Implementation

~35 lines total:

  • Injects small client script into HTML responses
  • Auto-enables WebSocket notifications when flag is used
  • Script reconnects if connection drops
<script>(function(){
  var ws = new WebSocket('ws://' + location.host + '/notifications/');
  ws.onmessage = function() { location.reload() };
  ws.onclose = function() { setTimeout(function(){ location.reload() }, 1000) };
})();</script>

Usage

jss start --live-reload

Testing

# Start server
jss start --root /tmp/test --public --live-reload

# Verify script injection
curl http://localhost:3000/index.html | grep 'notifications'

Closes #110

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 liveReload configuration/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.

Comment thread src/handlers/resource.js Outdated
Comment on lines +461 to +464
// Inject live reload script into HTML
if (actualContentType === 'text/html' && request.liveReloadEnabled) {
return reply.send(injectLiveReload(content, true));
}

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js Outdated
Comment on lines +28 to +35
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);

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js Outdated
/**
* 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>`;

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Suggested change
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>`;

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js Outdated
Comment on lines +461 to +464
// Inject live reload script into HTML
if (actualContentType === 'text/html' && request.liveReloadEnabled) {
return reply.send(injectLiveReload(content, true));
}

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js Outdated
Comment on lines +461 to +464
// Inject live reload script into HTML
if (actualContentType === 'text/html' && request.liveReloadEnabled) {
return reply.send(injectLiveReload(content, true));
}

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- 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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.notificationsEnabled is still derived only from options.notifications. When --live-reload is enabled without --notifications, the WebSocket route gets registered but change events are never emitted (all emitChange(...) calls are gated by request.notificationsEnabled), so live reload will not actually trigger. Consider setting request.notificationsEnabled = notificationsEnabled || liveReloadEnabled (and/or deriving notificationsEnabled the 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.

Comment thread src/handlers/resource.js
Comment on lines +465 to +469
// 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));
}

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js
/**
* 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>`;

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/server.js
Comment on lines +171 to 173
// Register WebSocket notifications plugin if enabled (or live reload needs it)
if (notificationsEnabled || liveReloadEnabled) {
fastify.register(notificationsPlugin);

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/handlers/resource.js
Comment on lines +465 to +468
// 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));

Copilot AI Jan 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@melvincarvalho
melvincarvalho merged commit e80ff35 into gh-pages Jan 28, 2026
@melvincarvalho
melvincarvalho deleted the feature/live-reload branch January 28, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Live reload support (--live-reload flag)

2 participants