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.
@@ -323,7 +323,8 @@
323
323
  "Bash(npm exec serve:*)",
324
324
  "Bash(npm link)",
325
325
  "Bash(npm link:*)",
326
- "Bash(git push)"
326
+ "Bash(git push)",
327
+ "Bash(ulimit:*)"
327
328
  ]
328
329
  }
329
330
  }
package/bin/jss.js CHANGED
@@ -105,6 +105,8 @@ program
105
105
 
106
106
  // Create and start server
107
107
  const server = createServer({
108
+ port: config.port,
109
+ host: config.host,
108
110
  logger: config.logger,
109
111
  conneg: config.conneg,
110
112
  notifications: config.notifications,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.84",
3
+ "version": "0.0.86",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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();