Skip to content

Commit e80ff35

Browse files
feat: add --live-reload flag for auto-refresh on file changes (JavaScriptSolidServer#111)
* feat: add --live-reload flag for auto-refresh on file changes 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 JavaScriptSolidServer#110 * fix: address Copilot review feedback - 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 * fix: enable notifications events when live-reload is on * fix: remove ETag header when injecting live reload script
1 parent f8764cb commit e80ff35

4 files changed

Lines changed: 43 additions & 3 deletions

File tree

bin/jss.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ program
7878
.option('--no-webid-tls', 'Disable WebID-TLS authentication')
7979
.option('--public', 'Allow unauthenticated access (skip WAC, open read/write)')
8080
.option('--read-only', 'Disable PUT/DELETE/PATCH methods (read-only mode)')
81+
.option('--live-reload', 'Inject live reload script into HTML (auto-refresh on changes)')
8182
.option('-q, --quiet', 'Suppress log output')
8283
.option('--print-config', 'Print configuration and exit')
8384
.action(async (options) => {
@@ -135,6 +136,7 @@ program
135136
singleUserName: config.singleUserName,
136137
public: config.public,
137138
readOnly: config.readOnly,
139+
liveReload: config.liveReload,
138140
});
139141

140142
await server.listen({ port: config.port, host: config.host });

src/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ export const defaults = {
7979
// Read-only mode - disable PUT/DELETE/PATCH
8080
readOnly: false,
8181

82+
// Live reload - inject script to auto-refresh browser on file changes
83+
liveReload: false,
84+
8285
// Logging
8386
logger: true,
8487
quiet: false,
@@ -125,6 +128,7 @@ const envMap = {
125128
JSS_DEFAULT_QUOTA: 'defaultQuota',
126129
JSS_PUBLIC: 'public',
127130
JSS_READ_ONLY: 'readOnly',
131+
JSS_LIVE_RELOAD: 'liveReload',
128132
};
129133

130134
/**

src/handlers/resource.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,23 @@ import { emitChange } from '../notifications/events.js';
1717
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
1818
import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
1919

20+
/**
21+
* Live reload script - injected into HTML when --live-reload is enabled
22+
*/
23+
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>`;
24+
25+
/**
26+
* Inject live reload script into HTML content
27+
*/
28+
function injectLiveReload(content) {
29+
const html = content.toString();
30+
// Inject before </body> or at end
31+
if (html.includes('</body>')) {
32+
return Buffer.from(html.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>'));
33+
}
34+
return Buffer.from(html + LIVE_RELOAD_SCRIPT);
35+
}
36+
2037
/**
2138
* Get the storage path and resource URL for a request
2239
* In subdomain mode, storage path includes pod name, URL uses subdomain
@@ -198,6 +215,12 @@ export async function handleGet(request, reply) {
198215
});
199216

200217
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
218+
// Inject live reload script for index.html
219+
if (request.liveReloadEnabled) {
220+
reply.header('Cache-Control', 'no-store');
221+
reply.removeHeader('ETag');
222+
return reply.send(injectLiveReload(content));
223+
}
201224
return reply.send(content);
202225
}
203226

@@ -439,6 +462,13 @@ export async function handleGet(request, reply) {
439462
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
440463

441464
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
465+
466+
// Inject live reload script into HTML (disable caching since content is modified)
467+
if (actualContentType === 'text/html' && request.liveReloadEnabled) {
468+
reply.header('Cache-Control', 'no-store');
469+
reply.removeHeader('ETag');
470+
return reply.send(injectLiveReload(content));
471+
}
442472
return reply.send(content);
443473
}
444474

src/server.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export function createServer(options = {}) {
7979
const defaultQuota = options.defaultQuota ?? 50 * 1024 * 1024;
8080
// WebID-TLS client certificate authentication is OFF by default
8181
const webidTlsEnabled = options.webidTls ?? false;
82+
// Live reload - injects script to auto-refresh browser on file changes
83+
const liveReloadEnabled = options.liveReload ?? false;
8284

8385
// Set data root via environment variable if provided
8486
if (options.root) {
@@ -136,9 +138,10 @@ export function createServer(options = {}) {
136138
fastify.decorateRequest('solidosUiEnabled', null);
137139
fastify.decorateRequest('defaultQuota', null);
138140
fastify.decorateRequest('config', null);
141+
fastify.decorateRequest('liveReloadEnabled', null);
139142
fastify.addHook('onRequest', async (request) => {
140143
request.connegEnabled = connegEnabled;
141-
request.notificationsEnabled = notificationsEnabled;
144+
request.notificationsEnabled = notificationsEnabled || liveReloadEnabled;
142145
request.idpEnabled = idpEnabled;
143146
request.subdomainsEnabled = subdomainsEnabled;
144147
request.baseDomain = baseDomain;
@@ -148,6 +151,7 @@ export function createServer(options = {}) {
148151
request.solidosUiEnabled = solidosUiEnabled;
149152
request.defaultQuota = defaultQuota;
150153
request.config = { public: options.public, readOnly: options.readOnly };
154+
request.liveReloadEnabled = liveReloadEnabled;
151155

152156
// Extract pod name from subdomain if enabled
153157
if (subdomainsEnabled && baseDomain) {
@@ -164,8 +168,8 @@ export function createServer(options = {}) {
164168
}
165169
});
166170

167-
// Register WebSocket notifications plugin if enabled
168-
if (notificationsEnabled) {
171+
// Register WebSocket notifications plugin if enabled (or live reload needs it)
172+
if (notificationsEnabled || liveReloadEnabled) {
169173
fastify.register(notificationsPlugin);
170174
}
171175

0 commit comments

Comments
 (0)