Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/jss.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ program
.option('--no-webid-tls', 'Disable WebID-TLS authentication')
.option('--public', 'Allow unauthenticated access (skip WAC, open read/write)')
.option('--read-only', 'Disable PUT/DELETE/PATCH methods (read-only mode)')
.option('--live-reload', 'Inject live reload script into HTML (auto-refresh on changes)')
.option('-q, --quiet', 'Suppress log output')
.option('--print-config', 'Print configuration and exit')
.action(async (options) => {
Expand Down Expand Up @@ -135,6 +136,7 @@ program
singleUserName: config.singleUserName,
public: config.public,
readOnly: config.readOnly,
liveReload: config.liveReload,
});

await server.listen({ port: config.port, host: config.host });
Expand Down
4 changes: 4 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export const defaults = {
// Read-only mode - disable PUT/DELETE/PATCH
readOnly: false,

// Live reload - inject script to auto-refresh browser on file changes
liveReload: false,

// Logging
logger: true,
quiet: false,
Expand Down Expand Up @@ -125,6 +128,7 @@ const envMap = {
JSS_DEFAULT_QUOTA: 'defaultQuota',
JSS_PUBLIC: 'public',
JSS_READ_ONLY: 'readOnly',
JSS_LIVE_RELOAD: 'liveReload',
};

/**
Expand Down
30 changes: 30 additions & 0 deletions src/handlers/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,23 @@ import { emitChange } from '../notifications/events.js';
import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.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.

/**
* Inject live reload script into HTML content
*/
function injectLiveReload(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);
}

/**
* Get the storage path and resource URL for a request
* In subdomain mode, storage path includes pod name, URL uses subdomain
Expand Down Expand Up @@ -198,6 +215,12 @@ export async function handleGet(request, reply) {
});

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
// Inject live reload script for index.html
if (request.liveReloadEnabled) {
reply.header('Cache-Control', 'no-store');
reply.removeHeader('ETag');
return reply.send(injectLiveReload(content));
}
return reply.send(content);
}

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

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));

// Inject live reload script into HTML (disable caching since content is modified)
if (actualContentType === 'text/html' && request.liveReloadEnabled) {
reply.header('Cache-Control', 'no-store');
reply.removeHeader('ETag');
return reply.send(injectLiveReload(content));
Comment on lines +466 to +470

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.
}
Comment on lines +466 to +471

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.
return reply.send(content);
}

Expand Down
10 changes: 7 additions & 3 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export function createServer(options = {}) {
const defaultQuota = options.defaultQuota ?? 50 * 1024 * 1024;
// WebID-TLS client certificate authentication is OFF by default
const webidTlsEnabled = options.webidTls ?? false;
// Live reload - injects script to auto-refresh browser on file changes
const liveReloadEnabled = options.liveReload ?? false;

// Set data root via environment variable if provided
if (options.root) {
Expand Down Expand Up @@ -136,9 +138,10 @@ export function createServer(options = {}) {
fastify.decorateRequest('solidosUiEnabled', null);
fastify.decorateRequest('defaultQuota', null);
fastify.decorateRequest('config', null);
fastify.decorateRequest('liveReloadEnabled', null);
fastify.addHook('onRequest', async (request) => {
request.connegEnabled = connegEnabled;
request.notificationsEnabled = notificationsEnabled;
request.notificationsEnabled = notificationsEnabled || liveReloadEnabled;
request.idpEnabled = idpEnabled;
request.subdomainsEnabled = subdomainsEnabled;
request.baseDomain = baseDomain;
Expand All @@ -148,6 +151,7 @@ export function createServer(options = {}) {
request.solidosUiEnabled = solidosUiEnabled;
request.defaultQuota = defaultQuota;
request.config = { public: options.public, readOnly: options.readOnly };
request.liveReloadEnabled = liveReloadEnabled;

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

// Register WebSocket notifications plugin if enabled
if (notificationsEnabled) {
// Register WebSocket notifications plugin if enabled (or live reload needs it)
if (notificationsEnabled || liveReloadEnabled) {
fastify.register(notificationsPlugin);
Comment on lines +171 to 173

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

Expand Down