javascript-solid-server 0.0.84 → 0.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/bin/jss.js +2 -0
- package/package.json +1 -1
- package/src/notifications/events.js +55 -0
- package/src/notifications/websocket.js +6 -0
- package/src/server.js +11 -0
- package/test/live-reload.test.js +265 -0
package/bin/jss.js
CHANGED
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
|
+
import { watch } from 'fs';
|
|
10
|
+
import { join, relative } from 'path';
|
|
9
11
|
|
|
10
12
|
// Singleton event emitter for resource changes
|
|
11
13
|
export const resourceEvents = new EventEmitter();
|
|
@@ -20,3 +22,56 @@ resourceEvents.setMaxListeners(1000);
|
|
|
20
22
|
export function emitChange(resourceUrl) {
|
|
21
23
|
resourceEvents.emit('change', resourceUrl);
|
|
22
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start watching filesystem for changes and emit notifications
|
|
28
|
+
* @param {string} rootDir - Directory to watch
|
|
29
|
+
* @param {string} baseUrl - Base URL for constructing resource URLs (e.g., http://localhost:3000)
|
|
30
|
+
*/
|
|
31
|
+
export function startFileWatcher(rootDir, baseUrl) {
|
|
32
|
+
// Debounce map to avoid duplicate events (editors often save multiple times)
|
|
33
|
+
const debounceMap = new Map();
|
|
34
|
+
const DEBOUNCE_MS = 100;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const watcher = watch(rootDir, { recursive: true }, (eventType, filename) => {
|
|
38
|
+
if (!filename) return;
|
|
39
|
+
|
|
40
|
+
// Skip hidden files and common temp files
|
|
41
|
+
if (filename.startsWith('.') || filename.endsWith('~') || filename.endsWith('.swp')) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Debounce: skip if we just emitted for this file
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const lastEmit = debounceMap.get(filename);
|
|
48
|
+
if (lastEmit && now - lastEmit < DEBOUNCE_MS) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
debounceMap.set(filename, now);
|
|
52
|
+
|
|
53
|
+
// Clean up old debounce entries periodically
|
|
54
|
+
if (debounceMap.size > 1000) {
|
|
55
|
+
for (const [key, time] of debounceMap) {
|
|
56
|
+
if (now - time > 5000) debounceMap.delete(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Construct resource URL
|
|
61
|
+
const resourcePath = '/' + filename.replace(/\\/g, '/');
|
|
62
|
+
const resourceUrl = baseUrl.replace(/\/$/, '') + resourcePath;
|
|
63
|
+
|
|
64
|
+
emitChange(resourceUrl);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Handle watcher errors gracefully
|
|
68
|
+
watcher.on('error', (err) => {
|
|
69
|
+
console.error('File watcher error:', err.message);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return watcher;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('Failed to start file watcher:', err.message);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -40,6 +40,7 @@ export function handleWebSocket(socket, request, webId = null) {
|
|
|
40
40
|
// Store webId and server info on socket for ACL checks
|
|
41
41
|
socket.webId = webId;
|
|
42
42
|
socket.serverOrigin = `${request.protocol}://${request.hostname}`;
|
|
43
|
+
socket.publicMode = request.config?.public || false;
|
|
43
44
|
|
|
44
45
|
// Send protocol greeting
|
|
45
46
|
socket.send('protocol solid-0.1');
|
|
@@ -123,6 +124,11 @@ async function checkSubscriptionAccess(url, socket) {
|
|
|
123
124
|
const stats = await storage.stat(resourcePath);
|
|
124
125
|
const isContainer = stats?.isDirectory || resourcePath.endsWith('/');
|
|
125
126
|
|
|
127
|
+
// Skip WAC check in public mode
|
|
128
|
+
if (socket.publicMode) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
// Check WAC read permission
|
|
127
133
|
const { allowed } = await checkAccess({
|
|
128
134
|
resourceUrl: url,
|
package/src/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as storage from './storage/filesystem.js';
|
|
|
9
9
|
import { getCorsHeaders } from './ldp/headers.js';
|
|
10
10
|
import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
11
11
|
import { notificationsPlugin } from './notifications/index.js';
|
|
12
|
+
import { startFileWatcher } from './notifications/events.js';
|
|
12
13
|
import { idpPlugin } from './idp/index.js';
|
|
13
14
|
import { isGitRequest, isGitWriteOperation, handleGit } from './handlers/git.js';
|
|
14
15
|
import { AccessMode } from './wac/parser.js';
|
|
@@ -542,6 +543,16 @@ export function createServer(options = {}) {
|
|
|
542
543
|
// Note: Quota not initialized for root-level pods (no user directory)
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
// Start file watcher for live reload (watches filesystem for external changes)
|
|
547
|
+
if (liveReloadEnabled) {
|
|
548
|
+
const dataRoot = options.root || process.env.DATA_ROOT || './data';
|
|
549
|
+
const protocol = options.ssl ? 'https' : 'http';
|
|
550
|
+
// Use configured port, or default; actual URL will be localhost
|
|
551
|
+
const port = options.port || 3000;
|
|
552
|
+
const baseUrl = `${protocol}://localhost:${port}`;
|
|
553
|
+
startFileWatcher(dataRoot, baseUrl);
|
|
554
|
+
}
|
|
555
|
+
|
|
545
556
|
return fastify;
|
|
546
557
|
}
|
|
547
558
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Reload Test
|
|
3
|
+
*
|
|
4
|
+
* Tests the full chain: file change → WebSocket pub → client receives
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer } from '../src/server.js';
|
|
8
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import WebSocket from 'ws';
|
|
11
|
+
|
|
12
|
+
const TEST_PORT = 9876;
|
|
13
|
+
const TEST_DIR = '/tmp/live-reload-test-suite';
|
|
14
|
+
const BASE_URL = `http://localhost:${TEST_PORT}`;
|
|
15
|
+
|
|
16
|
+
// Setup and teardown
|
|
17
|
+
function setupTestDir() {
|
|
18
|
+
if (existsSync(TEST_DIR)) {
|
|
19
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
22
|
+
writeFileSync(join(TEST_DIR, 'index.html'), '<!DOCTYPE html><html><body>Hello</body></html>');
|
|
23
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'initial content');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function cleanupTestDir() {
|
|
27
|
+
if (existsSync(TEST_DIR)) {
|
|
28
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Test 1: Verify WebSocket notifications work for HTTP PUT
|
|
33
|
+
async function testHttpPutNotification() {
|
|
34
|
+
console.log('\n=== Test 1: HTTP PUT triggers WebSocket notification ===');
|
|
35
|
+
|
|
36
|
+
setupTestDir();
|
|
37
|
+
|
|
38
|
+
const server = createServer({
|
|
39
|
+
root: TEST_DIR,
|
|
40
|
+
port: TEST_PORT,
|
|
41
|
+
logger: false,
|
|
42
|
+
liveReload: true,
|
|
43
|
+
public: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await server.listen({ port: TEST_PORT, host: '0.0.0.0' });
|
|
47
|
+
console.log('Server started on port', TEST_PORT);
|
|
48
|
+
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
ws.close();
|
|
52
|
+
server.close();
|
|
53
|
+
reject(new Error('Timeout: No WebSocket notification received'));
|
|
54
|
+
}, 5000);
|
|
55
|
+
|
|
56
|
+
const ws = new WebSocket(`ws://localhost:${TEST_PORT}/.notifications`);
|
|
57
|
+
|
|
58
|
+
ws.on('open', () => {
|
|
59
|
+
console.log('WebSocket connected');
|
|
60
|
+
// Subscribe to the test file
|
|
61
|
+
ws.send(`sub ${BASE_URL}/test.txt`);
|
|
62
|
+
console.log('Subscribed to', `${BASE_URL}/test.txt`);
|
|
63
|
+
|
|
64
|
+
// Wait a bit then do HTTP PUT
|
|
65
|
+
setTimeout(async () => {
|
|
66
|
+
console.log('Doing HTTP PUT...');
|
|
67
|
+
const res = await fetch(`${BASE_URL}/test.txt`, {
|
|
68
|
+
method: 'PUT',
|
|
69
|
+
body: 'updated via HTTP',
|
|
70
|
+
headers: { 'Content-Type': 'text/plain' }
|
|
71
|
+
});
|
|
72
|
+
console.log('PUT response:', res.status);
|
|
73
|
+
}, 500);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
ws.on('message', (data) => {
|
|
77
|
+
const msg = data.toString();
|
|
78
|
+
console.log('WebSocket received:', msg);
|
|
79
|
+
|
|
80
|
+
if (msg.startsWith('pub ')) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
console.log('SUCCESS: Received pub notification');
|
|
83
|
+
ws.close();
|
|
84
|
+
server.close().then(() => {
|
|
85
|
+
cleanupTestDir();
|
|
86
|
+
resolve(true);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
ws.on('error', (err) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
server.close();
|
|
94
|
+
reject(err);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Test 2: Verify file watcher detects filesystem changes
|
|
100
|
+
async function testFileWatcherNotification() {
|
|
101
|
+
console.log('\n=== Test 2: Filesystem change triggers WebSocket notification ===');
|
|
102
|
+
|
|
103
|
+
setupTestDir();
|
|
104
|
+
|
|
105
|
+
const server = createServer({
|
|
106
|
+
root: TEST_DIR,
|
|
107
|
+
port: TEST_PORT,
|
|
108
|
+
logger: false,
|
|
109
|
+
liveReload: true,
|
|
110
|
+
public: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await server.listen({ port: TEST_PORT, host: '0.0.0.0' });
|
|
114
|
+
console.log('Server started on port', TEST_PORT);
|
|
115
|
+
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const timeout = setTimeout(() => {
|
|
118
|
+
ws.close();
|
|
119
|
+
server.close();
|
|
120
|
+
reject(new Error('Timeout: No WebSocket notification received for filesystem change'));
|
|
121
|
+
}, 5000);
|
|
122
|
+
|
|
123
|
+
const ws = new WebSocket(`ws://localhost:${TEST_PORT}/.notifications`);
|
|
124
|
+
|
|
125
|
+
ws.on('open', () => {
|
|
126
|
+
console.log('WebSocket connected');
|
|
127
|
+
// Subscribe to the test file
|
|
128
|
+
ws.send(`sub ${BASE_URL}/test.txt`);
|
|
129
|
+
console.log('Subscribed to', `${BASE_URL}/test.txt`);
|
|
130
|
+
|
|
131
|
+
// Wait a bit then modify file directly on filesystem
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
console.log('Modifying file via filesystem...');
|
|
134
|
+
writeFileSync(join(TEST_DIR, 'test.txt'), 'updated via filesystem ' + Date.now());
|
|
135
|
+
console.log('File written');
|
|
136
|
+
}, 1000);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ws.on('message', (data) => {
|
|
140
|
+
const msg = data.toString();
|
|
141
|
+
console.log('WebSocket received:', msg);
|
|
142
|
+
|
|
143
|
+
if (msg.startsWith('pub ')) {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
console.log('SUCCESS: Received pub notification for filesystem change');
|
|
146
|
+
ws.close();
|
|
147
|
+
server.close().then(() => {
|
|
148
|
+
cleanupTestDir();
|
|
149
|
+
resolve(true);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
ws.on('error', (err) => {
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
server.close();
|
|
157
|
+
reject(err);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Test 3: Verify fs.watch works on this platform
|
|
163
|
+
async function testFsWatch() {
|
|
164
|
+
console.log('\n=== Test 3: Basic fs.watch functionality ===');
|
|
165
|
+
|
|
166
|
+
const { watch } = await import('fs');
|
|
167
|
+
const testFile = '/tmp/fswatch-test.txt';
|
|
168
|
+
|
|
169
|
+
writeFileSync(testFile, 'initial');
|
|
170
|
+
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const timeout = setTimeout(() => {
|
|
173
|
+
watcher.close();
|
|
174
|
+
reject(new Error('fs.watch did not detect file change'));
|
|
175
|
+
}, 3000);
|
|
176
|
+
|
|
177
|
+
const watcher = watch(testFile, (eventType, filename) => {
|
|
178
|
+
console.log('fs.watch detected:', eventType, filename);
|
|
179
|
+
clearTimeout(timeout);
|
|
180
|
+
watcher.close();
|
|
181
|
+
resolve(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Modify file after short delay
|
|
185
|
+
setTimeout(() => {
|
|
186
|
+
console.log('Modifying file...');
|
|
187
|
+
writeFileSync(testFile, 'modified ' + Date.now());
|
|
188
|
+
}, 500);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Test 4: Verify fs.watch with recursive option
|
|
193
|
+
async function testFsWatchRecursive() {
|
|
194
|
+
console.log('\n=== Test 4: fs.watch with recursive option ===');
|
|
195
|
+
|
|
196
|
+
const { watch } = await import('fs');
|
|
197
|
+
const testDir = '/tmp/fswatch-recursive-test';
|
|
198
|
+
|
|
199
|
+
if (existsSync(testDir)) rmSync(testDir, { recursive: true });
|
|
200
|
+
mkdirSync(testDir, { recursive: true });
|
|
201
|
+
writeFileSync(join(testDir, 'file.txt'), 'initial');
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const timeout = setTimeout(() => {
|
|
205
|
+
watcher.close();
|
|
206
|
+
console.log('FAIL: fs.watch recursive did not detect file change');
|
|
207
|
+
resolve(false); // Don't reject, just report failure
|
|
208
|
+
}, 3000);
|
|
209
|
+
|
|
210
|
+
let detected = false;
|
|
211
|
+
const watcher = watch(testDir, { recursive: true }, (eventType, filename) => {
|
|
212
|
+
if (!detected) {
|
|
213
|
+
detected = true;
|
|
214
|
+
console.log('fs.watch recursive detected:', eventType, filename);
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
watcher.close();
|
|
217
|
+
resolve(true);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
watcher.on('error', (err) => {
|
|
222
|
+
console.log('fs.watch error:', err.message);
|
|
223
|
+
clearTimeout(timeout);
|
|
224
|
+
resolve(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Modify file after short delay
|
|
228
|
+
setTimeout(() => {
|
|
229
|
+
console.log('Modifying file in watched directory...');
|
|
230
|
+
writeFileSync(join(testDir, 'file.txt'), 'modified ' + Date.now());
|
|
231
|
+
}, 500);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Run all tests
|
|
236
|
+
async function runTests() {
|
|
237
|
+
console.log('Live Reload Test Suite');
|
|
238
|
+
console.log('======================');
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Test basic fs.watch first
|
|
242
|
+
const fsWatchWorks = await testFsWatch();
|
|
243
|
+
console.log('Test 3 result: fs.watch works =', fsWatchWorks);
|
|
244
|
+
|
|
245
|
+
// Test recursive fs.watch
|
|
246
|
+
const fsWatchRecursiveWorks = await testFsWatchRecursive();
|
|
247
|
+
console.log('Test 4 result: fs.watch recursive works =', fsWatchRecursiveWorks);
|
|
248
|
+
|
|
249
|
+
// Test HTTP PUT notification
|
|
250
|
+
await testHttpPutNotification();
|
|
251
|
+
console.log('Test 1 result: PASSED');
|
|
252
|
+
|
|
253
|
+
// Test file watcher notification
|
|
254
|
+
await testFileWatcherNotification();
|
|
255
|
+
console.log('Test 2 result: PASSED');
|
|
256
|
+
|
|
257
|
+
console.log('\n=== All tests passed ===');
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.error('\n=== Test FAILED ===');
|
|
260
|
+
console.error(err.message);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
runTests();
|